GrandTheftAuto3: water and sky (#148)

* GrandTheftAuto3: water

* GrandTheftAuto3: ocean plane

* GrandTheftAuto3: sky rendering

* GrandTheftAuto3: fetch IMG for complete map
This commit is contained in:
David A Roberts
2019-10-14 02:07:30 +10:00
committed by Jasper St. Pierre
parent 6ee20affdf
commit 4ebdc794ad
8 changed files with 467 additions and 177 deletions
+1 -1
View File
@@ -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": {
+5 -3
View File
@@ -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;
+40 -2
View File
@@ -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
+155 -85
View File
@@ -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<string, number>();
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<GfxMegaStateDescriptor>;
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<string, MeshFragData[]>();
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<string, MeshData>();
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<GfxMegaStateDescriptor> = {
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();
+143 -81
View File
@@ -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<string, ArrayBufferSlice>();
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<ItemDefinition> {
const buffer = await dataFetcher.fetchData(`${pathBase}/data/maps/${id}.ide`);
const text = getTextDecoder('utf8')!.decode(buffer.arrayBuffer);
private async fetchIMG(dataFetcher: DataFetcher): Promise<void> {
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<ArrayBufferSlice> {
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<ItemDefinition> {
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<ItemPlacement> {
private async fetchIPL(dataFetcher: DataFetcher, id: string): Promise<ItemPlacement> {
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<ColorSet[]> {
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<Map<string, AABB>> {
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<ItemPlacement> {
const buffer = await this.fetch(dataFetcher, `${pathBase}/data/waterpro.dat`);
return parseWaterPro(buffer.createDataView());
}
private async fetchTXD(dataFetcher: DataFetcher, txdName: string, textures: Map<string, Texture>): Promise<void> {
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<void> {
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<Viewer.SceneGfx> {
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<string, DrawKey>();
const layers = new Map<DrawKey, [ItemInstance, ObjectDefinition][]>();
@@ -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<string, Promise<void>>();
const loadedDFF = new Map<string, Promise<void>>();
const textures = new Map<string, Texture>();
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<void>[] = [];
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<string, Texture[]>();
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<string, Set<Texture>>();
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;
}
+53 -1
View File
@@ -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);
}
+66
View File
@@ -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,
};
+4 -4
View File
@@ -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"