diff --git a/package.json b/package.json index 72ff43a5..82366571 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@sentry/browser": "^5.1.1", "crc-32": "^1.2.0", "gl-matrix": "^3.0.0", - "librw": "^0.1.0", + "librw": "^0.2.0", "pako": "^1.0.7" }, "scripts": { diff --git a/src/GrandTheftAuto3/item.ts b/src/GrandTheftAuto3/item.ts index 916fbae2..48a494cb 100644 --- a/src/GrandTheftAuto3/item.ts +++ b/src/GrandTheftAuto3/item.ts @@ -29,7 +29,7 @@ export enum ObjectFlags { } export interface ObjectDefinition { - id: number; + id?: number; modelName: string; txdName: string; drawDistance: number; @@ -37,6 +37,7 @@ export interface ObjectDefinition { tobj: boolean; timeOn?: number; timeOff?: number; + dynamic: boolean; } function parseObjectDefinition(row: string[], tobj: boolean): ObjectDefinition { @@ -46,7 +47,8 @@ function parseObjectDefinition(row: string[], tobj: boolean): ObjectDefinition { txdName: row[2], drawDistance: Number((row.length > 5) ? row[4] : row[3]), flags: Number(tobj ? row[row.length - 3] : row[row.length - 1]), - tobj + tobj, + dynamic: false }; if (tobj) { def.timeOn = Number(row[row.length - 2]); @@ -70,7 +72,7 @@ export function parseItemDefinition(text: string): ItemDefinition { } export interface ItemInstance { - id: number; + id?: number; modelName: string; translation: vec3; scale: vec3; diff --git a/src/GrandTheftAuto3/program.glsl b/src/GrandTheftAuto3/program.glsl index b80ef256..bf38c23c 100644 --- a/src/GrandTheftAuto3/program.glsl +++ b/src/GrandTheftAuto3/program.glsl @@ -1,24 +1,40 @@ precision mediump float; precision lowp sampler2DArray; -// Expected to be constant across the entire scene. layout(row_major, std140) uniform ub_SceneParams { Mat4x4 u_Projection; - vec4 u_AmbientColor; }; layout(row_major, std140) uniform ub_MeshFragParams { Mat4x3 u_ViewMatrix; + vec4 u_AmbientColor; +#ifdef SKY + Mat4x3 u_WorldMatrix; + vec4 u_Frustum; + vec4 u_SkyTopColor; + vec4 u_SkyBotColor; +#else float alphaThreshold; +#endif }; uniform sampler2DArray u_Texture; +#ifdef SKY +varying vec3 v_Position; +#else varying vec4 v_Color; varying vec3 v_TexCoord; +#endif #ifdef VERT layout(location = 0) in vec3 a_Position; +#ifdef SKY +void main() { + gl_Position = vec4(a_Position, 1.0); + v_Position = a_Position; +} +#else layout(location = 1) in vec4 a_Color; layout(location = 2) in vec3 a_TexCoord; @@ -28,8 +44,29 @@ void main() { v_TexCoord = a_TexCoord; } #endif +#endif #ifdef FRAG +#ifdef SKY +void main() { + vec3 nearPlane = v_Position * u_Frustum.xyz; + vec3 cameraRay = Mul(u_WorldMatrix, vec4(nearPlane, 0.0)); + vec3 cameraPos = Mul(u_WorldMatrix, vec4(vec3(0.0), 1.0)); + float t = -cameraPos.y / cameraRay.y; + vec3 oceanPlane = cameraPos + t * cameraRay; + + if (t > 0.0 && (abs(oceanPlane.z) > 2000.0 || abs(oceanPlane.x) > 2000.0)) { + vec2 uv = fract(oceanPlane.zx / 32.0); + vec4 t_Color = vec4(0,0,0,1); + t_Color.rgb += u_AmbientColor.rgb; + t_Color *= texture(u_Texture, vec3(uv, 0)); + gl_FragColor = t_Color; + } else { + float elevation = atan(cameraRay.y, length(cameraRay.zx)) * 180.0 / radians(180.0); + gl_FragColor = mix(u_SkyBotColor, u_SkyTopColor, clamp(abs(elevation / 45.0), 0.0, 1.0)); + } +} +#else void main() { vec4 t_Color = v_Color; t_Color.rgb += u_AmbientColor.rgb; @@ -39,3 +76,4 @@ void main() { gl_FragColor = t_Color; } #endif +#endif diff --git a/src/GrandTheftAuto3/render.ts b/src/GrandTheftAuto3/render.ts index a04c13da..3f4afe16 100644 --- a/src/GrandTheftAuto3/render.ts +++ b/src/GrandTheftAuto3/render.ts @@ -14,11 +14,11 @@ import { mat4, quat, vec3, vec2 } from "gl-matrix"; import { computeViewSpaceDepthFromWorldSpaceAABB } from "../Camera"; import { GfxRenderHelper } from "../gfx/render/GfxRenderGraph"; import { assert } from "../util"; -import { BasicRenderTarget, makeClearRenderPassDescriptor } from "../gfx/helpers/RenderTargetHelpers"; -import { GfxRenderInstManager, GfxRendererLayer, makeSortKey, setSortKeyDepth } from "../gfx/render/GfxRenderer"; +import { BasicRenderTarget, standardFullClearRenderPassDescriptor } from "../gfx/helpers/RenderTargetHelpers"; +import { GfxRenderInstManager, GfxRendererLayer, makeSortKey, setSortKeyDepth, GfxRenderInst } from "../gfx/render/GfxRenderer"; import { ItemInstance, ObjectDefinition } from "./item"; -import { colorNew, White, colorNewCopy, colorLerp, colorMult } from "../Color"; -import { ColorSet } from "./time"; +import { colorNew, White, colorNewCopy, colorMult, Color } from "../Color"; +import { ColorSet, emptyColorSet, lerpColorSet } from "./time"; import { AABB } from "../Geometry"; const TIME_FACTOR = 2500; // one day cycle per minute @@ -60,7 +60,7 @@ function halve(pixels: Uint8Array, width: number, height: number): Uint8Array { return halved; } -export class TextureAtlas extends TextureMapping { +export class TextureArray extends TextureMapping { public subimages = new Map(); constructor(device: GfxDevice, textures: Texture[]) { @@ -145,9 +145,105 @@ class GTA3Program extends DeviceProgram { public both = GTA3Program.program; } -const program = new GTA3Program(); +const mainProgram = new GTA3Program(); +const skyProgram = new GTA3Program(); +skyProgram.defines.set('SKY', '1'); -class MeshFragData { +class Renderer { + protected vertexBuffer: GfxBuffer; + protected indexBuffer: GfxBuffer; + protected inputLayout: GfxInputLayout; + protected inputState: GfxInputState; + protected megaStateFlags: Partial; + protected gfxProgram?: GfxProgram; + + protected indices: number; + + constructor(protected program: DeviceProgram, protected atlas?: TextureArray) {} + + public prepareToRender(device: GfxDevice, renderInstManager: GfxRenderInstManager, viewerInput: Viewer.ViewerRenderInput, colorSet: ColorSet): GfxRenderInst | undefined { + const renderInst = renderInstManager.pushRenderInst(); + renderInst.setInputLayoutAndState(this.inputLayout, this.inputState); + renderInst.drawIndexes(this.indices); + + if (this.gfxProgram === undefined) + this.gfxProgram = renderInstManager.gfxRenderCache.createProgram(device, this.program); + + renderInst.setGfxProgram(this.gfxProgram); + renderInst.setMegaStateFlags(this.megaStateFlags); + if (this.atlas !== undefined) + renderInst.setSamplerBindingsFromTextureMappings([this.atlas]); + return renderInst; + } + + public destroy(device: GfxDevice): void { + device.destroyBuffer(this.indexBuffer); + device.destroyBuffer(this.vertexBuffer); + device.destroyInputLayout(this.inputLayout); + device.destroyInputState(this.inputState); + if (this.gfxProgram !== undefined) + device.destroyProgram(this.gfxProgram); + if (this.atlas !== undefined) + this.atlas.destroy(device); + } +} + +export class SkyRenderer extends Renderer { + constructor(device: GfxDevice, atlas?: TextureArray) { + super(skyProgram, atlas); + // fullscreen quad + const vbuf = new Float32Array([ + -1, -1, 1, + -1, 1, 1, + 1, 1, 1, + 1, -1, 1, + ]); + const ibuf = new Uint16Array([0,1,2,0,2,3]); + this.vertexBuffer = makeStaticDataBuffer(device, GfxBufferUsage.VERTEX, vbuf.buffer); + this.indexBuffer = makeStaticDataBuffer(device, GfxBufferUsage.INDEX, ibuf.buffer); + this.indices = ibuf.length; + const vertexAttributeDescriptors: GfxVertexAttributeDescriptor[] = [ + { location: GTA3Program.a_Position, bufferIndex: 0, format: GfxFormat.F32_RGB, bufferByteOffset: 0 * 0x04, frequency: GfxVertexAttributeFrequency.PER_VERTEX }, + ]; + this.inputLayout = device.createInputLayout({ indexBufferFormat: GfxFormat.U16_R, vertexAttributeDescriptors }); + const buffers = [{ buffer: this.vertexBuffer, byteOffset: 0, byteStride: 3 * 0x04}]; + const indexBuffer = { buffer: this.indexBuffer, byteOffset: 0, byteStride: 0 }; + this.inputState = device.createInputState(this.inputLayout, buffers, indexBuffer); + this.megaStateFlags = { depthWrite: false }; + } + + public prepareToRender(device: GfxDevice, renderInstManager: GfxRenderInstManager, viewerInput: Viewer.ViewerRenderInput, colorSet: ColorSet): undefined { + if (viewerInput.camera.isOrthographic) return; + const renderInst = super.prepareToRender(device, renderInstManager, viewerInput, colorSet)!; + renderInst.sortKey = makeSortKey(GfxRendererLayer.BACKGROUND); + let offs = renderInst.allocateUniformBuffer(GTA3Program.ub_MeshFragParams, 12 + 4 + 12 + 4 + 4 + 4); + const mapped = renderInst.mapUniformBufferF32(GTA3Program.ub_MeshFragParams); + offs += fillMatrix4x3(mapped, offs, viewerInput.camera.viewMatrix); + mapped[offs++] = colorSet.amb.r + colorSet.dir.r; + mapped[offs++] = colorSet.amb.g + colorSet.dir.g; + mapped[offs++] = colorSet.amb.b + colorSet.dir.b; + mapped[offs++] = colorSet.amb.a + colorSet.dir.a; + offs += fillMatrix4x3(mapped, offs, viewerInput.camera.worldMatrix); + mapped[offs++] = viewerInput.camera.frustum.right; + mapped[offs++] = viewerInput.camera.frustum.top; + mapped[offs++] = viewerInput.camera.frustum.near; + mapped[offs++] = viewerInput.camera.frustum.far; + offs += fillColor(mapped, offs, colorSet.skyTop); + offs += fillColor(mapped, offs, colorSet.skyBot); + return; + } +} + +export interface MeshFragData { + indices: Uint16Array; + vertices: number; + texName?: string; + position(vertex: number): vec3; + color(vertex: number): Color; + texCoord(vertex: number): vec2; +} + +class RWMeshFragData implements MeshFragData { public indices: Uint16Array; public texName?: string; @@ -171,7 +267,7 @@ class MeshFragData { mesh.indices!.map(index => this.indexMap.indexOf(index)))); } - public vertices() { + public get vertices() { return this.indexMap.length; } @@ -199,26 +295,22 @@ class MeshFragData { } } -class MeshData { - public meshFragData: MeshFragData[] = []; +export class ModelCache { + public meshData = new Map(); - constructor(atomic: rw.Atomic, public obj: ObjectDefinition) { + private addAtomic(atomic: rw.Atomic, obj: ObjectDefinition) { const geom = atomic.geometry; - const positions = geom.morphTarget(0).vertices!.slice(); const texCoords = (geom.numTexCoordSets > 0) ? geom.texCoords(0)!.slice() : null; const colors = (geom.colors !== null) ? geom.colors.slice() : null; - - let h = geom.meshHeader; + const h = geom.meshHeader; + const frags: MeshFragData[] = []; for (let i = 0; i < h.numMeshes; i++) { - const frag = new MeshFragData(h.mesh(i), h.tristrip, obj.txdName, positions, texCoords, colors); - this.meshFragData.push(frag); + const frag = new RWMeshFragData(h.mesh(i), h.tristrip, obj.txdName, positions, texCoords, colors); + frags.push(frag); } + this.meshData.set(obj.modelName, frags); } -} - -export class ModelCache { - public meshData = new Map(); public addModel(model: rw.Clump, obj: ObjectDefinition) { let node: rw.Atomic | null = null; @@ -231,14 +323,14 @@ export class ModelCache { } } if (node !== null) - this.meshData.set(obj.modelName, new MeshData(node, obj)); + this.addAtomic(node, obj); } } export class MeshInstance { public modelMatrix = mat4.create(); - constructor(public meshData: MeshData, public item: ItemInstance) { + constructor(public frags: MeshFragData[], public item: ItemInstance) { mat4.fromRotationTranslationScale(this.modelMatrix, this.item.rotation, this.item.translation, this.item.scale); // convert Z-up to Y-up mat4.multiply(this.modelMatrix, mat4.fromQuat(mat4.create(), quat.fromValues(0.5, 0.5, 0.5, -0.5)), this.modelMatrix); @@ -250,6 +342,7 @@ export class DrawKey { public drawDistance?: number; public timeOn?: number; public timeOff?: number; + public dynamic: boolean; constructor(obj: ObjectDefinition, public zone: string) { if (obj.drawDistance < 99) { @@ -259,49 +352,38 @@ export class DrawKey { this.timeOn = obj.timeOn; this.timeOff = obj.timeOff; } + this.dynamic = obj.dynamic; } } -export class SceneRenderer { +export class SceneRenderer extends Renderer { public bbox = new AABB(); - private vertexBuffer: GfxBuffer; - private indexBuffer: GfxBuffer; - private inputLayout: GfxInputLayout; - private inputState: GfxInputState; - private gfxProgram?: GfxProgram; - - private megaStateFlags: Partial = { - blendMode: GfxBlendMode.ADD, - blendDstFactor: GfxBlendFactor.ONE_MINUS_SRC_ALPHA, - blendSrcFactor: GfxBlendFactor.SRC_ALPHA, - }; - - private vertices = 0; - private indices = 0; - - constructor(device: GfxDevice, public key: DrawKey, meshes: MeshInstance[], private atlas?: TextureAtlas) { + constructor(device: GfxDevice, public key: DrawKey, meshes: MeshInstance[], atlas?: TextureArray) { + super(mainProgram, atlas); const skipFrag = (frag: MeshFragData) => atlas !== undefined && frag.texName !== undefined && !atlas.subimages.has(frag.texName); + let vertices = 0; + this.indices = 0; for (const inst of meshes) { - for (const frag of inst.meshData.meshFragData) { + for (const frag of inst.frags) { if (skipFrag(frag)) continue; - this.vertices += frag.vertices(); + vertices += frag.vertices; this.indices += frag.indices.length; } } const points = [] as vec3[]; - const vbuf = new Float32Array(this.vertices * 10); + const vbuf = new Float32Array(vertices * 10); const ibuf = new Uint32Array(this.indices); let voffs = 0; let ioffs = 0; let lastIndex = 0; for (const inst of meshes) { - for (const frag of inst.meshData.meshFragData) { + for (const frag of inst.frags) { if (skipFrag(frag)) continue; - const n = frag.vertices(); + const n = frag.vertices; const texLayer = (frag.texName === undefined || atlas === undefined) ? undefined : atlas.subimages.get(frag.texName); for (let i = 0; i < n; i++) { const pos = vec3.transformMat4(vec3.create(), frag.position(i), inst.modelMatrix); @@ -339,9 +421,14 @@ export class SceneRenderer { const buffers = [{ buffer: this.vertexBuffer, byteOffset: 0, byteStride: 10 * 0x04}]; const indexBuffer = { buffer: this.indexBuffer, byteOffset: 0, byteStride: 0 }; this.inputState = device.createInputState(this.inputLayout, buffers, indexBuffer); + this.megaStateFlags = { + blendMode: GfxBlendMode.ADD, + blendDstFactor: GfxBlendFactor.ONE_MINUS_SRC_ALPHA, + blendSrcFactor: GfxBlendFactor.SRC_ALPHA, + }; } - public prepareToRender(device: GfxDevice, renderInstManager: GfxRenderInstManager, viewerInput: Viewer.ViewerRenderInput, dual: boolean): void { + public prepareToRender(device: GfxDevice, renderInstManager: GfxRenderInstManager, viewerInput: Viewer.ViewerRenderInput, colorSet: ColorSet, dual = false): undefined { const hour = Math.floor(viewerInput.time / TIME_FACTOR) % 24; const { timeOn, timeOff } = this.key; let renderLayer = this.key.renderLayer; @@ -358,36 +445,29 @@ export class SceneRenderer { if (this.key.drawDistance !== undefined && depth > this.bbox.boundingSphereRadius() + 3 * this.key.drawDistance) return; - const renderInst = renderInstManager.pushRenderInst(); - renderInst.setInputLayoutAndState(this.inputLayout, this.inputState); - renderInst.drawIndexes(this.indices); - - if (this.gfxProgram === undefined) - this.gfxProgram = renderInstManager.gfxRenderCache.createProgram(device, program); - - renderInst.setGfxProgram(this.gfxProgram); - renderInst.setMegaStateFlags(this.megaStateFlags); - if (dual) - renderInst.setMegaStateFlags({ depthWrite: false }); - if (this.atlas !== undefined) - renderInst.setSamplerBindingsFromTextureMappings([this.atlas]); + const renderInst = super.prepareToRender(device, renderInstManager, viewerInput, colorSet)!; renderInst.sortKey = setSortKeyDepth(makeSortKey(renderLayer), depth); - let offs = renderInst.allocateUniformBuffer(GTA3Program.ub_MeshFragParams, 12 + 4); + let offs = renderInst.allocateUniformBuffer(GTA3Program.ub_MeshFragParams, 12 + 4 + 4); const mapped = renderInst.mapUniformBufferF32(GTA3Program.ub_MeshFragParams); offs += fillMatrix4x3(mapped, offs, viewerInput.camera.viewMatrix); + if (this.key.dynamic) { + mapped[offs++] = colorSet.amb.r + colorSet.dir.r; + mapped[offs++] = colorSet.amb.g + colorSet.dir.g; + mapped[offs++] = colorSet.amb.b + colorSet.dir.b; + mapped[offs++] = colorSet.amb.a + colorSet.dir.a; + } else { + offs += fillColor(mapped, offs, colorSet.amb); + } mapped[offs++] = !(renderLayer & GfxRendererLayer.TRANSLUCENT) ? 0.01 : dual ? -0.9 : 0.9; - } - public destroy(device: GfxDevice): void { - device.destroyBuffer(this.indexBuffer); - device.destroyBuffer(this.vertexBuffer); - device.destroyInputLayout(this.inputLayout); - device.destroyInputState(this.inputState); - if (this.gfxProgram !== undefined) - device.destroyProgram(this.gfxProgram); - if (this.atlas !== undefined) - this.atlas.destroy(device); + // PS2 alpha test emulation, see http://skygfx.rockstarvision.com/skygfx.html + if (dual) { + renderInst.setMegaStateFlags({ depthWrite: false }); + } else if (!!(this.key.renderLayer & GfxRendererLayer.TRANSLUCENT)) { + this.prepareToRender(device, renderInstManager, viewerInput, colorSet, true); + } + return; } } @@ -396,12 +476,12 @@ const bindingLayouts: GfxBindingLayoutDescriptor[] = [ ]; export class GTA3Renderer implements Viewer.SceneGfx { - public sceneRenderers: SceneRenderer[] = []; + public sceneRenderers: Renderer[] = []; public onstatechanged!: () => void; private renderTarget = new BasicRenderTarget(); - private clearRenderPassDescriptor = makeClearRenderPassDescriptor(true, colorNewCopy(White)); - private ambient = colorNewCopy(White); + private clearRenderPassDescriptor = standardFullClearRenderPassDescriptor; + private currentColors = emptyColorSet(); private renderHelper: GfxRenderHelper; private weather = 0; private scenarioSelect: UI.SingleSelect; @@ -414,30 +494,20 @@ export class GTA3Renderer implements Viewer.SceneGfx { const t = viewerInput.time / TIME_FACTOR; const cs1 = this.colorSets[Math.floor(t) % 24 + 24 * this.weather]; const cs2 = this.colorSets[Math.floor(t+1) % 24 + 24 * this.weather]; - const skyTop = colorNewCopy(White); - const skyBot = colorNewCopy(White); - colorLerp(this.ambient, cs1.amb, cs2.amb, t % 1); - colorLerp(skyTop, cs1.skyTop, cs2.skyTop, t % 1); - colorLerp(skyBot, cs1.skyBot, cs2.skyBot, t % 1); - colorLerp(this.clearRenderPassDescriptor.colorClearColor, skyTop, skyBot, 0.67); // fog + lerpColorSet(this.currentColors, cs1, cs2, t % 1); viewerInput.camera.setClipPlanes(1); this.renderHelper.pushTemplateRenderInst(); const template = this.renderHelper.renderInstManager.pushTemplateRenderInst(); template.setBindingLayouts(bindingLayouts); - let offs = template.allocateUniformBuffer(GTA3Program.ub_SceneParams, 16 + 4); + let offs = template.allocateUniformBuffer(GTA3Program.ub_SceneParams, 16); const sceneParamsMapped = template.mapUniformBufferF32(GTA3Program.ub_SceneParams); offs += fillMatrix4x4(sceneParamsMapped, offs, viewerInput.camera.projectionMatrix); - offs += fillColor(sceneParamsMapped, offs, this.ambient); for (let i = 0; i < this.sceneRenderers.length; i++) { const sceneRenderer = this.sceneRenderers[i]; - sceneRenderer.prepareToRender(device, this.renderHelper.renderInstManager, viewerInput, false); - if (!!(sceneRenderer.key.renderLayer & GfxRendererLayer.TRANSLUCENT)) { - // PS2 alpha test emulation, see http://skygfx.rockstarvision.com/skygfx.html - sceneRenderer.prepareToRender(device, this.renderHelper.renderInstManager, viewerInput, true); - } + sceneRenderer.prepareToRender(device, this.renderHelper.renderInstManager, viewerInput, this.currentColors); } this.renderHelper.renderInstManager.popTemplateRenderInst(); diff --git a/src/GrandTheftAuto3/scenes.ts b/src/GrandTheftAuto3/scenes.ts index 4bf20579..cef7f1fb 100644 --- a/src/GrandTheftAuto3/scenes.ts +++ b/src/GrandTheftAuto3/scenes.ts @@ -3,23 +3,33 @@ import * as Viewer from '../viewer'; import * as rw from 'librw'; import { GfxDevice } from '../gfx/platform/GfxPlatform'; import { DataFetcher } from '../DataFetcher'; -import { GTA3Renderer, SceneRenderer, DrawKey, Texture, TextureAtlas, MeshInstance, ModelCache } from './render'; +import { GTA3Renderer, SceneRenderer, DrawKey, Texture, TextureArray, MeshInstance, ModelCache, SkyRenderer } from './render'; import { SceneContext } from '../SceneBase'; import { getTextDecoder, assert } from '../util'; import { parseItemPlacement, ItemPlacement, parseItemDefinition, ItemDefinition, ObjectDefinition, ItemInstance, parseZones } from './item'; import { parseTimeCycle, ColorSet } from './time'; +import { parseWaterPro, waterMeshFragData, waterDefinition } from './water'; import { quat, vec3 } from 'gl-matrix'; import { AABB } from '../Geometry'; import { GfxRendererLayer } from '../gfx/render/GfxRenderer'; +import ArrayBufferSlice from '../ArrayBufferSlice'; const pathBase = `GrandTheftAuto3`; +function UTF8ToString(array: Uint8Array) { + let length = 0; while (length < array.length && array[length]) length++; + return getTextDecoder('utf8')!.decode(array.subarray(0, length)); +} + class GTA3SceneDesc implements Viewer.SceneDesc { private static initialised = false; + private complete: boolean; + private assets = new Map(); private ids: string[]; constructor(public id: string, public name: string) { - if (this.id === 'all') { + this.complete = (this.id === 'all'); + if (this.complete) { this.ids = [ "comntop/comNtop", "comnbtm/comNbtm", @@ -49,13 +59,37 @@ class GTA3SceneDesc implements Viewer.SceneDesc { this.initialised = true; } - private async fetchIDE(id: string, dataFetcher: DataFetcher): Promise { - const buffer = await dataFetcher.fetchData(`${pathBase}/data/maps/${id}.ide`); - const text = getTextDecoder('utf8')!.decode(buffer.arrayBuffer); + private async fetchIMG(dataFetcher: DataFetcher): Promise { + const [bufferDIR, bufferIMG] = await Promise.all([ + dataFetcher.fetchData(`${pathBase}/models/gta3.dir`), + dataFetcher.fetchData(`${pathBase}/models/gta3.img`), + ]); + const view = bufferDIR.createDataView(); + for (let i = 0; i < view.byteLength; i += 32) { + const offset = view.getUint32(i + 0, true); + const size = view.getUint32(i + 4, true); + const name = UTF8ToString(bufferDIR.subarray(i + 8, 24).createTypedArray(Uint8Array)).toLowerCase(); + const data = bufferIMG.subarray(2048 * offset, 2048 * size); + this.assets.set(`${pathBase}/models/gta3/${name}`, data); + } + } + + private async fetch(dataFetcher: DataFetcher, path: string): Promise { + let buffer = this.assets.get(path); + if (buffer === undefined) { + buffer = await dataFetcher.fetchData(path); + this.assets.set(path, buffer); + } + return buffer; + } + + private async fetchIDE(dataFetcher: DataFetcher, id: string): Promise { + const buffer = await this.fetch(dataFetcher, `${pathBase}/data/maps/${id}.ide`); + const text = getTextDecoder('utf8')!.decode(buffer.createDataView()); return parseItemDefinition(text); } - private async fetchIPL(id: string, dataFetcher: DataFetcher): Promise { + private async fetchIPL(dataFetcher: DataFetcher, id: string): Promise { if (id === 'test') return { instances: [{ id: 0, @@ -65,23 +99,59 @@ class GTA3SceneDesc implements Viewer.SceneDesc { scale: vec3.fromValues(10,10,10), }] }; - const buffer = await dataFetcher.fetchData((id === 'props') ? `${pathBase}/data/maps/props.IPL` : `${pathBase}/data/maps/${id}.ipl`); - const text = getTextDecoder('utf8')!.decode(buffer.arrayBuffer); + const buffer = await this.fetch(dataFetcher, (id === 'props') ? `${pathBase}/data/maps/props.IPL` : `${pathBase}/data/maps/${id}.ipl`); + const text = getTextDecoder('utf8')!.decode(buffer.createDataView()); return parseItemPlacement(text); } private async fetchTimeCycle(dataFetcher: DataFetcher): Promise { - const buffer = await dataFetcher.fetchData(`${pathBase}/data/timecyc.dat`); - const text = getTextDecoder('utf8')!.decode(buffer.arrayBuffer); + const buffer = await this.fetch(dataFetcher, `${pathBase}/data/timecyc.dat`); + const text = getTextDecoder('utf8')!.decode(buffer.createDataView()); return parseTimeCycle(text); } private async fetchZones(dataFetcher: DataFetcher): Promise> { - const buffer = await dataFetcher.fetchData(`${pathBase}/data/gta3.zon`); - const text = getTextDecoder('utf8')!.decode(buffer.arrayBuffer); + const buffer = await this.fetch(dataFetcher, `${pathBase}/data/gta3.zon`); + const text = getTextDecoder('utf8')!.decode(buffer.createDataView()); return parseZones(text); } + private async fetchWater(dataFetcher: DataFetcher): Promise { + const buffer = await this.fetch(dataFetcher, `${pathBase}/data/waterpro.dat`); + return parseWaterPro(buffer.createDataView()); + } + + private async fetchTXD(dataFetcher: DataFetcher, txdName: string, textures: Map): Promise { + const txdPath = (txdName === 'generic' || txdName === 'particle') + ? `${pathBase}/models/${txdName}.txd` + : `${pathBase}/models/gta3/${txdName}.txd`; + const buffer = await this.fetch(dataFetcher, txdPath); + const stream = new rw.StreamMemory(buffer.createTypedArray(Uint8Array)); + const header = new rw.ChunkHeaderInfo(stream); + assert(header.type === rw.PluginID.ID_TEXDICTIONARY); + const txd = new rw.TexDictionary(stream); + header.delete(); + stream.delete(); + for (let lnk = txd.textures.begin; !lnk.is(txd.textures.end); lnk = lnk.next) { + const texture = new Texture(rw.Texture.fromDict(lnk), txdName); + textures.set(texture.name, texture); + } + txd.delete(); + } + + private async fetchDFF(dataFetcher: DataFetcher, modelName: string, cb: (clump: rw.Clump) => void): Promise { + const dffPath = `${pathBase}/models/gta3/${modelName}.dff`; + const buffer = await this.fetch(dataFetcher, dffPath); + const stream = new rw.StreamMemory(buffer.createTypedArray(Uint8Array)); + const header = new rw.ChunkHeaderInfo(stream); + assert(header.type === rw.PluginID.ID_CLUMP); + const clump = rw.Clump.streamRead(stream); + header.delete(); + stream.delete(); + cb(clump); + clump.delete(); + } + public async createScene(device: GfxDevice, context: SceneContext): Promise { await GTA3SceneDesc.initialise(); const dataFetcher = context.dataFetcher; @@ -90,11 +160,13 @@ class GTA3SceneDesc implements Viewer.SceneDesc { const ideids = ['generic', 'temppart/temppart', 'comroad/comroad', 'indroads/indroads', 'making/making', 'subroads/subroads']; for (const id of this.ids) if (id.match(/\//)) ideids.push(id.toLowerCase()); - const ides = await Promise.all(ideids.map(id => this.fetchIDE(id, dataFetcher))); + const ides = await Promise.all(ideids.map(id => this.fetchIDE(dataFetcher, id))); for (const ide of ides) for (const obj of ide.objects) objects.set(obj.modelName, obj); + objects.set('water', waterDefinition); - const ipls = await Promise.all(this.ids.map(id => this.fetchIPL(id, dataFetcher))); - const [colorSets, zones] = await Promise.all([this.fetchTimeCycle(dataFetcher), this.fetchZones(dataFetcher)]); + const ipls = await Promise.all(this.ids.map(id => this.fetchIPL(dataFetcher, id))); + const [colorSets, zones, water] = await Promise.all([this.fetchTimeCycle(dataFetcher), this.fetchZones(dataFetcher), this.fetchWater(dataFetcher)]); + ipls.push(water); const drawKeys = new Map(); const layers = new Map(); @@ -105,7 +177,7 @@ class GTA3SceneDesc implements Viewer.SceneDesc { console.warn('No definition for object', name); continue; } - if (name.startsWith('lod') || name.startsWith('islandlod')) continue; // ignore LOD objects + if ((name.startsWith('lod') && name !== 'lodistancoast01') || name.startsWith('islandlod')) continue; // ignore LOD objects let zone = 'cityzon'; for (const [name, bb] of zones) { @@ -123,81 +195,71 @@ class GTA3SceneDesc implements Viewer.SceneDesc { layers.get(drawKey)!.push([item, obj]); } + if (this.complete) + await this.fetchIMG(dataFetcher); + const renderer = new GTA3Renderer(device, colorSets); const loadedTXD = new Map>(); const loadedDFF = new Map>(); const textures = new Map(); const modelCache = new ModelCache(); - for (const [drawKey, items] of layers) (async () => { + + loadedTXD.set('particle', this.fetchTXD(dataFetcher, 'particle', textures)); + loadedDFF.set('water', (async () => {})()); + modelCache.meshData.set('water', [waterMeshFragData]); + + loadedTXD.get('particle')!.then(() => + renderer.sceneRenderers.push(new SkyRenderer(device, + new TextureArray(device, [textures.get('particle/water_old')!])))); + + for (const [drawKey, items] of layers) { const promises: Promise[] = []; for (const [item, obj] of items) { - if (!loadedTXD.has(obj.txdName)) { - const txdPath = (obj.txdName === 'generic') ? `${pathBase}/models/generic.txd` : `${pathBase}/models/gta3/${obj.txdName}.txd`; - loadedTXD.set(obj.txdName, dataFetcher.fetchData(txdPath).then(buffer => { - const stream = new rw.StreamMemory(buffer.arrayBuffer); - const header = new rw.ChunkHeaderInfo(stream); - assert(header.type === rw.PluginID.ID_TEXDICTIONARY); - const txd = new rw.TexDictionary(stream); - header.delete(); - stream.delete(); - for (let lnk = txd.textures.begin; !lnk.is(txd.textures.end); lnk = lnk.next) { - const texture = new Texture(rw.Texture.fromDict(lnk), obj.txdName); - textures.set(texture.name, texture); - } - txd.delete(); - })); - } - promises.push(loadedTXD.get(obj.txdName)!); - - if (!loadedDFF.has(obj.modelName)) { - const dffPath = `${pathBase}/models/gta3/${obj.modelName}.dff`; - loadedDFF.set(obj.modelName, dataFetcher.fetchData(dffPath).then(async buffer => { - const stream = new rw.StreamMemory(buffer.arrayBuffer); - const header = new rw.ChunkHeaderInfo(stream); - assert(header.type === rw.PluginID.ID_CLUMP); - const clump = rw.Clump.streamRead(stream); - header.delete(); - stream.delete(); - modelCache.addModel(clump, obj); - clump.delete(); - })); - } - promises.push(loadedDFF.get(obj.modelName)!); + if (!loadedTXD.has(obj.txdName)) + loadedTXD.set(obj.txdName, this.fetchTXD(dataFetcher, obj.txdName, textures)); + if (!loadedDFF.has(obj.modelName)) + loadedDFF.set(obj.modelName, this.fetchDFF(dataFetcher, obj.modelName, clump => modelCache.addModel(clump, obj))); + promises.push(loadedTXD.get(obj.txdName)!, loadedDFF.get(obj.modelName)!); } - await Promise.all(promises); - - const layerTextures = new Map(); - const layerMeshes: MeshInstance[] = []; - for (const [item, obj] of items) { - const model = modelCache.meshData.get(item.modelName); - if (model === undefined) { - console.warn('Missing model', item.modelName); - continue; - } - for (const frag of model.meshFragData) { - if (frag.texName === undefined) continue; - const texture = textures.get(frag.texName); - if (texture === undefined) { - console.warn('Missing texture', frag.texName, 'for', item.modelName); - } else { - let res = texture.width + 'x' + texture.height; - if (rw.Raster.formatHasAlpha(texture.format)) - res += 'alpha'; - if (!layerTextures.has(res)) layerTextures.set(res, []); - layerTextures.get(res)!.push(texture); + const promise = Promise.all(promises).then(() => { + const layerTextures = new Map>(); + const layerMeshes: MeshInstance[] = []; + for (const [item, obj] of items) { + const model = modelCache.meshData.get(item.modelName); + if (model === undefined) { + console.warn('Missing model', item.modelName); + continue; } + for (const frag of model) { + if (frag.texName === undefined) continue; + const texture = textures.get(frag.texName); + if (texture === undefined) { + console.warn('Missing texture', frag.texName, 'for', item.modelName); + } else { + let res = texture.width + 'x' + texture.height; + if (rw.Raster.formatHasAlpha(texture.format)) + res += 'alpha'; + if (!layerTextures.has(res)) layerTextures.set(res, new Set()); + layerTextures.get(res)!.add(texture); + } + } + layerMeshes.push(new MeshInstance(model, item)); } - layerMeshes.push(new MeshInstance(model, item)); - } - for (const [res, textures] of layerTextures) { - const key = Object.assign({}, drawKey); - if (res.endsWith('alpha')) - key.renderLayer = GfxRendererLayer.TRANSLUCENT; - const atlas = (textures.length > 0) ? new TextureAtlas(device, textures) : undefined; - const sceneRenderer = new SceneRenderer(device, key, layerMeshes, atlas); - renderer.sceneRenderers.push(sceneRenderer); - } - })(); + for (const [res, textures] of layerTextures) { + const key = Object.assign({}, drawKey); + if (res.endsWith('alpha')) + key.renderLayer = GfxRendererLayer.TRANSLUCENT; + const atlas = (textures.size > 0) ? new TextureArray(device, Array.from(textures)) : undefined; + const sceneRenderer = new SceneRenderer(device, key, layerMeshes, atlas); + renderer.sceneRenderers.push(sceneRenderer); + } + }); + if (this.complete) + await promise; + } + + if (this.complete) + this.assets.clear(); return renderer; } diff --git a/src/GrandTheftAuto3/time.ts b/src/GrandTheftAuto3/time.ts index 087b7381..76809100 100644 --- a/src/GrandTheftAuto3/time.ts +++ b/src/GrandTheftAuto3/time.ts @@ -1,4 +1,5 @@ -import { Color, colorNew } from '../Color'; +import { Color, colorNew, colorLerp, colorNewCopy, White } from '../Color'; +import { lerp } from '../MathHelpers'; function colorNorm(r: number, g: number, b: number, a: number = 255.0): Color { return colorNew(r/255.0, g/255.0, b/255.0, a/255.0); @@ -71,3 +72,54 @@ export async function parseTimeCycle(text: string) { } return sets; } + +export function emptyColorSet(): ColorSet { + return { + amb: colorNewCopy(White), + dir: colorNewCopy(White), + skyTop: colorNewCopy(White), + skyBot: colorNewCopy(White), + + sunCore: colorNewCopy(White), + sunCorona: colorNewCopy(White), + sunSz: 0, + sprSz: 0, + sprBght: 0, + shad: 0, + lightShad: 0, + treeShad: 0, + farClp: 0, + fogSt: 0, + lightGnd: 0, + + cloud: colorNewCopy(White), + fluffyTop: colorNewCopy(White), + fluffyBot: colorNewCopy(White), + blur: colorNewCopy(White), + }; +} + +export function lerpColorSet(dst: ColorSet, a: ColorSet, b: ColorSet, t: number) { + colorLerp(dst.amb, a.amb, b.amb, t); + colorLerp(dst.dir, a.dir, b.dir, t); + colorLerp(dst.skyTop, a.skyTop, b.skyTop, t); + colorLerp(dst.skyBot, a.skyBot, b.skyBot, t); + + colorLerp(dst.sunCore, a.sunCore, b.sunCore, t); + colorLerp(dst.sunCorona, a.sunCorona, b.sunCorona, t); + + dst.sunSz = lerp(a.sunSz, b.sunSz, t); + dst.sprSz = lerp(a.sprSz, b.sprSz, t); + dst.sprBght = lerp(a.sprBght, b.sprBght, t); + dst.shad = lerp(a.shad, b.shad, t); + dst.lightShad = lerp(a.lightShad, b.lightShad, t); + dst.treeShad = lerp(a.treeShad, b.treeShad, t); + dst.farClp = lerp(a.farClp, b.farClp, t); + dst.fogSt = lerp(a.fogSt, b.fogSt, t); + dst.lightGnd = lerp(a.lightGnd, b.lightGnd, t); + + colorLerp(dst.cloud, a.cloud, b.cloud, t); + colorLerp(dst.fluffyTop, a.fluffyTop, b.fluffyTop, t); + colorLerp(dst.fluffyBot, a.fluffyBot, b.fluffyBot, t); + colorLerp(dst.blur, a.blur, b.blur, t); +} diff --git a/src/GrandTheftAuto3/water.ts b/src/GrandTheftAuto3/water.ts new file mode 100644 index 00000000..ab02ed05 --- /dev/null +++ b/src/GrandTheftAuto3/water.ts @@ -0,0 +1,66 @@ + +import { vec3, vec2, vec4, quat } from 'gl-matrix'; +import { OpaqueBlack } from '../Color'; +import { ItemPlacement, ItemInstance, ObjectDefinition } from './item'; + +export function parseWaterPro(view: DataView, bounds = vec4.fromValues(-2048, -2048, 2048, 2048)): ItemPlacement { + const numLevels = view.getInt32(0, true); + const heights: number[] = []; + for (let i = 0; i < numLevels; i++) { + heights.push(view.getFloat32(0x4 + i * 0x4, true)); + } + const instances: ItemInstance[] = []; + const offs = 0x4 + 48 * 0x4 + 48 * 0x10 + 64 * 64; + const width = (bounds[2] - bounds[0]) / 128; + const height = (bounds[3] - bounds[1]) / 128; + const scale = vec3.fromValues(width, height, 1); + const rotation = quat.identity(quat.create()); + for (let i = 0; i < 128; i++) { + for (let j = 0; j < 128; j++) { + const level = view.getUint8(offs + 128 * i + j); + if (level & 0x80) continue; + instances.push({ + modelName: 'water', + translation: vec3.fromValues( + i * width + bounds[0], + j * height + bounds[1], + heights[level] + ), + scale, rotation + }); + } + } + return { instances }; +} + +export const waterDefinition: ObjectDefinition = { + modelName: 'water', + txdName: 'particle', + drawDistance: 1000, + flags: 0, + tobj: false, + dynamic: true +}; + +const squarePositions = [ + vec3.fromValues(0,0,0), + vec3.fromValues(0,1,0), + vec3.fromValues(1,1,0), + vec3.fromValues(1,0,0), +]; + +const squareTexCoords = [ + vec2.fromValues(0,0), + vec2.fromValues(0,1), + vec2.fromValues(1,1), + vec2.fromValues(1,0), +]; + +export const waterMeshFragData = { + texName: 'particle/water_old', + indices: new Uint16Array([0,1,2,0,2,3]), + vertices: 4, + position: (i: number) => squarePositions[i], + texCoord: (i: number) => squareTexCoords[i], + color: (i: number) => OpaqueBlack, +}; diff --git a/yarn.lock b/yarn.lock index 117daac8..888de977 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3166,10 +3166,10 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -librw@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/librw/-/librw-0.1.0.tgz#d1838acdfe55dc15e68a950f3d0fd252b47fa8b6" - integrity sha512-y9NeVRs9uDgJtZ/mvduN0iWPbznmWjHe21/0VQnqigwcR3XWA0+eqtaFpvTXzjepy07Up9YkKpHxgMPh7T/bOQ== +librw@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/librw/-/librw-0.2.0.tgz#179cd8b9d731f4e578cae8c1e16978740ec825a5" + integrity sha512-PF5s1PqoeTZiYn7d9tXQFcdEqg2RByOkY9uR0mmXcRtdxzn6SyWJEB/0gtE0JFHUcS96cyRcSd64zxwEVEZUUQ== lodash.clone@^4.5.0: version "4.5.0"