diff --git a/bun.lockb b/bun.lockb index 2cae2db..90d5ec2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7c6dbbf..e15f9d5 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/bun": "latest", "@types/node-forge": "^1.3.11", "@types/uglify-js": "^3.17.5", + "@webgpu/types": "^0.1.59", "axios": "^1.7.9", "javascript-obfuscator": "^4.1.1", "node-forge": "^1.3.1", @@ -22,5 +23,10 @@ }, "peerDependencies": { "typescript": "^5.0.0" + }, + "dependencies": { + "@types/stats.js": "^0.17.3", + "gl-matrix": "^3.4.3", + "stats.js": "^0.17.0" } } diff --git a/src/3rdparty/MobileKeyboard.ts b/src/3rdparty/MobileKeyboard.ts index 7118eef..3e6940e 100644 --- a/src/3rdparty/MobileKeyboard.ts +++ b/src/3rdparty/MobileKeyboard.ts @@ -1,4 +1,4 @@ -import { canvas, canvas2d } from "#/graphics/Canvas"; +import { canvasOverlay, canvas2d } from "#/graphics/Canvas"; // ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"£$%^&*()-_=+[{]};:\'@#~,<.>/?\\| // ^ Allowed characters in client @@ -435,8 +435,8 @@ class CanvasMobileKeyboard implements Keyboard { key: char, code: char, }) - canvas.dispatchEvent(downEvent); - canvas.dispatchEvent(upEvent); + canvasOverlay.dispatchEvent(downEvent); + canvasOverlay.dispatchEvent(upEvent); if (!this.animateBoxTimeout) { if (index >= 30 && index <= 35) { this.animateBoxIndex = 30; @@ -492,7 +492,7 @@ class CanvasMobileKeyboard implements Keyboard { this.startX = newStartX; this.startY = newStartY; // Focus event forces a re-draw of canvas - canvas.dispatchEvent(new FocusEvent('focus')); + canvasOverlay.dispatchEvent(new FocusEvent('focus')); } } } @@ -510,7 +510,7 @@ class NativeMobileKeyboard implements Keyboard { this.virtualInputElement.setAttribute('autofocus', 'autofocus'); this.virtualInputElement.setAttribute('spellcheck', 'false'); this.virtualInputElement.setAttribute('autocomplete', 'off'); - this.virtualInputElement.setAttribute('style', 'position: fixed; top: 0px; left: 0px; width: 1px; height: 1px; opacity: 0;'); + this.virtualInputElement.setAttribute('style', `position: fixed; top: 0px; left: 0px; width: 1px; height: 1px; opacity: 0; z-index: 20;`); if (this.isAndroid) { // Android uses `input` event for text entry rathern than `keydown` / `keyup` @@ -528,27 +528,27 @@ class NativeMobileKeyboard implements Keyboard { return; } - canvas.dispatchEvent(new KeyboardEvent('keydown', { key: data, code: data })); - canvas.dispatchEvent(new KeyboardEvent('keyup', { key: data, code: data })); + canvasOverlay.dispatchEvent(new KeyboardEvent('keydown', { key: data, code: data })); + canvasOverlay.dispatchEvent(new KeyboardEvent('keyup', { key: data, code: data })); }); this.virtualInputElement.addEventListener('keydown', (ev: KeyboardEvent) => { if (ev.key === 'Enter' || ev.key === 'Backspace') { - canvas.dispatchEvent(new KeyboardEvent('keydown', { key: ev.key, code: ev.key })); + canvasOverlay.dispatchEvent(new KeyboardEvent('keydown', { key: ev.key, code: ev.key })); } }); this.virtualInputElement.addEventListener('keyup', (ev: KeyboardEvent) => { if (ev.key === 'Enter' || ev.key === 'Backspace') { - canvas.dispatchEvent(new KeyboardEvent('keyup', { key: ev.key, code: ev.key })); + canvasOverlay.dispatchEvent(new KeyboardEvent('keyup', { key: ev.key, code: ev.key })); } }); } else { // Non-android can use `keydown` / `keyup` directly this.virtualInputElement.addEventListener('keydown', (ev: KeyboardEvent) => { - canvas.dispatchEvent(new KeyboardEvent('keydown', { key: ev.key, code: ev.key })); + canvasOverlay.dispatchEvent(new KeyboardEvent('keydown', { key: ev.key, code: ev.key })); }); this.virtualInputElement.addEventListener('keyup', (ev: KeyboardEvent) => { - canvas.dispatchEvent(new KeyboardEvent('keyup', { key: ev.key, code: ev.key })); + canvasOverlay.dispatchEvent(new KeyboardEvent('keyup', { key: ev.key, code: ev.key })); }); } document.body.appendChild(this.virtualInputElement); @@ -562,7 +562,7 @@ class NativeMobileKeyboard implements Keyboard { this.virtualInputElement.style.left = `${originX}px`; this.virtualInputElement.style.top = `${originY}px`; } - canvas.blur(); + canvasOverlay.blur(); this.virtualInputElement.focus(); this.virtualInputElement.click(); this.displayed = true; @@ -570,7 +570,7 @@ class NativeMobileKeyboard implements Keyboard { hide(): void { // Blur the virtual input field this.virtualInputElement.blur(); - canvas.focus(); + canvasOverlay.focus(); this.displayed = false; } isDisplayed(): boolean { diff --git a/src/client/Client.ts b/src/client/Client.ts index 8ea85f7..fa6a229 100644 --- a/src/client/Client.ts +++ b/src/client/Client.ts @@ -43,7 +43,7 @@ import { downloadUrl, sleep, arraycopy } from '#/util/JsUtil.js'; import AnimBase from '#/graphics/AnimBase.js'; import AnimFrame from '#/graphics/AnimFrame.js'; -import { canvas2d } from '#/graphics/Canvas.js'; +import { canvas2d, canvasContainer } from '#/graphics/Canvas.js'; import { Colors } from '#/graphics/Colors.js'; import Pix2D from '#/graphics/Pix2D.js'; import Pix3D from '#/graphics/Pix3D.js'; @@ -53,6 +53,10 @@ import Pix24 from '#/graphics/Pix24.js'; import PixFont from '#/graphics/PixFont.js'; import PixMap from '#/graphics/PixMap.js'; +import { Renderer } from '#/graphics/renderer/Renderer.js'; +import { RendererWebGPU } from '#/graphics/renderer/webgpu/RendererWebGPU.js'; +import { RendererWebGL } from '#/graphics/renderer/webgl/RendererWebGL.js'; + import ClientStream from '#/io/ClientStream.js'; import { ClientProt } from '#/io/ClientProt.js'; import Database from '#/io/Database.js'; @@ -65,12 +69,18 @@ import WordFilter from '#/wordenc/WordFilter.js'; import WordPack from '#/wordenc/WordPack.js'; import Wave from '#/sound/Wave.js'; +import { RendererWebGLC } from '#/graphics/renderer/webgl/RendererWebGLC.ts'; +import { PacketType } from '#/io/PacketType.ts'; +import { TypecodeEntity } from '#/dash3d/TypecodeEntity.ts'; +import { TileFlag } from '#/dash3d/TileFlag.ts'; +import { MouseButton } from '#/client/MouseButton.ts'; const enum Constants { CLIENT_VERSION = 225, MAX_CHATS = 50, MAX_PLAYER_COUNT = 2048, - LOCAL_PLAYER_INDEX = 2047 + MAX_NPC_COUNT = 8192, + LOCAL_PLAYER_INDEX = 2047, } export class Client extends GameShell { @@ -106,9 +116,9 @@ export class Client extends GameShell { private loopCycle: number = 0; private archiveChecksums: number[] = []; private netStream: ClientStream | null = null; - private in: Packet = Packet.alloc(1); - private out: Packet = Packet.alloc(1); - private loginout: Packet = Packet.alloc(1); + private in: Packet = Packet.alloc(PacketType.TYPE_5KB); + private out: Packet = Packet.alloc(PacketType.TYPE_5KB); + private loginout: Packet = Packet.alloc(PacketType.TYPE_5KB); private serverSeed: bigint = 0n; private idleNetCycles: number = 0; private idleTimeout: number = 0; @@ -150,8 +160,8 @@ export class Client extends GameShell { private fontQuill8: PixFont | null = null; // login screen pillar flames properties + private flameNext: number = 0; private imageRunes: Pix8[] = []; - private flameActive: boolean = false; private imageFlamesLeft: Pix24 | null = null; private imageFlamesRight: Pix24 | null = null; private flameBuffer1: Int32Array | null = null; @@ -166,7 +176,6 @@ export class Client extends GameShell { private flameCycle0: number = 0; private flameGradientCycle0: number = 0; private flameGradientCycle1: number = 0; - private flamesInterval: Timer | null = null; // game world properties private areaSidebar: PixMap | null = null; @@ -419,9 +428,9 @@ export class Client extends GameShell { private entityUpdateIds: Int32Array = new Int32Array(Constants.MAX_PLAYER_COUNT); private entityRemovalIds: Int32Array = new Int32Array(1000); private playerAppearanceBuffer: (Packet | null)[] = new TypedArray1d(Constants.MAX_PLAYER_COUNT, null); - private npcs: (NpcEntity | null)[] = new TypedArray1d(8192, null); + private npcs: (NpcEntity | null)[] = new TypedArray1d(Constants.MAX_NPC_COUNT, null); private npcCount: number = 0; - private npcIds: Int32Array = new Int32Array(8192); + private npcIds: Int32Array = new Int32Array(Constants.MAX_NPC_COUNT); private projectiles: LinkList = new LinkList(); private spotanims: LinkList = new LinkList(); private locList: LinkList = new LinkList(); @@ -485,8 +494,6 @@ export class Client extends GameShell { private midiSize: number = 0; private midiVolume: number = 64; - private displayFps: boolean = false; - static setHighMemory(): void { World3D.lowMemory = false; Pix3D.lowMemory = false; @@ -555,11 +562,6 @@ export class Client extends GameShell { // ---- private unloadTitle(): void { - this.flameActive = false; - if (this.flamesInterval) { - clearInterval(this.flamesInterval); - this.flamesInterval = null; - } this.imageTitlebox = null; this.imageTitlebutton = null; this.imageRunes = []; @@ -642,14 +644,13 @@ export class Client extends GameShell { } private drawError(): void { + Renderer.resetRenderer(); canvas2d.fillStyle = 'black'; canvas2d.fillRect(0, 0, this.width, this.height); - this.setFramerate(1); + this.setUpdateRate(1); - this.flameActive = false; let y: number = 35; - if (this.errorLoading) { canvas2d.font = 'bold 16px helvetica, sans-serif'; canvas2d.textAlign = 'left'; @@ -1413,11 +1414,6 @@ export class Client extends GameShell { } async load() { - if (this.isMobile && Client.lowMemory) { - // force mobile on low detail mode to 30 fps - this.setTargetedFramerate(30); - } - if (this.alreadyStarted) { this.errorStarted = true; return; @@ -1684,17 +1680,37 @@ export class Client extends GameShell { this.errorMessage = err.message; } } + + // todo: enable GPU support automatically when we're ready + // try { + // if (RendererWebGPU.hasWebGPUSupport()) { + // Renderer.renderer = await RendererWebGPU.init(canvasContainer, this.width, this.height); + // } + // if (!Renderer.renderer) { + // Renderer.renderer = RendererWebGLC.init(canvasContainer, this.width, this.height); + // } + // } catch (err) { + // console.error(err); + // } } - async update() { + async update(now: number) { if (this.errorStarted || this.errorLoading || this.errorHost) { return; } + this.loopCycle++; + if (this.ingame) { await this.updateGame(); } else { await this.updateTitleScreen(); + + if (now >= this.flameNext) { + this.updateFlames(); + this.updateFlames(); + this.flameNext = now + 35; + } } } @@ -1704,11 +1720,14 @@ export class Client extends GameShell { return; } + Renderer.startFrame(); if (this.ingame) { this.drawGame(); } else { await this.drawTitleScreen(); + this.drawFlames(); } + Renderer.endFrame(); this.dragCycles = 0; } @@ -1745,10 +1764,8 @@ export class Client extends GameShell { if (this.redrawTitleBackground) { this.redrawTitleBackground = false; - if (!this.flameActive) { - this.imageTitle0?.draw(0, 0); - this.imageTitle1?.draw(661, 0); - } + this.imageTitle0?.draw(0, 0); + this.imageTitle1?.draw(661, 0); this.imageTitle2?.draw(128, 0); this.imageTitle3?.draw(214, 386); this.imageTitle5?.draw(0, 265); @@ -1757,16 +1774,10 @@ export class Client extends GameShell { this.imageTitle8?.draw(574, 186); } - await sleep(5); // return a slice of time to the main loop so it can update the progress bar - } - - runFlames(): void { - if (!this.flameActive) { - return; - } this.updateFlames(); this.updateFlames(); this.drawFlames(); + await sleep(0); // return a slice of time to the main loop so it can update the progress bar } private async loadTitle(): Promise { @@ -1992,12 +2003,7 @@ export class Client extends GameShell { this.flameBuffer3 = new Int32Array(32768); this.flameBuffer2 = new Int32Array(32768); - this.showProgress(10, 'Connecting to fileserver').then((): void => { - if (!this.flameActive) { - this.flameActive = true; - this.flamesInterval = setInterval(this.runFlames.bind(this), 35); - } - }); + this.showProgress(10, 'Connecting to fileserver'); } private async updateTitleScreen(): Promise { @@ -2006,13 +2012,13 @@ export class Client extends GameShell { let y: number = ((this.height / 2) | 0) + 20; y += 20; - if (this.mouseClickButton === 1 && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { + if (this.mouseClickButton === MouseButton.LEFT && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { this.titleScreenState = 3; this.titleLoginField = 0; } x = ((this.width / 2) | 0) + 80; - if (this.mouseClickButton === 1 && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { + if (this.mouseClickButton === MouseButton.LEFT && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { this.loginMessage0 = ''; this.loginMessage1 = 'Enter your username & password.'; this.titleScreenState = 2; @@ -2023,12 +2029,12 @@ export class Client extends GameShell { y += 30; y += 25; - if (this.mouseClickButton === 1 && this.mouseClickY >= y - 15 && this.mouseClickY < y) { + if (this.mouseClickButton === MouseButton.LEFT && this.mouseClickY >= y - 15 && this.mouseClickY < y) { this.titleLoginField = 0; } y += 15; - if (this.mouseClickButton === 1 && this.mouseClickY >= y - 15 && this.mouseClickY < y) { + if (this.mouseClickButton === MouseButton.LEFT && this.mouseClickY >= y - 15 && this.mouseClickY < y) { this.titleLoginField = 1; } // y += 15; dead code @@ -2037,12 +2043,12 @@ export class Client extends GameShell { let buttonY: number = ((this.height / 2) | 0) + 50; buttonY += 20; - if (this.mouseClickButton === 1 && this.mouseClickX >= buttonX - 75 && this.mouseClickX <= buttonX + 75 && this.mouseClickY >= buttonY - 20 && this.mouseClickY <= buttonY + 20) { + if (this.mouseClickButton === MouseButton.LEFT && this.mouseClickX >= buttonX - 75 && this.mouseClickX <= buttonX + 75 && this.mouseClickY >= buttonY - 20 && this.mouseClickY <= buttonY + 20) { await this.tryLogin(this.usernameInput, this.passwordInput, false); } buttonX = ((this.width / 2) | 0) + 80; - if (this.mouseClickButton === 1 && this.mouseClickX >= buttonX - 75 && this.mouseClickX <= buttonX + 75 && this.mouseClickY >= buttonY - 20 && this.mouseClickY <= buttonY + 20) { + if (this.mouseClickButton === MouseButton.LEFT && this.mouseClickX >= buttonX - 75 && this.mouseClickX <= buttonX + 75 && this.mouseClickY >= buttonY - 20 && this.mouseClickY <= buttonY + 20) { this.titleScreenState = 0; this.usernameInput = ''; this.passwordInput = ''; @@ -2102,7 +2108,7 @@ export class Client extends GameShell { let y: number = ((this.height / 2) | 0) + 50; y += 20; - if (this.mouseClickButton === 1 && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { + if (this.mouseClickButton === MouseButton.LEFT && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { this.titleScreenState = 0; } } @@ -2286,7 +2292,7 @@ export class Client extends GameShell { this.playerAppearanceBuffer[i] = null; } - for (let i: number = 0; i < 8192; i++) { + for (let i: number = 0; i < Constants.MAX_NPC_COUNT; i++) { this.npcs[i] = null; } @@ -2553,7 +2559,7 @@ export class Client extends GameShell { this.objGrabThreshold = true; } - if (this.mouseButton === 0) { + if (this.mouseButton === MouseButton.NONE) { if (this.objDragArea === 2) { this.redrawSidebar = true; } @@ -2591,7 +2597,7 @@ export class Client extends GameShell { } this.selectedCycle = 10; - this.mouseClickButton = 0; + this.mouseClickButton = MouseButton.NONE; } } @@ -2618,10 +2624,10 @@ export class Client extends GameShell { } } - if (this.mouseClickButton === 1 && this.modalMessage) { + if (this.mouseClickButton === MouseButton.LEFT && this.modalMessage) { this.modalMessage = null; this.redrawChatback = true; - this.mouseClickButton = 0; + this.mouseClickButton = MouseButton.NONE; } await this.handleMouseInput(); // this is because of varps that set midi that we have to wait... @@ -2629,7 +2635,7 @@ export class Client extends GameShell { this.handleTabInput(); this.handleChatSettingsInput(); - if (this.mouseButton === 1 || this.mouseClickButton === 1) { + if (this.mouseButton === MouseButton.LEFT || this.mouseClickButton === MouseButton.LEFT) { this.dragCycles++; } @@ -2805,7 +2811,7 @@ export class Client extends GameShell { loc.duration--; } - if (loc.duration != 0) { + if (loc.duration !== 0) { if (loc.delay > 0) { loc.delay--; } @@ -3271,8 +3277,12 @@ export class Client extends GameShell { Model.pickedCount = 0; Model.mouseX = this.mouseX - 8; Model.mouseY = this.mouseY - 11; - Pix2D.clear(); + + Pix2D.clear(Renderer.getSceneClearColor()); + Renderer.startRenderScene(); this.scene?.draw(this.cameraX, this.cameraY, this.cameraZ, level, this.cameraYaw, this.cameraPitch, this.loopCycle); + Renderer.endRenderScene(); + this.scene?.clearTemporaryLocs(); this.draw2DEntityElements(); this.drawTileHint(); @@ -3575,29 +3585,6 @@ export class Client extends GameShell { this.fontPlain12?.drawStringCenter(484, 329, 'Arena', Colors.YELLOW); } - if (this.displayFps) { - let x: number = 507; - let y: number = 20; - - let color: number = Colors.YELLOW; - if (this.fps < 15) { - color = Colors.RED; - } - - this.fontPlain12?.drawStringRight(x, y, 'Fps:' + this.fps, color); - y += 15; - - let memoryUsage = -1; - if (typeof window.performance['memory' as keyof Performance] !== 'undefined') { - const memory = window.performance['memory' as keyof Performance] as any; - memoryUsage = (memory.usedJSHeapSize / 1024) | 0; - } - - if (memoryUsage !== -1) { - this.fontPlain12?.drawStringRight(x, y, 'Mem:' + memoryUsage + 'k', Colors.YELLOW); - } - } - if (this.systemUpdateTimer !== 0) { let seconds: number = (this.systemUpdateTimer / 50) | 0; const minutes: number = (seconds / 60) | 0; @@ -3953,11 +3940,11 @@ export class Client extends GameShell { let offset: number = (CollisionConstants.SIZE - 1 - z) * 512 * 4 + 24628; for (let x: number = 1; x < CollisionConstants.SIZE - 1; x++) { - if (this.levelTileFlags && (this.levelTileFlags[level][x][z] & 0x18) === 0) { + if (this.levelTileFlags && (this.levelTileFlags[level][x][z] & (TileFlag.WALL | TileFlag.LOWMEMORY)) === TileFlag.OPEN) { this.scene?.drawMinimapTile(level, x, z, pixels, offset, 512); } - if (level < 3 && this.levelTileFlags && (this.levelTileFlags[level + 1][x][z] & 0x8) !== 0) { + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[level + 1][x][z] & TileFlag.WALL) !== TileFlag.OPEN) { this.scene?.drawMinimapTile(level + 1, x, z, pixels, offset, 512); } @@ -3972,11 +3959,11 @@ export class Client extends GameShell { for (let z: number = 1; z < CollisionConstants.SIZE - 1; z++) { for (let x: number = 1; x < CollisionConstants.SIZE - 1; x++) { - if (this.levelTileFlags && (this.levelTileFlags[level][x][z] & 0x18) === 0) { + if (this.levelTileFlags && (this.levelTileFlags[level][x][z] & (TileFlag.WALL | TileFlag.LOWMEMORY)) === TileFlag.OPEN) { this.drawMinimapLoc(x, z, level, wallRgb, doorRgb); } - if (level < 3 && this.levelTileFlags && (this.levelTileFlags[level + 1][x][z] & 0x8) !== 0) { + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[level + 1][x][z] & TileFlag.WALL) !== TileFlag.OPEN) { this.drawMinimapLoc(x, z, level + 1, wallRgb, doorRgb); } } @@ -4247,11 +4234,11 @@ export class Client extends GameShell { let button: number = this.mouseClickButton; if (this.spellSelected === 1 && this.mouseClickX >= 520 && this.mouseClickY >= 165 && this.mouseClickX <= 788 && this.mouseClickY <= 230) { - button = 0; + button = MouseButton.NONE; } if (this.menuVisible) { - if (button !== 1) { + if (button !== MouseButton.LEFT) { let x: number = this.mouseX; let y: number = this.mouseY; @@ -4277,7 +4264,7 @@ export class Client extends GameShell { } } - if (button === 1) { + if (button === MouseButton.LEFT) { const menuX: number = this.menuX; const menuY: number = this.menuY; const menuWidth: number = this.menuWidth; @@ -4316,7 +4303,7 @@ export class Client extends GameShell { } } } else { - if (button === 1 && this.menuSize > 0) { + if (button === MouseButton.LEFT && this.menuSize > 0) { const action: number = this.menuAction[this.menuSize - 1]; if (action === 602 || action === 596 || action === 22 || action === 892 || action === 415 || action === 405 || action === 38 || action === 422 || action === 478 || action === 347 || action === 188) { @@ -4346,15 +4333,15 @@ export class Client extends GameShell { } } - if (button === 1 && (this.mouseButtonsOption === 1 || this.isAddFriendOption(this.menuSize - 1)) && this.menuSize > 2) { - button = 2; + if (button === MouseButton.LEFT && (this.mouseButtonsOption === 1 || this.isAddFriendOption(this.menuSize - 1)) && this.menuSize > 2) { + button = MouseButton.RIGHT; } - if (button === 1 && this.menuSize > 0) { + if (button === MouseButton.LEFT && this.menuSize > 0) { await this.useMenuOption(this.menuSize - 1); } - if (button !== 2 || this.menuSize <= 0) { + if (button !== MouseButton.RIGHT || this.menuSize <= 0) { return; } @@ -4363,7 +4350,7 @@ export class Client extends GameShell { } handleMinimapInput(): void { - if (this.mouseClickButton === 1 && this.localPlayer) { + if (this.mouseClickButton === MouseButton.LEFT && this.localPlayer) { let x: number = this.mouseClickX - 21 - 561; let y: number = this.mouseClickY - 9 - 5; @@ -5224,7 +5211,7 @@ export class Client extends GameShell { } private handleTabInput(): void { - if (this.mouseClickButton === 1) { + if (this.mouseClickButton === MouseButton.LEFT) { if (this.mouseClickX >= 549 && this.mouseClickX <= 583 && this.mouseClickY >= 195 && this.mouseClickY < 231 && this.tabInterfaceId[0] !== -1) { this.redrawSidebar = true; this.selectedTab = 0; @@ -5412,17 +5399,69 @@ export class Client extends GameShell { if ((key === 13 || key === 10) && this.chatTyped.length > 0) { if (this.chatTyped.startsWith('::')) { if (this.chatTyped === '::fpson') { - // authentic in later revs - this.displayFps = true; + // authentic command in later revs + this.drawStats.dom.style.display = 'block'; + this.updateStats.dom.style.display = 'block'; } else if (this.chatTyped === '::fpsoff') { - // authentic in later revs - this.displayFps = false; - } else if (this.chatTyped.startsWith('::fps ')) { - // custom ::fps command for setting a target framerate + // authentic command in later revs + this.drawStats.dom.style.display = 'none'; + this.updateStats.dom.style.display = 'none'; + } else if (this.chatTyped === '::tk0') { + // CPU renderer + if (Renderer.renderer) { + Renderer.resetRenderer(); + this.redrawAll(); + } + } else if (this.chatTyped === '::tk1') { + // WebGPU renderer (1:1 - not widespread yet) + try { + Renderer.renderer = await RendererWebGPU.init(canvasContainer, this.width, this.height); + this.redrawAll(); + + if (!Renderer.renderer) { + this.addMessage(0, 'Failed to change renderer', ''); + } + } catch (e) { + if (e instanceof Error) { + this.addMessage(0, 'Error enabling renderer: ' + e.message, ''); + } + + console.error('Failed enabling renderer', e); + } + } else if (this.chatTyped === '::tk2') { + // WebGL renderer (working towards 1:1 rasterizing in fragment shaders) try { - const desiredFps = parseInt(this.chatTyped.substring(6)) || 50; - this.setTargetedFramerate(desiredFps); - } catch (e) { } + Renderer.renderer = RendererWebGL.init(canvasContainer, this.width, this.height); + this.redrawAll(); + + if (!Renderer.renderer) { + this.addMessage(0, 'Failed to change renderer', ''); + } + } catch (e) { + if (e instanceof Error) { + this.addMessage(0, 'Error enabling renderer: ' + e.message, ''); + } + + console.error('Failed enabling renderer', e); + } + } else if (this.chatTyped === '::tk3') { + // WebGL renderer (not 1:1 - closer to typical GL rendering) + try { + Renderer.renderer = RendererWebGLC.init(canvasContainer, this.width, this.height); + RendererWebGLC.onSceneLoaded(this.scene); + RendererWebGLC.setBrightness(0.8); // todo: preserve brightness + this.redrawAll(); + + if (!Renderer.renderer) { + this.addMessage(0, 'Failed to change renderer', ''); + } + } catch (e) { + if (e instanceof Error) { + this.addMessage(0, 'Error enabling renderer: ' + e.message, ''); + } + + console.error('Failed enabling renderer', e); + } } else { this.out.p1isaac(ClientProt.CLIENT_CHEAT); this.out.p1(this.chatTyped.length - 1); @@ -5521,7 +5560,7 @@ export class Client extends GameShell { } private handleChatSettingsInput(): void { - if (this.mouseClickButton === 1) { + if (this.mouseClickButton === MouseButton.LEFT) { if (this.mouseClickX >= 8 && this.mouseClickX <= 108 && this.mouseClickY >= 490 && this.mouseClickY <= 522) { this.publicChatSetting = (this.publicChatSetting + 1) % 4; this.redrawPrivacySettings = true; @@ -5617,7 +5656,7 @@ export class Client extends GameShell { Pix2D.clear(); this.imageMapback?.draw(0, 0); this.areaSidebar = new PixMap(190, 261); - this.areaViewport = new PixMap(512, 334); + this.areaViewport = RendererWebGLC.areaViewport = new PixMap(512, 334); Pix2D.clear(); this.areaBackbase1 = new PixMap(501, 61); this.areaBackbase2 = new PixMap(288, 40); @@ -5878,7 +5917,7 @@ export class Client extends GameShell { if (id >= 0) { let tileLevel: number = level; - if (this.levelTileFlags && level < 3 && (this.levelTileFlags[1][x][z] & 0x2) === 2) { + if (this.levelTileFlags && level < 3 && (this.levelTileFlags[1][x][z] & TileFlag.BRIDGE) === TileFlag.BRIDGE) { tileLevel = level + 1; } @@ -6204,7 +6243,7 @@ export class Client extends GameShell { this.scenePrevBaseTileX = this.sceneBaseTileX; this.scenePrevBaseTileZ = this.sceneBaseTileZ; - for (let i: number = 0; i < 8192; i++) { + for (let i: number = 0; i < Constants.MAX_NPC_COUNT; i++) { const npc: NpcEntity | null = this.npcs[i]; if (npc) { for (let j: number = 0; j < 10; j++) { @@ -7230,15 +7269,17 @@ export class Client extends GameShell { this.out.p1isaac(ClientProt.NO_TIMEOUT); for (let loc: LocEntity | null = this.locList.head() as LocEntity | null; loc; loc = this.locList.next() as LocEntity | null) { - if ((this.levelTileFlags && this.levelTileFlags[1][loc.heightmapNE][loc.heightmapNW] & 0x2) === 2) { - loc.heightmapSW--; + if ((this.levelTileFlags && this.levelTileFlags[1][loc.x][loc.z] & TileFlag.BRIDGE) === TileFlag.BRIDGE) { + loc.level--; - if (loc.heightmapSW < 0) { + if (loc.level < 0) { loc.unlink(); } } } + RendererWebGLC.onSceneLoaded(this.scene); + for (let x: number = 0; x < CollisionConstants.SIZE; x++) { for (let z: number = 0; z < CollisionConstants.SIZE; z++) { this.sortObjStacks(x, z); @@ -7282,7 +7323,7 @@ export class Client extends GameShell { private addMessage(type: number, text: string, sender: string): void { if (type === 0 && this.stickyChatInterfaceId !== -1) { this.modalMessage = text; - this.mouseClickButton = 0; + this.mouseClickButton = MouseButton.NONE; } if (this.chatInterfaceId === -1) { this.redrawChatback = true; @@ -7772,7 +7813,7 @@ export class Client extends GameShell { lastTypecode = typecode; - if (entityType === 2 && this.scene && this.scene.getInfo(this.currentLevel, x, z, typecode) >= 0) { + if (entityType === TypecodeEntity.LOC && this.scene && this.scene.getInfo(this.currentLevel, x, z, typecode) >= 0) { const loc: LocType = LocType.get(typeId); if (this.objSelected === 1) { this.menuOption[this.menuSize] = 'Use ' + this.objSelectedName + ' with @cya@' + loc.name; @@ -7828,9 +7869,7 @@ export class Client extends GameShell { this.menuParamC[this.menuSize] = z; this.menuSize++; } - } - - if (entityType === 1) { + } else if (entityType === TypecodeEntity.NPC) { const npc: NpcEntity | null = this.npcs[typeId]; if (npc && npc.npcType && npc.npcType.size === 1 && (npc.x & 0x7f) === 64 && (npc.z & 0x7f) === 64) { for (let i: number = 0; i < this.npcCount; i++) { @@ -7845,9 +7884,7 @@ export class Client extends GameShell { if (npc && npc.npcType) { this.addNpcOptions(npc.npcType, typeId, x, z); } - } - - if (entityType === 0) { + } else if (entityType === TypecodeEntity.PLAYER) { const player: PlayerEntity | null = this.players[typeId]; if (player && (player.x & 0x7f) === 64 && (player.z & 0x7f) === 64) { for (let i: number = 0; i < this.npcCount; i++) { @@ -7870,9 +7907,7 @@ export class Client extends GameShell { if (player) { this.addPlayerOptions(player, typeId, x, z); } - } - - if (entityType === 3) { + } else if (entityType === TypecodeEntity.OBJ) { const objs: LinkList | null = this.objStacks[this.currentLevel][x][z]; if (!objs) { continue; @@ -8418,7 +8453,7 @@ export class Client extends GameShell { x > 0 && z > 0 && this.bfsDirection[index] === 0 && - (flags[index] & CollisionFlag.BLOCK_SOUTH_WEST) === 0 && + (flags[index] & CollisionFlag.BLOCK_SOUTH_WEST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x - 1, z)] & CollisionFlag.BLOCK_WEST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x, z - 1)] & CollisionFlag.BLOCK_SOUTH) === CollisionFlag.OPEN ) { @@ -8434,7 +8469,7 @@ export class Client extends GameShell { x < sceneWidth - 1 && z > 0 && this.bfsDirection[index] === 0 && - (flags[index] & CollisionFlag.BLOCK_SOUTH_EAST) === 0 && + (flags[index] & CollisionFlag.BLOCK_SOUTH_EAST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x + 1, z)] & CollisionFlag.BLOCK_EAST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x, z - 1)] & CollisionFlag.BLOCK_SOUTH) === CollisionFlag.OPEN ) { @@ -8450,7 +8485,7 @@ export class Client extends GameShell { x > 0 && z < sceneLength - 1 && this.bfsDirection[index] === 0 && - (flags[index] & CollisionFlag.BLOCK_NORTH_WEST) === 0 && + (flags[index] & CollisionFlag.BLOCK_NORTH_WEST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x - 1, z)] & CollisionFlag.BLOCK_WEST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x, z + 1)] & CollisionFlag.BLOCK_NORTH) === CollisionFlag.OPEN ) { @@ -8466,7 +8501,7 @@ export class Client extends GameShell { x < sceneWidth - 1 && z < sceneLength - 1 && this.bfsDirection[index] === 0 && - (flags[index] & CollisionFlag.BLOCK_NORTH_EAST) === 0 && + (flags[index] & CollisionFlag.BLOCK_NORTH_EAST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x + 1, z)] & CollisionFlag.BLOCK_EAST) === CollisionFlag.OPEN && (flags[CollisionMap.index(x, z + 1)] & CollisionFlag.BLOCK_NORTH) === CollisionFlag.OPEN ) { @@ -9360,18 +9395,18 @@ export class Client extends GameShell { } if (append && this.scene) { - const level: number = loc.heightmapSW; - const x: number = loc.heightmapNE; - const z: number = loc.heightmapNW; + const level: number = loc.level; + const x: number = loc.x; + const z: number = loc.z; let typecode: number = 0; - if (loc.heightmapSE === 0) { + if (loc.layer === LocLayer.WALL) { typecode = this.scene.getWallTypecode(level, x, z); - } else if (loc.heightmapSE === 1) { + } else if (loc.layer === LocLayer.WALL_DECOR) { typecode = this.scene.getDecorTypecode(level, z, x); - } else if (loc.heightmapSE === 2) { + } else if (loc.layer === LocLayer.GROUND) { typecode = this.scene.getLocTypecode(level, x, z); - } else if (loc.heightmapSE === 3) { + } else if (loc.layer === LocLayer.GROUND_DECOR) { typecode = this.scene.getGroundDecorTypecode(level, x, z); } @@ -9387,7 +9422,7 @@ export class Client extends GameShell { seqId = loc.seq.seqFrames[loc.seqFrame]; } - if (loc.heightmapSE === 2) { + if (loc.layer === LocLayer.GROUND) { const info: number = this.scene.getInfo(level, x, z, typecode); let shape: number = info & 0x1f; const rotation: number = info >> 6; @@ -9397,9 +9432,9 @@ export class Client extends GameShell { } this.scene?.setLocModel(level, x, z, type.getModel(shape, rotation, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); - } else if (loc.heightmapSE === 1) { + } else if (loc.layer === LocLayer.WALL_DECOR) { this.scene?.setWallDecorationModel(level, x, z, type.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, 0, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); - } else if (loc.heightmapSE === 0) { + } else if (loc.layer === LocLayer.WALL) { const info: number = this.scene.getInfo(level, x, z, typecode); const shape: number = info & 0x1f; const rotation: number = info >> 6; @@ -9416,7 +9451,7 @@ export class Client extends GameShell { } else { this.scene?.setWallModel(level, x, z, type.getModel(shape, rotation, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); } - } else if (loc.heightmapSE === 3) { + } else if (loc.layer === LocLayer.GROUND_DECOR) { const info: number = this.scene.getInfo(level, x, z, typecode); const rotation: number = info >> 6; this.scene?.setGroundDecorationModel(level, x, z, type.getModel(LocShape.GROUND_DECOR.id, rotation, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); @@ -9778,7 +9813,7 @@ export class Client extends GameShell { let cameraLocalTileZ: number = this.cameraZ >> 7; const playerLocalTileX: number = this.localPlayer.x >> 7; const playerLocalTileZ: number = this.localPlayer.z >> 7; - if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & TileFlag.ROOF) !== TileFlag.OPEN) { top = this.currentLevel; } let tileDeltaX: number; @@ -9804,7 +9839,7 @@ export class Client extends GameShell { } else if (cameraLocalTileX > playerLocalTileX) { cameraLocalTileX--; } - if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & TileFlag.ROOF) !== TileFlag.OPEN) { top = this.currentLevel; } accumulator += delta; @@ -9815,7 +9850,7 @@ export class Client extends GameShell { } else if (cameraLocalTileZ > playerLocalTileZ) { cameraLocalTileZ--; } - if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & TileFlag.ROOF) !== TileFlag.OPEN) { top = this.currentLevel; } } @@ -9829,7 +9864,7 @@ export class Client extends GameShell { } else if (cameraLocalTileZ > playerLocalTileZ) { cameraLocalTileZ--; } - if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & TileFlag.ROOF) !== TileFlag.OPEN) { top = this.currentLevel; } accumulator += delta; @@ -9840,14 +9875,14 @@ export class Client extends GameShell { } else if (cameraLocalTileX > playerLocalTileX) { cameraLocalTileX--; } - if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & TileFlag.ROOF) !== TileFlag.OPEN) { top = this.currentLevel; } } } } } - if (this.localPlayer && this.levelTileFlags && (this.levelTileFlags[this.currentLevel][this.localPlayer.x >> 7][this.localPlayer.z >> 7] & 0x4) !== 0) { + if (this.localPlayer && this.levelTileFlags && (this.levelTileFlags[this.currentLevel][this.localPlayer.x >> 7][this.localPlayer.z >> 7] & TileFlag.ROOF) !== TileFlag.OPEN) { top = this.currentLevel; } return top; @@ -9858,7 +9893,7 @@ export class Client extends GameShell { return 0; // custom } const y: number = this.getHeightmapY(this.currentLevel, this.cameraX, this.cameraZ); - return y - this.cameraY >= 800 || (this.levelTileFlags[this.currentLevel][this.cameraX >> 7][this.cameraZ >> 7] & 0x4) === 0 ? 3 : this.currentLevel; + return y - this.cameraY >= 800 || (this.levelTileFlags[this.currentLevel][this.cameraX >> 7][this.cameraZ >> 7] & TileFlag.ROOF) === TileFlag.OPEN ? 3 : this.currentLevel; } private getHeightmapY(level: number, sceneX: number, sceneZ: number): number { @@ -9868,7 +9903,7 @@ export class Client extends GameShell { const tileX: number = Math.min(sceneX >> 7, CollisionConstants.SIZE - 1); const tileZ: number = Math.min(sceneZ >> 7, CollisionConstants.SIZE - 1); let realLevel: number = level; - if (level < 3 && this.levelTileFlags && (this.levelTileFlags[1][tileX][tileZ] & 0x2) === 2) { + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[1][tileX][tileZ] & TileFlag.BRIDGE) === TileFlag.BRIDGE) { realLevel = level + 1; } @@ -9961,7 +9996,7 @@ export class Client extends GameShell { for (let x: number = orbitTileX - 4; x <= orbitTileX + 4; x++) { for (let z: number = orbitTileZ - 4; z <= orbitTileZ + 4; z++) { let level: number = this.currentLevel; - if (level < 3 && this.levelTileFlags && (this.levelTileFlags[1][x][z] & 0x2) === 2) { + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[1][x][z] & TileFlag.BRIDGE) === TileFlag.BRIDGE) { level++; } @@ -10535,4 +10570,12 @@ export class Client extends GameShell { MobileKeyboard.draw(); } } + + private redrawAll() { + this.redrawChatback = true; + this.redrawPrivacySettings = true; + this.redrawSidebar = true; + this.redrawSideicons = true; + this.redrawTitleBackground = true; + } } diff --git a/src/client/GameShell.ts b/src/client/GameShell.ts index b211f29..f1d0cfb 100644 --- a/src/client/GameShell.ts +++ b/src/client/GameShell.ts @@ -1,4 +1,4 @@ -import { canvas, canvas2d } from '#/graphics/Canvas.js'; +import { canvas, canvas2d, canvasContainer, canvasOverlay } from '#/graphics/Canvas.js'; import Pix3D from '#/graphics/Pix3D.js'; import PixMap from '#/graphics/PixMap.js'; @@ -7,6 +7,9 @@ import { sleep } from '#/util/JsUtil.js'; import { CanvasEnabledKeys, KeyCodes } from '#/client/KeyCodes.js'; import InputTracking from '#/client/InputTracking.js'; import { MobileKeyboard } from '#3rdparty/deps.js'; +import { Renderer } from '#/graphics/renderer/Renderer.ts'; +import Stats from 'stats.js'; +import { MouseButton } from '#/client/MouseButton.ts'; export default abstract class GameShell { protected slowestMS: number = 0.0; // custom @@ -15,15 +18,8 @@ export default abstract class GameShell { protected drawArea: PixMap | null = null; protected state: number = 0; - protected deltime: number = 20; - protected mindel: number = 1; - protected otim: number[] = []; - protected fps: number = 0; - protected fpos: number = 0; - protected frameTime: number[] = []; protected redrawScreen: boolean = true; protected resizeToFit: boolean = false; - protected tfps: number = 50; // custom protected hasFocus: boolean = true; // mapview applet protected ingame: boolean = false; @@ -52,6 +48,15 @@ export default abstract class GameShell { private nx: number = 0; private ny: number = 0; + // game loop + drawStats: Stats = new Stats(); + updateStats: Stats = new Stats(); + + rafId: number = 0; + updateRate: number = 20; + updateAcc: number = 0; + lastUpdate: number = performance.now(); + abstract getTitleScreenState(): number; abstract isChatBackInputOpen(): boolean; abstract isShowSocialInput(): boolean; @@ -59,13 +64,13 @@ export default abstract class GameShell { abstract getViewportInterfaceId(): number; abstract getReportAbuseInterfaceId(): number; // custom: report abuse input on mobile - protected async load() {} - protected async update() {} - protected async draw() {} - protected async refresh() {} + protected async load() { } + protected async update(now: number) { } + protected async draw(now: number) { } + protected async refresh() { } - constructor(resizetoFit: boolean = false) { - canvas.tabIndex = -1; + protected constructor(resizetoFit: boolean = false) { + canvasOverlay.tabIndex = -1; canvas2d.fillStyle = 'black'; canvas2d.fillRect(0, 0, canvas.width, canvas.height); this.resizeToFit = resizetoFit; @@ -89,6 +94,7 @@ export default abstract class GameShell { canvas.height = height; this.drawArea = new PixMap(width, height); Pix3D.init2D(); + Renderer.resize(width, height); } async run() { @@ -102,27 +108,29 @@ export default abstract class GameShell { false ); - canvas.onfocus = this.onfocus.bind(this); - canvas.onblur = this.onblur.bind(this); + canvasOverlay.focus(); + + canvasOverlay.onfocus = this.onfocus.bind(this); + canvasOverlay.onblur = this.onblur.bind(this); // pc - canvas.onmousedown = this.onmousedown.bind(this); - canvas.onmouseup = this.onmouseup.bind(this); - canvas.onmouseenter = this.onmouseenter.bind(this); - canvas.onmouseleave = this.onmouseleave.bind(this); - canvas.onmousemove = this.onmousemove.bind(this); - canvas.onkeydown = this.onkeydown.bind(this); - canvas.onkeyup = this.onkeyup.bind(this); + canvasOverlay.onmousedown = this.onmousedown.bind(this); + canvasOverlay.onmouseup = this.onmouseup.bind(this); + canvasOverlay.onmouseenter = this.onmouseenter.bind(this); + canvasOverlay.onmouseleave = this.onmouseleave.bind(this); + canvasOverlay.onmousemove = this.onmousemove.bind(this); + canvasOverlay.onkeydown = this.onkeydown.bind(this); + canvasOverlay.onkeyup = this.onkeyup.bind(this); if (this.isMobile) { - canvas.ontouchstart = this.ontouchstart.bind(this); - canvas.ontouchend = this.ontouchend.bind(this); - canvas.ontouchmove = this.ontouchmove.bind(this); + canvasOverlay.ontouchstart = this.ontouchstart.bind(this); + canvasOverlay.ontouchend = this.ontouchend.bind(this); + canvasOverlay.ontouchmove = this.ontouchmove.bind(this); } // Preventing mouse events from bubbling up to the context menu in the browser for our canvas. // This may need to be hooked up to our own context menu in the future. - canvas.oncontextmenu = (e: MouseEvent): void => { + canvasOverlay.oncontextmenu = (e: MouseEvent): void => { e.preventDefault(); }; @@ -133,17 +141,25 @@ export default abstract class GameShell { await this.showProgress(0, 'Loading...'); await this.load(); - for (let i: number = 0; i < 10; i++) { - this.otim[i] = performance.now(); - } + this.drawStats.showPanel(0); + this.drawStats.dom.style.cssText = 'display:none;position:absolute;top:0px;right:0px;'; + canvasContainer.appendChild(this.drawStats.dom); + + this.updateStats.showPanel(1); + this.updateStats.dom.style.cssText = 'display:none;position:absolute;top:48px;right:0px;'; + canvasContainer.appendChild(this.updateStats.dom); - let ntime: number; - let opos: number = 0; - let ratio: number = 256; - let delta: number = 1; - let count: number = 0; + setTimeout(this.mainupdate.bind(this), 0); + window.requestAnimationFrame(this.maindraw.bind(this)); + } - while (this.state >= 0) { + protected async mainupdate() { + const now = performance.now(); + const elapsed = now - this.lastUpdate; + this.lastUpdate = now; + + this.updateAcc += elapsed; + if (this.updateAcc >= this.updateRate) { if (this.state > 0) { this.state--; @@ -153,92 +169,47 @@ export default abstract class GameShell { } } - const lastRatio: number = ratio; - const lastDelta: number = delta; - ratio = 300; - delta = 1; - - ntime = performance.now(); - const otim: number = this.otim[opos]; - - if (otim === 0) { - ratio = lastRatio; - delta = lastDelta; - } else if (ntime > otim) { - ratio = ((this.deltime * 2560) / (ntime - otim)) | 0; - } - - if (ratio < 25) { - ratio = 25; - } else if (ratio > 256) { - ratio = 256; - delta = (this.deltime - (ntime - otim) / 10) | 0; - } - - this.otim[opos] = ntime; - opos = (opos + 1) % 10; - - if (delta > 1) { - for (let i: number = 0; i < 10; i++) { - if (this.otim[i] !== 0) { - this.otim[i] += delta; - } - } - } - - if (delta < this.mindel) { - delta = this.mindel; - } - - await sleep(delta); + this.updateStats.update(); + } - while (count < 256) { - await this.update(); - this.mouseClickButton = 0; - this.keyQueueReadPos = this.keyQueueWritePos; - count += ratio; - } + while (this.updateAcc >= this.updateRate) { + await this.mainupdateinner(now); + this.updateAcc -= this.updateRate; + } - count &= 0xff; + setTimeout(this.mainupdate.bind(this), 1); + } - if (this.deltime > 0) { - this.fps = ((ratio * 1000) / (this.deltime * 256)) | 0; - } + protected async mainupdateinner(now: number) { + await this.update(now); + this.mouseClickButton = MouseButton.NONE; + this.keyQueueReadPos = this.keyQueueWritePos; + } - const time: number = performance.now(); + protected async maindraw(now: number) { + this.drawStats.begin(); + await this.maindrawinner(now); + this.drawStats.end(); - await this.draw(); - // CUSTOM: MobileKeyboard - if (this.isMobile) { - MobileKeyboard.draw(); - } + this.rafId = window.requestAnimationFrame(this.maindraw.bind(this)); // MDN says to put it at the start. DO NOT :') + } - this.frameTime[this.fpos] = (performance.now() - time) / 1000; - this.fpos = (this.fpos + 1) % this.frameTime.length; + protected async maindrawinner(now: number) { + await this.draw(now); - // this is custom for targeting specific fps (on mobile). - if (this.tfps < 50) { - const tfps: number = 1000 / this.tfps - (performance.now() - ntime); - if (tfps > 0) { - await sleep(tfps); - } - } - } - if (this.state === -1) { - this.shutdown(); + // CUSTOM: MobileKeyboard + if (this.isMobile) { + MobileKeyboard.draw(); } } protected shutdown() { this.state = -2; + window.cancelAnimationFrame(this.rafId); } - protected setFramerate(rate: number) { - this.deltime = (1000 / rate) | 0; - } - - protected setTargetedFramerate(rate: number) { - this.tfps = Math.max(Math.min(50, rate | 0), 0); + protected setUpdateRate(rate: number) { + this.updateRate = (1000 / rate) | 0; } protected start() { @@ -249,7 +220,7 @@ export default abstract class GameShell { protected stop() { if (this.state >= 0) { - this.state = (4000 / this.deltime) | 0; + this.state = (4000 / this.updateRate) | 0; } } @@ -270,8 +241,9 @@ export default abstract class GameShell { const y: number = height / 2 - 18; // draw full progress bar + canvas2d.strokeStyle = 'rgb(140, 17, 17)'; + canvas2d.strokeRect(((width / 2) | 0) - 152, y, 304, 34); canvas2d.fillStyle = 'rgb(140, 17, 17)'; - canvas2d.rect(((width / 2) | 0) - 152, y, 304, 34); canvas2d.fillRect(((width / 2) | 0) - 150, y + 2, progress * 3, 30); // cover up progress bar @@ -296,25 +268,6 @@ export default abstract class GameShell { return key; } - protected get ms(): number { - const length: number = this.frameTime.length; - let ft: number = 0; - for (let index: number = 0; index < length; index++) { - ft += this.frameTime[index]; - } - const ms: number = (ft / length) * 1000; - if (ms > this.slowestMS) { - this.slowestMS = ms; - } - this.averageMS[this.averageIndexMS] = ms; - this.averageIndexMS = (this.averageIndexMS + 1) % 250; // 250 circular limit - return ms; - } - - protected get msAvg(): number { - return this.averageMS.reduce((accumulator: number, currentValue: number): number => accumulator + currentValue, 0) / 250; // 250 circular limit - } - // ---- private onkeydown(e: KeyboardEvent) { this.idleCycles = performance.now(); @@ -327,7 +280,7 @@ export default abstract class GameShell { let ch: number = keyCode.ch; if (e.ctrlKey) { - if ((ch >= 'A'.charCodeAt(0) && ch <= ']'.charCodeAt(0)) || ch == '_'.charCodeAt(0)) { + if ((ch >= 'A'.charCodeAt(0) && ch <= ']'.charCodeAt(0)) || ch === '_'.charCodeAt(0)) { ch -= 'A'.charCodeAt(0) - 1; } else if (ch >= 'a'.charCodeAt(0) && ch <= 'z'.charCodeAt(0)) { ch -= 'a'.charCodeAt(0) - 1; @@ -363,7 +316,7 @@ export default abstract class GameShell { let ch: number = keyCode.ch; if (e.ctrlKey) { - if ((ch >= 'A'.charCodeAt(0) && ch <= ']'.charCodeAt(0)) || ch == '_'.charCodeAt(0)) { + if ((ch >= 'A'.charCodeAt(0) && ch <= ']'.charCodeAt(0)) || ch === '_'.charCodeAt(0)) { ch -= 'A'.charCodeAt(0) - 1; } else if (ch >= 'a'.charCodeAt(0) && ch <= 'z'.charCodeAt(0)) { ch -= 'a'.charCodeAt(0) - 1; @@ -397,35 +350,35 @@ export default abstract class GameShell { if (this.insideMobileInputArea() && !this.insideUsernameArea() && !this.inPasswordArea()) { // Negate the mousedown event - it's inside mobile input area // It will be handled by mouseup. - this.mouseClickButton = 0; - this.mouseButton = 0; + this.mouseClickButton = MouseButton.LEFT; + this.mouseButton = MouseButton.LEFT; return; } const eventTime: number = e.timeStamp; if (eventTime >= this.time + 500) { - this.mouseClickButton = 2; - this.mouseButton = 2; + this.mouseClickButton = MouseButton.RIGHT; + this.mouseButton = MouseButton.RIGHT; } else { - this.mouseClickButton = 1; - this.mouseButton = 1; + this.mouseClickButton = MouseButton.LEFT; + this.mouseButton = MouseButton.LEFT; } } else { if (e.button === 2) { - this.mouseClickButton = 2; - this.mouseButton = 2; + this.mouseClickButton = MouseButton.RIGHT; + this.mouseButton = MouseButton.RIGHT; } else if (e.button === 0) { // custom: explicitly check left-mouse button so middle mouse is ignored - this.mouseClickButton = 1; - this.mouseButton = 1; + this.mouseClickButton = MouseButton.LEFT; + this.mouseButton = MouseButton.LEFT; } } // CUSTOM: Mobile Keyboard if (MobileKeyboard.isDisplayed()) { if (MobileKeyboard.captureMouseDown(this.mouseX, this.mouseY)) { // Negate MouseDown if Keyboard shown and inside of Keyboard area - this.mouseButton = 0; - this.mouseClickButton = 0; + this.mouseButton = MouseButton.NONE; + this.mouseClickButton = MouseButton.NONE; } } @@ -437,7 +390,7 @@ export default abstract class GameShell { private onmouseup(e: MouseEvent) { this.setMousePosition(e); this.idleCycles = performance.now(); - this.mouseButton = 0; + this.mouseButton = MouseButton.NONE; if (InputTracking.trackingActive) { InputTracking.mouseReleased(e.button); @@ -475,7 +428,7 @@ export default abstract class GameShell { this.mouseY = -1; // custom (prevent mouse click from being stuck) - this.mouseButton = 0; + this.mouseButton = MouseButton.NONE; this.mouseClickX = -1; this.mouseClickY = -1; @@ -603,23 +556,23 @@ export default abstract class GameShell { this.nx = touch.screenX | 0; this.ny = touch.screenY | 0; if (!MobileKeyboard.isWithinCanvasKeyboard(this.mouseX, this.mouseY)) { // CUSTOM: MobileKeyboard - if (this.startedInViewport && this.getViewportInterfaceId() === -1) { - // Camera panning - if (this.mx - this.nx > 0) { - this.rotate(2); - } else if (this.mx - this.nx < 0) { - this.rotate(0); - } + if (this.startedInViewport && this.getViewportInterfaceId() === -1) { + // Camera panning + if (this.mx - this.nx > 0) { + this.rotate(2); + } else if (this.mx - this.nx < 0) { + this.rotate(0); + } - if (this.my - this.ny > 0) { - this.rotate(3); - } else if (this.my - this.ny < 0) { - this.rotate(1); + if (this.my - this.ny > 0) { + this.rotate(3); + } else if (this.my - this.ny < 0) { + this.rotate(1); + } + } else if (this.startedInTabArea || this.getViewportInterfaceId() !== -1) { + // Drag and drop + this.onmousedown(new MouseEvent('mousedown', { clientX, clientY, button: 1 })); } - } else if (this.startedInTabArea || this.getViewportInterfaceId() !== -1) { - // Drag and drop - this.onmousedown(new MouseEvent('mousedown', { clientX, clientY, button: 1 })); - } } // CUSTOM: MobileKeyboard this.mx = this.nx; diff --git a/src/client/InputTracking.ts b/src/client/InputTracking.ts index 2528bcd..8383901 100644 --- a/src/client/InputTracking.ts +++ b/src/client/InputTracking.ts @@ -1,4 +1,6 @@ import Packet from '#/io/Packet.js'; +import { PacketType } from '#/io/PacketType.ts'; +import { InputTrackingType } from '#/client/InputTrackingType.ts'; export default class InputTracking { static trackingActive: boolean = false; @@ -10,8 +12,10 @@ export default class InputTracking { static lastX: number = 0; static lastY: number = 0; + private static readonly MAX_BYTES: number = 500; + static setEnabled(): void { - this.outBuffer = Packet.alloc(1); + this.outBuffer = Packet.alloc(PacketType.TYPE_5KB); this.oldBuffer = null; this.lastTime = performance.now(); this.trackingActive = true; @@ -53,9 +57,9 @@ export default class InputTracking { this.lastTime = now; this.ensureCapacity(5); if (button === 2) { - this.outBuffer?.p1(1); + this.outBuffer?.p1(InputTrackingType.MOUSEDOWNR); } else { - this.outBuffer?.p1(2); + this.outBuffer?.p1(InputTrackingType.MOUSEDOWNL); } this.outBuffer?.p1(delta); this.outBuffer?.p3(x + (y << 10)); @@ -74,9 +78,9 @@ export default class InputTracking { this.lastTime = now; this.ensureCapacity(2); if (button === 2) { - this.outBuffer?.p1(3); + this.outBuffer?.p1(InputTrackingType.MOUSEUPR); } else { - this.outBuffer?.p1(4); + this.outBuffer?.p1(InputTrackingType.MOUSEUPL); } this.outBuffer?.p1(delta); } @@ -98,18 +102,18 @@ export default class InputTracking { this.lastTime = now; if (x - this.lastX < 8 && x - this.lastX >= -8 && y - this.lastY < 8 && y - this.lastY >= -8) { this.ensureCapacity(3); - this.outBuffer?.p1(5); + this.outBuffer?.p1(InputTrackingType.MOUSEMOVE1); this.outBuffer?.p1(delta); this.outBuffer?.p1(x + ((y - this.lastY + 8) << 4) + 8 - this.lastX); } else if (x - this.lastX < 128 && x - this.lastX >= -128 && y - this.lastY < 128 && y - this.lastY >= -128) { this.ensureCapacity(4); - this.outBuffer?.p1(6); + this.outBuffer?.p1(InputTrackingType.MOUSEMOVE2); this.outBuffer?.p1(delta); this.outBuffer?.p1(x + 128 - this.lastX); this.outBuffer?.p1(y + 128 - this.lastY); } else { this.ensureCapacity(5); - this.outBuffer?.p1(7); + this.outBuffer?.p1(InputTrackingType.MOUSEMOVE3); this.outBuffer?.p1(delta); this.outBuffer?.p3(x + (y << 10)); } @@ -142,7 +146,7 @@ export default class InputTracking { key -= 992; } this.ensureCapacity(3); - this.outBuffer?.p1(8); + this.outBuffer?.p1(InputTrackingType.KEYDOWN); this.outBuffer?.p1(delta); this.outBuffer?.p1(key); } @@ -170,7 +174,7 @@ export default class InputTracking { key -= 992; } this.ensureCapacity(3); - this.outBuffer?.p1(9); + this.outBuffer?.p1(InputTrackingType.KEYUP); this.outBuffer?.p1(delta); this.outBuffer?.p1(key); } @@ -187,7 +191,7 @@ export default class InputTracking { } this.lastTime = now; this.ensureCapacity(2); - this.outBuffer?.p1(10); + this.outBuffer?.p1(InputTrackingType.FOCUS); this.outBuffer?.p1(delta); } @@ -203,7 +207,7 @@ export default class InputTracking { } this.lastTime = now; this.ensureCapacity(2); - this.outBuffer?.p1(11); + this.outBuffer?.p1(InputTrackingType.BLUR); this.outBuffer?.p1(delta); } @@ -219,7 +223,7 @@ export default class InputTracking { } this.lastTime = now; this.ensureCapacity(2); - this.outBuffer?.p1(12); + this.outBuffer?.p1(InputTrackingType.MOUSEENTER); this.outBuffer?.p1(delta); } @@ -235,7 +239,7 @@ export default class InputTracking { } this.lastTime = now; this.ensureCapacity(2); - this.outBuffer?.p1(13); + this.outBuffer?.p1(InputTrackingType.MOUSELEAVE); this.outBuffer?.p1(delta); } @@ -243,9 +247,9 @@ export default class InputTracking { if (!this.outBuffer) { return; } - if (this.outBuffer.pos + n >= 500) { + if (this.outBuffer.pos + n >= InputTracking.MAX_BYTES) { const buffer: Packet = this.outBuffer; - this.outBuffer = Packet.alloc(1); + this.outBuffer = Packet.alloc(PacketType.TYPE_5KB); this.oldBuffer = buffer; } } diff --git a/src/client/InputTrackingType.ts b/src/client/InputTrackingType.ts new file mode 100644 index 0000000..7c7447c --- /dev/null +++ b/src/client/InputTrackingType.ts @@ -0,0 +1,15 @@ +export const enum InputTrackingType { + MOUSEDOWNR = 1, + MOUSEDOWNL = 2, + MOUSEUPR = 3, + MOUSEUPL = 4, + MOUSEMOVE1 = 5, + MOUSEMOVE2 = 6, + MOUSEMOVE3 = 7, + KEYDOWN = 8, + KEYUP = 9, + FOCUS = 10, + BLUR = 11, + MOUSEENTER = 12, + MOUSELEAVE = 13, +} \ No newline at end of file diff --git a/src/client/MouseButton.ts b/src/client/MouseButton.ts new file mode 100644 index 0000000..3bcf4d2 --- /dev/null +++ b/src/client/MouseButton.ts @@ -0,0 +1,5 @@ +export const enum MouseButton { + NONE = 0, + LEFT = 1, + RIGHT = 2, +} \ No newline at end of file diff --git a/src/config/NpcType.ts b/src/config/NpcType.ts index 3d84cee..ba84d1f 100644 --- a/src/config/NpcType.ts +++ b/src/config/NpcType.ts @@ -198,7 +198,7 @@ export default class NpcType extends ConfigType { tmp.labelVertices = null; if (this.size === 1) { - tmp.pickable = true; + tmp.pickAabb = true; } return tmp; } diff --git a/src/config/ObjType.ts b/src/config/ObjType.ts index 3d731c6..7bb1d76 100644 --- a/src/config/ObjType.ts +++ b/src/config/ObjType.ts @@ -426,7 +426,7 @@ export default class ObjType extends ConfigType { } model.calculateNormals(64, 768, -50, -10, -50, true); - model.pickable = true; + model.pickAabb = true; ObjType.modelCache?.put(BigInt(this.id), model); return model; } diff --git a/src/dash3d/LocAngle.ts b/src/dash3d/LocAngle.ts index 84a5f0e..0600a83 100644 --- a/src/dash3d/LocAngle.ts +++ b/src/dash3d/LocAngle.ts @@ -3,4 +3,4 @@ export const enum LocAngle { NORTH = 1, EAST = 2, SOUTH = 3 -}; +} diff --git a/src/dash3d/LocSpans.ts b/src/dash3d/LocSpans.ts new file mode 100644 index 0000000..40ad5ea --- /dev/null +++ b/src/dash3d/LocSpans.ts @@ -0,0 +1,16 @@ +export const enum LocSpans { + NONE = 0x0, + WEST = 0x1, + NORTH = 0x2, + EAST = 0x4, + SOUTH = 0x8, + // ---- + CORNER_WEST = 0x10, + CORNER_NORTH = 0x20, + CORNER_EAST = 0x40, + CORNER_SOUTH = 0x80, + // ---- + DECOR_OFFSET = 0x100, + DECOR_NOOFFSET = 0x200, + DECOR_BOTH = 0x300, +} \ No newline at end of file diff --git a/src/dash3d/Occlude.ts b/src/dash3d/Occlude.ts index eb7a9ef..c89d64f 100644 --- a/src/dash3d/Occlude.ts +++ b/src/dash3d/Occlude.ts @@ -1,3 +1,5 @@ +import { OccludeMode } from '#/dash3d/OccludeMode.ts'; + export default class Occlude { // constructor readonly minTileX: number; @@ -13,7 +15,7 @@ export default class Occlude { readonly maxY: number; // runtime - mode: number = 0; + mode: number = OccludeMode.NONE; minDeltaX: number = 0; maxDeltaX: number = 0; minDeltaZ: number = 0; diff --git a/src/dash3d/OccludeMode.ts b/src/dash3d/OccludeMode.ts new file mode 100644 index 0000000..95b9a95 --- /dev/null +++ b/src/dash3d/OccludeMode.ts @@ -0,0 +1,8 @@ +export const enum OccludeMode { + NONE = 0, + FRONT = 1, + BACK = 2, + RIGHT = 3, + LEFT = 4, + ABOVE = 5 +} \ No newline at end of file diff --git a/src/dash3d/OccludeType.ts b/src/dash3d/OccludeType.ts new file mode 100644 index 0000000..8d94d1d --- /dev/null +++ b/src/dash3d/OccludeType.ts @@ -0,0 +1,6 @@ +export const enum OccludeType { + NONE = 0x0, + HORIZONTAL = 0x1, + VERTICAL = 0x2, + FLAT = 0x4, +} \ No newline at end of file diff --git a/src/dash3d/TileFlag.ts b/src/dash3d/TileFlag.ts new file mode 100644 index 0000000..0fd6c07 --- /dev/null +++ b/src/dash3d/TileFlag.ts @@ -0,0 +1,8 @@ +export const enum TileFlag { + OPEN = 0x0, + BLOCKED = 0x1, + BRIDGE = 0x2, + ROOF = 0x4, + WALL = 0x8, + LOWMEMORY = 0x10, +} \ No newline at end of file diff --git a/src/dash3d/TypecodeEntity.ts b/src/dash3d/TypecodeEntity.ts new file mode 100644 index 0000000..b99534f --- /dev/null +++ b/src/dash3d/TypecodeEntity.ts @@ -0,0 +1,6 @@ +export const enum TypecodeEntity { + PLAYER = 0, + NPC = 1, + LOC = 2, + OBJ = 3, +} \ No newline at end of file diff --git a/src/dash3d/World.ts b/src/dash3d/World.ts index aa85250..a358799 100644 --- a/src/dash3d/World.ts +++ b/src/dash3d/World.ts @@ -20,11 +20,27 @@ import Model from '#/graphics/Model.js'; import Packet from '#/io/Packet.js'; import { Int32Array2d, Int32Array3d, Uint8Array3d } from '#/util/Arrays.js'; +import { LocLayer } from '#/dash3d/LocLayer.ts'; +import { TileFlag } from '#/dash3d/TileFlag.ts'; +import { LocSpans } from '#/dash3d/LocSpans.ts'; +import { OccludeType } from '#/dash3d/OccludeType.ts'; // noinspection JSSuspiciousNameCombination,DuplicatedCode export default class World { - static readonly ROTATION_WALL_TYPE: Int8Array = Int8Array.of(1, 2, 4, 8); - static readonly ROTATION_WALL_CORNER_TYPE: Uint8Array = Uint8Array.of(16, 32, 64, 128); + static readonly ROTATION_WALL_TYPE: Int8Array = Int8Array.of( + LocSpans.WEST, + LocSpans.NORTH, + LocSpans.EAST, + LocSpans.SOUTH + ); + + static readonly ROTATION_WALL_CORNER_TYPE: Uint8Array = Uint8Array.of( + LocSpans.CORNER_WEST, + LocSpans.CORNER_NORTH, + LocSpans.CORNER_EAST, + LocSpans.CORNER_SOUTH + ); + static readonly WALL_DECORATION_ROTATION_FORWARD_X: Int8Array = Int8Array.of(1, 0, -1, 0); static readonly WALL_DECORATION_ROTATION_FORWARD_Z: Int8Array = Int8Array.of(0, -1, 0, 1); @@ -103,7 +119,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 3, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.CENTREPIECE_STRAIGHT.id || shape === LocShape.CENTREPIECE_DIAGONAL.id) { const model: Model | null = loc.getModel(LocShape.CENTREPIECE_STRAIGHT.id, angle, heightSW, heightSE, heightNW, heightNE, -1); @@ -131,7 +147,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 2, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND, x, z, SeqType.instances[loc.anim], true)); } } else if (shape >= LocShape.ROOF_STRAIGHT.id) { scene?.addLoc(level, x, z, y, loc.getModel(shape, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info, 1, 1, 0); @@ -141,7 +157,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 2, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_STRAIGHT.id) { scene?.addWall(level, x, z, y, World.ROTATION_WALL_TYPE[angle], 0, loc.getModel(LocShape.WALL_STRAIGHT.id, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info); @@ -151,7 +167,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_DIAGONAL_CORNER.id) { scene?.addWall(level, x, z, y, World.ROTATION_WALL_CORNER_TYPE[angle], 0, loc.getModel(LocShape.WALL_DIAGONAL_CORNER.id, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info); @@ -161,7 +177,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_L.id) { const offset: number = (angle + 1) & 0x3; @@ -184,7 +200,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_SQUARE_CORNER.id) { scene?.addWall(level, x, z, y, World.ROTATION_WALL_CORNER_TYPE[angle], 0, loc.getModel(LocShape.WALL_SQUARE_CORNER.id, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info); @@ -194,7 +210,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_DIAGONAL.id) { scene?.addLoc(level, x, z, y, loc.getModel(shape, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info, 1, 1, 0); @@ -204,13 +220,13 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 2, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id) { scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle * 512, World.ROTATION_WALL_TYPE[angle]); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_STRAIGHT_OFFSET.id) { let offset: number = 16; @@ -236,25 +252,25 @@ export default class World { ); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_DIAGONAL_OFFSET.id) { - scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, 256); + scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, LocSpans.DECOR_OFFSET); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_DIAGONAL_NOOFFSET.id) { - scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, 512); + scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, LocSpans.DECOR_NOOFFSET); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_DIAGONAL_BOTH.id) { - scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, 768); + scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, LocSpans.DECOR_BOTH); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } } @@ -302,11 +318,11 @@ export default class World { for (let x: number = 0; x < CollisionConstants.SIZE; x++) { for (let z: number = 0; z < CollisionConstants.SIZE; z++) { // solid - if ((this.levelTileFlags[level][x][z] & 0x1) === 1) { + if ((this.levelTileFlags[level][x][z] & TileFlag.BLOCKED) === TileFlag.BLOCKED) { let trueLevel: number = level; // bridge - if ((this.levelTileFlags[1][x][z] & 0x2) === 2) { + if ((this.levelTileFlags[1][x][z] & TileFlag.BRIDGE) === TileFlag.BRIDGE) { trueLevel--; } @@ -423,7 +439,7 @@ export default class World { magnitudeAccumulator -= this.blendMagnitude[dz2]; } - if (z0 >= 1 && z0 < this.maxTileZ - 1 && (!World.lowMemory || ((this.levelTileFlags[level][x0][z0] & 0x10) === 0 && this.getDrawLevel(level, x0, z0) === World.levelBuilt))) { + if (z0 >= 1 && z0 < this.maxTileZ - 1 && (!World.lowMemory || ((this.levelTileFlags[level][x0][z0] & TileFlag.LOWMEMORY) === TileFlag.OPEN && this.getDrawLevel(level, x0, z0) === World.levelBuilt))) { const underlayId: number = this.levelTileUnderlayIds[level][x0][z0] & 0xff; const overlayId: number = this.levelTileOverlayIds[level][x0][z0] & 0xff; @@ -559,44 +575,44 @@ export default class World { for (let x: number = 0; x < this.maxTileX; x++) { for (let z: number = 0; z < this.maxTileZ; z++) { - if ((this.levelTileFlags[1][x][z] & 0x2) === 2) { + if ((this.levelTileFlags[1][x][z] & TileFlag.BRIDGE) === TileFlag.BRIDGE) { scene?.setBridge(x, z); } } } if (!World.fullbright) { - let wall0: number = 0x1; // this flag is set by walls with rotation 0 or 2 - let wall1: number = 0x2; // this flag is set by walls with rotation 1 or 3 - let floor: number = 0x4; // this flag is set by floors which are flat + let horizontal: number = OccludeType.HORIZONTAL; // this flag is set by walls with rotation 0 or 2 + let vertical: number = OccludeType.VERTICAL; // this flag is set by walls with rotation 1 or 3 + let flat: number = OccludeType.FLAT; // this flag is set by floors which are flat for (let topLevel: number = 0; topLevel < CollisionConstants.LEVELS; topLevel++) { if (topLevel > 0) { - wall0 <<= 0x3; - wall1 <<= 0x3; - floor <<= 0x3; + horizontal <<= 0x3; + vertical <<= 0x3; + flat <<= 0x3; } for (let level: number = 0; level <= topLevel; level++) { for (let tileZ: number = 0; tileZ <= this.maxTileZ; tileZ++) { for (let tileX: number = 0; tileX <= this.maxTileX; tileX++) { - if ((this.levelOccludemap[level][tileX][tileZ] & wall0) !== 0) { + if ((this.levelOccludemap[level][tileX][tileZ] & horizontal) !== OccludeType.NONE) { let minTileZ: number = tileZ; let maxTileZ: number = tileZ; let minLevel: number = level; let maxLevel: number = level; - while (minTileZ > 0 && (this.levelOccludemap[level][tileX][minTileZ - 1] & wall0) !== 0) { + while (minTileZ > 0 && (this.levelOccludemap[level][tileX][minTileZ - 1] & horizontal) !== OccludeType.NONE) { minTileZ--; } - while (maxTileZ < this.maxTileZ && (this.levelOccludemap[level][tileX][maxTileZ + 1] & wall0) !== 0) { + while (maxTileZ < this.maxTileZ && (this.levelOccludemap[level][tileX][maxTileZ + 1] & horizontal) !== OccludeType.NONE) { maxTileZ++; } find_min_level: while (minLevel > 0) { for (let z: number = minTileZ; z <= maxTileZ; z++) { - if ((this.levelOccludemap[minLevel - 1][tileX][z] & wall0) === 0) { + if ((this.levelOccludemap[minLevel - 1][tileX][z] & horizontal) === OccludeType.NONE) { break find_min_level; } } @@ -605,7 +621,7 @@ export default class World { find_max_level: while (maxLevel < topLevel) { for (let z: number = minTileZ; z <= maxTileZ; z++) { - if ((this.levelOccludemap[maxLevel + 1][tileX][z] & wall0) === 0) { + if ((this.levelOccludemap[maxLevel + 1][tileX][z] & horizontal) === OccludeType.NONE) { break find_max_level; } } @@ -617,33 +633,33 @@ export default class World { const minY: number = this.levelHeightmap[maxLevel][tileX][minTileZ] - 240; const maxX: number = this.levelHeightmap[minLevel][tileX][minTileZ]; - World3D.addOccluder(topLevel, 1, tileX * 128, minY, minTileZ * 128, tileX * 128, maxX, maxTileZ * 128 + 128); + World3D.addOccluder(topLevel, OccludeType.HORIZONTAL, tileX * 128, minY, minTileZ * 128, tileX * 128, maxX, maxTileZ * 128 + 128); for (let l: number = minLevel; l <= maxLevel; l++) { for (let z: number = minTileZ; z <= maxTileZ; z++) { - this.levelOccludemap[l][tileX][z] &= ~wall0; + this.levelOccludemap[l][tileX][z] &= ~horizontal; } } } } - if ((this.levelOccludemap[level][tileX][tileZ] & wall1) !== 0) { + if ((this.levelOccludemap[level][tileX][tileZ] & vertical) !== OccludeType.NONE) { let minTileX: number = tileX; let maxTileX: number = tileX; let minLevel: number = level; let maxLevel: number = level; - while (minTileX > 0 && (this.levelOccludemap[level][minTileX - 1][tileZ] & wall1) !== 0) { + while (minTileX > 0 && (this.levelOccludemap[level][minTileX - 1][tileZ] & vertical) !== OccludeType.NONE) { minTileX--; } - while (maxTileX < this.maxTileX && (this.levelOccludemap[level][maxTileX + 1][tileZ] & wall1) !== 0) { + while (maxTileX < this.maxTileX && (this.levelOccludemap[level][maxTileX + 1][tileZ] & vertical) !== OccludeType.NONE) { maxTileX++; } find_min_level2: while (minLevel > 0) { for (let x: number = minTileX; x <= maxTileX; x++) { - if ((this.levelOccludemap[minLevel - 1][x][tileZ] & wall1) === 0) { + if ((this.levelOccludemap[minLevel - 1][x][tileZ] & vertical) === OccludeType.NONE) { break find_min_level2; } } @@ -652,7 +668,7 @@ export default class World { find_max_level2: while (maxLevel < topLevel) { for (let x: number = minTileX; x <= maxTileX; x++) { - if ((this.levelOccludemap[maxLevel + 1][x][tileZ] & wall1) === 0) { + if ((this.levelOccludemap[maxLevel + 1][x][tileZ] & vertical) === OccludeType.NONE) { break find_max_level2; } } @@ -665,32 +681,32 @@ export default class World { const minY: number = this.levelHeightmap[maxLevel][minTileX][tileZ] - 240; const maxY: number = this.levelHeightmap[minLevel][minTileX][tileZ]; - World3D.addOccluder(topLevel, 2, minTileX * 128, minY, tileZ * 128, maxTileX * 128 + 128, maxY, tileZ * 128); + World3D.addOccluder(topLevel, OccludeType.VERTICAL, minTileX * 128, minY, tileZ * 128, maxTileX * 128 + 128, maxY, tileZ * 128); for (let l: number = minLevel; l <= maxLevel; l++) { for (let x: number = minTileX; x <= maxTileX; x++) { - this.levelOccludemap[l][x][tileZ] &= ~wall1; + this.levelOccludemap[l][x][tileZ] &= ~vertical; } } } } - if ((this.levelOccludemap[level][tileX][tileZ] & floor) !== 0) { + if ((this.levelOccludemap[level][tileX][tileZ] & flat) !== OccludeType.NONE) { let minTileX: number = tileX; let maxTileX: number = tileX; let minTileZ: number = tileZ; let maxTileZ: number = tileZ; - while (minTileZ > 0 && (this.levelOccludemap[level][tileX][minTileZ - 1] & floor) !== 0) { + while (minTileZ > 0 && (this.levelOccludemap[level][tileX][minTileZ - 1] & flat) !== OccludeType.NONE) { minTileZ--; } - while (maxTileZ < this.maxTileZ && (this.levelOccludemap[level][tileX][maxTileZ + 1] & floor) !== 0) { + while (maxTileZ < this.maxTileZ && (this.levelOccludemap[level][tileX][maxTileZ + 1] & flat) !== OccludeType.NONE) { maxTileZ++; } find_min_tile_xz: while (minTileX > 0) { for (let z: number = minTileZ; z <= maxTileZ; z++) { - if ((this.levelOccludemap[level][minTileX - 1][z] & floor) === 0) { + if ((this.levelOccludemap[level][minTileX - 1][z] & flat) === OccludeType.NONE) { break find_min_tile_xz; } } @@ -699,7 +715,7 @@ export default class World { find_max_tile_xz: while (maxTileX < this.maxTileX) { for (let z: number = minTileZ; z <= maxTileZ; z++) { - if ((this.levelOccludemap[level][maxTileX + 1][z] & floor) === 0) { + if ((this.levelOccludemap[level][maxTileX + 1][z] & flat) === OccludeType.NONE) { break find_max_tile_xz; } } @@ -709,11 +725,11 @@ export default class World { if ((maxTileX + 1 - minTileX) * (maxTileZ + 1 - minTileZ) >= 4) { const y: number = this.levelHeightmap[level][minTileX][minTileZ]; - World3D.addOccluder(topLevel, 4, minTileX * 128, y, minTileZ * 128, maxTileX * 128 + 128, y, maxTileZ * 128 + 128); + World3D.addOccluder(topLevel, OccludeType.FLAT, minTileX * 128, y, minTileZ * 128, maxTileX * 128 + 128, y, maxTileZ * 128 + 128); for (let x: number = minTileX; x <= maxTileX; x++) { for (let z: number = minTileZ; z <= maxTileZ; z++) { - this.levelOccludemap[level][x][z] &= ~floor; + this.levelOccludemap[level][x][z] &= ~flat; } } } @@ -741,7 +757,7 @@ export default class World { for (let level: number = 0; level < CollisionConstants.LEVELS; level++) { this.levelHeightmap[level][x][z] = 0; - this.levelTileFlags[level][x][z] = 0; + this.levelTileFlags[level][x][z] = TileFlag.OPEN; } } } @@ -759,7 +775,7 @@ export default class World { let opcode: number; if (stx >= 0 && stx < CollisionConstants.SIZE && stz >= 0 && stz < CollisionConstants.SIZE) { - this.levelTileFlags[level][stx][stz] = 0; + this.levelTileFlags[level][stx][stz] = TileFlag.OPEN; // eslint-disable-next-line no-constant-condition while (true) { opcode = buf.g1(); @@ -852,7 +868,7 @@ export default class World { if (stx > 0 && stz > 0 && stx < CollisionConstants.SIZE - 1 && stz < CollisionConstants.SIZE - 1) { let currentLevel: number = level; - if ((this.levelTileFlags[1][stx][stz] & 0x2) === 2) { + if ((this.levelTileFlags[1][stx][stz] & TileFlag.BRIDGE) === TileFlag.BRIDGE) { currentLevel = level - 1; } @@ -869,7 +885,7 @@ export default class World { private addLoc(level: number, x: number, z: number, scene: World3D | null, locs: LinkList, collision: CollisionMap | null, locId: number, shape: number, angle: number): void { if (World.lowMemory) { - if ((this.levelTileFlags[level][x][z] & 0x10) !== 0) { + if ((this.levelTileFlags[level][x][z] & TileFlag.LOWMEMORY) !== TileFlag.OPEN) { return; } @@ -903,7 +919,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 3, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND_DECOR, x, z, SeqType.instances[loc.anim], true)); } } } else if (shape === LocShape.CENTREPIECE_STRAIGHT.id || shape === LocShape.CENTREPIECE_DIAGONAL.id) { @@ -945,7 +961,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 2, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND, x, z, SeqType.instances[loc.anim], true)); } } else if (shape >= LocShape.ROOF_STRAIGHT.id) { scene?.addLoc(level, x, z, y, loc.getModel(shape, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info, 1, 1, 0); @@ -959,7 +975,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 2, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_STRAIGHT.id) { scene?.addWall(level, x, z, y, World.ROTATION_WALL_TYPE[angle], 0, loc.getModel(LocShape.WALL_STRAIGHT.id, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info); @@ -1007,7 +1023,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } if (loc.wallwidth !== 16) { @@ -1033,7 +1049,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_L.id) { const offset: number = (angle + 1) & 0x3; @@ -1072,7 +1088,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } if (loc.wallwidth !== 16) { @@ -1098,7 +1114,7 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 0, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALL_DIAGONAL.id) { scene?.addLoc(level, x, z, y, loc.getModel(shape, angle, heightSW, heightSE, heightNW, heightNE, -1), null, typecode, info, 1, 1, 0); @@ -1108,13 +1124,13 @@ export default class World { } if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 2, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.GROUND, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id) { scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle * 512, World.ROTATION_WALL_TYPE[angle]); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_STRAIGHT_OFFSET.id) { let offset: number = 16; @@ -1140,32 +1156,32 @@ export default class World { ); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_DIAGONAL_OFFSET.id) { - scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, 256); + scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, LocSpans.DECOR_OFFSET); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_DIAGONAL_NOOFFSET.id) { - scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, 512); + scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, LocSpans.DECOR_NOOFFSET); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } else if (shape === LocShape.WALLDECOR_DIAGONAL_BOTH.id) { - scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, 768); + scene?.setWallDecoration(level, x, z, y, 0, 0, typecode, loc.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id, LocAngle.WEST, heightSW, heightSE, heightNW, heightNE, -1), info, angle, LocSpans.DECOR_BOTH); if (loc.anim !== -1) { - locs.addTail(new LocEntity(locId, level, 1, x, z, SeqType.instances[loc.anim], true)); + locs.addTail(new LocEntity(locId, level, LocLayer.WALL_DECOR, x, z, SeqType.instances[loc.anim], true)); } } } private getDrawLevel(level: number, stx: number, stz: number): number { - if ((this.levelTileFlags[level][stx][stz] & 0x8) === 0) { - return level <= 0 || (this.levelTileFlags[1][stx][stz] & 0x2) === 0 ? level : level - 1; + if ((this.levelTileFlags[level][stx][stz] & TileFlag.WALL) === TileFlag.OPEN) { + return level <= 0 || (this.levelTileFlags[1][stx][stz] & TileFlag.BRIDGE) === TileFlag.OPEN ? level : level - 1; } return 0; } diff --git a/src/dash3d/World3D.ts b/src/dash3d/World3D.ts index 66843e8..c5e54bd 100644 --- a/src/dash3d/World3D.ts +++ b/src/dash3d/World3D.ts @@ -5,7 +5,7 @@ import Occlude from '#/dash3d/Occlude.js'; import Entity from '#/dash3d/entity/Entity.js'; import GroundDecor from '#/dash3d/type/GroundDecor.js'; -import Location from '#/dash3d/type/Loc.js'; +import Location from '#/dash3d/type/Location.js'; import ObjStack from '#/dash3d/type/ObjStack.js'; import Ground from '#/dash3d/type/Ground.js'; import TileOverlay from '#/dash3d/type/TileOverlay.js'; @@ -21,6 +21,12 @@ import Pix3D from '#/graphics/Pix3D.js'; import Model, { VertexNormal } from '#/graphics/Model.js'; import { Int32Array3d, TypedArray1d, TypedArray2d, TypedArray3d, TypedArray4d } from '#/util/Arrays.js'; +import { RendererWebGLC } from '#/graphics/renderer/webgl/RendererWebGLC.ts'; +import { Renderer } from '#/graphics/renderer/Renderer.ts'; +import { LocSpans } from '#/dash3d/LocSpans.ts'; +import { TypecodeEntity } from '#/dash3d/TypecodeEntity.ts'; +import { OccludeType } from '#/dash3d/OccludeType.ts'; +import { OccludeMode } from '#/dash3d/OccludeMode.ts'; export default class World3D { private static visibilityMatrix: boolean[][][][] = new TypedArray4d(8, 32, 51, 51, false); @@ -34,8 +40,8 @@ export default class World3D { private static viewportLeft: number = 0; private static viewportTop: number = 0; - private static viewportRight: number = 0; - private static viewportBottom: number = 0; + static viewportRight: number = 0; + static viewportBottom: number = 0; private static viewportCenterX: number = 0; private static viewportCenterY: number = 0; @@ -44,9 +50,9 @@ export default class World3D { private static sinEyeYaw: number = 0; private static cosEyeYaw: number = 0; - private static eyeX: number = 0; - private static eyeY: number = 0; - private static eyeZ: number = 0; + static eyeX: number = 0; + static eyeY: number = 0; + static eyeZ: number = 0; private static eyeTileX: number = 0; private static eyeTileZ: number = 0; @@ -64,10 +70,56 @@ export default class World3D { static readonly FRONT_WALL_TYPES: Uint8Array = Uint8Array.of(19, 55, 38, 155, 255, 110, 137, 205, 76); static readonly DIRECTION_ALLOW_WALL_CORNER_TYPE: Uint8Array = Uint8Array.of(160, 192, 80, 96, 0, 144, 80, 48, 160); static readonly BACK_WALL_TYPES: Uint8Array = Uint8Array.of(76, 8, 137, 4, 0, 1, 38, 2, 19); - static readonly WALL_CORNER_TYPE_16_BLOCK_LOC_SPANS: Int8Array = Int8Array.of(0, 0, 2, 0, 0, 2, 1, 1, 0); - static readonly WALL_CORNER_TYPE_32_BLOCK_LOC_SPANS: Int8Array = Int8Array.of(2, 0, 0, 2, 0, 0, 0, 4, 4); - static readonly WALL_CORNER_TYPE_64_BLOCK_LOC_SPANS: Int8Array = Int8Array.of(0, 4, 4, 8, 0, 0, 8, 0, 0); - static readonly WALL_CORNER_TYPE_128_BLOCK_LOC_SPANS: Int8Array = Int8Array.of(1, 1, 0, 0, 0, 8, 0, 0, 8); + + static readonly WALL_CORNER_TYPE_16_BLOCK_LOC_SPANS: Int8Array = Int8Array.of( + LocSpans.NONE, + LocSpans.NONE, + LocSpans.NORTH, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.NORTH, + LocSpans.WEST, + LocSpans.WEST, + LocSpans.NONE, + ); + + static readonly WALL_CORNER_TYPE_32_BLOCK_LOC_SPANS: Int8Array = Int8Array.of( + LocSpans.NORTH, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.NORTH, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.EAST, + LocSpans.EAST, + ); + + static readonly WALL_CORNER_TYPE_64_BLOCK_LOC_SPANS: Int8Array = Int8Array.of( + LocSpans.NONE, + LocSpans.EAST, + LocSpans.EAST, + LocSpans.SOUTH, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.SOUTH, + LocSpans.NONE, + LocSpans.NONE, + ); + + static readonly WALL_CORNER_TYPE_128_BLOCK_LOC_SPANS: Int8Array = Int8Array.of( + LocSpans.WEST, + LocSpans.WEST, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.SOUTH, + LocSpans.NONE, + LocSpans.NONE, + LocSpans.SOUTH, + ); + + static readonly WALL_DECORATION_INSET_X: Int8Array = Int8Array.of(53, -53, -53, 53); static readonly WALL_DECORATION_INSET_Z: Int8Array = Int8Array.of(-53, -53, 53, 53); static readonly WALL_DECORATION_OUTSET_X: Int8Array = Int8Array.of(-45, 45, 45, -45); @@ -185,9 +237,11 @@ export default class World3D { } } } + static addOccluder(level: number, type: number, minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): void { World3D.levelOccluders[level][World3D.levelOccluderCount[level]++] = new Occlude((minX / 128) | 0, (maxX / 128) | 0, (minZ / 128) | 0, (maxZ / 128) | 0, type, minX, maxX, minZ, maxZ, minY, maxY); } + private static testPoint(x: number, z: number, y: number): boolean { const px: number = (z * this.sinEyeYaw + x * this.cosEyeYaw) >> 16; const tmp: number = (z * this.cosEyeYaw - x * this.sinEyeYaw) >> 16; @@ -202,11 +256,11 @@ export default class World3D { } // ---- - private readonly maxLevel: number; - private readonly maxTileX: number; - private readonly maxTileZ: number; - private readonly levelHeightmaps: Int32Array[][]; - private readonly levelTiles: (Ground | null)[][][]; + readonly maxLevel: number; + readonly maxTileX: number; + readonly maxTileZ: number; + readonly levelHeightmaps: Int32Array[][]; + readonly levelTiles: (Ground | null)[][][]; private readonly temporaryLocs: (Location | null)[]; private readonly levelTileOcclusionCycles: Int32Array[][]; private readonly mergeIndexA: Int32Array; @@ -253,6 +307,7 @@ export default class World3D { this.temporaryLocCount = 0; World3D.locBuffer.fill(null); + RendererWebGLC.onSceneReset(this); } setMinLevel(level: number): void { @@ -374,7 +429,7 @@ export default class World3D { } const tile: Ground | null = this.levelTiles[tileLevel][tileX][tileZ]; if (tile) { - tile.groundDecoration = new GroundDecor(y, tileX * 128 + 64, tileZ * 128 + 64, model, typecode, info); + tile.groundDecor = new GroundDecor(y, tileX * 128 + 64, tileZ * 128 + 64, model, typecode, info); } } @@ -384,7 +439,7 @@ export default class World3D { return; } - tile.groundDecoration = null; + tile.groundDecor = null; } addObjStack(stx: number, stz: number, y: number, level: number, typecode: number, topObj: Model | null, middleObj: Model | null, bottomObj: Model | null): void { @@ -419,7 +474,7 @@ export default class World3D { tile.objStack = null; } - addWall(level: number, tileX: number, tileZ: number, y: number, typeA: number, typeB: number, modelA: Model | null, modelB: Model | null, typecode: number, info: number): void { + addWall(level: number, tileX: number, tileZ: number, y: number, spansA: number, spansB: number, modelA: Model | null, modelB: Model | null, typecode: number, info: number): void { if (!modelA && !modelB) { return; } @@ -430,7 +485,7 @@ export default class World3D { } const tile: Ground | null = this.levelTiles[level][tileX][tileZ]; if (tile) { - tile.wall = new Wall(y, tileX * 128 + 64, tileZ * 128 + 64, typeA, typeB, modelA, modelB, typecode, info); + tile.wall = new Wall(y, tileX * 128 + 64, tileZ * 128 + 64, spansA, spansB, modelA, modelB, typecode, info); } } @@ -441,7 +496,7 @@ export default class World3D { } } - setWallDecoration(level: number, tileX: number, tileZ: number, y: number, offsetX: number, offsetZ: number, typecode: number, model: Model | null, info: number, angle: number, type: number): void { + setWallDecoration(level: number, tileX: number, tileZ: number, y: number, offsetX: number, offsetZ: number, typecode: number, model: Model | null, info: number, angle: number, spans: number): void { if (!model) { return; } @@ -452,7 +507,7 @@ export default class World3D { } const tile: Ground | null = this.levelTiles[level][tileX][tileZ]; if (tile) { - tile.wallDecoration = new Decor(y, tileX * 128 + offsetX + 64, tileZ * 128 + offsetZ + 64, type, angle, model, typecode, info); + tile.decor = new Decor(y, tileX * 128 + offsetX + 64, tileZ * 128 + offsetZ + 64, spans, angle, model, typecode, info); } } @@ -462,7 +517,7 @@ export default class World3D { return; } - tile.wallDecoration = null; + tile.decor = null; } setWallDecorationOffset(level: number, x: number, z: number, offset: number): void { @@ -471,7 +526,7 @@ export default class World3D { return; } - const decor: Decor | null = tile.wallDecoration; + const decor: Decor | null = tile.decor; if (!decor) { return; } @@ -492,7 +547,7 @@ export default class World3D { return; } - const decor: Decor | null = tile.wallDecoration; + const decor: Decor | null = tile.decor; if (!decor) { return; } @@ -510,7 +565,7 @@ export default class World3D { return; } - const decor: GroundDecor | null = tile.groundDecoration; + const decor: GroundDecor | null = tile.groundDecor; if (!decor) { return; } @@ -605,7 +660,7 @@ export default class World3D { for (let l: number = 0; l < tile.locCount; l++) { const loc: Location | null = tile.locs[l]; - if (loc && ((loc.typecode >> 29) & 0x3) === 2 && loc.minSceneTileX === x && loc.minSceneTileZ === z) { + if (loc && ((loc.typecode >> 29) & 0x3) === TypecodeEntity.LOC && loc.minSceneTileX === x && loc.minSceneTileZ === z) { this.removeLoc2(loc); return; } @@ -624,7 +679,7 @@ export default class World3D { for (let i: number = 0; i < tile.locCount; i++) { const loc: Location | null = tile.locs[i]; - if (loc && ((loc.typecode >> 29) & 0x3) === 2) { + if (loc && ((loc.typecode >> 29) & 0x3) === TypecodeEntity.LOC) { loc.model = model; return; } @@ -650,7 +705,7 @@ export default class World3D { getDecorTypecode(level: number, z: number, x: number): number { const tile: Ground | null = this.levelTiles[level][x][z]; - return !tile || !tile.wallDecoration ? 0 : tile.wallDecoration.typecode; + return !tile || !tile.decor ? 0 : tile.decor.typecode; } getLocTypecode(level: number, x: number, z: number): number { @@ -661,7 +716,7 @@ export default class World3D { for (let l: number = 0; l < tile.locCount; l++) { const loc: Location | null = tile.locs[l]; - if (loc && ((loc.typecode >> 29) & 0x3) === 2 && loc.minSceneTileX === x && loc.minSceneTileZ === z) { + if (loc && ((loc.typecode >> 29) & 0x3) === TypecodeEntity.LOC && loc.minSceneTileX === x && loc.minSceneTileZ === z) { return loc.typecode; } } @@ -671,7 +726,7 @@ export default class World3D { getGroundDecorTypecode(level: number, x: number, z: number): number { const tile: Ground | null = this.levelTiles[level][x][z]; - return !tile || !tile.groundDecoration ? 0 : tile.groundDecoration.typecode; + return !tile || !tile.groundDecor ? 0 : tile.groundDecor.typecode; } getInfo(level: number, x: number, z: number, typecode: number): number { @@ -680,10 +735,10 @@ export default class World3D { return -1; } else if (tile.wall && tile.wall.typecode === typecode) { return tile.wall.info & 0xff; - } else if (tile.wallDecoration && tile.wallDecoration.typecode === typecode) { - return tile.wallDecoration.info & 0xff; - } else if (tile.groundDecoration && tile.groundDecoration.typecode === typecode) { - return tile.groundDecoration.info & 0xff; + } else if (tile.decor && tile.decor.typecode === typecode) { + return tile.decor.info & 0xff; + } else if (tile.groundDecor && tile.groundDecor.typecode === typecode) { + return tile.groundDecor.info & 0xff; } else { for (let i: number = 0; i < tile.locCount; i++) { const loc: Location | null = tile.locs[i]; @@ -726,7 +781,7 @@ export default class World3D { } } - const decor: GroundDecor | null = tile.groundDecoration; + const decor: GroundDecor | null = tile.groundDecor; if (decor && decor.model && decor.model.vertexNormal) { this.mergeGroundDecorationNormals(level, tileX, tileZ, decor.model); decor.model.applyLighting(lightAmbient, attenuation, lightSrcX, lightSrcY, lightSrcZ); @@ -739,29 +794,29 @@ export default class World3D { mergeGroundDecorationNormals(level: number, tileX: number, tileZ: number, model: Model): void { if (tileX < this.maxTileX) { const tile: Ground | null = this.levelTiles[level][tileX + 1][tileZ]; - if (tile && tile.groundDecoration && tile.groundDecoration.model && tile.groundDecoration.model.vertexNormal) { - this.mergeNormals(model, tile.groundDecoration.model, 128, 0, 0, true); + if (tile && tile.groundDecor && tile.groundDecor.model && tile.groundDecor.model.vertexNormal) { + this.mergeNormals(model, tile.groundDecor.model, 128, 0, 0, true); } } if (tileZ < this.maxTileX) { const tile: Ground | null = this.levelTiles[level][tileX][tileZ + 1]; - if (tile && tile.groundDecoration && tile.groundDecoration.model && tile.groundDecoration.model.vertexNormal) { - this.mergeNormals(model, tile.groundDecoration.model, 0, 0, 128, true); + if (tile && tile.groundDecor && tile.groundDecor.model && tile.groundDecor.model.vertexNormal) { + this.mergeNormals(model, tile.groundDecor.model, 0, 0, 128, true); } } if (tileX < this.maxTileX && tileZ < this.maxTileZ) { const tile: Ground | null = this.levelTiles[level][tileX + 1][tileZ + 1]; - if (tile && tile.groundDecoration && tile.groundDecoration.model && tile.groundDecoration.model.vertexNormal) { - this.mergeNormals(model, tile.groundDecoration.model, 128, 0, 128, true); + if (tile && tile.groundDecor && tile.groundDecor.model && tile.groundDecor.model.vertexNormal) { + this.mergeNormals(model, tile.groundDecor.model, 128, 0, 128, true); } } if (tileX < this.maxTileX && tileZ > 0) { const tile: Ground | null = this.levelTiles[level][tileX + 1][tileZ - 1]; - if (tile && tile.groundDecoration && tile.groundDecoration.model && tile.groundDecoration.model.vertexNormal) { - this.mergeNormals(model, tile.groundDecoration.model, 128, 0, -128, true); + if (tile && tile.groundDecor && tile.groundDecor.model && tile.groundDecor.model.vertexNormal) { + this.mergeNormals(model, tile.groundDecor.model, 128, 0, -128, true); } } } @@ -972,6 +1027,9 @@ export default class World3D { } draw(eyeX: number, eyeY: number, eyeZ: number, topLevel: number, eyeYaw: number, eyePitch: number, loopCycle: number): void { + RendererWebGLC.cameraYaw = eyeYaw; + RendererWebGLC.cameraPitch = eyePitch; + if (eyeX < 0) { eyeX = 0; } else if (eyeX >= this.maxTileX * 128) { @@ -1038,7 +1096,7 @@ export default class World3D { } else { tile.groundVisible = false; tile.update = false; - tile.checkLocSpans = 0; + tile.checkLocSpans = LocSpans.NONE; } } } @@ -1185,18 +1243,18 @@ export default class World3D { const loc: Location = new Location(level, y, x, z, model, entity, yaw, tileX, tileX + tileSizeX - 1, tileZ, tileZ + tileSizeZ - 1, typecode, info); for (let tx: number = tileX; tx < tileX + tileSizeX; tx++) { for (let tz: number = tileZ; tz < tileZ + tileSizeZ; tz++) { - let spans: number = 0; + let spans: number = LocSpans.NONE; if (tx > tileX) { - spans |= 0x1; + spans |= LocSpans.WEST; } if (tx < tileX + tileSizeX - 1) { - spans += 0x4; + spans += LocSpans.EAST; } if (tz > tileZ) { - spans += 0x8; + spans += LocSpans.SOUTH; } if (tz < tileZ + tileSizeZ - 1) { - spans += 0x2; + spans += LocSpans.NORTH; } for (let l: number = level; l >= 0; l--) { if (!this.levelTiles[l][tx][tz]) { @@ -1238,7 +1296,7 @@ export default class World3D { } } - tile.locSpans = 0; + tile.locSpans = LocSpans.NONE; for (let i: number = 0; i < tile.locCount; i++) { tile.locSpans |= tile.locSpan[i]; @@ -1261,7 +1319,7 @@ export default class World3D { let deltaMinTileZ: number; let deltaMaxTileZ: number; let deltaMaxTileX: number; - if (occluder.type === 1) { + if (occluder.type === OccludeType.HORIZONTAL) { deltaMaxY = occluder.minTileX + 25 - World3D.eyeTileX; if (deltaMaxY >= 0 && deltaMaxY <= 50) { deltaMinTileZ = occluder.minTileZ + 25 - World3D.eyeTileZ; @@ -1282,12 +1340,12 @@ export default class World3D { if (ok) { deltaMaxTileX = World3D.eyeX - occluder.minX; if (deltaMaxTileX > 32) { - occluder.mode = 1; + occluder.mode = OccludeMode.FRONT; } else { if (deltaMaxTileX >= -32) { continue; } - occluder.mode = 2; + occluder.mode = OccludeMode.BACK; deltaMaxTileX = -deltaMaxTileX; } occluder.minDeltaZ = (((occluder.minZ - World3D.eyeZ) << 8) / deltaMaxTileX) | 0; @@ -1297,7 +1355,7 @@ export default class World3D { World3D.activeOccluders[World3D.activeOccluderCount++] = occluder; } } - } else if (occluder.type === 2) { + } else if (occluder.type === OccludeType.VERTICAL) { deltaMaxY = occluder.minTileZ + 25 - World3D.eyeTileZ; if (deltaMaxY >= 0 && deltaMaxY <= 50) { deltaMinTileZ = occluder.minTileX + 25 - World3D.eyeTileX; @@ -1318,12 +1376,12 @@ export default class World3D { if (ok) { deltaMaxTileX = World3D.eyeZ - occluder.minZ; if (deltaMaxTileX > 32) { - occluder.mode = 3; + occluder.mode = OccludeMode.RIGHT; } else { if (deltaMaxTileX >= -32) { continue; } - occluder.mode = 4; + occluder.mode = OccludeMode.LEFT; deltaMaxTileX = -deltaMaxTileX; } occluder.minDeltaX = (((occluder.minX - World3D.eyeX) << 8) / deltaMaxTileX) | 0; @@ -1333,7 +1391,7 @@ export default class World3D { World3D.activeOccluders[World3D.activeOccluderCount++] = occluder; } } - } else if (occluder.type === 4) { + } else if (occluder.type === OccludeType.FLAT) { deltaMaxY = occluder.minY - World3D.eyeY; if (deltaMaxY > 128) { deltaMinTileZ = occluder.minTileZ + 25 - World3D.eyeTileZ; @@ -1363,7 +1421,7 @@ export default class World3D { } } if (ok) { - occluder.mode = 5; + occluder.mode = OccludeMode.ABOVE; occluder.minDeltaX = (((occluder.minX - World3D.eyeX) << 8) / deltaMaxY) | 0; occluder.maxDeltaX = (((occluder.maxX - World3D.eyeX) << 8) / deltaMaxY) | 0; occluder.minDeltaZ = (((occluder.minZ - World3D.eyeZ) << 8) / deltaMaxY) | 0; @@ -1410,7 +1468,7 @@ export default class World3D { if (tileX <= World3D.eyeTileX && tileX > World3D.minDrawTileX) { const adjacent: Ground | null = tiles[tileX - 1][tileZ]; - if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & 0x1) === 0)) { + if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & LocSpans.WEST) === LocSpans.NONE)) { continue; } } @@ -1418,7 +1476,7 @@ export default class World3D { if (tileX >= World3D.eyeTileX && tileX < World3D.maxDrawTileX - 1) { const adjacent: Ground | null = tiles[tileX + 1][tileZ]; - if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & 0x4) === 0)) { + if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & LocSpans.EAST) === LocSpans.NONE)) { continue; } } @@ -1426,7 +1484,7 @@ export default class World3D { if (tileZ <= World3D.eyeTileZ && tileZ > World3D.minDrawTileZ) { const adjacent: Ground | null = tiles[tileX][tileZ - 1]; - if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & 0x8) === 0)) { + if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & LocSpans.SOUTH) === LocSpans.NONE)) { continue; } } @@ -1434,7 +1492,7 @@ export default class World3D { if (tileZ >= World3D.eyeTileZ && tileZ < World3D.maxDrawTileZ - 1) { const adjacent: Ground | null = tiles[tileX][tileZ + 1]; - if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & 0x2) === 0)) { + if (adjacent && adjacent.update && (adjacent.groundVisible || (tile.locSpans & LocSpans.NORTH) === LocSpans.NONE)) { continue; } } @@ -1488,7 +1546,7 @@ export default class World3D { let frontWallTypes: number = 0; const wall: Wall | null = tile.wall; - const decor: Decor | null = tile.wallDecoration; + const decor: Decor | null = tile.decor; if (wall || decor) { if (World3D.eyeTileX === tileX) { @@ -1508,43 +1566,43 @@ export default class World3D { } if (wall) { - if ((wall.typeA & World3D.DIRECTION_ALLOW_WALL_CORNER_TYPE[direction]) === 0) { - tile.checkLocSpans = 0; - } else if (wall.typeA === 16) { - tile.checkLocSpans = 3; - tile.blockLocSpans = World3D.WALL_CORNER_TYPE_16_BLOCK_LOC_SPANS[direction]; - tile.inverseBlockLocSpans = 3 - tile.blockLocSpans; - } else if (wall.typeA === 32) { - tile.checkLocSpans = 6; - tile.blockLocSpans = World3D.WALL_CORNER_TYPE_32_BLOCK_LOC_SPANS[direction]; - tile.inverseBlockLocSpans = 6 - tile.blockLocSpans; - } else if (wall.typeA === 64) { - tile.checkLocSpans = 12; - tile.blockLocSpans = World3D.WALL_CORNER_TYPE_64_BLOCK_LOC_SPANS[direction]; - tile.inverseBlockLocSpans = 12 - tile.blockLocSpans; + if ((wall.spansA & World3D.DIRECTION_ALLOW_WALL_CORNER_TYPE[direction]) === LocSpans.NONE) { + tile.checkLocSpans = LocSpans.NONE; + } else if (wall.spansA === LocSpans.CORNER_WEST) { + tile.checkLocSpans = LocSpans.NONE | LocSpans.WEST | LocSpans.NORTH; + tile.blockLocSpans = World3D.WALL_CORNER_TYPE_16_BLOCK_LOC_SPANS[direction]; // northwest basically + tile.inverseBlockLocSpans = (LocSpans.NONE | LocSpans.WEST | LocSpans.NORTH) - tile.blockLocSpans; + } else if (wall.spansA === LocSpans.CORNER_NORTH) { + tile.checkLocSpans = LocSpans.NONE | LocSpans.NORTH | LocSpans.EAST; + tile.blockLocSpans = World3D.WALL_CORNER_TYPE_32_BLOCK_LOC_SPANS[direction]; // northeast basically + tile.inverseBlockLocSpans = (LocSpans.NONE | LocSpans.NORTH | LocSpans.EAST) - tile.blockLocSpans; + } else if (wall.spansA === LocSpans.CORNER_EAST) { + tile.checkLocSpans = LocSpans.NONE | LocSpans.EAST | LocSpans.SOUTH; + tile.blockLocSpans = World3D.WALL_CORNER_TYPE_64_BLOCK_LOC_SPANS[direction]; // southeast basically + tile.inverseBlockLocSpans = (LocSpans.NONE | LocSpans.EAST | LocSpans.SOUTH) - tile.blockLocSpans; } else { - tile.checkLocSpans = 9; - tile.blockLocSpans = World3D.WALL_CORNER_TYPE_128_BLOCK_LOC_SPANS[direction]; - tile.inverseBlockLocSpans = 9 - tile.blockLocSpans; + tile.checkLocSpans = LocSpans.NONE | LocSpans.WEST | LocSpans.SOUTH; + tile.blockLocSpans = World3D.WALL_CORNER_TYPE_128_BLOCK_LOC_SPANS[direction]; // southwest basically + tile.inverseBlockLocSpans = (LocSpans.NONE | LocSpans.WEST | LocSpans.SOUTH) - tile.blockLocSpans; } - if ((wall.typeA & frontWallTypes) !== 0 && !this.wallVisible(occludeLevel, tileX, tileZ, wall.typeA)) { + if ((wall.spansA & frontWallTypes) !== LocSpans.NONE && !this.wallVisible(occludeLevel, tileX, tileZ, wall.spansA)) { wall.modelA?.draw(0, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, wall.x - World3D.eyeX, wall.y - World3D.eyeY, wall.z - World3D.eyeZ, wall.typecode); } - if ((wall.typeB & frontWallTypes) !== 0 && !this.wallVisible(occludeLevel, tileX, tileZ, wall.typeB)) { + if ((wall.spansB & frontWallTypes) !== LocSpans.NONE && !this.wallVisible(occludeLevel, tileX, tileZ, wall.spansB)) { wall.modelB?.draw(0, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, wall.x - World3D.eyeX, wall.y - World3D.eyeY, wall.z - World3D.eyeZ, wall.typecode); } } if (decor && !this.visible(occludeLevel, tileX, tileZ, decor.model.maxY)) { - if ((decor.decorType & frontWallTypes) !== 0) { - decor.model.draw(decor.decorAngle, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, decor.x - World3D.eyeX, decor.y - World3D.eyeY, decor.z - World3D.eyeZ, decor.typecode); - } else if ((decor.decorType & 0x300) !== 0) { + if ((decor.spans & frontWallTypes) !== LocSpans.NONE) { + decor.model.draw(decor.angle, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, decor.x - World3D.eyeX, decor.y - World3D.eyeY, decor.z - World3D.eyeZ, decor.typecode); + } else if ((decor.spans & LocSpans.DECOR_BOTH) !== LocSpans.NONE) { const x: number = decor.x - World3D.eyeX; const y: number = decor.y - World3D.eyeY; const z: number = decor.z - World3D.eyeZ; - const angle: number = decor.decorAngle; + const angle: number = decor.angle; let nearestX: number; if (angle === LocAngle.NORTH || angle === LocAngle.EAST) { @@ -1560,13 +1618,13 @@ export default class World3D { nearestZ = z; } - if ((decor.decorType & 0x100) !== 0 && nearestZ < nearestX) { + if ((decor.spans & LocSpans.DECOR_OFFSET) !== LocSpans.NONE && nearestZ < nearestX) { const drawX: number = x + World3D.WALL_DECORATION_INSET_X[angle]; const drawZ: number = z + World3D.WALL_DECORATION_INSET_Z[angle]; decor.model.draw(angle * 512 + 256, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, drawX, y, drawZ, decor.typecode); } - if ((decor.decorType & 0x200) !== 0 && nearestZ > nearestX) { + if ((decor.spans & LocSpans.DECOR_NOOFFSET) !== LocSpans.NONE && nearestZ > nearestX) { const drawX: number = x + World3D.WALL_DECORATION_OUTSET_X[angle]; const drawZ: number = z + World3D.WALL_DECORATION_OUTSET_Z[angle]; decor.model.draw((angle * 512 + 1280) & 0x7ff, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, drawX, y, drawZ, decor.typecode); @@ -1575,7 +1633,7 @@ export default class World3D { } if (tileDrawn) { - const groundDecor: GroundDecor | null = tile.groundDecoration; + const groundDecor: GroundDecor | null = tile.groundDecor; if (groundDecor) { groundDecor.model?.draw(0, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, groundDecor.x - World3D.eyeX, groundDecor.y - World3D.eyeY, groundDecor.z - World3D.eyeZ, groundDecor.typecode); } @@ -1598,29 +1656,29 @@ export default class World3D { const spans: number = tile.locSpans; - if (spans !== 0) { - if (tileX < World3D.eyeTileX && (spans & 0x4) !== 0) { + if (spans !== LocSpans.NONE) { + if (tileX < World3D.eyeTileX && (spans & LocSpans.EAST) !== LocSpans.NONE) { const adjacent: Ground | null = tiles[tileX + 1][tileZ]; if (adjacent && adjacent.update) { World3D.drawTileQueue.addTail(adjacent); } } - if (tileZ < World3D.eyeTileZ && (spans & 0x2) !== 0) { + if (tileZ < World3D.eyeTileZ && (spans & LocSpans.NORTH) !== LocSpans.NONE) { const adjacent: Ground | null = tiles[tileX][tileZ + 1]; if (adjacent && adjacent.update) { World3D.drawTileQueue.addTail(adjacent); } } - if (tileX > World3D.eyeTileX && (spans & 0x1) !== 0) { + if (tileX > World3D.eyeTileX && (spans & LocSpans.WEST) !== LocSpans.NONE) { const adjacent: Ground | null = tiles[tileX - 1][tileZ]; if (adjacent && adjacent.update) { World3D.drawTileQueue.addTail(adjacent); } } - if (tileZ > World3D.eyeTileZ && (spans & 0x8) !== 0) { + if (tileZ > World3D.eyeTileZ && (spans & LocSpans.SOUTH) !== LocSpans.NONE) { const adjacent: Ground | null = tiles[tileX][tileZ - 1]; if (adjacent && adjacent.update) { World3D.drawTileQueue.addTail(adjacent); @@ -1629,7 +1687,7 @@ export default class World3D { } } - if (tile.checkLocSpans !== 0) { + if (tile.checkLocSpans !== LocSpans.NONE) { let draw: boolean = true; for (let i: number = 0; i < tile.locCount; i++) { const loc: Location | null = tile.locs[i]; @@ -1645,11 +1703,11 @@ export default class World3D { if (draw) { const wall: Wall | null = tile.wall; - if (wall && !this.wallVisible(occludeLevel, tileX, tileZ, wall.typeA)) { + if (wall && !this.wallVisible(occludeLevel, tileX, tileZ, wall.spansA)) { wall.modelA?.draw(0, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, wall.x - World3D.eyeX, wall.y - World3D.eyeY, wall.z - World3D.eyeZ, wall.typecode); } - tile.checkLocSpans = 0; + tile.checkLocSpans = LocSpans.NONE; } } @@ -1674,26 +1732,26 @@ export default class World3D { } if (!other.groundVisible) { - if (other.checkLocSpans === 0) { + if (other.checkLocSpans === LocSpans.NONE) { continue; } - let spans: number = 0; + let spans: number = LocSpans.NONE; if (x > loc.minSceneTileX) { - spans += 1; + spans += LocSpans.WEST; } if (x < loc.maxSceneTileX) { - spans += 4; + spans += LocSpans.EAST; } if (z > loc.minSceneTileZ) { - spans += 8; + spans += LocSpans.SOUTH; } if (z < loc.maxSceneTileZ) { - spans += 2; + spans += LocSpans.NORTH; } if ((spans & other.checkLocSpans) !== tile.inverseBlockLocSpans) { @@ -1768,7 +1826,7 @@ export default class World3D { continue; } - if (occupied.checkLocSpans !== 0) { + if (occupied.checkLocSpans !== LocSpans.NONE) { World3D.drawTileQueue.addTail(occupied); } else if ((x !== tileX || z !== tileZ) && occupied.update) { World3D.drawTileQueue.addTail(occupied); @@ -1783,7 +1841,7 @@ export default class World3D { } } - if (!tile.update || tile.checkLocSpans !== 0) { + if (!tile.update || tile.checkLocSpans !== LocSpans.NONE) { continue; } @@ -1833,17 +1891,17 @@ export default class World3D { } } - if (tile.backWallTypes !== 0) { - const decor: Decor | null = tile.wallDecoration; + if (tile.backWallTypes !== LocSpans.NONE) { + const decor: Decor | null = tile.decor; if (decor && !this.visible(occludeLevel, tileX, tileZ, decor.model.maxY)) { - if ((decor.decorType & tile.backWallTypes) !== 0) { - decor.model.draw(decor.decorAngle, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, decor.x - World3D.eyeX, decor.y - World3D.eyeY, decor.z - World3D.eyeZ, decor.typecode); - } else if ((decor.decorType & 0x300) !== 0) { + if ((decor.spans & tile.backWallTypes) !== LocSpans.NONE) { + decor.model.draw(decor.angle, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, decor.x - World3D.eyeX, decor.y - World3D.eyeY, decor.z - World3D.eyeZ, decor.typecode); + } else if ((decor.spans & LocSpans.DECOR_BOTH) !== LocSpans.NONE) { const x: number = decor.x - World3D.eyeX; const y: number = decor.y - World3D.eyeY; const z: number = decor.z - World3D.eyeZ; - const angle: number = decor.decorAngle; + const angle: number = decor.angle; let nearestX: number; if (angle === LocAngle.NORTH || angle === LocAngle.EAST) { @@ -1859,13 +1917,13 @@ export default class World3D { nearestZ = z; } - if ((decor.decorType & 0x100) !== 0 && nearestZ >= nearestX) { + if ((decor.spans & LocSpans.DECOR_OFFSET) !== LocSpans.NONE && nearestZ >= nearestX) { const drawX: number = x + World3D.WALL_DECORATION_INSET_X[angle]; const drawZ: number = z + World3D.WALL_DECORATION_INSET_Z[angle]; decor.model.draw(angle * 512 + 256, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, drawX, y, drawZ, decor.typecode); } - if ((decor.decorType & 0x200) !== 0 && nearestZ <= nearestX) { + if ((decor.spans & LocSpans.DECOR_NOOFFSET) !== LocSpans.NONE && nearestZ <= nearestX) { const drawX: number = x + World3D.WALL_DECORATION_OUTSET_X[angle]; const drawZ: number = z + World3D.WALL_DECORATION_OUTSET_Z[angle]; decor.model.draw((angle * 512 + 1280) & 0x7ff, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, drawX, y, drawZ, decor.typecode); @@ -1875,11 +1933,11 @@ export default class World3D { const wall: Wall | null = tile.wall; if (wall) { - if ((wall.typeB & tile.backWallTypes) !== 0 && !this.wallVisible(occludeLevel, tileX, tileZ, wall.typeB)) { + if ((wall.spansB & tile.backWallTypes) !== LocSpans.NONE && !this.wallVisible(occludeLevel, tileX, tileZ, wall.spansB)) { wall.modelB?.draw(0, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, wall.x - World3D.eyeX, wall.y - World3D.eyeY, wall.z - World3D.eyeZ, wall.typecode); } - if ((wall.typeA & tile.backWallTypes) !== 0 && !this.wallVisible(occludeLevel, tileX, tileZ, wall.typeA)) { + if ((wall.spansA & tile.backWallTypes) !== LocSpans.NONE && !this.wallVisible(occludeLevel, tileX, tileZ, wall.spansA)) { wall.modelA?.draw(0, World3D.sinEyePitch, World3D.cosEyePitch, World3D.sinEyeYaw, World3D.cosEyeYaw, wall.x - World3D.eyeX, wall.y - World3D.eyeY, wall.z - World3D.eyeZ, wall.typecode); } } @@ -1923,6 +1981,8 @@ export default class World3D { } private drawTileUnderlay(underlay: TileUnderlay, level: number, tileX: number, tileZ: number, sinEyePitch: number, cosEyePitch: number, sinEyeYaw: number, cosEyeYaw: number): void { + const rendererEnabled: boolean = Renderer.drawTileUnderlay(this, underlay, level, tileX, tileZ); + let x3: number; let x0: number = (x3 = (tileX << 7) - World3D.eyeX); let z1: number; @@ -2002,36 +2062,44 @@ export default class World3D { World3D.clickTileX = tileX; World3D.clickTileZ = tileZ; } - if (underlay.textureId === -1) { - if (underlay.northeastColor !== 12345678) { - Pix3D.fillGouraudTriangle(py1, px3, pz0, pz1, py3, px1, underlay.northeastColor, underlay.northwestColor, underlay.southeastColor); + + if (!rendererEnabled) { + if (underlay.textureId === -1) { + if (underlay.northeastColor !== 12345678) { + Pix3D.fillGouraudTriangle(py1, px3, pz0, pz1, py3, px1, underlay.northeastColor, underlay.northwestColor, underlay.southeastColor); + } + } else if (World3D.lowMemory) { + const averageColor: number = World3D.TEXTURE_HSL[underlay.textureId]; + Pix3D.fillGouraudTriangle(py1, px3, pz0, pz1, py3, px1, this.mulLightness(averageColor, underlay.northeastColor), this.mulLightness(averageColor, underlay.northwestColor), this.mulLightness(averageColor, underlay.southeastColor)); + } else if (underlay.flat) { + Pix3D.fillTexturedTriangle(py1, px3, pz0, pz1, py3, px1, underlay.northeastColor, underlay.northwestColor, underlay.southeastColor, x0, y0, z0, x1, x3, y1, y3, z1, z3, underlay.textureId); + } else { + Pix3D.fillTexturedTriangle(py1, px3, pz0, pz1, py3, px1, underlay.northeastColor, underlay.northwestColor, underlay.southeastColor, x2, y2, z2, x3, x1, y3, y1, z3, z1, underlay.textureId); } - } else if (World3D.lowMemory) { - const averageColor: number = World3D.TEXTURE_HSL[underlay.textureId]; - Pix3D.fillGouraudTriangle(py1, px3, pz0, pz1, py3, px1, this.mulLightness(averageColor, underlay.northeastColor), this.mulLightness(averageColor, underlay.northwestColor), this.mulLightness(averageColor, underlay.southeastColor)); - } else if (underlay.flat) { - Pix3D.fillTexturedTriangle(py1, px3, pz0, pz1, py3, px1, underlay.northeastColor, underlay.northwestColor, underlay.southeastColor, x0, y0, z0, x1, x3, y1, y3, z1, z3, underlay.textureId); - } else { - Pix3D.fillTexturedTriangle(py1, px3, pz0, pz1, py3, px1, underlay.northeastColor, underlay.northwestColor, underlay.southeastColor, x2, y2, z2, x3, x1, y3, y1, z3, z1, underlay.textureId); } } + if ((px0 - pz0) * (py3 - px1) - (py0 - px1) * (px3 - pz0) <= 0) { return; } + Pix3D.clipX = px0 < 0 || pz0 < 0 || px3 < 0 || px0 > Pix2D.boundX || pz0 > Pix2D.boundX || px3 > Pix2D.boundX; if (World3D.takingInput && this.pointInsideTriangle(World3D.mouseX, World3D.mouseY, py0, px1, py3, px0, pz0, px3)) { World3D.clickTileX = tileX; World3D.clickTileZ = tileZ; } - if (underlay.textureId !== -1) { - if (!World3D.lowMemory) { - Pix3D.fillTexturedTriangle(px0, pz0, px3, py0, px1, py3, underlay.southwestColor, underlay.southeastColor, underlay.northwestColor, x0, y0, z0, x1, x3, y1, y3, z1, z3, underlay.textureId); - return; + + if (!rendererEnabled) { + if (underlay.textureId !== -1) { + if (!World3D.lowMemory) { + Pix3D.fillTexturedTriangle(px0, pz0, px3, py0, px1, py3, underlay.southwestColor, underlay.southeastColor, underlay.northwestColor, x0, y0, z0, x1, x3, y1, y3, z1, z3, underlay.textureId); + return; + } + const averageColor: number = World3D.TEXTURE_HSL[underlay.textureId]; + Pix3D.fillGouraudTriangle(px0, pz0, px3, py0, px1, py3, this.mulLightness(averageColor, underlay.southwestColor), this.mulLightness(averageColor, underlay.southeastColor), this.mulLightness(averageColor, underlay.northwestColor)); + } else if (underlay.southwestColor !== 12345678) { + Pix3D.fillGouraudTriangle(px0, pz0, px3, py0, px1, py3, underlay.southwestColor, underlay.southeastColor, underlay.northwestColor); } - const averageColor: number = World3D.TEXTURE_HSL[underlay.textureId]; - Pix3D.fillGouraudTriangle(px0, pz0, px3, py0, px1, py3, this.mulLightness(averageColor, underlay.southwestColor), this.mulLightness(averageColor, underlay.southeastColor), this.mulLightness(averageColor, underlay.northwestColor)); - } else if (underlay.southwestColor !== 12345678) { - Pix3D.fillGouraudTriangle(px0, pz0, px3, py0, px1, py3, underlay.southwestColor, underlay.southeastColor, underlay.northwestColor); } } @@ -2064,6 +2132,7 @@ export default class World3D { TileOverlay.tmpScreenY[i] = Pix3D.centerY + (((y << 9) / z) | 0); } + const rendererEnabled: boolean = Renderer.drawTileOverlay(this, overlay, tileX, tileZ); Pix3D.alpha = 0; vertexCount = overlay.triangleVertexA.length; @@ -2085,6 +2154,11 @@ export default class World3D { World3D.clickTileX = tileX; World3D.clickTileZ = tileZ; } + + if (rendererEnabled) { + continue; + } + if (!overlay.triangleTextureIds || overlay.triangleTextureIds[v] === -1) { if (overlay.triangleColorA[v] !== 12345678) { Pix3D.fillGouraudTriangle(x0, x1, x2, y0, y1, y2, overlay.triangleColorA[v], overlay.triangleColorB[v], overlay.triangleColorC[v]); @@ -2165,7 +2239,7 @@ export default class World3D { } } - private wallVisible(level: number, x: number, z: number, type: number): boolean { + private wallVisible(level: number, x: number, z: number, spans: number): boolean { if (!this.tileVisible(level, x, z)) { return false; } @@ -2175,8 +2249,8 @@ export default class World3D { const y0: number = sceneY - 120; const y1: number = sceneY - 230; const y2: number = sceneY - 238; - if (type < 16) { - if (type === 1) { + if (spans < LocSpans.CORNER_WEST) { + if (spans === LocSpans.WEST) { if (sceneX > World3D.eyeX) { if (!this.occluded(sceneX, sceneY, sceneZ)) { return false; @@ -2198,7 +2272,7 @@ export default class World3D { } return this.occluded(sceneX, y1, sceneZ + 128); } - if (type === 2) { + if (spans === LocSpans.NORTH) { if (sceneZ < World3D.eyeZ) { if (!this.occluded(sceneX, sceneY, sceneZ + 128)) { return false; @@ -2220,7 +2294,7 @@ export default class World3D { } return this.occluded(sceneX + 128, y1, sceneZ + 128); } - if (type === 4) { + if (spans === LocSpans.EAST) { if (sceneX < World3D.eyeX) { if (!this.occluded(sceneX + 128, sceneY, sceneZ)) { return false; @@ -2242,7 +2316,7 @@ export default class World3D { } return this.occluded(sceneX + 128, y1, sceneZ + 128); } - if (type === 8) { + if (spans === LocSpans.SOUTH) { if (sceneZ > World3D.eyeZ) { if (!this.occluded(sceneX, sceneY, sceneZ)) { return false; @@ -2267,13 +2341,13 @@ export default class World3D { } if (!this.occluded(sceneX + 64, y2, sceneZ + 64)) { return false; - } else if (type === 16) { + } else if (spans === LocSpans.CORNER_WEST) { return this.occluded(sceneX, y1, sceneZ + 128); - } else if (type === 32) { + } else if (spans === LocSpans.CORNER_NORTH) { return this.occluded(sceneX + 128, y1, sceneZ + 128); - } else if (type === 64) { + } else if (spans === LocSpans.CORNER_EAST) { return this.occluded(sceneX + 128, y1, sceneZ); - } else if (type === 128) { + } else if (spans === LocSpans.CORNER_SOUTH) { return this.occluded(sceneX, y1, sceneZ); } console.warn('Warning unsupported wall type!'); @@ -2339,7 +2413,7 @@ export default class World3D { continue; } - if (occluder.mode === 1) { + if (occluder.mode === OccludeMode.FRONT) { const dx: number = occluder.minX - x; if (dx > 0) { const minZ: number = occluder.minZ + ((occluder.minDeltaZ * dx) >> 8); @@ -2350,7 +2424,7 @@ export default class World3D { return true; } } - } else if (occluder.mode === 2) { + } else if (occluder.mode === OccludeMode.BACK) { const dx: number = x - occluder.minX; if (dx > 0) { const minZ: number = occluder.minZ + ((occluder.minDeltaZ * dx) >> 8); @@ -2361,7 +2435,7 @@ export default class World3D { return true; } } - } else if (occluder.mode === 3) { + } else if (occluder.mode === OccludeMode.RIGHT) { const dz: number = occluder.minZ - z; if (dz > 0) { const minX: number = occluder.minX + ((occluder.minDeltaX * dz) >> 8); @@ -2372,7 +2446,7 @@ export default class World3D { return true; } } - } else if (occluder.mode === 4) { + } else if (occluder.mode === OccludeMode.LEFT) { const dz: number = z - occluder.minZ; if (dz > 0) { const minX: number = occluder.minX + ((occluder.minDeltaX * dz) >> 8); @@ -2383,7 +2457,7 @@ export default class World3D { return true; } } - } else if (occluder.mode === 5) { + } else if (occluder.mode === OccludeMode.ABOVE) { const dy: number = y - occluder.minY; if (dy > 0) { const minX: number = occluder.minX + ((occluder.minDeltaX * dy) >> 8); diff --git a/src/dash3d/entity/LocEntity.ts b/src/dash3d/entity/LocEntity.ts index 8be3693..1d677b0 100644 --- a/src/dash3d/entity/LocEntity.ts +++ b/src/dash3d/entity/LocEntity.ts @@ -3,21 +3,21 @@ import SeqType from '#/config/SeqType.js'; import Linkable from '#/datastruct/Linkable.js'; export default class LocEntity extends Linkable { - heightmapSW: number; - readonly heightmapSE: number; - readonly heightmapNE: number; - readonly heightmapNW: number; + level: number; + readonly layer: number; + readonly x: number; + readonly z: number; readonly index: number; readonly seq: SeqType; seqFrame: number; seqCycle: number; - constructor(index: number, heightmapSW: number, heightmapSE: number, heightmapNE: number, heightmapNW: number, seq: SeqType, randomFrame: boolean) { + constructor(index: number, level: number, layer: number, x: number, z: number, seq: SeqType, randomFrame: boolean) { super(); - this.heightmapSW = heightmapSW; - this.heightmapSE = heightmapSE; - this.heightmapNE = heightmapNE; - this.heightmapNW = heightmapNW; + this.level = level; + this.layer = layer; + this.x = x; + this.z = z; this.index = index; this.seq = seq; diff --git a/src/dash3d/entity/NpcEntity.ts b/src/dash3d/entity/NpcEntity.ts index f120f5d..c36b6e3 100644 --- a/src/dash3d/entity/NpcEntity.ts +++ b/src/dash3d/entity/NpcEntity.ts @@ -52,7 +52,7 @@ export default class NpcEntity extends PathingEntity { const tmp: Model = Model.modelFromModelsBounds(models, 2); if (this.npcType.size === 1) { - tmp.pickable = true; + tmp.pickAabb = true; } return tmp; diff --git a/src/dash3d/entity/PlayerEntity.ts b/src/dash3d/entity/PlayerEntity.ts index adec348..fc8f01a 100644 --- a/src/dash3d/entity/PlayerEntity.ts +++ b/src/dash3d/entity/PlayerEntity.ts @@ -222,7 +222,7 @@ export default class PlayerEntity extends PathingEntity { let model: Model = this.getSequencedModel(); this.maxY = model.maxY; - model.pickable = true; + model.pickAabb = true; if (this.lowMemory) { return model; @@ -285,7 +285,7 @@ export default class PlayerEntity extends PathingEntity { } } - model.pickable = true; + model.pickAabb = true; return model; } diff --git a/src/dash3d/type/Decor.ts b/src/dash3d/type/Decor.ts index 93d5039..4bb2ee7 100644 --- a/src/dash3d/type/Decor.ts +++ b/src/dash3d/type/Decor.ts @@ -4,18 +4,18 @@ export default class Decor { readonly y: number; x: number; z: number; - readonly decorType: number; - readonly decorAngle: number; + readonly spans: number; + readonly angle: number; model: Model; readonly typecode: number; readonly info: number; // byte - constructor(y: number, x: number, z: number, type: number, angle: number, model: Model, typecode: number, info: number) { + constructor(y: number, x: number, z: number, spans: number, angle: number, model: Model, typecode: number, info: number) { this.y = y; this.x = x; this.z = z; - this.decorType = type; - this.decorAngle = angle; + this.spans = spans; + this.angle = angle; this.model = model; this.typecode = typecode; this.info = info; diff --git a/src/dash3d/type/Ground.ts b/src/dash3d/type/Ground.ts index 31a43c5..dfdcb6f 100644 --- a/src/dash3d/type/Ground.ts +++ b/src/dash3d/type/Ground.ts @@ -1,7 +1,7 @@ import Linkable from '#/datastruct/Linkable.js'; import GroundDecor from '#/dash3d/type/GroundDecor.js'; -import Location from '#/dash3d/type/Loc.js'; +import Location from '#/dash3d/type/Location.js'; import ObjStack from '#/dash3d/type/ObjStack.js'; import TileOverlay from '#/dash3d/type/TileOverlay.js'; import TileUnderlay from '#/dash3d/type/TileUnderlay.js'; @@ -9,6 +9,7 @@ import Wall from '#/dash3d/type/Wall.js'; import Decor from '#/dash3d/type/Decor.js'; import { TypedArray1d } from '#/util/Arrays.js'; +import { LocSpans } from '#/dash3d/LocSpans.ts'; export default class Ground extends Linkable { // constructor @@ -23,20 +24,20 @@ export default class Ground extends Linkable { underlay: TileUnderlay | null = null; overlay: TileOverlay | null = null; wall: Wall | null = null; - wallDecoration: Decor | null = null; - groundDecoration: GroundDecor | null = null; + decor: Decor | null = null; + groundDecor: GroundDecor | null = null; objStack: ObjStack | null = null; bridge: Ground | null = null; locCount: number = 0; - locSpans: number = 0; + locSpans: number = LocSpans.NONE; drawLevel: number = 0; groundVisible: boolean = false; update: boolean = false; containsLocs: boolean = false; - checkLocSpans: number = 0; - blockLocSpans: number = 0; - inverseBlockLocSpans: number = 0; - backWallTypes: number = 0; + checkLocSpans: number = LocSpans.NONE; + blockLocSpans: number = LocSpans.NONE; + inverseBlockLocSpans: number = LocSpans.NONE; + backWallTypes: number = LocSpans.NONE; constructor(level: number, x: number, z: number) { super(); diff --git a/src/dash3d/type/Loc.ts b/src/dash3d/type/Location.ts similarity index 96% rename from src/dash3d/type/Loc.ts rename to src/dash3d/type/Location.ts index 5e724ec..6acfb0f 100644 --- a/src/dash3d/type/Loc.ts +++ b/src/dash3d/type/Location.ts @@ -1,40 +1,40 @@ -import Entity from '#/dash3d/entity/Entity.js'; - -import Model from '#/graphics/Model.js'; - -export default class Location { - // constructor - readonly locLevel: number; - readonly y: number; - readonly x: number; - readonly z: number; - model: Model | null; - readonly entity: Entity | null; - readonly yaw: number; - readonly minSceneTileX: number; - readonly maxSceneTileX: number; - readonly minSceneTileZ: number; - readonly maxSceneTileZ: number; - readonly typecode: number; - readonly info: number; // byte - - // runtime - distance: number = 0; - cycle: number = 0; - - constructor(level: number, y: number, x: number, z: number, model: Model | null, entity: Entity | null, yaw: number, minSceneTileX: number, maxSceneTileX: number, minSceneTileZ: number, maxSceneTileZ: number, typecode: number, info: number) { - this.locLevel = level; - this.y = y; - this.x = x; - this.z = z; - this.model = model; - this.entity = entity; - this.yaw = yaw; - this.minSceneTileX = minSceneTileX; - this.maxSceneTileX = maxSceneTileX; - this.minSceneTileZ = minSceneTileZ; - this.maxSceneTileZ = maxSceneTileZ; - this.typecode = typecode; - this.info = info; - } -} +import Entity from '#/dash3d/entity/Entity.js'; + +import Model from '#/graphics/Model.js'; + +export default class Location { + // constructor + readonly locLevel: number; + readonly y: number; + readonly x: number; + readonly z: number; + model: Model | null; + readonly entity: Entity | null; + readonly yaw: number; + readonly minSceneTileX: number; + readonly maxSceneTileX: number; + readonly minSceneTileZ: number; + readonly maxSceneTileZ: number; + readonly typecode: number; + readonly info: number; // byte + + // runtime + distance: number = 0; + cycle: number = 0; + + constructor(level: number, y: number, x: number, z: number, model: Model | null, entity: Entity | null, yaw: number, minSceneTileX: number, maxSceneTileX: number, minSceneTileZ: number, maxSceneTileZ: number, typecode: number, info: number) { + this.locLevel = level; + this.y = y; + this.x = x; + this.z = z; + this.model = model; + this.entity = entity; + this.yaw = yaw; + this.minSceneTileX = minSceneTileX; + this.maxSceneTileX = maxSceneTileX; + this.minSceneTileZ = minSceneTileZ; + this.maxSceneTileZ = maxSceneTileZ; + this.typecode = typecode; + this.info = info; + } +} diff --git a/src/dash3d/type/Wall.ts b/src/dash3d/type/Wall.ts index 5ab90de..54f4537 100644 --- a/src/dash3d/type/Wall.ts +++ b/src/dash3d/type/Wall.ts @@ -4,19 +4,19 @@ export default class Wall { readonly y: number; readonly x: number; readonly z: number; - readonly typeA: number; - readonly typeB: number; + readonly spansA: number; + readonly spansB: number; modelA: Model | null; modelB: Model | null; readonly typecode: number; readonly info: number; // byte - constructor(y: number, x: number, z: number, typeA: number, typeB: number, modelA: Model | null, modelB: Model | null, typecode: number, info: number) { + constructor(y: number, x: number, z: number, spansA: number, spansB: number, modelA: Model | null, modelB: Model | null, typecode: number, info: number) { this.y = y; this.x = x; this.z = z; - this.typeA = typeA; - this.typeB = typeB; + this.spansA = spansA; + this.spansB = spansB; this.modelA = modelA; this.modelB = modelB; this.typecode = typecode; diff --git a/src/datastruct/Linkable.ts b/src/datastruct/Linkable.ts index 6324739..982097a 100644 --- a/src/datastruct/Linkable.ts +++ b/src/datastruct/Linkable.ts @@ -4,7 +4,7 @@ export default class Linkable { prev: Linkable | null = null; unlink(): void { - if (this.prev != null) { + if (this.prev !== null) { this.prev.next = this.next; if (this.next) { this.next.prev = this.prev; diff --git a/src/graphics/Canvas.ts b/src/graphics/Canvas.ts index f33c0ca..4ec7e81 100644 --- a/src/graphics/Canvas.ts +++ b/src/graphics/Canvas.ts @@ -1,3 +1,6 @@ +export const canvasContainer: HTMLElement = document.getElementById('game')!; +export const canvasOverlay: HTMLElement = document.getElementById('canvas-overlay')!; + export const canvas: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement; export const canvas2d: CanvasRenderingContext2D = canvas.getContext('2d', { willReadFrequently: true })!; diff --git a/src/graphics/Model.ts b/src/graphics/Model.ts index c670ca9..4acfe0e 100644 --- a/src/graphics/Model.ts +++ b/src/graphics/Model.ts @@ -9,6 +9,7 @@ import Packet from '#/io/Packet.js'; import DoublyLinkable from '#/datastruct/DoublyLinkable.js'; import { Int32Array2d, TypedArray1d } from '#/util/Arrays.js'; +import { Renderer } from '#/graphics/renderer/Renderer.ts'; class Metadata { vertexCount: number = 0; @@ -1157,7 +1158,7 @@ export default class Model extends DoublyLinkable { // runtime objRaise: number = 0; - pickable: boolean = false; + pickAabb: boolean = false; pickedFace: number = -1; pickedFaceDepth: number = -1; @@ -1686,6 +1687,8 @@ export default class Model extends DoublyLinkable { // todo: better name, Java relies on overloads draw(yaw: number, sinEyePitch: number, cosEyePitch: number, sinEyeYaw: number, cosEyeYaw: number, relativeX: number, relativeY: number, relativeZ: number, typecode: number): void { + Renderer.startDrawModel(this, yaw, relativeX, relativeY, relativeZ, typecode); + const zPrime: number = (relativeZ * cosEyeYaw - relativeX * sinEyeYaw) >> 16; const midZ: number = (relativeY * sinEyePitch + zPrime * cosEyePitch) >> 16; const radiusCosEyePitch: number = (this.radius * cosEyePitch) >> 16; @@ -1750,7 +1753,7 @@ export default class Model extends DoublyLinkable { const mouseX: number = Model.mouseX - Pix3D.centerX; const mouseY: number = Model.mouseY - Pix3D.centerY; if (mouseX > leftX && mouseX < rightX && mouseY > topY && mouseY < bottomY) { - if (this.pickable) { + if (this.pickAabb) { Model.picked[Model.pickedCount++] = typecode; } else { picking = true; @@ -1817,6 +1820,8 @@ export default class Model extends DoublyLinkable { } catch (err) { /* empty */ } + + Renderer.endDrawModel(this, yaw, relativeX, relativeY, relativeZ, typecode); } // todo: better name, Java relies on overloads @@ -2091,6 +2096,10 @@ export default class Model extends DoublyLinkable { } private drawFace(face: number, wireframe: boolean = false): void { + if (Renderer.drawModelTriangle(this, face)) { + return; + } + if (Model.faceNearClipped && Model.faceNearClipped[face]) { this.drawNearClippedFace(face, wireframe); return; diff --git a/src/graphics/Pix2D.ts b/src/graphics/Pix2D.ts index deb33e1..53a2cfa 100644 --- a/src/graphics/Pix2D.ts +++ b/src/graphics/Pix2D.ts @@ -57,10 +57,10 @@ export default class Pix2D extends DoublyLinkable { this.centerY2d = (this.bottom / 2) | 0; } - static clear(): void { + static clear(v: number = 0): void { const len: number = this.width2d * this.height2d; for (let i: number = 0; i < len; i++) { - this.pixels[i] = 0; + this.pixels[i] = v; } } diff --git a/src/graphics/Pix3D.ts b/src/graphics/Pix3D.ts index 5b0988e..f92fb52 100644 --- a/src/graphics/Pix3D.ts +++ b/src/graphics/Pix3D.ts @@ -1,5 +1,6 @@ import Pix2D from '#/graphics/Pix2D.js'; import Pix8 from '#/graphics/Pix8.js'; +import { Renderer } from '#/graphics/renderer/Renderer.ts'; import Jagfile from '#/io/Jagfile.js'; import { Int32Array2d, TypedArray1d } from '#/util/Arrays.js'; @@ -33,7 +34,7 @@ export default class Pix3D extends Pix2D { static texPal: (Int32Array | null)[] = new TypedArray1d(50, null); private static opaque: boolean = false; - private static textureTranslucent: boolean[] = new TypedArray1d(50, false); + static textureTranslucent: boolean[] = new TypedArray1d(50, false); private static averageTextureRGB: Int32Array = new Int32Array(50); static { @@ -123,6 +124,67 @@ export default class Pix3D extends Pix2D { return rgb; } + static convertHsl(hue: number, saturation: number, lightness: number): number { + let r: number = lightness; + let g: number = lightness; + let b: number = lightness; + + if (saturation !== 0.0) { + let q: number; + if (lightness < 0.5) { + q = lightness * (saturation + 1.0); + } else { + q = lightness + saturation - lightness * saturation; + } + + const p: number = lightness * 2.0 - q; + let t: number = hue + 0.3333333333333333; + if (t > 1.0) { + t--; + } + + let d11: number = hue - 0.3333333333333333; + if (d11 < 0.0) { + d11++; + } + + if (t * 6.0 < 1.0) { + r = p + (q - p) * 6.0 * t; + } else if (t * 2.0 < 1.0) { + r = q; + } else if (t * 3.0 < 2.0) { + r = p + (q - p) * (0.6666666666666666 - t) * 6.0; + } else { + r = p; + } + + if (hue * 6.0 < 1.0) { + g = p + (q - p) * 6.0 * hue; + } else if (hue * 2.0 < 1.0) { + g = q; + } else if (hue * 3.0 < 2.0) { + g = p + (q - p) * (0.6666666666666666 - hue) * 6.0; + } else { + g = p; + } + + if (d11 * 6.0 < 1.0) { + b = p + (q - p) * 6.0 * d11; + } else if (d11 * 2.0 < 1.0) { + b = q; + } else if (d11 * 3.0 < 2.0) { + b = p + (q - p) * (0.6666666666666666 - d11) * 6.0; + } else { + b = p; + } + } + + const intR: number = (r * 256.0) | 0; + const intG: number = (g * 256.0) | 0; + const intB: number = (b * 256.0) | 0; + return (intR << 16) + (intG << 8) + intB; + } + static setBrightness(brightness: number): void { const randomBrightness: number = brightness + Math.random() * 0.03 - 0.015; let offset: number = 0; @@ -131,57 +193,7 @@ export default class Pix3D extends Pix2D { const saturation: number = (y & 0x7) / 8.0 + 0.0625; for (let x: number = 0; x < 128; x++) { const lightness: number = x / 128.0; - let r: number = lightness; - let g: number = lightness; - let b: number = lightness; - if (saturation !== 0.0) { - let q: number; - if (lightness < 0.5) { - q = lightness * (saturation + 1.0); - } else { - q = lightness + saturation - lightness * saturation; - } - const p: number = lightness * 2.0 - q; - let t: number = hue + 0.3333333333333333; - if (t > 1.0) { - t--; - } - let d11: number = hue - 0.3333333333333333; - if (d11 < 0.0) { - d11++; - } - if (t * 6.0 < 1.0) { - r = p + (q - p) * 6.0 * t; - } else if (t * 2.0 < 1.0) { - r = q; - } else if (t * 3.0 < 2.0) { - r = p + (q - p) * (0.6666666666666666 - t) * 6.0; - } else { - r = p; - } - if (hue * 6.0 < 1.0) { - g = p + (q - p) * 6.0 * hue; - } else if (hue * 2.0 < 1.0) { - g = q; - } else if (hue * 3.0 < 2.0) { - g = p + (q - p) * (0.6666666666666666 - hue) * 6.0; - } else { - g = p; - } - if (d11 * 6.0 < 1.0) { - b = p + (q - p) * 6.0 * d11; - } else if (d11 * 2.0 < 1.0) { - b = q; - } else if (d11 * 3.0 < 2.0) { - b = p + (q - p) * (0.6666666666666666 - d11) * 6.0; - } else { - b = p; - } - } - const intR: number = (r * 256.0) | 0; - const intG: number = (g * 256.0) | 0; - const intB: number = (b * 256.0) | 0; - const rgb: number = (intR << 16) + (intG << 8) + intB; + const rgb = this.convertHsl(hue, saturation, lightness); this.hslPal[offset++] = this.setGamma(rgb, randomBrightness); } } @@ -204,6 +216,8 @@ export default class Pix3D extends Pix2D { for (let id: number = 0; id < 50; id++) { this.pushTexture(id); } + + Renderer.setBrightness(randomBrightness); } private static setGamma(rgb: number, gamma: number): number { @@ -233,6 +247,10 @@ export default class Pix3D extends Pix2D { } static fillGouraudTriangle(xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, colorA: number, colorB: number, colorC: number): void { + if (Renderer.fillGouraudTriangle(xA, xB, xC, yA, yB, yC, colorA, colorB, colorC)) { + return; + } + let xStepAB: number = 0; let colorStepAB: number = 0; if (yB !== yA) { @@ -856,6 +874,10 @@ export default class Pix3D extends Pix2D { } static fillTriangle(x0: number, x1: number, x2: number, y0: number, y1: number, y2: number, color: number): void { + if (Renderer.fillTriangle(x0, x1, x2, y0, y1, y2, color)) { + return; + } + let xStepAB: number = 0; if (y1 !== y0) { xStepAB = (((x1 - x0) << 16) / (y1 - y0)) | 0; @@ -1298,6 +1320,10 @@ export default class Pix3D extends Pix2D { tzC: number, texture: number ): void { + if (Renderer.fillTexturedTriangle(xA, xB, xC, yA, yB, yC, shadeA, shadeB, shadeC, originX, originY, originZ, txB, txC, tyB, tyC, tzB, tzC, texture)) { + return; + } + const texels: Int32Array | null = this.getTexels(texture); this.opaque = !this.textureTranslucent[texture]; @@ -2496,9 +2522,11 @@ export default class Pix3D extends Pix2D { this.texelPool[this.poolSize++] = this.activeTexels[id]; this.activeTexels[id] = null; } + + Renderer.updateTexture(id); } - private static getTexels(id: number): Int32Array | null { + static getTexels(id: number): Int32Array | null { this.textureCycle[id] = this.cycle++; if (this.activeTexels[id]) { return this.activeTexels[id]; diff --git a/src/graphics/Pix8.ts b/src/graphics/Pix8.ts index 243c3cc..be89472 100644 --- a/src/graphics/Pix8.ts +++ b/src/graphics/Pix8.ts @@ -292,77 +292,72 @@ export default class Pix8 extends DoublyLinkable { } } - clip(arg0: number, arg1: number, arg2: number, arg3: number): void { + clip(x: number, y: number, w: number, h: number): void { try { - const local2: number = this.width2d; - const local5: number = this.height2d; - let local7: number = 0; - let local9: number = 0; - const local15: number = ((local2 << 16) / arg2) | 0; - const local21: number = ((local5 << 16) / arg3) | 0; - const local24: number = this.cropW; - const local27: number = this.cropH; - const local33: number = ((local24 << 16) / arg2) | 0; - const local39: number = ((local27 << 16) / arg3) | 0; - arg0 = (arg0 + (this.cropX * arg2 + local24 - 1) / local24) | 0; - arg1 = (arg1 + (this.cropY * arg3 + local27 - 1) / local27) | 0; - if ((this.cropX * arg2) % local24 != 0) { - local7 = (((local24 - ((this.cropX * arg2) % local24)) << 16) / arg2) | 0; + let cropX: number = 0; + let cropY: number = 0; + const cropW: number = this.cropW; + const cropH: number = this.cropH; + const offX: number = ((cropW << 16) / w) | 0; + const offY: number = ((cropH << 16) / h) | 0; + x = (x + (this.cropX * w + cropW - 1) / cropW) | 0; + y = (y + (this.cropY * h + cropH - 1) / cropH) | 0; + if ((this.cropX * w) % cropW !== 0) { + cropX = (((cropW - ((this.cropX * w) % cropW)) << 16) / w) | 0; } - if ((this.cropY * arg3) % local27 != 0) { - local9 = (((local27 - ((this.cropY * arg3) % local27)) << 16) / arg3) | 0; + if ((this.cropY * h) % cropH !== 0) { + cropY = (((cropH - ((this.cropY * h) % cropH)) << 16) / h) | 0; } - arg2 = ((arg2 * (this.width2d - (local7 >> 16))) / local24) | 0; - arg3 = ((arg3 * (this.height2d - (local9 >> 16))) / local27) | 0; - let local133: number = arg0 + arg1 * Pix2D.width2d; - let local137: number = Pix2D.width2d - arg2; - let local144: number; - if (arg1 < Pix2D.top) { - local144 = Pix2D.top - arg1; - arg3 -= local144; - arg1 = 0; - local133 += local144 * Pix2D.width2d; - local9 += local39 * local144; + w = ((w * (this.width2d - (cropX >> 16))) / cropW) | 0; + h = ((h * (this.height2d - (cropY >> 16))) / cropH) | 0; + let dstOff: number = x + y * Pix2D.width2d; + let dstStep: number = Pix2D.width2d - w; + if (y < Pix2D.top) { + const cutoff: number = Pix2D.top - y; + h -= cutoff; + y = 0; + dstOff += cutoff * Pix2D.width2d; + cropY += offY * cutoff; } - if (arg1 + arg3 > Pix2D.bottom) { - arg3 -= arg1 + arg3 - Pix2D.bottom; + if (y + h > Pix2D.bottom) { + h -= y + h - Pix2D.bottom; } - if (arg0 < Pix2D.left) { - local144 = Pix2D.left - arg0; - arg2 -= local144; - arg0 = 0; - local133 += local144; - local7 += local33 * local144; - local137 += local144; + if (x < Pix2D.left) { + const cutoff: number = Pix2D.left - x; + w -= cutoff; + x = 0; + dstOff += cutoff; + cropX += offX * cutoff; + dstStep += cutoff; } - if (arg0 + arg2 > Pix2D.right) { - local144 = arg0 + arg2 - Pix2D.right; - arg2 -= local144; - local137 += local144; + if (x + w > Pix2D.right) { + const cutoff: number = x + w - Pix2D.right; + w -= cutoff; + dstStep += cutoff; } - this.plot_scale(Pix2D.pixels, this.pixels, this.rgbPal, local7, local9, local133, local137, arg2, arg3, local33, local39, local2); + this.plot_scale(Pix2D.pixels, this.pixels, cropX, cropY, dstOff, dstStep, w, h, offX, offY); } catch (ignore) { console.log('error in sprite clipping routine'); } } - private plot_scale(arg0: Int32Array, arg1: Int8Array, arg2: Int32Array, arg3: number, arg4: number, arg5: number, arg6: number, arg7: number, arg8: number, arg9: number, arg10: number, arg11: number): void { + private plot_scale(dst: Int32Array, src: Int8Array, cropX: number, cropY: number, dstOff: number, dstStep: number, w: number, h: number, offX: number, offY: number): void { try { - const local3: number = arg3; - for (let local6: number = -arg8; local6 < 0; local6++) { - const local14: number = (arg4 >> 16) * arg11; - for (let local17: number = -arg7; local17 < 0; local17++) { - const local27: number = arg1[(arg3 >> 16) + local14]; - if (local27 == 0) { - arg5++; + const startX: number = cropX; + for (let x: number = -h; x < 0; x++) { + const offY: number = (cropY >> 16) * this.width2d; + for (let y: number = -w; y < 0; y++) { + const palIndex: number = src[(cropX >> 16) + offY]; + if (palIndex === 0) { + dstOff++; } else { - arg0[arg5++] = arg2[local27 & 0xff]; + dst[dstOff++] = this.rgbPal[palIndex & 0xff]; } - arg3 += arg9; + cropX += offX; } - arg4 += arg10; - arg3 = local3; - arg5 += arg6; + cropY += offY; + cropX = startX; + dstOff += dstStep; } } catch (ignore) { console.log('error in plot_scale'); diff --git a/src/graphics/PixFont.ts b/src/graphics/PixFont.ts index 0213e11..2de2443 100644 --- a/src/graphics/PixFont.ts +++ b/src/graphics/PixFont.ts @@ -257,7 +257,7 @@ export default class PixFont extends DoublyLinkable { for (let i: number = 0; i < str.length; i++) { const c: number = PixFont.CHARCODESET[str.charCodeAt(i)]; - if (c != 94) { + if (c !== 94) { this.drawChar(this.charMask[c], x + this.charOffsetX[c], offY + this.charOffsetY[c] + ((Math.sin(i / 2.0 + phase / 5.0) * 5.0) | 0), this.charMaskWidth[c], this.charMaskHeight[c], color); } diff --git a/src/graphics/PixMap.ts b/src/graphics/PixMap.ts index 4ebec10..8a076a4 100644 --- a/src/graphics/PixMap.ts +++ b/src/graphics/PixMap.ts @@ -1,10 +1,11 @@ import { canvas2d } from '#/graphics/Canvas.js'; import Pix2D from '#/graphics/Pix2D.js'; +import { Renderer } from '#/graphics/renderer/Renderer.ts'; export default class PixMap { private readonly image: ImageData; - private readonly width2d: number; - private readonly height2d: number; + readonly width2d: number; + readonly height2d: number; private readonly ctx: CanvasRenderingContext2D; private readonly paint: Uint32Array; readonly pixels: Int32Array; @@ -28,6 +29,9 @@ export default class PixMap { } draw(x: number, y: number): void { + if (Renderer.renderPixMap(this, x, y)) { + return; + } this.#setPixels(); this.ctx.putImageData(this.image, x, y); } diff --git a/src/graphics/renderer/Renderer.ts b/src/graphics/renderer/Renderer.ts new file mode 100644 index 0000000..7c92aaa --- /dev/null +++ b/src/graphics/renderer/Renderer.ts @@ -0,0 +1,189 @@ +import type TileOverlay from '#/dash3d/type/TileOverlay.ts'; +import type TileUnderlay from '#/dash3d/type/TileUnderlay.ts'; +import type World3D from '#/dash3d/World3D.ts'; +import type Model from '#/graphics/Model.ts'; +import PixMap from '#/graphics/PixMap.js'; + +export abstract class Renderer { + static renderer: Renderer | undefined; + + constructor(readonly canvas: HTMLCanvasElement) {} + + static resetRenderer(): void { + if (Renderer.renderer) { + Renderer.renderer.destroy(); + Renderer.renderer.canvas.remove(); + Renderer.renderer = undefined; + } + } + + static resize(width: number, height: number): void { + Renderer.renderer?.resize(width, height); + } + + static startFrame(): void { + Renderer.renderer?.startFrame(); + } + + static endFrame(): void { + Renderer.renderer?.endFrame(); + } + + static updateTexture(id: number): void { + Renderer.renderer?.updateTexture(id); + } + + static setBrightness(brightness: number): void { + Renderer.renderer?.setBrightness(brightness); + } + + static renderPixMap(pixmap: PixMap, x: number, y: number): boolean { + if (Renderer.renderer) { + return Renderer.renderer.renderPixMap(pixmap, x, y); + } + return false; + } + + static getSceneClearColor(): number { + if (Renderer.renderer) { + return -1; + } + return 0; + } + + static startRenderScene(): void { + Renderer.renderer?.startRenderScene(); + } + + static endRenderScene(): void { + Renderer.renderer?.endRenderScene(); + } + + static fillTriangle(x0: number, x1: number, x2: number, y0: number, y1: number, y2: number, color: number): boolean { + if (Renderer.renderer) { + return Renderer.renderer.fillTriangle(x0, x1, x2, y0, y1, y2, color); + } + return false; + }; + + static fillGouraudTriangle(xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, colorA: number, colorB: number, colorC: number): boolean { + if (Renderer.renderer) { + return Renderer.renderer.fillGouraudTriangle(xA, xB, xC, yA, yB, yC, colorA, colorB, colorC); + } + return false; + }; + + static fillTexturedTriangle( + xA: number, + xB: number, + xC: number, + yA: number, + yB: number, + yC: number, + shadeA: number, + shadeB: number, + shadeC: number, + originX: number, + originY: number, + originZ: number, + txB: number, + txC: number, + tyB: number, + tyC: number, + tzB: number, + tzC: number, + texture: number + ): boolean { + if (Renderer.renderer) { + return Renderer.renderer.fillTexturedTriangle(xA, xB, xC, yA, yB, yC, shadeA, shadeB, shadeC, originX, originY, originZ, txB, txC, tyB, tyC, tzB, tzC, texture); + } + return false; + }; + + static drawTileUnderlay(world: World3D, underlay: TileUnderlay, level: number, tileX: number, tileZ: number): boolean { + if (Renderer.renderer) { + return Renderer.renderer.drawTileUnderlay(world, underlay, level, tileX, tileZ); + } + return false; + } + + static drawTileOverlay(world: World3D, overlay: TileOverlay, tileX: number, tileZ: number): boolean { + if (Renderer.renderer) { + return Renderer.renderer.drawTileOverlay(world, overlay, tileX, tileZ); + } + return false; + } + + static startDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + if (Renderer.renderer) { + Renderer.renderer.startDrawModel(model, yaw, relativeX, relativeY, relativeZ, bitset); + } + } + + static endDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + if (Renderer.renderer) { + Renderer.renderer.endDrawModel(model, yaw, relativeX, relativeY, relativeZ, bitset); + } + } + + static drawModelTriangle(model: Model, index: number): boolean { + if (Renderer.renderer) { + return Renderer.renderer.drawModelTriangle(model, index); + } + return false; + } + + resize(width: number, height: number): void { + this.canvas.width = width; + this.canvas.height = height; + } + + abstract startFrame(): void; + + abstract endFrame(): void; + + abstract updateTexture(id: number): void; + + abstract setBrightness(brightness: number): void; + + abstract renderPixMap(pixMap: PixMap, x: number, y: number): boolean; + + abstract startRenderScene(): void; + + abstract endRenderScene(): void; + + abstract fillTriangle(x0: number, x1: number, x2: number, y0: number, y1: number, y2: number, color: number): boolean; + + abstract fillGouraudTriangle(xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, colorA: number, colorB: number, colorC: number): boolean; + + abstract fillTexturedTriangle( + xA: number, + xB: number, + xC: number, + yA: number, + yB: number, + yC: number, + shadeA: number, + shadeB: number, + shadeC: number, + originX: number, + originY: number, + originZ: number, + txB: number, + txC: number, + tyB: number, + tyC: number, + tzB: number, + tzC: number, + texture: number + ): boolean; + + abstract destroy(): void; + + abstract drawTileUnderlay(world: World3D, underlay: TileUnderlay, level: number, tileX: number, tileZ: number): boolean; + abstract drawTileOverlay(world: World3D, overlay: TileOverlay, tileX: number, tileZ: number): boolean; + + abstract startDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void; + abstract endDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void; + abstract drawModelTriangle(model: Model, index: number): boolean; +} diff --git a/src/graphics/renderer/webgl/RendererWebGL.ts b/src/graphics/renderer/webgl/RendererWebGL.ts new file mode 100644 index 0000000..b8b8ac9 --- /dev/null +++ b/src/graphics/renderer/webgl/RendererWebGL.ts @@ -0,0 +1,271 @@ +import type TileOverlay from '#/dash3d/type/TileOverlay.ts'; +import type TileUnderlay from '#/dash3d/type/TileUnderlay.ts'; +import World3D from '#/dash3d/World3D.js'; +import { canvas as cpuCanvas } from '#/graphics/Canvas.ts'; +import type Model from '#/graphics/Model.ts'; +import PixMap from '#/graphics/PixMap.js'; +import { Renderer } from '#/graphics/renderer/Renderer.js'; + +import { Shader } from './Shader'; +import { createProgram, ShaderProgram } from './ShaderProgram'; + +import { SHADER_CODE as pixMapFragShaderCode } from './shaders/fullscreen-pixmap.frag.glsl'; +import { SHADER_CODE as pixMapVertShaderCode } from './shaders/fullscreen-pixmap.vert.glsl'; +import { SHADER_CODE as textureFragShaderCode } from './shaders/fullscreen-texture.frag.glsl'; +import { SHADER_CODE as textureVertShaderCode } from './shaders/fullscreen-texture.vert.glsl'; +import { SHADER_CODE as mainFragShaderCode } from './shaders/main.frag.glsl'; +import { SHADER_CODE as mainVertShaderCode } from './shaders/main.vert.glsl'; + +const INITIAL_TRIANGLES: number = 100000; + +export class RendererWebGL extends Renderer { + drawTileUnderlay(world: World3D, underlay: TileUnderlay, level: number, tileX: number, tileZ: number): boolean { + return false; + } + + drawTileOverlay(world: World3D, overlay: TileOverlay, tileX: number, tileZ: number): boolean { + return false; + } + + startDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + } + + endDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + } + + drawModelTriangle(model: Model, index: number): boolean { + return false; + } + + pixMapProgram!: ShaderProgram; + textureProgram!: ShaderProgram; + mainProgram!: ShaderProgram; + + viewportFramebuffer!: WebGLFramebuffer; + viewportColorTarget!: WebGLTexture; + + texturesToDelete: WebGLTexture[] = []; + + isRenderingScene: boolean = false; + + gouraudTriangleData: Uint32Array = new Uint32Array(INITIAL_TRIANGLES * 4); + gouraudTriangleDataView: DataView = new DataView(this.gouraudTriangleData.buffer); + gouraudTriangleCount: number = 0; + + static init(container: HTMLElement, width: number, height: number): RendererWebGL { + const canvas: HTMLCanvasElement = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.style.display = cpuCanvas.style.display; + canvas.style.position = cpuCanvas.style.position; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.imageRendering = cpuCanvas.style.imageRendering; + container.appendChild(canvas); + + const gl: WebGL2RenderingContext | null = canvas.getContext('webgl2', { + preserveDrawingBuffer: true + }); + if (!gl) { + canvas.remove(); + throw new Error('WebGL2 is not supported.'); + } + + Renderer.resetRenderer(); + return new RendererWebGL(canvas, gl); + } + + constructor( + canvas: HTMLCanvasElement, + readonly gl: WebGL2RenderingContext + ) { + super(canvas); + this.init(); + } + + init(): void { + this.gl.enable(this.gl.CULL_FACE); + + const pixMapVertShader: Shader = new Shader(this.gl, this.gl.VERTEX_SHADER, pixMapVertShaderCode); + const pixMapFragShader: Shader = new Shader(this.gl, this.gl.FRAGMENT_SHADER, pixMapFragShaderCode); + this.pixMapProgram = createProgram(this.gl, [pixMapVertShader, pixMapFragShader]); + const textureVertShader: Shader = new Shader(this.gl, this.gl.VERTEX_SHADER, textureVertShaderCode); + const textureFragShader: Shader = new Shader(this.gl, this.gl.FRAGMENT_SHADER, textureFragShaderCode); + this.textureProgram = createProgram(this.gl, [textureVertShader, textureFragShader]); + const mainVertShader: Shader = new Shader(this.gl, this.gl.VERTEX_SHADER, mainVertShaderCode); + const mainFragShader: Shader = new Shader(this.gl, this.gl.FRAGMENT_SHADER, mainFragShaderCode); + this.mainProgram = createProgram(this.gl, [mainVertShader, mainFragShader]); + + const viewportWidth: number = World3D.viewportRight; + const viewportHeight: number = World3D.viewportBottom; + + this.viewportFramebuffer = this.gl.createFramebuffer()!; + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.viewportFramebuffer); + + this.viewportColorTarget = this.gl.createTexture()!; + this.gl.bindTexture(this.gl.TEXTURE_2D, this.viewportColorTarget); + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, viewportWidth, viewportHeight, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + + this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, this.viewportColorTarget, 0); + + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); + } + + override startFrame(): void { + // this.gl.clearColor(0.2, 0, 0, 1); + // this.gl.clear(this.gl.COLOR_BUFFER_BIT); + } + + override endFrame(): void { + for (const texture of this.texturesToDelete) { + this.gl.deleteTexture(texture); + } + this.texturesToDelete.length = 0; + } + + override updateTexture(id: number): void { } + + override setBrightness(brightness: number): void { } + + override renderPixMap(pixMap: PixMap, x: number, y: number): boolean { + this.gl.viewport(x, this.canvas.height - y - pixMap.height2d, pixMap.width2d, pixMap.height2d); + + const viewportWidth: number = World3D.viewportRight; + const viewportHeight: number = World3D.viewportBottom; + + if (pixMap.width2d === viewportWidth && pixMap.height2d === viewportHeight) { + this.gl.bindTexture(this.gl.TEXTURE_2D, this.viewportColorTarget); + this.textureProgram.use(); + this.gl.drawArrays(this.gl.TRIANGLES, 0, 3); + } + + const pixels: Uint8Array = new Uint8Array(pixMap.pixels.buffer); + + const texture: WebGLTexture = this.gl.createTexture()!; + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + this.gl.texStorage2D(this.gl.TEXTURE_2D, 1, this.gl.RGBA8, pixMap.width2d, pixMap.height2d); + this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, pixMap.width2d, pixMap.height2d, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels); + // this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, pixMap.width, pixMap.height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, new Uint8Array(pixMap.pixels.buffer)); + + this.pixMapProgram.use(); + this.gl.drawArrays(this.gl.TRIANGLES, 0, 3); + + this.texturesToDelete.push(texture); + + return true; + } + + override startRenderScene(): void { + this.isRenderingScene = true; + this.gouraudTriangleCount = 0; + } + + override endRenderScene(): void { + this.isRenderingScene = false; + + const viewportWidth: number = World3D.viewportRight; + const viewportHeight: number = World3D.viewportBottom; + + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.viewportFramebuffer); + this.gl.viewport(0, 0, viewportWidth, viewportHeight); + + this.gl.clearColor(0.0, 0.0, 0.0, 1.0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + + // console.log('Rendering scene', this.gouraudTriangleCount); + + if (this.gouraudTriangleCount > 0) { + const texture: WebGLTexture = this.gl.createTexture()!; + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + this.gl.texStorage2D(this.gl.TEXTURE_2D, 1, this.gl.RGBA32UI, this.gouraudTriangleCount, 1); + this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, this.gouraudTriangleCount, 1, this.gl.RGBA_INTEGER, this.gl.UNSIGNED_INT, this.gouraudTriangleData); + + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + + this.mainProgram.use(); + this.gl.drawArrays(this.gl.TRIANGLES, 0, this.gouraudTriangleCount * 3); + + this.texturesToDelete.push(texture); + } + + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); + } + + override fillTriangle(x0: number, x1: number, x2: number, y0: number, y1: number, y2: number, color: number): boolean { + if (!this.isRenderingScene) { + return false; + } + return true; + } + + override fillGouraudTriangle(xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, colorA: number, colorB: number, colorC: number): boolean { + if (!this.isRenderingScene) { + return false; + } + + let offset: number = this.gouraudTriangleCount * 4; + + if (offset >= this.gouraudTriangleData.length) { + const newData: Uint32Array = new Uint32Array(this.gouraudTriangleData.length * 2); + newData.set(this.gouraudTriangleData); + this.gouraudTriangleData = newData; + this.gouraudTriangleDataView = new DataView(this.gouraudTriangleData.buffer); + } + + xA += 2048; + xB += 2048; + xC += 2048; + yA += 2048; + yB += 2048; + yC += 2048; + + this.gouraudTriangleDataView.setUint32(offset++ * 4, (xA << 20) | (xB << 8) | (xC >> 4), true); + this.gouraudTriangleDataView.setUint32(offset++ * 4, (yA << 20) | (yB << 8) | (xC & 0xf), true); + this.gouraudTriangleDataView.setUint32(offset++ * 4, (yC << 16) | colorA, true); + this.gouraudTriangleDataView.setUint32(offset++ * 4, (colorB << 16) | colorC, true); + + // this.gouraudTriangleData[offset++] = (xA << 20) | (xB << 8) | (xC >> 4); + // this.gouraudTriangleData[offset++] = (yA << 20) | (yB << 8) | (xC & 0xf); + // this.gouraudTriangleData[offset++] = (yC << 16) | colorA; + // this.gouraudTriangleData[offset++] = (colorB << 16) | colorC; + + this.gouraudTriangleCount++; + + return true; + } + + override fillTexturedTriangle( + xA: number, + xB: number, + xC: number, + yA: number, + yB: number, + yC: number, + shadeA: number, + shadeB: number, + shadeC: number, + originX: number, + originY: number, + originZ: number, + txB: number, + txC: number, + tyB: number, + tyC: number, + tzB: number, + tzC: number, + texture: number + ): boolean { + if (!this.isRenderingScene) { + return false; + } + return true; + } + + override destroy(): void { } +} diff --git a/src/graphics/renderer/webgl/RendererWebGLC.ts b/src/graphics/renderer/webgl/RendererWebGLC.ts new file mode 100644 index 0000000..2a92717 --- /dev/null +++ b/src/graphics/renderer/webgl/RendererWebGLC.ts @@ -0,0 +1,1322 @@ +import { mat4, vec3 } from 'gl-matrix'; + +import Ground from '#/dash3d/type/Ground.js'; +import TileOverlay from '#/dash3d/type/TileOverlay.js'; +import TileUnderlay from '#/dash3d/type/TileUnderlay.js'; +import World3D from '#/dash3d/World3D'; + +import { canvas as cpuCanvas } from '#/graphics/Canvas.ts'; +import Pix3D from '#/graphics/Pix3D.js'; +import Model from '#/graphics/Model.js'; +import PixMap from '#/graphics/PixMap.js'; +import { Renderer } from '#/graphics/renderer/Renderer.ts'; + +import { Shader } from './Shader'; +import { createProgram, ShaderProgram } from './ShaderProgram'; + +export type Context = WebGL2RenderingContext; + +const clamp = (num: number, min: number, max: number): number => Math.min(Math.max(num, min), max); + +function packFloat11(v: number): number { + return 1024 - Math.round(v / (1 / 64)); +} + +class VertexDataBuffer { + static readonly STRIDE: number = 12; + + data: Uint8Array; + view: DataView; + + pos: number = 0; + + constructor(initialVertexCount: number) { + this.data = new Uint8Array(initialVertexCount * VertexDataBuffer.STRIDE); + this.view = new DataView(this.data.buffer, this.data.byteOffset, this.data.byteLength); + } + + growIfRequired(vertexCount: number): void { + const byteLengthRequired: number = this.pos + vertexCount * VertexDataBuffer.STRIDE; + if (this.data.byteLength < byteLengthRequired) { + const newData: Uint8Array = new Uint8Array(byteLengthRequired * 2); + newData.set(this.data); + this.data = newData; + this.view = new DataView(this.data.buffer, this.data.byteOffset, this.data.byteLength); + } + } + + addVertex(x: number, y: number, z: number, hsl: number, alpha: number, textureId: number, texCoordU: number, texCoordV: number): number { + this.growIfRequired(1); + const index: number = this.pos / VertexDataBuffer.STRIDE; + + const xPos: number = clamp(x + 0x20000, 0, 0x3ffff); // 18 bit + const yPos: number = clamp(-y + 0x800, 0, 0xfff); // 12 bit + const zPos: number = clamp(z + 0x20000, 0, 0x3ffff); // 18 bit + + const isTextured: boolean = textureId !== -1; + const isTexturedBit: number = isTextured ? 1 : 0; + + let hslPacked: number = hsl & 0xffff; // 16 bit + if (isTextured) { + hslPacked &= 0x7f; + hslPacked |= textureId << 7; + } + + const uPacked: number = clamp(packFloat11(texCoordU), 0, 0x7ff); + const vPacked: number = clamp(packFloat11(texCoordV), 0, 0x7ff); + + const v0: number = (isTexturedBit << 30) | (yPos << 18) | xPos; + const v1: number = ((hslPacked >> 2) << 18) | zPos; + const v2: number = ((hslPacked & 0x3) << 30) | (alpha << 22) | (uPacked << 11) | vPacked; + + this.view.setUint32(this.pos, v0, true); + this.view.setUint32(this.pos + 4, v1, true); + this.view.setUint32(this.pos + 8, v2, true); + + this.pos += VertexDataBuffer.STRIDE; + return index; + } + + addData(data: Uint8Array): void { + this.growIfRequired(data.length / VertexDataBuffer.STRIDE); + this.data.set(data, this.pos); + this.pos += data.length; + } +} + +class IndexDataBuffer { + indices: Int32Array; + + pos: number = 0; + + constructor(initialIndexCount: number) { + this.indices = new Int32Array(initialIndexCount); + } + + growIfRequired(indexCount: number): void { + const lengthRequired: number = this.pos + indexCount; + if (this.indices.length < lengthRequired) { + const newIndices: Int32Array = new Int32Array(lengthRequired * 2); + newIndices.set(this.indices); + this.indices = newIndices; + } + } + + addIndices(...indices: number[]): void { + this.growIfRequired(indices.length); + for (const index of indices) { + this.indices[this.pos++] = index; + } + } +} + +class DrawCommands { + positions: Int32Array; + yaws: Int32Array; + offsets: Int32Array; + counts: Int32Array; + + count: number = 0; + + constructor(count: number) { + this.positions = new Int32Array(count * 3); + this.yaws = new Int32Array(count); + this.offsets = new Int32Array(count); + this.counts = new Int32Array(count); + } + + reset(): void { + this.count = 0; + } + + reduce(): void { + const originalCount: number = this.count; + + for (let i: number = 0; i < originalCount; i++) { + const x: number = this.positions[i * 3]; + const y: number = this.positions[i * 3 + 1]; + const z: number = this.positions[i * 3 + 2]; + const yaw: number = this.yaws[i]; + const offset: number = this.offsets[i]; + const count: number = this.counts[i]; + + if (count === 0) { + continue; + } + + for (let j: number = i + 1; j < originalCount; j++) { + if (this.positions[j * 3] === x && this.positions[j * 3 + 1] === y && this.positions[j * 3 + 2] === z && this.yaws[j] === yaw && offset + this.counts[i] === this.offsets[j]) { + this.counts[i] += this.counts[j]; + this.counts[j] = 0; + } else { + break; + } + } + } + } + + addCommand(x: number, y: number, z: number, yaw: number, offset: number, count: number): void { + if (count === 0) { + return; + } + if (this.count >= this.yaws.length) { + const newPositions: Int32Array = new Int32Array(this.count * 3 * 2); + const newYaws: Int32Array = new Int32Array(this.count * 2); + const newOffsets: Int32Array = new Int32Array(this.count * 2); + const newCounts: Int32Array = new Int32Array(this.count * 2); + newPositions.set(this.positions); + newYaws.set(this.yaws); + newOffsets.set(this.offsets); + newCounts.set(this.counts); + this.positions = newPositions; + this.yaws = newYaws; + this.offsets = newOffsets; + this.counts = newCounts; + } + this.positions[this.count * 3] = x; + this.positions[this.count * 3 + 1] = y; + this.positions[this.count * 3 + 2] = z; + this.yaws[this.count] = yaw; + this.offsets[this.count] = offset; + this.counts[this.count] = count; + this.count++; + } +} + +interface PixMapTexture { + lastFrameUsed: number; + texture: WebGLTexture; +} + +interface CachedVertexData { + frame: number; + data: Uint8Array; +} + +const frameVertSource: string = ` +#version 300 es + +out vec2 v_texCoord; + +const vec2 vertices[3] = vec2[3]( + vec2(-1,-1), + vec2(3,-1), + vec2(-1, 3) +); + +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + v_texCoord = gl_Position.xy * 0.5 + 0.5; +} +`.trim(); +const frameFragSource: string = ` +#version 300 es + +precision highp float; + +uniform highp sampler2D u_frame; + +in vec2 v_texCoord; + +out vec4 fragColor; + +void main() { + fragColor = texture(u_frame, v_texCoord); +} +`.trim(); + +const pixMapVertSource: string = ` +#version 300 es + +out vec2 v_texCoord; + +const vec2 vertices[3] = vec2[3]( + vec2(-1,-1), + vec2(3,-1), + vec2(-1, 3) +); + +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + v_texCoord = gl_Position.xy * 0.5 + 0.5; + // flip y + v_texCoord.y = 1.0 - v_texCoord.y; +} +`.trim(); +const pixMapFragSource: string = ` +#version 300 es + +precision highp float; + +uniform highp sampler2D u_frame; + +in vec2 v_texCoord; + +out vec4 fragColor; + +void main() { + fragColor = texture(u_frame, v_texCoord).bgra; + fragColor.a = fragColor == vec4(1.0) ? 0.0 : 1.0; +} +`.trim(); + +// https://stackoverflow.com/a/17309861 +const hslToRgbGlsl: string = ` +vec3 hslToRgb(int hsl, float brightness) { + const float onethird = 1.0 / 3.0; + const float twothird = 2.0 / 3.0; + const float rcpsixth = 6.0; + + float hue = float(hsl >> 10) / 64.0 + 0.0078125; + float sat = float((hsl >> 7) & 0x7) / 8.0 + 0.0625; + float lum = (float(hsl & 0x7f) / 128.0); + + vec3 xt = vec3( + rcpsixth * (hue - twothird), + 0.0, + rcpsixth * (1.0 - hue) + ); + + if (hue < twothird) { + xt.r = 0.0; + xt.g = rcpsixth * (twothird - hue); + xt.b = rcpsixth * (hue - onethird); + } + + if (hue < onethird) { + xt.r = rcpsixth * (onethird - hue); + xt.g = rcpsixth * hue; + xt.b = 0.0; + } + + xt = min( xt, 1.0 ); + + float sat2 = 2.0 * sat; + float satinv = 1.0 - sat; + float luminv = 1.0 - lum; + float lum2m1 = (2.0 * lum) - 1.0; + vec3 ct = (sat2 * xt) + satinv; + + vec3 rgb; + if (lum >= 0.5) + rgb = (luminv * ct) + lum2m1; + else rgb = lum * ct; + + return pow(rgb, vec3(brightness)); +} +`; + +const mainVertSource: string = ` +#version 300 es + +#ifdef GL_NV_shader_noperspective_interpolation +#extension GL_NV_shader_noperspective_interpolation : require +#endif + +#define TEXTURE_ANIM_UNIT (1.0f / 128.0f) + +uniform float u_time; +uniform float u_brightness; +uniform mat4 u_viewProjectionMatrix; + +uniform float u_angle; +uniform vec3 u_translation; + +layout(location = 0) in uvec3 a_packed; + +#ifdef GL_NV_shader_noperspective_interpolation +noperspective centroid out float v_hsl; +#else +out float v_hsl; +#endif +out vec4 v_color; +out vec3 v_texCoord; + +${hslToRgbGlsl} + +float unpackFloat11(uint v) { + return 16.0 - float(v) / 64.0; +} + +struct Vertex { + ivec3 position; + int hsl; + float alpha; + int textureId; + vec2 texCoord; +}; + +Vertex decodeVertex(uint v0, uint v1, uint v2) { + Vertex vertex; + + int isTextured = int(v0 >> 30u); + + vertex.position.x = int(v0 & 0x3ffffu) - 0x20000; + vertex.position.y = -(int((v0 >> 18u) & 0xfffu) - 0x800); + vertex.position.z = int(v1 & 0x3ffffu) - 0x20000; + + int hslPacked = int(v1 >> 18u) << 2 | int(v2 >> 30u); + int textureId = ((hslPacked >> 7) + 1); + + vertex.hsl = hslPacked * (1 - isTextured) + (hslPacked & 0x7f) * isTextured; + vertex.alpha = float((v2 >> 22u) & 0xffu) / 255.0; + + vertex.textureId = textureId * isTextured; + + vertex.texCoord.x = unpackFloat11(uint(v2 >> 11u) & 0x7ffu); + vertex.texCoord.y = unpackFloat11(uint(v2 & 0x7ffu)); + + return vertex; +} + +mat4 rotationY( in float angle ) { + return mat4(cos(angle), 0, sin(angle), 0, + 0, 1.0, 0, 0, + -sin(angle), 0, cos(angle), 0, + 0, 0, 0, 1); +} + +void main() { + Vertex vertex = decodeVertex(a_packed.x, a_packed.y, a_packed.z); + + gl_Position = rotationY(u_angle) * vec4(vec3(vertex.position), 1.0); + gl_Position.xyz += u_translation; + gl_Position = u_viewProjectionMatrix * gl_Position; + int textureId = vertex.textureId - 1; + if (textureId >= 0) { + v_color.r = float(vertex.hsl) / 127.0; + } else { + v_color = vec4(hslToRgb(vertex.hsl, u_brightness), vertex.alpha); + } + v_hsl = float(vertex.hsl); + v_texCoord = vec3(vertex.texCoord, float(textureId + 1)); + // scrolling textures + if (textureId == 17 || textureId == 24) { + v_texCoord.y += u_time / 0.02 * -2.0 * TEXTURE_ANIM_UNIT; + } +} +`.trim(); +const mainFragSource: string = ` +#version 300 es + +#ifdef GL_NV_shader_noperspective_interpolation +#extension GL_NV_shader_noperspective_interpolation : require +#endif + +precision highp float; + +uniform float u_brightness; + +uniform highp sampler2DArray u_textures; + +#ifdef GL_NV_shader_noperspective_interpolation +noperspective centroid in float v_hsl; +#endif +in vec4 v_color; +in vec3 v_texCoord; + +out vec4 fragColor; + +${hslToRgbGlsl} + +vec3 getShadedColor(int rgb, int shadowFactor) { + int shadowFactors[4] = int[]( + rgb & 0xf8f8ff, + (rgb - (rgb >> 3)) & 0xf8f8ff, + (rgb - (rgb >> 2)) & 0xf8f8ff, + (rgb - (rgb >> 2) - (rgb >> 3)) & 0xf8f8ff + ); + rgb = shadowFactors[shadowFactor % 4] >> (shadowFactor / 4); + + return vec3(float((rgb >> 16) & 0xff) / 255.0, float((rgb >> 8) & 0xff) / 255.0, float(rgb & 0xff) / 255.0); +} + +void main() { + if (v_texCoord.z > 0.0) { + // emulate texture color shading + vec4 texColor = texture(u_textures, v_texCoord).bgra; + int rgb = int(texColor.r * 255.0) << 16 | int(texColor.g * 255.0) << 8 | int(texColor.b * 255.0); + int shadowFactor = int(floor(v_color.r / 0.125)); + + fragColor.rgb = getShadedColor(rgb, shadowFactor); + fragColor.a = texColor.a; + } else { + #ifdef GL_NV_shader_noperspective_interpolation + fragColor.rgb = hslToRgb(int(v_hsl), u_brightness); + #else + // emulate color banding + fragColor.rgb = round(v_color.rgb * 64.0) / 64.0; + #endif + fragColor.a = v_color.a; + } +} +`.trim(); + +const PI: number = Math.PI; +const TAU: number = PI * 2; +const RS_TO_RADIANS: number = TAU / 2048.0; + +const FRUSTUM_SCALE: number = 25.0 / 256.0; +const DEFAULT_ZOOM: number = 512; + +const NEAR: number = 50; +const FAR: number = 3500; + +interface WEBGL_provoking_vertex { + FIRST_VERTEX_CONVENTION_WEBGL: number; + LAST_VERTEX_CONVENTION_WEBGL: number; + provokingVertexWEBGL(mode: number): void; +} + +function optimizeAssumingFlatsHaveSameFirstAndLastData(gl: WebGL2RenderingContext): void { + const epv: WEBGL_provoking_vertex = gl.getExtension('WEBGL_provoking_vertex'); + if (epv) { + epv.provokingVertexWEBGL(epv.FIRST_VERTEX_CONVENTION_WEBGL); + } +} + +// TODO: try using a separate buffer for static models to reduce bandwidth +export class RendererWebGLC extends Renderer { + updateTexture(id: number): void { + } + + fillTriangle(x0: number, x1: number, x2: number, y0: number, y1: number, y2: number, color: number): boolean { + return false; + } + + fillGouraudTriangle(xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, colorA: number, colorB: number, colorC: number): boolean { + return false; + } + + fillTexturedTriangle(xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, shadeA: number, shadeB: number, shadeC: number, originX: number, originY: number, originZ: number, txB: number, txC: number, tyB: number, tyC: number, tzB: number, tzC: number, texture: number): boolean { + return false; + } + + destroy(): void { + delete RendererWebGLC.viewportFramebuffer; + RendererWebGLC.textureArray = null; + } + + frame: number = 0; + + pixMapTextures: Map = new Map(); + + drawCommands: DrawCommands = new DrawCommands(1024); + + static frameProgram: ShaderProgram; + static frameLoc: WebGLUniformLocation; + + static pixMapProgram: ShaderProgram; + static pixMapLoc: WebGLUniformLocation; + + static mainProgram: ShaderProgram; + static timeLoc: WebGLUniformLocation; + static brightnessLoc: WebGLUniformLocation; + static viewProjectionMatrixLoc: WebGLUniformLocation; + static angleLoc: WebGLUniformLocation; + static translationLoc: WebGLUniformLocation; + static texturesLoc: WebGLUniformLocation; + static packedAttrLoc: number; + + static areaViewport: PixMap; + static viewportWidth: number; + static viewportHeight: number; + static viewportFramebuffer?: WebGLFramebuffer; + static viewportColorTexture: WebGLTexture; + static viewportDepthBuffer: WebGLRenderbuffer; + + isDrawingScene: boolean = false; + + static textureArray: WebGLTexture | null; + + brightness: number = 0.9; + + cameraPos: vec3 = vec3.create(); + static cameraYaw: number = 0; + static cameraPitch: number = 0; + + projectionMatrix: mat4 = mat4.create(); + cameraMatrix: mat4 = mat4.create(); + viewMatrix: mat4 = mat4.create(); + viewProjectionMatrix: mat4 = mat4.create(); + + vertexDataBuffer: VertexDataBuffer = new VertexDataBuffer(1024); + indexDataBuffer: IndexDataBuffer = new IndexDataBuffer(1024 * 2); + + static vertexArray: WebGLVertexArrayObject; + + static vertexBuffer: WebGLBuffer | null; + static indexBuffer: WebGLBuffer | null; + + static modelStartIndex: number = 0; + static modelElementOffset: number = 0; + + static modelStartIndexMap: Map = new Map(); + static modelVertexDataCache: Map = new Map(); + static modelsToCache: Set = new Set(); + + static init(container: HTMLElement, width: number, height: number): RendererWebGLC { + const canvas: HTMLCanvasElement = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.style.display = cpuCanvas.style.display; + canvas.style.position = cpuCanvas.style.position; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.imageRendering = cpuCanvas.style.imageRendering; + container.appendChild(canvas); + + const gl: WebGL2RenderingContext | null = canvas.getContext('webgl2', { + preserveDrawingBuffer: true + }); + if (!gl) { + canvas.remove(); + throw new Error('WebGL2 is not supported.'); + } + + Renderer.resetRenderer(); + return new RendererWebGLC(canvas, gl); + } + + constructor( + canvas: HTMLCanvasElement, + readonly gl: WebGL2RenderingContext + ) { + super(canvas); + this.init(); + } + + init(): void { + this.gl.enable(this.gl.CULL_FACE); + + this.gl.enable(this.gl.BLEND); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + + optimizeAssumingFlatsHaveSameFirstAndLastData(this.gl); + + this.gl.getExtension('NV_shader_noperspective_interpolation'); + + const frameVertShader: Shader = new Shader(this.gl, this.gl.VERTEX_SHADER, frameVertSource); + const frameFragShader: Shader = new Shader(this.gl, this.gl.FRAGMENT_SHADER, frameFragSource); + RendererWebGLC.frameProgram = createProgram(this.gl, [frameVertShader, frameFragShader]); + RendererWebGLC.frameLoc = this.gl.getUniformLocation(RendererWebGLC.frameProgram.program, 'u_frame')!; + + const pixMapVertShader: Shader = new Shader(this.gl, this.gl.VERTEX_SHADER, pixMapVertSource); + const pixMapFragShader: Shader = new Shader(this.gl, this.gl.FRAGMENT_SHADER, pixMapFragSource); + RendererWebGLC.pixMapProgram = createProgram(this.gl, [pixMapVertShader, pixMapFragShader]); + RendererWebGLC.pixMapLoc = this.gl.getUniformLocation(RendererWebGLC.pixMapProgram.program, 'u_frame')!; + + const mainVertShader: Shader = new Shader(this.gl, this.gl.VERTEX_SHADER, mainVertSource); + const mainFragShader: Shader = new Shader(this.gl, this.gl.FRAGMENT_SHADER, mainFragSource); + RendererWebGLC.mainProgram = createProgram(this.gl, [mainVertShader, mainFragShader]); + RendererWebGLC.timeLoc = this.gl.getUniformLocation(RendererWebGLC.mainProgram.program, 'u_time')!; + RendererWebGLC.brightnessLoc = this.gl.getUniformLocation(RendererWebGLC.mainProgram.program, 'u_brightness')!; + RendererWebGLC.viewProjectionMatrixLoc = this.gl.getUniformLocation(RendererWebGLC.mainProgram.program, 'u_viewProjectionMatrix')!; + RendererWebGLC.angleLoc = this.gl.getUniformLocation(RendererWebGLC.mainProgram.program, 'u_angle')!; + RendererWebGLC.translationLoc = this.gl.getUniformLocation(RendererWebGLC.mainProgram.program, 'u_translation')!; + RendererWebGLC.texturesLoc = this.gl.getUniformLocation(RendererWebGLC.mainProgram.program, 'u_textures')!; + RendererWebGLC.packedAttrLoc = this.gl.getAttribLocation(RendererWebGLC.mainProgram.program, 'a_packed'); + + const vertexArray: WebGLVertexArrayObject = this.gl.createVertexArray()!; + this.gl.bindVertexArray(vertexArray); + this.gl.enableVertexAttribArray(RendererWebGLC.packedAttrLoc); + RendererWebGLC.vertexArray = vertexArray; + this.gl.bindVertexArray(null); + } + + setBrightness(brightness: number): void { + this.brightness = brightness; + this.initTextureArray(true); + } + + initTextureArray(force: boolean = false): void { + if (RendererWebGLC.textureArray && !force) { + return; + } + if (RendererWebGLC.textureArray) { + this.gl.deleteTexture(RendererWebGLC.textureArray); + } + const textureSize: number = 128; + const pixelCount: number = textureSize * textureSize; + + const textureCount: number = Pix3D.textureCount; + const textureArrayLayers: number = textureCount + 1; + + const textureArray: WebGLTexture = this.gl.createTexture()!; + this.gl.bindTexture(this.gl.TEXTURE_2D_ARRAY, textureArray); + this.gl.texParameteri(this.gl.TEXTURE_2D_ARRAY, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST); + this.gl.texParameteri(this.gl.TEXTURE_2D_ARRAY, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST); + + this.gl.texParameteri(this.gl.TEXTURE_2D_ARRAY, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + + const pixels: Int32Array = new Int32Array(textureArrayLayers * pixelCount); + // White texture + pixels.fill(0xffffffff, 0, pixelCount); + + for (let t: number = 0; t < textureCount; t++) { + const texels: Int32Array | null = Pix3D.getTexels(t); + if (!texels) { + continue; + } + for (let i: number = 0; i < pixelCount; i++) { + let rgb: number = texels[i]; + if (rgb !== 0) { + rgb |= 0xff000000; + } + pixels[(t + 1) * pixelCount + i] = rgb; + } + } + + this.gl.texImage3D(this.gl.TEXTURE_2D_ARRAY, 0, this.gl.RGBA, textureSize, textureSize, textureArrayLayers, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, new Uint8Array(pixels.buffer)); + + RendererWebGLC.textureArray = textureArray; + } + + startFrame(): void { + this.frame++; + this.drawCommands.count = 0; + RendererWebGLC.modelStartIndexMap.clear(); + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clearColor(0.0, 0.0, 0.0, 1.0); + // this.gl.clear(this.gl.COLOR_BUFFER_BIT); + } + + setFrustumProjectionMatrix(matrix: mat4, offsetX: number, offsetY: number, centerX: number, centerY: number, width: number, height: number, yaw: number, pitch: number, zoom: number): mat4 { + const left: number = ((offsetX - centerX) << 9) / zoom; + const right: number = ((offsetX + width - centerX) << 9) / zoom; + const top: number = ((offsetY - centerY) << 9) / zoom; + const bottom: number = ((offsetY + height - centerY) << 9) / zoom; + + mat4.identity(matrix); + mat4.frustum(matrix, left * FRUSTUM_SCALE, right * FRUSTUM_SCALE, -bottom * FRUSTUM_SCALE, -top * FRUSTUM_SCALE, NEAR, FAR); + + mat4.rotateX(matrix, matrix, PI); + if (pitch !== 0) { + mat4.rotateX(matrix, matrix, pitch * RS_TO_RADIANS); + } + if (yaw !== 0) { + mat4.rotateY(matrix, matrix, yaw * RS_TO_RADIANS); + } + + return matrix; + } + + setCameraMatrix(matrix: mat4, pos: vec3): mat4 { + mat4.identity(matrix); + mat4.translate(matrix, matrix, pos); + return matrix; + } + + renderPixMap(pixMap: PixMap, x: number, y: number): boolean { + let pixMapTexture: PixMapTexture | undefined = this.pixMapTextures.get(pixMap); + if (!pixMapTexture) { + // console.log("Creating texture for pixMap", x, y, pixMap.width, pixMap.height); + const texture: WebGLTexture = this.gl.createTexture()!; + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + this.gl.texStorage2D(this.gl.TEXTURE_2D, 1, this.gl.RGBA8, pixMap.width2d, pixMap.height2d); + pixMapTexture = { + lastFrameUsed: this.frame, + texture: texture + }; + this.pixMapTextures.set(pixMap, pixMapTexture); + } else { + pixMapTexture.lastFrameUsed = this.frame; + } + + // Render scene framebuffer + if (pixMap === RendererWebGLC.areaViewport) { + this.drawTexture(RendererWebGLC.viewportColorTexture, x, y, pixMap.width2d, pixMap.height2d); + + // Draw right side 1 pixel border + this.drawTexture(null, x + pixMap.width2d - 1, y, 1, pixMap.height2d); + } + + const pixels: Uint8Array = new Uint8Array(pixMap.pixels.buffer); + + this.gl.bindTexture(this.gl.TEXTURE_2D, pixMapTexture.texture); + + this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, pixMap.width2d, pixMap.height2d, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels); + + this.drawPixMapTexture(pixMapTexture.texture, x, y, pixMap.width2d, pixMap.height2d); + + return true; + } + + private drawTexture(texture: WebGLTexture | null, x: number, y: number, width: number, height: number): void { + RendererWebGLC.frameProgram.use(); + + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + + this.gl.viewport(x, this.gl.canvas.height - y - height, width, height); + // this.gl.uniform1i(Renderer.frameLoc, 0); + + this.gl.drawArrays(this.gl.TRIANGLES, 0, 3); + } + + private drawPixMapTexture(texture: WebGLTexture, x: number, y: number, width: number, height: number): void { + RendererWebGLC.pixMapProgram.use(); + + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + + this.gl.viewport(x, this.gl.canvas.height - y - height, width, height); + + this.gl.drawArrays(this.gl.TRIANGLES, 0, 3); + } + + startRenderScene(): void { + this.isDrawingScene = true; + this.vertexDataBuffer.pos = 0; + this.indexDataBuffer.pos = 0; + this.initTextureArray(); + } + + endRenderScene(): void { + this.isDrawingScene = false; + + // Render scene + const pixMap: PixMap = RendererWebGLC.areaViewport; + const viewportWidth: number = pixMap.width2d; + const viewportHeight: number = pixMap.height2d; + if (RendererWebGLC.viewportFramebuffer === undefined || RendererWebGLC.viewportWidth !== viewportWidth || RendererWebGLC.viewportHeight !== viewportHeight) { + if (RendererWebGLC.viewportFramebuffer !== undefined) { + this.gl.deleteFramebuffer(RendererWebGLC.viewportFramebuffer); + this.gl.deleteTexture(RendererWebGLC.viewportColorTexture); + this.gl.deleteRenderbuffer(RendererWebGLC.viewportDepthBuffer); + } + // console.log("Creating viewport framebuffer", x, y, pixMap.width, pixMap.height); + RendererWebGLC.viewportFramebuffer = this.gl.createFramebuffer()!; + RendererWebGLC.viewportWidth = viewportWidth; + RendererWebGLC.viewportHeight = viewportHeight; + const colorTexture: WebGLTexture = (RendererWebGLC.viewportColorTexture = this.gl.createTexture()!); + this.gl.bindTexture(this.gl.TEXTURE_2D, colorTexture); + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, viewportWidth, viewportHeight, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + + const depthBuffer: WebGLRenderbuffer = (RendererWebGLC.viewportDepthBuffer = this.gl.createRenderbuffer()!); + this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, depthBuffer); + this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, viewportWidth, viewportHeight); + + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, RendererWebGLC.viewportFramebuffer); + + this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorTexture, 0); + this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, depthBuffer); + } else { + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, RendererWebGLC.viewportFramebuffer); + } + + // this.gl.enable(this.gl.DEPTH_TEST); + + const centerX: number = Pix3D.centerX; + const centerY: number = Pix3D.centerY; + + this.gl.viewport(0, 0, viewportWidth, viewportHeight); + + this.gl.clearColor(0.0, 0, 0, 1); + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + + RendererWebGLC.mainProgram.use(); + + this.gl.uniform1f(RendererWebGLC.timeLoc, performance.now() / 1000); + this.gl.uniform1f(RendererWebGLC.brightnessLoc, this.brightness); + + this.cameraPos[0] = World3D.eyeX; + this.cameraPos[1] = World3D.eyeY; + this.cameraPos[2] = World3D.eyeZ; + const yaw: number = RendererWebGLC.cameraYaw; + const pitch: number = RendererWebGLC.cameraPitch; + const zoom: number = DEFAULT_ZOOM; + this.setFrustumProjectionMatrix(this.projectionMatrix, 0, 0, centerX, centerY, viewportWidth, viewportHeight, yaw, pitch, zoom); + this.setCameraMatrix(this.cameraMatrix, this.cameraPos); + mat4.invert(this.viewMatrix, this.cameraMatrix); + mat4.multiply(this.viewProjectionMatrix, this.projectionMatrix, this.viewMatrix); + + this.gl.uniformMatrix4fv(RendererWebGLC.viewProjectionMatrixLoc, false, this.viewProjectionMatrix); + + if (RendererWebGLC.vertexBuffer) { + this.gl.deleteBuffer(RendererWebGLC.vertexBuffer); + RendererWebGLC.vertexBuffer = null; + } + if (RendererWebGLC.indexBuffer) { + this.gl.deleteBuffer(RendererWebGLC.indexBuffer); + RendererWebGLC.indexBuffer = null; + } + + const vertexDataBuffer: VertexDataBuffer = this.vertexDataBuffer; + const indexDataBuffer: IndexDataBuffer = this.indexDataBuffer; + + const vertexCount: number = vertexDataBuffer.pos / VertexDataBuffer.STRIDE; + const elementCount: number = indexDataBuffer.pos; + + if (elementCount > 0) { + this.gl.bindTexture(this.gl.TEXTURE_2D_ARRAY, RendererWebGLC.textureArray); + + this.gl.bindVertexArray(RendererWebGLC.vertexArray); + + RendererWebGLC.vertexBuffer = this.gl.createBuffer()!; + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, RendererWebGLC.vertexBuffer); + this.gl.bufferData(this.gl.ARRAY_BUFFER, vertexDataBuffer.data, this.gl.STATIC_DRAW, 0, vertexDataBuffer.pos); + + // console.log("Uploading", vertexCount, "vertices and", elementCount, "elements"); + + this.gl.vertexAttribIPointer(RendererWebGLC.packedAttrLoc, 3, this.gl.UNSIGNED_INT, VertexDataBuffer.STRIDE, 0); + // this.gl.vertexAttribPointer(Renderer.packedAttrLoc, 3, this.gl.UNSIGNED_INT, false, 0, 0); + + RendererWebGLC.indexBuffer = this.gl.createBuffer()!; + this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, RendererWebGLC.indexBuffer); + this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, indexDataBuffer.indices, this.gl.STATIC_DRAW, 0, indexDataBuffer.pos); + + const position: Float32Array = new Float32Array(3); + + let drawCount: number = 0; + const drawCommands: DrawCommands = this.drawCommands; + drawCommands.reduce(); + for (let i: number = 0; i < drawCommands.count; i++) { + const offset: number = drawCommands.offsets[i]; + const count: number = drawCommands.counts[i]; + if (count === 0) { + continue; + } + const angle: number = ((2048 - drawCommands.yaws[i]) & 0x7ff) * RS_TO_RADIANS; + position[0] = drawCommands.positions[i * 3]; + position[1] = drawCommands.positions[i * 3 + 1]; + position[2] = drawCommands.positions[i * 3 + 2]; + this.gl.uniform1f(RendererWebGLC.angleLoc, angle); + this.gl.uniform3fv(RendererWebGLC.translationLoc, position); + this.gl.drawElements(this.gl.TRIANGLES, count, this.gl.UNSIGNED_INT, offset * 4); + drawCount++; + } + + this.gl.bindVertexArray(null); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null); + this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, null); + } + + this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); + } + + drawTileUnderlay(world: World3D, underlay: TileUnderlay, level: number, tileX: number, tileZ: number): boolean { + if (underlay.southwestColor === 12345678 && underlay.northeastColor === 12345678) { + return true; + } + const x0: number = tileX << 7; + const x1: number = (tileX + 1) << 7; + const z0: number = tileZ << 7; + const z1: number = (tileZ + 1) << 7; + + const y00: number = world.levelHeightmaps[level][tileX][tileZ]; + const y10: number = world.levelHeightmaps[level][tileX + 1][tileZ]; + const y11: number = world.levelHeightmaps[level][tileX + 1][tileZ + 1]; + const y01: number = world.levelHeightmaps[level][tileX][tileZ + 1]; + + const vertexDataBuffer: VertexDataBuffer = this.vertexDataBuffer; + const indexDataBuffer: IndexDataBuffer = this.indexDataBuffer; + + const textureId: number = underlay.textureId; + const texCoordU00: number = 0; + const texCoordV00: number = 0; + const texCoordU10: number = 1; + const texCoordV10: number = 0; + const texCoordU11: number = 1; + const texCoordV11: number = 1; + const texCoordU01: number = 0; + const texCoordV01: number = 1; + + const elementOffset: number = indexDataBuffer.pos; + + // Software renderer has wrong texture coordinates + if (underlay.northeastColor !== 12345678) { + if (underlay.flat) { + const index11: number = vertexDataBuffer.addVertex(x1, y11, z1, underlay.northeastColor, 0xff, textureId, texCoordU11, texCoordV11); + const index01: number = vertexDataBuffer.addVertex(x0, y01, z1, underlay.northwestColor, 0xff, textureId, texCoordU01, texCoordV01); + const index10: number = vertexDataBuffer.addVertex(x1, y10, z0, underlay.southeastColor, 0xff, textureId, texCoordU10, texCoordV10); + indexDataBuffer.addIndices(index11, index01, index10); + } else { + const index11: number = vertexDataBuffer.addVertex(x1, y11, z1, underlay.northeastColor, 0xff, textureId, texCoordU00, texCoordV00); + const index01: number = vertexDataBuffer.addVertex(x0, y01, z1, underlay.northwestColor, 0xff, textureId, texCoordU10, texCoordV10); + const index10: number = vertexDataBuffer.addVertex(x1, y10, z0, underlay.southeastColor, 0xff, textureId, texCoordU01, texCoordV01); + indexDataBuffer.addIndices(index11, index01, index10); + } + } + if (underlay.southwestColor !== 12345678) { + const index00: number = vertexDataBuffer.addVertex(x0, y00, z0, underlay.southwestColor, 0xff, textureId, texCoordU00, texCoordV00); + const index10: number = vertexDataBuffer.addVertex(x1, y10, z0, underlay.southeastColor, 0xff, textureId, texCoordU10, texCoordV10); + const index01: number = vertexDataBuffer.addVertex(x0, y01, z1, underlay.northwestColor, 0xff, textureId, texCoordU01, texCoordV01); + indexDataBuffer.addIndices(index00, index10, index01); + } + + this.drawCommands.addCommand(0, 0, 0, 0, elementOffset, indexDataBuffer.pos - elementOffset); + + return true; + } + + tmpOverlayU: Float32Array = new Float32Array(6); + tmpOverlayV: Float32Array = new Float32Array(6); + + drawTileOverlay(world: World3D, overlay: TileOverlay, tileX: number, tileZ: number): boolean { + const vertexDataBuffer: VertexDataBuffer = this.vertexDataBuffer; + const indexDataBuffer: IndexDataBuffer = this.indexDataBuffer; + + const offsetX: number = tileX << 7; + const offsetZ: number = tileZ << 7; + + const elementOffset: number = indexDataBuffer.pos; + + const vertexCount: number = overlay.vertexX.length; + const triangleCount: number = overlay.triangleVertexA.length; + + if (overlay.triangleTextureIds) { + for (let i: number = 0; i < vertexCount; i++) { + const x: number = overlay.vertexX[i] - offsetX; + const z: number = overlay.vertexZ[i] - offsetZ; + + this.tmpOverlayU[i] = x / 128.0; + this.tmpOverlayV[i] = z / 128.0; + } + } + + for (let i: number = 0; i < triangleCount; i++) { + const a: number = overlay.triangleVertexA[i]; + const b: number = overlay.triangleVertexB[i]; + const c: number = overlay.triangleVertexC[i]; + + const xa: number = overlay.vertexX[a]; + const ya: number = overlay.vertexY[a]; + const za: number = overlay.vertexZ[a]; + + const xb: number = overlay.vertexX[b]; + const yb: number = overlay.vertexY[b]; + const zb: number = overlay.vertexZ[b]; + + const xc: number = overlay.vertexX[c]; + const yc: number = overlay.vertexY[c]; + const zc: number = overlay.vertexZ[c]; + + const colorA: number = overlay.triangleColorA[i]; + const colorB: number = overlay.triangleColorB[i]; + const colorC: number = overlay.triangleColorC[i]; + + const textureId: number = overlay.triangleTextureIds?.[i] ?? -1; + if (textureId === -1 && colorA === 12345678) { + continue; + } + + if (overlay.flat) { + const indexA: number = vertexDataBuffer.addVertex(xa, ya, za, colorA, 0xff, textureId, this.tmpOverlayU[a], this.tmpOverlayV[a]); + const indexB: number = vertexDataBuffer.addVertex(xb, yb, zb, colorB, 0xff, textureId, this.tmpOverlayU[b], this.tmpOverlayV[b]); + const indexC: number = vertexDataBuffer.addVertex(xc, yc, zc, colorC, 0xff, textureId, this.tmpOverlayU[c], this.tmpOverlayV[c]); + + indexDataBuffer.addIndices(indexA, indexB, indexC); + } else { + const indexA: number = vertexDataBuffer.addVertex(xa, ya, za, colorA, 0xff, textureId, this.tmpOverlayU[0], this.tmpOverlayV[0]); + const indexB: number = vertexDataBuffer.addVertex(xb, yb, zb, colorB, 0xff, textureId, this.tmpOverlayU[1], this.tmpOverlayV[1]); + const indexC: number = vertexDataBuffer.addVertex(xc, yc, zc, colorC, 0xff, textureId, this.tmpOverlayU[3], this.tmpOverlayV[3]); + + indexDataBuffer.addIndices(indexA, indexB, indexC); + } + } + + this.drawCommands.addCommand(0, 0, 0, 0, elementOffset, indexDataBuffer.pos - elementOffset); + + return true; + } + + cacheModelVertexData(model: Model, resetData: boolean): void { + if (RendererWebGLC.modelVertexDataCache.has(model)) { + return; + } + + const vertexDataBuffer: VertexDataBuffer = this.vertexDataBuffer; + const startPos: number = vertexDataBuffer.pos; + this.addModelVertexData(vertexDataBuffer, model); + const endPos: number = vertexDataBuffer.pos; + if (resetData) { + vertexDataBuffer.pos = startPos; + } + const length: number = endPos - startPos; + if (length > 0) { + const data: Uint8Array = vertexDataBuffer.data.slice(startPos, endPos); + RendererWebGLC.modelVertexDataCache.set(model, { frame: this.frame, data }); + } + } + + static onSceneLoaded(world: World3D | null): void { + if (!world) { + return; + } + + // Cache model vertex data + RendererWebGLC.modelVertexDataCache.clear(); + RendererWebGLC.modelsToCache.clear(); + + for (let level: number = 0; level < world.maxLevel; level++) { + for (let x: number = 0; x < world.maxTileX; x++) { + for (let z: number = 0; z < world.maxTileZ; z++) { + const tile: Ground | null = world.levelTiles[level][x][z]; + RendererWebGLC.cacheTile(tile); + } + } + } + } + + static cacheTile(tile: Ground | null): void { + if (!tile) { + return; + } + if (tile.bridge) { + this.cacheTile(tile.bridge); + } + const wallModelA: Model | null | undefined = tile.wall?.modelA; + const wallModelB: Model | null | undefined = tile.wall?.modelB; + const wallDecModel: Model | null | undefined = tile.wallDecoration?.model; + const groundDecModel: Model | null | undefined = tile.groundDecoration?.model; + if (wallModelA) { + this.modelsToCache.add(wallModelA); + // Renderer.cacheModelVertexData(wallModelA); + } + if (wallModelB) { + this.modelsToCache.add(wallModelB); + // Renderer.cacheModelVertexData(wallModelB); + } + if (wallDecModel) { + this.modelsToCache.add(wallDecModel); + // Renderer.cacheModelVertexData(wallDecModel); + } + if (groundDecModel) { + this.modelsToCache.add(groundDecModel); + // Renderer.cacheModelVertexData(groundDecModel); + } + for (const loc of tile.locs) { + if (!loc) { + continue; + } + const model: Model | null = loc.model; + if (!model) { + continue; + } + this.modelsToCache.add(model); + // Renderer.cacheModelVertexData(model); + } + } + + static onSceneReset(world: World3D): void { + RendererWebGLC.modelStartIndexMap.clear(); + RendererWebGLC.modelVertexDataCache.clear(); + RendererWebGLC.modelsToCache.clear(); + } + + addModelVertexData(vertexDataBuffer: VertexDataBuffer, model: Model): void { + const triangleColorsA: Int32Array | null = model.faceColorA; + const triangleColorsB: Int32Array | null = model.faceColorB; + const triangleColorsC: Int32Array | null = model.faceColorC; + + if (!triangleColorsA || !triangleColorsB || !triangleColorsC) { + return; + } + + const verticesX: Int32Array = model.vertexX; + const verticesY: Int32Array = model.vertexY; + const verticesZ: Int32Array = model.vertexZ; + + const triangleA: Int32Array = model.faceVertexA; + const triangleB: Int32Array = model.faceVertexB; + const triangleC: Int32Array = model.faceVertexC; + + const triangleColors: Int32Array | null = model.faceColor; + + const triangleAlphas: Int32Array | null = model.faceAlpha; + + const triangleInfos: Int32Array | null = model.faceInfo; + + const textureMappingP: Int32Array = model.texturedVertexA; + const textureMappingM: Int32Array = model.texturedVertexB; + const textureMappingN: Int32Array = model.texturedVertexC; + + const triangleCount: number = model.faceCount; + for (let t: number = 0; t < triangleCount; t++) { + const a: number = triangleA[t]; + const b: number = triangleB[t]; + const c: number = triangleC[t]; + + const xa: number = verticesX[a]; + const ya: number = verticesY[a]; + const za: number = verticesZ[a]; + + const xb: number = verticesX[b]; + const yb: number = verticesY[b]; + const zb: number = verticesZ[b]; + + const xc: number = verticesX[c]; + const yc: number = verticesY[c]; + const zc: number = verticesZ[c]; + + const colorA: number = triangleColorsA[t]; + let colorB: number = triangleColorsB[t]; + let colorC: number = triangleColorsC[t]; + + let alpha: number = 0xff; + if (triangleAlphas) { + alpha = 0xff - triangleAlphas[t]; + } + + let info: number = 0; + if (triangleInfos) { + info = triangleInfos[t]; + } + + const type: number = info & 0x3; + + // Flat shading + if (type === 1 || type === 3) { + colorC = colorB = colorA; + } + + let textureId: number = -1; + + let u0: number = 0.0; + let v0: number = 0.0; + let u1: number = 0.0; + let v1: number = 0.0; + let u2: number = 0.0; + let v2: number = 0.0; + + // Textured + if ((type === 2 || type === 3) && triangleColors) { + textureId = triangleColors[t]; + + const texCoord: number = info >> 2; + const p: number = textureMappingP[texCoord]; + const m: number = textureMappingM[texCoord]; + const n: number = textureMappingN[texCoord]; + + const vx: number = verticesX[p]; + const vy: number = verticesY[p]; + const vz: number = verticesZ[p]; + + const f_882_: number = verticesX[m] - vx; + const f_883_: number = verticesY[m] - vy; + const f_884_: number = verticesZ[m] - vz; + const f_885_: number = verticesX[n] - vx; + const f_886_: number = verticesY[n] - vy; + const f_887_: number = verticesZ[n] - vz; + const f_888_: number = verticesX[a] - vx; + const f_889_: number = verticesY[a] - vy; + const f_890_: number = verticesZ[a] - vz; + const f_891_: number = verticesX[b] - vx; + const f_892_: number = verticesY[b] - vy; + const f_893_: number = verticesZ[b] - vz; + const f_894_: number = verticesX[c] - vx; + const f_895_: number = verticesY[c] - vy; + const f_896_: number = verticesZ[c] - vz; + + const f_897_: number = f_883_ * f_887_ - f_884_ * f_886_; + const f_898_: number = f_884_ * f_885_ - f_882_ * f_887_; + const f_899_: number = f_882_ * f_886_ - f_883_ * f_885_; + let f_900_: number = f_886_ * f_899_ - f_887_ * f_898_; + let f_901_: number = f_887_ * f_897_ - f_885_ * f_899_; + let f_902_: number = f_885_ * f_898_ - f_886_ * f_897_; + let f_903_: number = 1.0 / (f_900_ * f_882_ + f_901_ * f_883_ + f_902_ * f_884_); + + u0 = (f_900_ * f_888_ + f_901_ * f_889_ + f_902_ * f_890_) * f_903_; + u1 = (f_900_ * f_891_ + f_901_ * f_892_ + f_902_ * f_893_) * f_903_; + u2 = (f_900_ * f_894_ + f_901_ * f_895_ + f_902_ * f_896_) * f_903_; + + f_900_ = f_883_ * f_899_ - f_884_ * f_898_; + f_901_ = f_884_ * f_897_ - f_882_ * f_899_; + f_902_ = f_882_ * f_898_ - f_883_ * f_897_; + f_903_ = 1.0 / (f_900_ * f_885_ + f_901_ * f_886_ + f_902_ * f_887_); + + v0 = (f_900_ * f_888_ + f_901_ * f_889_ + f_902_ * f_890_) * f_903_; + v1 = (f_900_ * f_891_ + f_901_ * f_892_ + f_902_ * f_893_) * f_903_; + v2 = (f_900_ * f_894_ + f_901_ * f_895_ + f_902_ * f_896_) * f_903_; + } + + vertexDataBuffer.addVertex(xa, ya, za, colorA, alpha, textureId, u0, v0); + vertexDataBuffer.addVertex(xb, yb, zb, colorB, alpha, textureId, u1, v1); + vertexDataBuffer.addVertex(xc, yc, zc, colorC, alpha, textureId, u2, v2); + } + } + + startDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + if (!this.isDrawingScene) { + return; + } + const vertexDataBuffer: VertexDataBuffer = this.vertexDataBuffer; + const indexDataBuffer: IndexDataBuffer = this.indexDataBuffer; + + const modelStartIndex: number | undefined = RendererWebGLC.modelStartIndexMap.get(model); + if (modelStartIndex !== undefined) { + RendererWebGLC.modelStartIndex = modelStartIndex; + RendererWebGLC.modelElementOffset = indexDataBuffer.pos; + return; + } + + const triangleColorsA: Int32Array | null = model.faceColorA; + const triangleColorsB: Int32Array | null = model.faceColorB; + const triangleColorsC: Int32Array | null = model.faceColorC; + + if (!triangleColorsA || !triangleColorsB || !triangleColorsC) { + return; + } + + const vertexDataStartPos: number = vertexDataBuffer.pos; + RendererWebGLC.modelStartIndex = vertexDataStartPos / VertexDataBuffer.STRIDE; + RendererWebGLC.modelElementOffset = indexDataBuffer.pos; + + RendererWebGLC.modelStartIndexMap.set(model, RendererWebGLC.modelStartIndex); + + const cachedVertexData: CachedVertexData | undefined = RendererWebGLC.modelVertexDataCache.get(model); + if (cachedVertexData) { + vertexDataBuffer.addData(cachedVertexData.data); + return; + } + + if (RendererWebGLC.modelsToCache.has(model)) { + this.cacheModelVertexData(model, false); + } else { + this.addModelVertexData(vertexDataBuffer, model); + } + } + + endDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + if (!this.isDrawingScene) { + return; + } + + const elementCount: number = this.indexDataBuffer.pos - RendererWebGLC.modelElementOffset; + + if (elementCount > 0) { + const x: number = relativeX + World3D.eyeX; + const y: number = relativeY + World3D.eyeY; + const z: number = relativeZ + World3D.eyeZ; + + this.drawCommands.addCommand(x, y, z, yaw, RendererWebGLC.modelElementOffset, elementCount); + } + } + + drawModelTriangle(model: Model, index: number): boolean { + if (!this.isDrawingScene) { + return false; + } + + // if (Renderer.modelsToCache.has(model)) { + // return true; + // } + + const startIndex: number = RendererWebGLC.modelStartIndex + index * 3; + + this.indexDataBuffer.addIndices(startIndex, startIndex + 1, startIndex + 2); + + return true; + } + + endFrame(): void { + for (const [pixMap, pixMapTexture] of this.pixMapTextures) { + if (this.frame - pixMapTexture.lastFrameUsed > 5) { + this.gl.deleteTexture(pixMapTexture.texture); + this.pixMapTextures.delete(pixMap); + } + } + } +} diff --git a/src/graphics/renderer/webgl/Shader.ts b/src/graphics/renderer/webgl/Shader.ts new file mode 100644 index 0000000..eb7139e --- /dev/null +++ b/src/graphics/renderer/webgl/Shader.ts @@ -0,0 +1,31 @@ +import { WebGLResource } from './WebGLResource'; + +export class Shader extends WebGLResource { + readonly shader: WebGLShader; + + constructor(ctx: WebGL2RenderingContext, type: number, source: string) { + super(ctx); + + const shader: WebGLShader = ctx.createShader(type)!; + + ctx.shaderSource(shader, source); + + ctx.compileShader(shader); + + const success: number = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS); + + if (!success) { + const log: string | null = ctx.getShaderInfoLog(shader); + + ctx.deleteShader(shader); + + throw Error(`Failed to compile WebGL shader:\n${log}`); + } + + this.shader = shader; + } + + override delete(): void { + this.ctx.deleteShader(this.shader); + } +} diff --git a/src/graphics/renderer/webgl/ShaderProgram.ts b/src/graphics/renderer/webgl/ShaderProgram.ts new file mode 100644 index 0000000..7e5f082 --- /dev/null +++ b/src/graphics/renderer/webgl/ShaderProgram.ts @@ -0,0 +1,56 @@ +import { Shader } from './Shader'; +import { WebGLResource } from './WebGLResource'; + +export function createProgram(ctx: WebGL2RenderingContext, shaders: Iterable): ShaderProgram { + const program: ShaderProgram = new ShaderProgram(ctx); + + for (const shader of shaders) { + program.attach(shader); + } + + program.link(); + + for (const shader of shaders) { + shader.delete(); + } + + return program; +} + +export class ShaderProgram extends WebGLResource { + readonly program: WebGLProgram; + + constructor(ctx: WebGL2RenderingContext) { + super(ctx); + + this.program = ctx.createProgram()!; + } + + attach(shader: Shader): void { + this.ctx.attachShader(this.program, shader.shader); + } + + link(): void { + const ctx: WebGL2RenderingContext = this.ctx; + + ctx.linkProgram(this.program); + + const success: number = ctx.getProgramParameter(this.program, ctx.LINK_STATUS); + + if (!success) { + const log: string | null = ctx.getProgramInfoLog(this.program); + + this.delete(); + + throw new Error(`Failed to link shader program: ${log}`); + } + } + + use(): void { + this.ctx.useProgram(this.program); + } + + override delete(): void { + this.ctx.deleteProgram(this.program); + } +} diff --git a/src/graphics/renderer/webgl/WebGLResource.ts b/src/graphics/renderer/webgl/WebGLResource.ts new file mode 100644 index 0000000..701b5ff --- /dev/null +++ b/src/graphics/renderer/webgl/WebGLResource.ts @@ -0,0 +1,5 @@ +export abstract class WebGLResource { + constructor(readonly ctx: WebGL2RenderingContext) { } + + abstract delete(): void; +} diff --git a/src/graphics/renderer/webgl/shaders/commons.glsl.ts b/src/graphics/renderer/webgl/shaders/commons.glsl.ts new file mode 100644 index 0000000..961e8ba --- /dev/null +++ b/src/graphics/renderer/webgl/shaders/commons.glsl.ts @@ -0,0 +1,45 @@ +// https://stackoverflow.com/a/17309861 +export const hslToRgbFunction: string = ` +vec3 hslToRgb(int hsl, float brightness) { + const float onethird = 1.0 / 3.0; + const float twothird = 2.0 / 3.0; + const float rcpsixth = 6.0; + + float hue = float(hsl >> 10) / 64.0 + 0.0078125; + float sat = float((hsl >> 7) & 0x7) / 8.0 + 0.0625; + float lum = (float(hsl & 0x7f) / 128.0); + + vec3 xt = vec3( + rcpsixth * (hue - twothird), + 0.0, + rcpsixth * (1.0 - hue) + ); + + if (hue < twothird) { + xt.r = 0.0; + xt.g = rcpsixth * (twothird - hue); + xt.b = rcpsixth * (hue - onethird); + } + + if (hue < onethird) { + xt.r = rcpsixth * (onethird - hue); + xt.g = rcpsixth * hue; + xt.b = 0.0; + } + + xt = min( xt, 1.0 ); + + float sat2 = 2.0 * sat; + float satinv = 1.0 - sat; + float luminv = 1.0 - lum; + float lum2m1 = (2.0 * lum) - 1.0; + vec3 ct = (sat2 * xt) + satinv; + + vec3 rgb; + if (lum >= 0.5) + rgb = (luminv * ct) + lum2m1; + else rgb = lum * ct; + + return pow(rgb, vec3(brightness)); +} +`; diff --git a/src/graphics/renderer/webgl/shaders/fullscreen-pixmap.frag.glsl.ts b/src/graphics/renderer/webgl/shaders/fullscreen-pixmap.frag.glsl.ts new file mode 100644 index 0000000..0bc9ee1 --- /dev/null +++ b/src/graphics/renderer/webgl/shaders/fullscreen-pixmap.frag.glsl.ts @@ -0,0 +1,19 @@ +export const SHADER_CODE: string = ` +#version 300 es + +precision highp float; + +uniform highp sampler2D u_frame; + +in vec2 v_texCoord; + +out vec4 fragColor; + +void main() { + fragColor = texture(u_frame, v_texCoord).bgra; + if (fragColor == vec4(1.0)) { + discard; + } + fragColor.a = 1.0; +} +`.trim(); diff --git a/src/graphics/renderer/webgl/shaders/fullscreen-pixmap.vert.glsl.ts b/src/graphics/renderer/webgl/shaders/fullscreen-pixmap.vert.glsl.ts new file mode 100644 index 0000000..b145e4b --- /dev/null +++ b/src/graphics/renderer/webgl/shaders/fullscreen-pixmap.vert.glsl.ts @@ -0,0 +1,18 @@ +export const SHADER_CODE: string = ` +#version 300 es + +out vec2 v_texCoord; + +const vec2 vertices[3] = vec2[3]( + vec2(-1, -1), + vec2( 3, -1), + vec2(-1, 3) +); + +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + v_texCoord = gl_Position.xy * 0.5 + 0.5; + // flip y + v_texCoord.y = 1.0 - v_texCoord.y; +} +`.trim(); diff --git a/src/graphics/renderer/webgl/shaders/fullscreen-texture.frag.glsl.ts b/src/graphics/renderer/webgl/shaders/fullscreen-texture.frag.glsl.ts new file mode 100644 index 0000000..b7919f6 --- /dev/null +++ b/src/graphics/renderer/webgl/shaders/fullscreen-texture.frag.glsl.ts @@ -0,0 +1,15 @@ +export const SHADER_CODE: string = ` +#version 300 es + +precision highp float; + +uniform highp sampler2D u_frame; + +in vec2 v_texCoord; + +out vec4 fragColor; + +void main() { + fragColor = texture(u_frame, v_texCoord); +} +`.trim(); diff --git a/src/graphics/renderer/webgl/shaders/fullscreen-texture.vert.glsl.ts b/src/graphics/renderer/webgl/shaders/fullscreen-texture.vert.glsl.ts new file mode 100644 index 0000000..838aa25 --- /dev/null +++ b/src/graphics/renderer/webgl/shaders/fullscreen-texture.vert.glsl.ts @@ -0,0 +1,16 @@ +export const SHADER_CODE: string = ` +#version 300 es + +out vec2 v_texCoord; + +const vec2 vertices[3] = vec2[3]( + vec2(-1, -1), + vec2( 3, -1), + vec2(-1, 3) +); + +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + v_texCoord = gl_Position.xy * 0.5 + 0.5; +} +`.trim(); diff --git a/src/graphics/renderer/webgl/shaders/main.frag.glsl.ts b/src/graphics/renderer/webgl/shaders/main.frag.glsl.ts new file mode 100644 index 0000000..94f5bac --- /dev/null +++ b/src/graphics/renderer/webgl/shaders/main.frag.glsl.ts @@ -0,0 +1,772 @@ +import { hslToRgbFunction } from './commons.glsl'; + +export const SHADER_CODE: string = ` +#version 300 es + +precision highp float; +precision highp int; + +flat in ivec3 xs; +flat in ivec3 ys; +flat in ivec3 colors; + +out vec4 fragColor; + +const vec2 vertices[3] = vec2[3]( + vec2(20, 200), + vec2(400, 190), + vec2(200, 20) +); + +// const int colors[3] = int[3]( +// 56255, +// 959, +// 22463 +// ); + +const float brightness = 0.9; + + +const int width = 512; +const int height = 334; + +const int boundBottom = height; + +${hslToRgbFunction} + +int reciprocal15(int value) { + return 32768 / value; +} + +bool isOutsideScanline(int xA, int xB) { + return false; + // int fragX = int(gl_FragCoord.x); + // return fragX < xA || fragX >= xB || xA >= xB; +} + +vec3 getScanlineColor(int xA, int xB, int colorA, int colorB) { + int fragX = int(gl_FragCoord.x); + if (fragX < xA || fragX >= xB) { + // discard; + } + int colorStep; + int length; + if (xA < xB) { + length = (xB - xA) >> 2; + if (length > 0) { + colorStep = (colorB - colorA) * reciprocal15(length) >> 15; + } + } else { + // discard; + } + int scanlineX = fragX - xA; + colorA += colorStep * (scanlineX >> 2); + return hslToRgb(colorA >> 8, brightness); +} + +void main() { + // float x = v_barycentric.x * vertices[0].x + v_barycentric.y * vertices[1].x + v_barycentric.z * vertices[2].x; + // float y = float(gl_FragCoord.y); + fragColor = vec4(1.0, 0.0, 0.0, 1.0); + // fragColor.r = x / 512.0; + // fragColor.r = y / 255.0; + int xA = xs.x; + int xB = xs.y; + int xC = xs.z; + int yA = ys.x; + int yB = ys.y; + int yC = ys.z; + int colorA = colors.x; + int colorB = colors.y; + int colorC = colors.z; + + // fragColor.rgb = hslToRgb(colorA, brightness); + + int minScanlineY = min(yA, min(yB, yC)); + int maxScanlineY = max(yA, max(yB, yC)); + int scanlineY = height - int(gl_FragCoord.y) - 1 - minScanlineY + 1; + if (scanlineY < 0 || scanlineY > maxScanlineY - minScanlineY) { + // discard; + } + // scanlineY++; + // fragColor.r = float(scanlineY) / 255.0; + + int dxAB = xB - xA; + int dyAB = yB - yA; + int dxAC = xC - xA; + int dyAC = yC - yA; + + int xStepAB = 0; + int colorStepAB = 0; + if (yB != yA) { + xStepAB = (dxAB << 16) / dyAB; + colorStepAB = ((colorB - colorA) << 15) / dyAB; + } + + int xStepBC = 0; + int colorStepBC = 0; + if (yC != yB) { + xStepBC = ((xC - xB) << 16) / (yC - yB); + colorStepBC = ((colorC - colorB) << 15) / (yC - yB); + } + + int xStepAC = 0; + int colorStepAC = 0; + if (yC != yA) { + xStepAC = ((xA - xC) << 16) / (yA - yC); + colorStepAC = ((colorA - colorC) << 15) / (yA - yC); + } + + int currentScanline = 0; + + if (yA <= yB && yA <= yC) { + if (yA < boundBottom) { + if (yB > boundBottom) { + yB = boundBottom; + } + + if (yC > boundBottom) { + yC = boundBottom; + } + + if (yB < yC) { + xC = xA <<= 16; + colorC = colorA <<= 15; + if (yA < 0) { + xC -= xStepAC * yA; + xA -= xStepAB * yA; + colorC -= colorStepAC * yA; + colorA -= colorStepAB * yA; + yA = 0; + } + + xB <<= 16; + colorB <<= 15; + if (yB < 0) { + xB -= xStepBC * yB; + colorB -= colorStepBC * yB; + yB = 0; + } + + if (yA != yB && xStepAC < xStepAB || yA == yB && xStepAC > xStepBC) { + yC -= yB; + yB -= yA; + // yA = lineOffset[yA]; + + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorA >> 7); + return; + } + // gouraudRaster(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, data, yA, 0); + xC += xStepAC; + xA += xStepAB; + colorC += colorStepAC; + colorA += colorStepAB; + // yA += width2d; + currentScanline++; + } + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xC >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorB >> 7); + return; + } + // gouraudRaster(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, data, yA, 0); + xC += xStepAC; + xB += xStepBC; + colorC += colorStepAC; + colorB += colorStepBC; + // yA += width2d; + currentScanline++; + } + } else { + yC -= yB; + yB -= yA; + // yA = lineOffset[yA]; + + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorC >> 7); + return; + } + // gouraudRaster(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, data, yA, 0); + xC += xStepAC; + xA += xStepAB; + colorC += colorStepAC; + colorA += colorStepAB; + // yA += width2d; + currentScanline++; + } + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorC >> 7); + return; + } + // gouraudRaster(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, data, yA, 0); + xC += xStepAC; + xB += xStepBC; + colorC += colorStepAC; + colorB += colorStepBC; + // yA += width2d; + currentScanline++; + } + } + } else { + xB = xA <<= 16; + colorB = colorA <<= 15; + if (yA < 0) { + xB -= xStepAC * yA; + xA -= xStepAB * yA; + colorB -= colorStepAC * yA; + colorA -= colorStepAB * yA; + yA = 0; + } + + xC <<= 16; + colorC <<= 15; + if (yC < 0) { + xC -= xStepBC * yC; + colorC -= colorStepBC * yC; + yC = 0; + } + + if (yA != yC && xStepAC < xStepAB || yA == yC && xStepBC > xStepAB) { + yB -= yC; + yC -= yA; + // yA = lineOffset[yA]; + + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xA >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorA >> 7); + return; + } + // gouraudRaster(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, data, yA, 0); + xB += xStepAC; + xA += xStepAB; + colorB += colorStepAC; + colorA += colorStepAB; + // yA += width2d; + currentScanline++; + } + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xC >> 16; + int scanlineXB = xA >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorA >> 7); + return; + } + // gouraudRaster(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, data, yA, 0); + xC += xStepBC; + xA += xStepAB; + colorC += colorStepBC; + colorA += colorStepAB; + // yA += width2d; + currentScanline++; + } + } else { + yB -= yC; + yC -= yA; + // yA = lineOffset[yA]; + + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorB >> 7); + return; + } + // gouraudRaster(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, data, yA, 0); + xB += xStepAC; + xA += xStepAB; + colorB += colorStepAC; + colorA += colorStepAB; + // yA += width2d; + currentScanline++; + } + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorC >> 7); + return; + } + // gouraudRaster(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, data, yA, 0); + xC += xStepBC; + xA += xStepAB; + colorC += colorStepBC; + colorA += colorStepAB; + // yA += width2d; + currentScanline++; + } + } + } + } + } else if (yB <= yC) { + if (yB < boundBottom) { + if (yC > boundBottom) { + yC = boundBottom; + } + + if (yA > boundBottom) { + yA = boundBottom; + } + + if (yC < yA) { + xA = xB <<= 16; + colorA = colorB <<= 15; + if (yB < 0) { + xA -= xStepAB * yB; + xB -= xStepBC * yB; + colorA -= colorStepAB * yB; + colorB -= colorStepBC * yB; + yB = 0; + } + + xC <<= 16; + colorC <<= 15; + if (yC < 0) { + xC -= xStepAC * yC; + colorC -= colorStepAC * yC; + yC = 0; + } + + if (yB != yC && xStepAB < xStepBC || yB == yC && xStepAB > xStepAC) { + yA -= yC; + yC -= yB; + // yB = lineOffset[yB]; + + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorB >> 7); + return; + } + // gouraudRaster(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, data, yB, 0); + xA += xStepAB; + xB += xStepBC; + colorA += colorStepAB; + colorB += colorStepBC; + // yB += width2d; + currentScanline++; + } + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorC >> 7); + return; + } + // gouraudRaster(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, data, yB, 0); + xA += xStepAB; + xC += xStepAC; + colorA += colorStepAB; + colorC += colorStepAC; + // yB += width2d; + currentScanline++; + } + } else { + yA -= yC; + yC -= yB; + // yB = lineOffset[yB]; + + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xA >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorA >> 7); + return; + } + // gouraudRaster(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, data, yB, 0); + xA += xStepAB; + xB += xStepBC; + colorA += colorStepAB; + colorB += colorStepBC; + // yB += width2d; + currentScanline++; + } + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xC >> 16; + int scanlineXB = xA >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorA >> 7); + return; + } + // gouraudRaster(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, data, yB, 0); + xA += xStepAB; + xC += xStepAC; + colorA += colorStepAB; + colorC += colorStepAC; + // yB += width2d; + currentScanline++; + } + } + } else { + xC = xB <<= 16; + colorC = colorB <<= 15; + if (yB < 0) { + xC -= xStepAB * yB; + xB -= xStepBC * yB; + colorC -= colorStepAB * yB; + colorB -= colorStepBC * yB; + yB = 0; + } + + xA <<= 16; + colorA <<= 15; + if (yA < 0) { + xA -= xStepAC * yA; + colorA -= colorStepAC * yA; + yA = 0; + } + + if (xStepAB < xStepBC) { + yC -= yA; + yA -= yB; + // yB = lineOffset[yB]; + + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xC >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorB >> 7); + return; + } + // gouraudRaster(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, data, yB, 0); + xC += xStepAB; + xB += xStepBC; + colorC += colorStepAB; + colorB += colorStepBC; + // yB += width2d; + currentScanline++; + } + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorB >> 7); + return; + } + // gouraudRaster(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, data, yB, 0); + xA += xStepAC; + xB += xStepBC; + colorA += colorStepAC; + colorB += colorStepBC; + // yB += width2d; + currentScanline++; + } + } else { + yC -= yA; + yA -= yB; + // yB = lineOffset[yB]; + + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorC >> 7); + return; + } + // gouraudRaster(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, data, yB, 0); + xC += xStepAB; + xB += xStepBC; + colorC += colorStepAB; + colorB += colorStepBC; + // yB += width2d; + currentScanline++; + } + while (--yC >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xA >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorA >> 7); + return; + } + // gouraudRaster(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, data, yB, 0); + xA += xStepAC; + xB += xStepBC; + colorA += colorStepAC; + colorB += colorStepBC; + // yB += width2d; + currentScanline++; + } + } + } + } + } else if (yC < boundBottom) { + if (yA > boundBottom) { + yA = boundBottom; + } + + if (yB > boundBottom) { + yB = boundBottom; + } + + if (yA < yB) { + xB = xC <<= 16; + colorB = colorC <<= 15; + if (yC < 0) { + xB -= xStepBC * yC; + xC -= xStepAC * yC; + colorB -= colorStepBC * yC; + colorC -= colorStepAC * yC; + yC = 0; + } + + xA <<= 16; + colorA <<= 15; + if (yA < 0) { + xA -= xStepAB * yA; + colorA -= colorStepAB * yA; + yA = 0; + } + + if (xStepBC < xStepAC) { + yB -= yA; + yA -= yC; + // yC = lineOffset[yC]; + + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorC >> 7); + return; + } + // gouraudRaster(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, data, yC, 0); + xB += xStepBC; + xC += xStepAC; + colorB += colorStepBC; + colorC += colorStepAC; + // yC += width2d; + currentScanline++; + } + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xA >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorA >> 7); + return; + } + // gouraudRaster(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, data, yC, 0); + xB += xStepBC; + xA += xStepAB; + colorB += colorStepBC; + colorA += colorStepAB; + // yC += width2d; + currentScanline++; + } + } else { + yB -= yA; + yA -= yC; + // yC = lineOffset[yC]; + + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xC >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorB >> 7); + return; + } + // gouraudRaster(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, data, yC, 0); + xB += xStepBC; + xC += xStepAC; + colorB += colorStepBC; + colorC += colorStepAC; + // yC += width2d; + currentScanline++; + } + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorB >> 7); + return; + } + // gouraudRaster(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, data, yC, 0); + xB += xStepBC; + xA += xStepAB; + colorB += colorStepBC; + colorA += colorStepAB; + // yC += width2d; + currentScanline++; + } + } + } else { + xA = xC <<= 16; + colorA = colorC <<= 15; + if (yC < 0) { + xA -= xStepBC * yC; + xC -= xStepAC * yC; + colorA -= colorStepBC * yC; + colorC -= colorStepAC * yC; + yC = 0; + } + + xB <<= 16; + colorB <<= 15; + if (yB < 0) { + xB -= xStepAB * yB; + colorB -= colorStepAB * yB; + yB = 0; + } + + if (xStepBC < xStepAC) { + yA -= yB; + yB -= yC; + + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xA >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorA >> 7, colorC >> 7); + return; + } + // gouraudRaster(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, data, yC, 0); + xA += xStepBC; + xC += xStepAC; + colorA += colorStepBC; + colorC += colorStepAC; + // yC += width2d; + currentScanline++; + } + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xB >> 16; + int scanlineXB = xC >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorB >> 7, colorC >> 7); + return; + } + // gouraudRaster(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, data, yC, 0); + xB += xStepAB; + xC += xStepAC; + colorB += colorStepAB; + colorC += colorStepAC; + // yC += width2d; + currentScanline++; + } + } else { + yA -= yB; + yB -= yC; + + while (--yB >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xC >> 16; + int scanlineXB = xA >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorA >> 7); + return; + } + // gouraudRaster(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, data, yC, 0); + xA += xStepBC; + xC += xStepAC; + colorA += colorStepBC; + colorC += colorStepAC; + // yC += width2d; + currentScanline++; + } + while (--yA >= 0) { + if (currentScanline == scanlineY) { + int scanlineXA = xC >> 16; + int scanlineXB = xB >> 16; + if (isOutsideScanline(scanlineXA, scanlineXB)) { + discard; + } + fragColor.rgb = getScanlineColor(scanlineXA, scanlineXB, colorC >> 7, colorB >> 7); + return; + } + // gouraudRaster(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, data, yC, 0); + xB += xStepAB; + xC += xStepAC; + colorB += colorStepAB; + colorC += colorStepAC; + // yC += width2d; + currentScanline++; + } + } + } + } + + // discard; + // fragColor = vec4(1.0, 1.0, 1.0, 1.0); +} + +`.trim(); diff --git a/src/graphics/renderer/webgl/shaders/main.vert.glsl.ts b/src/graphics/renderer/webgl/shaders/main.vert.glsl.ts new file mode 100644 index 0000000..7b4973d --- /dev/null +++ b/src/graphics/renderer/webgl/shaders/main.vert.glsl.ts @@ -0,0 +1,67 @@ +export const SHADER_CODE: string = ` +#version 300 es + +uniform highp usampler2D u_triangleData; + +flat out ivec3 xs; +flat out ivec3 ys; +flat out ivec3 colors; + +const float width = 512.0; +const float height = 334.0; +const vec2 dimensions = vec2(width, height); + +// const vec2 vertices[3] = vec2[3]( +// vec2(20, 200), +// vec2(400, 190), +// vec2(200, 20) +// ); + +// const vec3 barycentric[3] = vec3[3]( +// vec3(1, 0, 0), +// vec3(0, 1, 0), +// vec3(0, 0, 1) +// ); + +const vec2 vertices[3] = vec2[3]( + vec2(-1, -1), + vec2( 3, -1), + vec2(-1, 3) +); + +void main() { + int triangleIndex = gl_VertexID / 3; + + uvec4 triangleData = texelFetch(u_triangleData, ivec2(triangleIndex, 0), 0); + xs = ivec3( + int(triangleData.x >> 20u), + int((triangleData.x >> 8u) & 0xFFFu), + (int(triangleData.x & 0xFFu) << 4) | int(triangleData.y & 0xFFu) + ) - 2048; + ys = ivec3( + int(triangleData.y >> 20u), + int((triangleData.y >> 8u) & 0xFFFu), + int(triangleData.z >> 16u) + ) - 2048; + colors = ivec3( + int(triangleData.z & 0xFFFFu), + int(triangleData.w >> 16u), + int(triangleData.w & 0xFFFFu) + ); + + int vertexIndex = gl_VertexID % 0x3; + + // vec2 screenPos = vertices[gl_VertexID]; + vec2 screenPos = vec2(xs[vertexIndex], ys[vertexIndex]); + // screenPos.y = height - screenPos.y - 1.0; + screenPos += 0.5; + // screenPos *= 1.1; + gl_Position = vec4(screenPos * 2.0 / dimensions - 1.0, 0.0, 1.0); + // flip y + gl_Position.y *= -1.0; + // v_texCoord = gl_Position.xy * 0.5 + 0.5; + // v_barycentric = barycentric[gl_VertexID]; + + // gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); +} +`.trim(); diff --git a/src/graphics/renderer/webgpu/RendererWebGPU.ts b/src/graphics/renderer/webgpu/RendererWebGPU.ts new file mode 100644 index 0000000..04add2a --- /dev/null +++ b/src/graphics/renderer/webgpu/RendererWebGPU.ts @@ -0,0 +1,988 @@ +import type TileOverlay from '#/dash3d/type/TileOverlay.ts'; +import type TileUnderlay from '#/dash3d/type/TileUnderlay.ts'; +import World3D from '#/dash3d/World3D.js'; +import { canvas as cpuCanvas } from '#/graphics/Canvas.js'; +import type Model from '#/graphics/Model.ts'; +import Pix3D from '#/graphics/Pix3D.js'; +import PixMap from '#/graphics/PixMap.js'; +import { Renderer } from '#/graphics/renderer/Renderer.js'; + +import { SHADER_CODE as computeRasterizerShaderCode } from './shaders/compute-rasterizer.wgsl'; +import { SHADER_CODE as fullscreenPixelsShaderCode } from './shaders/fullscreen-pixels.wgsl'; +import { SHADER_CODE as fullscreenPixMapShaderCode } from './shaders/fullscreen-pixmap.wgsl'; +import { SHADER_CODE as fullscreenTextureShaderCode } from './shaders/fullscreen-texture.wgsl'; +import { SHADER_CODE as fullscreenVertexShaderCode } from './shaders/fullscreen-vertex.wgsl'; + +const INITIAL_TRIANGLES: number = 65536; + +const TEXTURE_COUNT: number = 50; + +const TEXTURE_SIZE: number = 128; +const TEXTURE_PIXEL_COUNT: number = TEXTURE_SIZE * TEXTURE_SIZE; + +const PALETTE_BYTES: number = 65536 * 4; +const TEXTURES_TRANSLUCENT_BYTES: number = TEXTURE_COUNT * 4; +const TEXTURES_BYTES: number = TEXTURE_COUNT * TEXTURE_PIXEL_COUNT * 4 * 4; + +interface QueuedRenderPixMapCommand { + pixMap: PixMap; + x: number; + y: number; +} + +export class RendererWebGPU extends Renderer { + drawTileUnderlay(world: World3D, underlay: TileUnderlay, level: number, tileX: number, tileZ: number): boolean { + return false; + } + + drawTileOverlay(world: World3D, overlay: TileOverlay, tileX: number, tileZ: number): boolean { + return false; + } + + startDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + } + + endDrawModel(model: Model, yaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void { + } + + drawModelTriangle(model: Model, index: number): boolean { + return false; + } + + device: GPUDevice; + context: GPUCanvasContext; + + defaultSampler!: GPUSampler; + samplerTextureGroupLayout!: GPUBindGroupLayout; + frameTexture!: GPUTexture; + frameBindGroup!: GPUBindGroup; + + uniformBuffer!: GPUBuffer; + + pixelBuffer!: GPUBuffer; + depthBuffer!: GPUBuffer; + + // Lookup tables + lutsBuffer!: GPUBuffer; + + // Rasterizer + rasterizerShaderModule!: GPUShaderModule; + rasterizerBindGroupLayout!: GPUBindGroupLayout; + triangleDataBindGroupLayout!: GPUBindGroupLayout; + rasterizerBindGroup!: GPUBindGroup; + + // Compute pipelines + clearPipeline!: GPUComputePipeline; + // Depth + renderFlatDepthPipeline!: GPUComputePipeline; + renderGouraudDepthPipeline!: GPUComputePipeline; + renderTexturedDepthPipeline!: GPUComputePipeline; + // Color + renderFlatPipeline!: GPUComputePipeline; + renderGouraudPipeline!: GPUComputePipeline; + renderTexturedPipeline!: GPUComputePipeline; + renderAlphaPipeline!: GPUComputePipeline; + + fullscreenVertexShaderModule!: GPUShaderModule; + pixMapShaderModule!: GPUShaderModule; + pixMapPipeline!: GPURenderPipeline; + textureShaderModule!: GPUShaderModule; + frameTexturePipeline!: GPURenderPipeline; + + pixelBufferShaderModule!: GPUShaderModule; + pixelBufferPipeline!: GPURenderPipeline; + pixelBufferBindGroup!: GPUBindGroup; + + frameRenderPassDescriptor!: GPURenderPassDescriptor; + renderPassDescriptor!: GPURenderPassDescriptor; + + flatTriangleDataBuffer: GPUBuffer | undefined; + gouraudTriangleDataBuffer: GPUBuffer | undefined; + texturedTriangleDataBuffer: GPUBuffer | undefined; + alphaTriangleDataBuffer: GPUBuffer | undefined; + + encoder!: GPUCommandEncoder; + mainPass!: GPURenderPassEncoder; + + isRenderingFrame: boolean = false; + + isRenderingScene: boolean = false; + + queuedRenderPixMapCommands: QueuedRenderPixMapCommand[] = []; + + triangleCount: number = 0; + + flatTriangleData: Uint32Array = new Uint32Array(INITIAL_TRIANGLES * 8); + flatTriangleCount: number = 0; + + gouraudTriangleData: Uint32Array = new Uint32Array(INITIAL_TRIANGLES * 10); + gouraudTriangleCount: number = 0; + + texturedTriangleData: Uint32Array = new Uint32Array(INITIAL_TRIANGLES * 20); + texturedTriangleCount: number = 0; + + alphaTriangleData: Uint32Array = new Uint32Array(INITIAL_TRIANGLES * 10); + alphaTriangleCount: number = 0; + + texturesToDelete: GPUTexture[] = []; + + texturesUsed: boolean[] = new Array(TEXTURE_COUNT).fill(false); + + textureStagingBuffers: GPUBuffer[] = []; + + frameCount: number = 0; + + static hasWebGPUSupport(): boolean { + return 'gpu' in navigator; + } + + static async init(container: HTMLElement, width: number, height: number): Promise { + if (!RendererWebGPU.hasWebGPUSupport()) { + throw Error('WebGPU is not supported.'); + } + + const adapter: GPUAdapter | null = await navigator.gpu.requestAdapter(); + if (!adapter) { + throw Error('Request for WebGPU adapter failed.'); + } + + const device: GPUDevice | undefined = await adapter.requestDevice(); + if (!device) { + throw new Error('Request for WebGPU device failed.'); + } + + const canvas: HTMLCanvasElement = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.style.display = cpuCanvas.style.display; + canvas.style.position = cpuCanvas.style.position; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.imageRendering = cpuCanvas.style.imageRendering; + container.appendChild(canvas); + + const context: GPUCanvasContext | null = canvas.getContext('webgpu'); + if (!context) { + canvas.remove(); + throw new Error('WebGPU context could not be created.'); + } + + const presentationFormat: GPUTextureFormat = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ + device, + format: presentationFormat + }); + + Renderer.resetRenderer(); + return new RendererWebGPU(canvas, device, context); + } + + constructor(canvas: HTMLCanvasElement, device: GPUDevice, context: GPUCanvasContext) { + super(canvas); + this.device = device; + this.context = context; + this.init(); + } + + init(): void { + const viewportWidth: number = World3D.viewportRight; + const viewportHeight: number = World3D.viewportBottom; + + this.defaultSampler = this.device.createSampler(); + this.samplerTextureGroupLayout = this.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + sampler: { type: 'filtering' } + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + texture: { sampleType: 'float' } + } + ] + }); + + this.frameTexture = this.device.createTexture({ + size: { width: this.canvas.width, height: this.canvas.height }, + format: 'rgba8unorm', + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING + }); + this.frameBindGroup = this.device.createBindGroup({ + layout: this.samplerTextureGroupLayout, + entries: [ + { + binding: 0, + resource: this.defaultSampler + }, + { + binding: 1, + resource: this.frameTexture.createView() + } + ] + }); + + this.device.queue.copyExternalImageToTexture({ source: cpuCanvas }, { texture: this.frameTexture }, { width: this.canvas.width, height: this.canvas.height }); + + this.uniformBuffer = this.device.createBuffer({ + size: 2 * 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST + }); + this.device.queue.writeBuffer(this.uniformBuffer, 0, new Float32Array([viewportWidth, viewportHeight])); + + this.pixelBuffer = this.device.createBuffer({ + size: viewportWidth * viewportHeight * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + this.depthBuffer = this.device.createBuffer({ + size: viewportWidth * viewportHeight * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + + this.lutsBuffer = this.device.createBuffer({ + size: PALETTE_BYTES + TEXTURES_TRANSLUCENT_BYTES + TEXTURES_BYTES, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + + this.updatePalette(); + this.updateTextures(); + + this.rasterizerShaderModule = this.device.createShaderModule({ + label: 'rasterizer shaders', + code: computeRasterizerShaderCode + }); + + this.rasterizerBindGroupLayout = this.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'storage' } + }, + { + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'storage' } + }, + { + binding: 2, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'read-only-storage' } + } + ] + }); + + this.triangleDataBindGroupLayout = this.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'read-only-storage' } + } + ] + }); + + this.rasterizerBindGroup = this.device.createBindGroup({ + layout: this.rasterizerBindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: this.pixelBuffer + } + }, + { + binding: 1, + resource: { + buffer: this.depthBuffer + } + }, + { + binding: 2, + resource: { + buffer: this.lutsBuffer + } + } + ] + }); + + this.clearPipeline = this.device.createComputePipeline({ + label: 'clear pixels pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'clear' + } + }); + + // depth + this.renderFlatDepthPipeline = this.device.createComputePipeline({ + label: 'render flat depth pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout, this.triangleDataBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'renderFlatDepth' + } + }); + this.renderGouraudDepthPipeline = this.device.createComputePipeline({ + label: 'render gouraud depth pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout, this.triangleDataBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'renderGouraudDepth' + } + }); + this.renderTexturedDepthPipeline = this.device.createComputePipeline({ + label: 'render textured depth pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout, this.triangleDataBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'renderTexturedDepth' + } + }); + + // color + this.renderFlatPipeline = this.device.createComputePipeline({ + label: 'render flat pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout, this.triangleDataBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'renderFlat' + } + }); + this.renderGouraudPipeline = this.device.createComputePipeline({ + label: 'render gouraud pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout, this.triangleDataBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'renderGouraud' + } + }); + this.renderTexturedPipeline = this.device.createComputePipeline({ + label: 'render textured pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout, this.triangleDataBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'renderTextured' + } + }); + this.renderAlphaPipeline = this.device.createComputePipeline({ + label: 'render alpha pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.rasterizerBindGroupLayout, this.triangleDataBindGroupLayout] }), + compute: { + module: this.rasterizerShaderModule, + entryPoint: 'renderAlpha' + } + }); + + this.fullscreenVertexShaderModule = this.device.createShaderModule({ + label: 'fullscreen vertex shader', + code: fullscreenVertexShaderCode + }); + this.pixMapShaderModule = this.device.createShaderModule({ + label: 'pixmap shader', + code: fullscreenPixMapShaderCode + }); + this.pixMapPipeline = this.device.createRenderPipeline({ + label: 'pixmap pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.samplerTextureGroupLayout] }), + vertex: { + module: this.fullscreenVertexShaderModule + }, + fragment: { + module: this.pixMapShaderModule, + targets: [{ format: 'rgba8unorm' }] + } + }); + this.textureShaderModule = this.device.createShaderModule({ + label: 'texture shader', + code: fullscreenTextureShaderCode + }); + this.frameTexturePipeline = this.device.createRenderPipeline({ + label: 'frame texture pipeline', + layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.samplerTextureGroupLayout] }), + vertex: { + module: this.fullscreenVertexShaderModule + }, + fragment: { + module: this.textureShaderModule, + targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }] + } + }); + + this.pixelBufferShaderModule = this.device.createShaderModule({ + label: 'pixel buffer shader', + code: fullscreenPixelsShaderCode + }); + + this.pixelBufferPipeline = this.device.createRenderPipeline({ + label: 'pixel buffer pipeline', + layout: 'auto', + vertex: { + module: this.fullscreenVertexShaderModule + }, + fragment: { + module: this.pixelBufferShaderModule, + targets: [{ format: 'rgba8unorm' }] + } + }); + + this.pixelBufferBindGroup = this.device.createBindGroup({ + layout: this.pixelBufferPipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: this.uniformBuffer + } + }, + { + binding: 1, + resource: { + buffer: this.pixelBuffer + } + } + ] + }); + + this.frameRenderPassDescriptor = { + label: 'frame render pass', + colorAttachments: [ + { + view: this.context.getCurrentTexture().createView(), + clearValue: { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0 + }, + loadOp: 'load', + storeOp: 'store' + } + ] + }; + this.renderPassDescriptor = { + label: 'main render pass', + colorAttachments: [ + { + view: this.frameTexture.createView(), + clearValue: { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0 + }, + loadOp: 'load', + storeOp: 'store' + } + ] + }; + } + + override resize(width: number, height: number): void { + super.resize(width, height); + } + + override startFrame(): void { + this.isRenderingFrame = true; + + this.texturesUsed.fill(false); + + this.encoder = this.device.createCommandEncoder({ + label: 'render command encoder' + }); + this.mainPass = this.encoder.beginRenderPass(this.renderPassDescriptor); + + for (const command of this.queuedRenderPixMapCommands) { + this.renderPixMap(command.pixMap, command.x, command.y); + } + this.queuedRenderPixMapCommands.length = 0; + } + + override endFrame(): void { + if (!this.isRenderingFrame) { + return; + } + this.isRenderingFrame = false; + + this.mainPass.end(); + + for (const colorAttachment of this.frameRenderPassDescriptor.colorAttachments) { + colorAttachment!.view = this.context.getCurrentTexture().createView(); + } + + const framePass: GPURenderPassEncoder = this.encoder.beginRenderPass(this.frameRenderPassDescriptor); + framePass.setViewport(0, 0, this.canvas.width, this.canvas.height, 0, 1); + framePass.setPipeline(this.frameTexturePipeline); + framePass.setBindGroup(0, this.frameBindGroup); + framePass.draw(3); + framePass.end(); + + const commandBuffer: GPUCommandBuffer = this.encoder.finish(); + + this.device.queue.submit([commandBuffer]); + + for (const texture of this.texturesToDelete) { + texture.destroy(); + } + this.texturesToDelete.length = 0; + } + + updatePalette(): void { + this.device.queue.writeBuffer(this.lutsBuffer, 0, Pix3D.hslPal); + } + + updateTextures(): void { + for (let i: number = 0; i < TEXTURE_COUNT; i++) { + this.updateTexture(i, false); + } + const texturesTranslucentData: Uint32Array = new Uint32Array(TEXTURES_TRANSLUCENT_BYTES); + for (let i: number = 0; i < TEXTURE_COUNT; i++) { + texturesTranslucentData[i] = Pix3D.textureTranslucent[i] ? 1 : 0; + } + this.device.queue.writeBuffer(this.lutsBuffer, PALETTE_BYTES, texturesTranslucentData); + } + + override updateTexture(id: number, stage: boolean = true): void { + const texels: Int32Array | null = Pix3D.getTexels(id); + if (!texels) { + return; + } + const textureBytes: number = TEXTURE_PIXEL_COUNT * 4 * 4; + const lutsOffset: number = PALETTE_BYTES + TEXTURES_TRANSLUCENT_BYTES + id * textureBytes; + if (stage) { + let stagingBuffer: GPUBuffer; + if (this.textureStagingBuffers.length > 0) { + stagingBuffer = this.textureStagingBuffers.pop()!; + } else { + stagingBuffer = this.device.createBuffer({ + size: textureBytes, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, + mappedAtCreation: true + }); + } + + new Uint32Array(stagingBuffer.getMappedRange()).set(texels); + + stagingBuffer.unmap(); + + const encoder: GPUCommandEncoder = this.device.createCommandEncoder(); + encoder.copyBufferToBuffer(stagingBuffer, 0, this.lutsBuffer, lutsOffset, textureBytes); + this.device.queue.submit([encoder.finish()]); + + stagingBuffer.mapAsync(GPUMapMode.WRITE).then((): void => { + this.textureStagingBuffers.push(stagingBuffer); + }); + } else { + this.device.queue.writeBuffer(this.lutsBuffer, lutsOffset, texels); + } + } + + override setBrightness(brightness: number): void { + this.updatePalette(); + } + + override renderPixMap(pixMap: PixMap, x: number, y: number): boolean { + if (!this.isRenderingFrame) { + // Login screen flames are rendered outside of the frame loop + this.queuedRenderPixMapCommands.push({ pixMap, x, y }); + return true; + } + + const viewportWidth: number = World3D.viewportRight; + const viewportHeight: number = World3D.viewportBottom; + + if (pixMap.width2d === viewportWidth && pixMap.height2d === viewportHeight) { + this.mainPass.setViewport(x, y, viewportWidth, viewportHeight, 0, 1); + + this.mainPass.setPipeline(this.pixelBufferPipeline); + this.mainPass.setBindGroup(0, this.pixelBufferBindGroup); + this.mainPass.draw(3); + } + + const texture: GPUTexture = this.device.createTexture({ + size: { width: pixMap.width2d, height: pixMap.height2d }, + format: 'rgba8unorm', + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING + }); + this.device.queue.writeTexture({ texture }, pixMap.pixels, { bytesPerRow: pixMap.width2d * 4 }, { width: pixMap.width2d, height: pixMap.height2d }); + + const bindGroup: GPUBindGroup = this.device.createBindGroup({ + layout: this.samplerTextureGroupLayout, + entries: [ + { + binding: 0, + resource: this.defaultSampler + }, + { + binding: 1, + resource: texture.createView() + } + ] + }); + + this.mainPass.setViewport(x, y, pixMap.width2d, pixMap.height2d, 0, 1); + this.mainPass.setPipeline(this.pixMapPipeline); + this.mainPass.setBindGroup(0, bindGroup); + this.mainPass.draw(3); + + this.texturesToDelete.push(texture); + + return true; + } + + override startRenderScene(): void { + this.isRenderingScene = true; + this.triangleCount = 0; + this.flatTriangleCount = 0; + this.texturedTriangleCount = 0; + this.gouraudTriangleCount = 0; + this.alphaTriangleCount = 0; + } + + override endRenderScene(): void { + this.isRenderingScene = false; + this.renderScene(); + } + + renderScene(): void { + const viewportWidth: number = World3D.viewportRight; + const viewportHeight: number = World3D.viewportBottom; + + let flatTriangleDataBuffer: GPUBuffer | undefined = this.flatTriangleDataBuffer; + if (flatTriangleDataBuffer) { + flatTriangleDataBuffer.destroy(); + this.flatTriangleDataBuffer = undefined; + } + let gouraudTriangleDataBuffer: GPUBuffer | undefined = this.gouraudTriangleDataBuffer; + if (gouraudTriangleDataBuffer) { + gouraudTriangleDataBuffer.destroy(); + this.gouraudTriangleDataBuffer = undefined; + } + let texturedTriangleDataBuffer: GPUBuffer | undefined = this.texturedTriangleDataBuffer; + if (texturedTriangleDataBuffer) { + texturedTriangleDataBuffer.destroy(); + this.texturedTriangleDataBuffer = undefined; + } + let alphaTriangleDataBuffer: GPUBuffer | undefined = this.alphaTriangleDataBuffer; + if (alphaTriangleDataBuffer) { + alphaTriangleDataBuffer.destroy(); + this.alphaTriangleDataBuffer = undefined; + } + + let flatTriangleDataBindGroup: GPUBindGroup | undefined; + if (this.flatTriangleCount > 0) { + flatTriangleDataBuffer = this.device.createBuffer({ + size: this.flatTriangleCount * 8 * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + this.flatTriangleDataBuffer = flatTriangleDataBuffer; + this.device.queue.writeBuffer(flatTriangleDataBuffer, 0, this.flatTriangleData.subarray(0, this.flatTriangleCount * 8)); + + flatTriangleDataBindGroup = this.device.createBindGroup({ + layout: this.triangleDataBindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: flatTriangleDataBuffer + } + } + ] + }); + } + let gouraudTriangleDataBindGroup: GPUBindGroup | undefined; + if (this.gouraudTriangleCount > 0) { + gouraudTriangleDataBuffer = this.device.createBuffer({ + size: this.gouraudTriangleCount * 10 * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + this.gouraudTriangleDataBuffer = gouraudTriangleDataBuffer; + this.device.queue.writeBuffer(gouraudTriangleDataBuffer, 0, this.gouraudTriangleData.subarray(0, this.gouraudTriangleCount * 10)); + + gouraudTriangleDataBindGroup = this.device.createBindGroup({ + layout: this.triangleDataBindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: gouraudTriangleDataBuffer + } + } + ] + }); + } + let texturedTriangleDataBindGroup: GPUBindGroup | undefined; + if (this.texturedTriangleCount > 0) { + texturedTriangleDataBuffer = this.device.createBuffer({ + size: this.texturedTriangleCount * 20 * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + this.texturedTriangleDataBuffer = texturedTriangleDataBuffer; + this.device.queue.writeBuffer(this.texturedTriangleDataBuffer, 0, this.texturedTriangleData.subarray(0, this.texturedTriangleCount * 20)); + + texturedTriangleDataBindGroup = this.device.createBindGroup({ + layout: this.triangleDataBindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: texturedTriangleDataBuffer + } + } + ] + }); + } + let alphaTriangleDataBindGroup: GPUBindGroup | undefined; + if (this.alphaTriangleCount > 0) { + alphaTriangleDataBuffer = this.device.createBuffer({ + size: this.alphaTriangleCount * 10 * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + this.alphaTriangleDataBuffer = alphaTriangleDataBuffer; + this.device.queue.writeBuffer(alphaTriangleDataBuffer, 0, this.alphaTriangleData.subarray(0, this.alphaTriangleCount * 10)); + + alphaTriangleDataBindGroup = this.device.createBindGroup({ + layout: this.triangleDataBindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: alphaTriangleDataBuffer + } + } + ] + }); + } + + const encoder: GPUCommandEncoder = this.device.createCommandEncoder({ + label: 'render scene command encoder' + }); + + const computePass: GPUComputePassEncoder = encoder.beginComputePass(); + + computePass.setPipeline(this.clearPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.dispatchWorkgroups(Math.ceil((viewportWidth * viewportHeight) / 256)); + + // render depth + if (this.flatTriangleCount > 0 && flatTriangleDataBindGroup) { + computePass.setPipeline(this.renderFlatDepthPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.setBindGroup(1, flatTriangleDataBindGroup); + computePass.dispatchWorkgroups(this.flatTriangleCount); + } + if (this.gouraudTriangleCount > 0 && gouraudTriangleDataBindGroup) { + computePass.setPipeline(this.renderGouraudDepthPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.setBindGroup(1, gouraudTriangleDataBindGroup); + computePass.dispatchWorkgroups(this.gouraudTriangleCount); + } + if (this.texturedTriangleCount > 0 && texturedTriangleDataBindGroup) { + computePass.setPipeline(this.renderTexturedDepthPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.setBindGroup(1, texturedTriangleDataBindGroup); + computePass.dispatchWorkgroups(this.texturedTriangleCount); + } + + // render color + if (this.flatTriangleCount > 0 && flatTriangleDataBindGroup) { + computePass.setPipeline(this.renderFlatPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.setBindGroup(1, flatTriangleDataBindGroup); + computePass.dispatchWorkgroups(this.flatTriangleCount); + } + if (this.gouraudTriangleCount > 0 && gouraudTriangleDataBindGroup) { + computePass.setPipeline(this.renderGouraudPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.setBindGroup(1, gouraudTriangleDataBindGroup); + computePass.dispatchWorkgroups(this.gouraudTriangleCount); + } + if (this.texturedTriangleCount > 0 && texturedTriangleDataBindGroup) { + computePass.setPipeline(this.renderTexturedPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.setBindGroup(1, texturedTriangleDataBindGroup); + computePass.dispatchWorkgroups(this.texturedTriangleCount); + } + if (this.alphaTriangleCount > 0 && alphaTriangleDataBindGroup) { + computePass.setPipeline(this.renderAlphaPipeline); + computePass.setBindGroup(0, this.rasterizerBindGroup); + computePass.setBindGroup(1, alphaTriangleDataBindGroup); + computePass.dispatchWorkgroups(1); + } + + computePass.end(); + + const commandBuffer: GPUCommandBuffer = encoder.finish(); + this.device.queue.submit([commandBuffer]); + } + + override fillTriangle(x0: number, x1: number, x2: number, y0: number, y1: number, y2: number, color: number): boolean { + if (!this.isRenderingScene) { + return false; + } + const triangleIndex: number = this.triangleCount++; + if (Pix3D.alpha !== 0) { + let offset: number = this.alphaTriangleCount * 10; + + if (offset >= this.alphaTriangleData.length) { + const newData: Uint32Array = new Uint32Array(this.alphaTriangleData.length * 2); + newData.set(this.alphaTriangleData); + this.alphaTriangleData = newData; + } + + this.alphaTriangleData[offset++] = (1 << 31) | (Pix3D.alpha << 23) | triangleIndex; + this.alphaTriangleData[offset++] = x0; + this.alphaTriangleData[offset++] = x1; + this.alphaTriangleData[offset++] = x2; + this.alphaTriangleData[offset++] = y0; + this.alphaTriangleData[offset++] = y1; + this.alphaTriangleData[offset++] = y2; + this.alphaTriangleData[offset++] = color; + + this.alphaTriangleCount++; + } else { + let offset: number = this.flatTriangleCount * 8; + + if (offset >= this.flatTriangleData.length) { + const newData: Uint32Array = new Uint32Array(this.flatTriangleData.length * 2); + newData.set(this.flatTriangleData); + this.flatTriangleData = newData; + } + + this.flatTriangleData[offset++] = triangleIndex; + this.flatTriangleData[offset++] = x0; + this.flatTriangleData[offset++] = x1; + this.flatTriangleData[offset++] = x2; + this.flatTriangleData[offset++] = y0; + this.flatTriangleData[offset++] = y1; + this.flatTriangleData[offset++] = y2; + this.flatTriangleData[offset++] = color; + + this.flatTriangleCount++; + } + return true; + } + + override fillGouraudTriangle(xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, colorA: number, colorB: number, colorC: number): boolean { + if (!this.isRenderingScene) { + return false; + } + const triangleIndex: number = this.triangleCount++; + if (Pix3D.alpha !== 0) { + let offset: number = this.alphaTriangleCount * 10; + + if (offset >= this.alphaTriangleData.length) { + const newData: Uint32Array = new Uint32Array(this.alphaTriangleData.length * 2); + newData.set(this.alphaTriangleData); + this.alphaTriangleData = newData; + } + + this.alphaTriangleData[offset++] = (Pix3D.alpha << 23) | triangleIndex; + this.alphaTriangleData[offset++] = xA; + this.alphaTriangleData[offset++] = xB; + this.alphaTriangleData[offset++] = xC; + this.alphaTriangleData[offset++] = yA; + this.alphaTriangleData[offset++] = yB; + this.alphaTriangleData[offset++] = yC; + this.alphaTriangleData[offset++] = colorA; + this.alphaTriangleData[offset++] = colorB; + this.alphaTriangleData[offset++] = colorC; + + this.alphaTriangleCount++; + } else { + let offset: number = this.gouraudTriangleCount * 10; + + if (offset >= this.gouraudTriangleData.length) { + const newData: Uint32Array = new Uint32Array(this.gouraudTriangleData.length * 2); + newData.set(this.gouraudTriangleData); + this.gouraudTriangleData = newData; + } + + this.gouraudTriangleData[offset++] = triangleIndex; + this.gouraudTriangleData[offset++] = xA; + this.gouraudTriangleData[offset++] = xB; + this.gouraudTriangleData[offset++] = xC; + this.gouraudTriangleData[offset++] = yA; + this.gouraudTriangleData[offset++] = yB; + this.gouraudTriangleData[offset++] = yC; + this.gouraudTriangleData[offset++] = colorA; + this.gouraudTriangleData[offset++] = colorB; + this.gouraudTriangleData[offset++] = colorC; + + this.gouraudTriangleCount++; + } + return true; + } + + override fillTexturedTriangle( + xA: number, + xB: number, + xC: number, + yA: number, + yB: number, + yC: number, + shadeA: number, + shadeB: number, + shadeC: number, + originX: number, + originY: number, + originZ: number, + txB: number, + txC: number, + tyB: number, + tyC: number, + tzB: number, + tzC: number, + texture: number + ): boolean { + if (!this.isRenderingScene) { + return false; + } + + // Flag texture as used for animated textures + if (!this.texturesUsed[texture]) { + Pix3D.textureCycle[texture] = Pix3D.cycle++; + this.texturesUsed[texture] = true; + } + + const triangleIndex: number = this.triangleCount++; + + let offset: number = this.texturedTriangleCount * 20; + + if (offset >= this.texturedTriangleData.length) { + const newData: Uint32Array = new Uint32Array(this.texturedTriangleData.length * 2); + newData.set(this.texturedTriangleData); + this.texturedTriangleData = newData; + } + + this.texturedTriangleData[offset++] = triangleIndex; + this.texturedTriangleData[offset++] = xA; + this.texturedTriangleData[offset++] = xB; + this.texturedTriangleData[offset++] = xC; + this.texturedTriangleData[offset++] = yA; + this.texturedTriangleData[offset++] = yB; + this.texturedTriangleData[offset++] = yC; + this.texturedTriangleData[offset++] = shadeA; + this.texturedTriangleData[offset++] = shadeB; + this.texturedTriangleData[offset++] = shadeC; + this.texturedTriangleData[offset++] = originX; + this.texturedTriangleData[offset++] = originY; + this.texturedTriangleData[offset++] = originZ; + this.texturedTriangleData[offset++] = txB; + this.texturedTriangleData[offset++] = txC; + this.texturedTriangleData[offset++] = tyB; + this.texturedTriangleData[offset++] = tyC; + this.texturedTriangleData[offset++] = tzB; + this.texturedTriangleData[offset++] = tzC; + this.texturedTriangleData[offset++] = texture; + + this.texturedTriangleCount++; + return true; + } + + override destroy(): void { + this.device.destroy(); + } +} diff --git a/src/graphics/renderer/webgpu/shaders/commons.wgsl.ts b/src/graphics/renderer/webgpu/shaders/commons.wgsl.ts new file mode 100644 index 0000000..a1d6492 --- /dev/null +++ b/src/graphics/renderer/webgpu/shaders/commons.wgsl.ts @@ -0,0 +1,8 @@ +export const UNPACK_COLOR888: string = ` +fn unpackColor888(rgb: u32) -> vec3f { + let r = f32((rgb >> 16) & 0xff) / 255.0; + let g = f32((rgb >> 8) & 0xff) / 255.0; + let b = f32(rgb & 0xff) / 255.0; + return vec3f(r, g, b); +} +`; diff --git a/src/graphics/renderer/webgpu/shaders/compute-rasterizer.wgsl.ts b/src/graphics/renderer/webgpu/shaders/compute-rasterizer.wgsl.ts new file mode 100644 index 0000000..43c8276 --- /dev/null +++ b/src/graphics/renderer/webgpu/shaders/compute-rasterizer.wgsl.ts @@ -0,0 +1,2242 @@ +export const SHADER_CODE: string = ` +struct PixelBuffer { + data: array, +}; + +const textureCount = 50; + +// Look-up tables +struct LUTs { + palette: array, + texturesTranslucent: array, + textures: array, textureCount>, +}; + +@group(0) @binding(0) var pixelBuffer: PixelBuffer; +@group(0) @binding(1) var depthBuffer: array>; +@group(0) @binding(2) var luts: LUTs; +@group(1) @binding(0) var triangleData: array; + +@compute @workgroup_size(256, 1) +fn clear(@builtin(global_invocation_id) global_id: vec3u) { + let index = global_id.x; + pixelBuffer.data[index] = 0u; + atomicStore(&depthBuffer[index], 0u); +} + +const width = 512; +const height = 334; + +const centerX = width / 2; +const centerY = height / 2; +// const centerX = 256; +// const centerY = 167; + +const boundRight = width; +const boundBottom = height; +// const boundBottom = 334; +const boundX = width - 1; +// const boundX = 512 - 1; + +var jagged = true; +var clipX = false; +var alpha = 0u; + +var opaqueTexture = true; + +var depth = 0u; +var writeDepth = false; + +@compute @workgroup_size(1, 1) +fn renderFlat(@builtin(global_invocation_id) global_id: vec3u) { + let offset = global_id.x * 8; + depth = u32(triangleData[offset]); + rasterTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + u32(triangleData[offset + 7]), + ); +} + +@compute @workgroup_size(1, 1) +fn renderFlatDepth(@builtin(global_invocation_id) global_id: vec3u) { + let offset = global_id.x * 8; + depth = u32(triangleData[offset]); + writeDepth = true; + rasterTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + 0, + ); +} + +@compute @workgroup_size(1, 1) +fn renderGouraud(@builtin(global_invocation_id) global_id: vec3u) { + let offset = global_id.x * 10; + depth = u32(triangleData[offset]); + rasterGouraudTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + triangleData[offset + 7], triangleData[offset + 8], triangleData[offset + 9], + ); +} + +@compute @workgroup_size(1, 1) +fn renderGouraudDepth(@builtin(global_invocation_id) global_id: vec3u) { + let offset = global_id.x * 10; + depth = u32(triangleData[offset]); + writeDepth = true; + rasterTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + 0, + ); +} + +@compute @workgroup_size(1, 1) +fn renderTextured(@builtin(global_invocation_id) global_id: vec3u) { + let offset = global_id.x * 20; + depth = u32(triangleData[offset]); + rasterTexturedTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + triangleData[offset + 7], triangleData[offset + 8], triangleData[offset + 9], + triangleData[offset + 10], triangleData[offset + 11], triangleData[offset + 12], + triangleData[offset + 13], triangleData[offset + 14], triangleData[offset + 15], + triangleData[offset + 16], triangleData[offset + 17], triangleData[offset + 18], + triangleData[offset + 19], + ); +} + +@compute @workgroup_size(1, 1) +fn renderTexturedDepth(@builtin(global_invocation_id) global_id: vec3u) { + let offset = global_id.x * 20; + depth = u32(triangleData[offset]); + writeDepth = true; + opaqueTexture = luts.texturesTranslucent[triangleData[offset + 19]] == 0; + if (opaqueTexture) { + rasterTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + 0, + ); + } else { + rasterTexturedTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + triangleData[offset + 7], triangleData[offset + 8], triangleData[offset + 9], + triangleData[offset + 10], triangleData[offset + 11], triangleData[offset + 12], + triangleData[offset + 13], triangleData[offset + 14], triangleData[offset + 15], + triangleData[offset + 16], triangleData[offset + 17], triangleData[offset + 18], + triangleData[offset + 19], + ); + } +} + +@compute @workgroup_size(1, 1) +fn renderAlpha(@builtin(global_invocation_id) global_id: vec3u) { + let triangleCount = i32(arrayLength(&triangleData)) / 10; + for (var i = 0; i < triangleCount; i++) { + let offset = i * 10; + depth = u32(triangleData[offset] & 0x7fffff); + alpha = u32((triangleData[offset] >> 23) & 0xff); + var isFlat = (u32(triangleData[offset]) >> 31) == 1; + if (isFlat) { + rasterTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + u32(triangleData[offset + 7]), + ); + } else { + rasterGouraudTriangle( + triangleData[offset + 1], triangleData[offset + 2], triangleData[offset + 3], + triangleData[offset + 4], triangleData[offset + 5], triangleData[offset + 6], + triangleData[offset + 7], triangleData[offset + 8], triangleData[offset + 9], + ); + } + } +} + +fn setPixel(index: i32, value: u32) { + if (atomicLoad(&depthBuffer[index]) <= depth) { + pixelBuffer.data[index] = value; + } +} + +fn rasterTriangle(x0In: i32, x1In: i32, x2In: i32, y0In: i32, y1In: i32, y2In: i32, color: u32) { + var x0 = x0In; + var x1 = x1In; + var x2 = x2In; + var y0 = y0In; + var y1 = y1In; + var y2 = y2In; + clipX = x0 < 0 || x1 < 0 || x2 < 0 || x0 > boundX || x1 > boundX || x2 > boundX; + var xStepAB = 0; + if (y1 != y0) { + xStepAB = ((x1 - x0) << 16) / (y1 - y0); + } + var xStepBC = 0; + if (y2 != y1) { + xStepBC = ((x2 - x1) << 16) / (y2 - y1); + } + var xStepAC = 0; + if (y2 != y0) { + xStepAC = ((x0 - x2) << 16) / (y0 - y2); + } + if (y0 <= y1 && y0 <= y2) { + if (y0 < boundBottom) { + if (y1 > boundBottom) { + y1 = boundBottom; + } + if (y2 > boundBottom) { + y2 = boundBottom; + } + if (y1 < y2) { + x0 <<= 0x10; + x2 = x0; + if (y0 < 0) { + x2 -= xStepAC * y0; + x0 -= xStepAB * y0; + y0 = 0; + } + x1 <<= 0x10; + if (y1 < 0) { + x1 -= xStepBC * y1; + y1 = 0; + } + if ((y0 != y1 && xStepAC < xStepAB) || (y0 == y1 && xStepAC > xStepBC)) { + y2 -= y1; + y1 -= y0; + y0 = y0 * width; + while (true) { + y1--; + if (y1 < 0) { + while (true) { + y2--; + if (y2 < 0) { + return; + } + rasterScanline(x2 >> 16, x1 >> 16, y0, color); + x2 += xStepAC; + x1 += xStepBC; + y0 += width; + } + } + rasterScanline(x2 >> 16, x0 >> 16, y0, color); + x2 += xStepAC; + x0 += xStepAB; + y0 += width; + } + } else { + y2 -= y1; + y1 -= y0; + y0 = y0 * width; + while (true) { + y1--; + if (y1 < 0) { + while (true) { + y2--; + if (y2 < 0) { + return; + } + rasterScanline(x1 >> 16, x2 >> 16, y0, color); + x2 += xStepAC; + x1 += xStepBC; + y0 += width; + } + } + rasterScanline(x0 >> 16, x2 >> 16, y0, color); + x2 += xStepAC; + x0 += xStepAB; + y0 += width; + } + } + } else { + x0 <<= 0x10; + x1 = x0; + if (y0 < 0) { + x1 -= xStepAC * y0; + x0 -= xStepAB * y0; + y0 = 0; + } + x2 <<= 0x10; + if (y2 < 0) { + x2 -= xStepBC * y2; + y2 = 0; + } + if ((y0 != y2 && xStepAC < xStepAB) || (y0 == y2 && xStepBC > xStepAB)) { + y1 -= y2; + y2 -= y0; + y0 = y0 * width; + while (true) { + y2--; + if (y2 < 0) { + while (true) { + y1--; + if (y1 < 0) { + return; + } + rasterScanline(x2 >> 16, x0 >> 16, y0, color); + x2 += xStepBC; + x0 += xStepAB; + y0 += width; + } + } + rasterScanline(x1 >> 16, x0 >> 16, y0, color); + x1 += xStepAC; + x0 += xStepAB; + y0 += width; + } + } else { + y1 -= y2; + y2 -= y0; + y0 = y0 * width; + while (true) { + y2--; + if (y2 < 0) { + while (true) { + y1--; + if (y1 < 0) { + return; + } + rasterScanline(x0 >> 16, x2 >> 16, y0, color); + x2 += xStepBC; + x0 += xStepAB; + y0 += width; + } + } + rasterScanline(x0 >> 16, x1 >> 16, y0, color); + x1 += xStepAC; + x0 += xStepAB; + y0 += width; + } + } + } + } + } else if (y1 <= y2) { + if (y1 < boundBottom) { + if (y2 > boundBottom) { + y2 = boundBottom; + } + if (y0 > boundBottom) { + y0 = boundBottom; + } + if (y2 < y0) { + x1 <<= 0x10; + x0 = x1; + if (y1 < 0) { + x0 -= xStepAB * y1; + x1 -= xStepBC * y1; + y1 = 0; + } + x2 <<= 0x10; + if (y2 < 0) { + x2 -= xStepAC * y2; + y2 = 0; + } + if ((y1 != y2 && xStepAB < xStepBC) || (y1 == y2 && xStepAB > xStepAC)) { + y0 -= y2; + y2 -= y1; + y1 = y1 * width; + while (true) { + y2--; + if (y2 < 0) { + while (true) { + y0--; + if (y0 < 0) { + return; + } + rasterScanline(x0 >> 16, x2 >> 16, y1, color); + x0 += xStepAB; + x2 += xStepAC; + y1 += width; + } + } + rasterScanline(x0 >> 16, x1 >> 16, y1, color); + x0 += xStepAB; + x1 += xStepBC; + y1 += width; + } + } else { + y0 -= y2; + y2 -= y1; + y1 = y1 * width; + while (true) { + y2--; + if (y2 < 0) { + while (true) { + y0--; + if (y0 < 0) { + return; + } + rasterScanline(x2 >> 16, x0 >> 16, y1, color); + x0 += xStepAB; + x2 += xStepAC; + y1 += width; + } + } + rasterScanline(x1 >> 16, x0 >> 16, y1, color); + x0 += xStepAB; + x1 += xStepBC; + y1 += width; + } + } + } else { + x1 <<= 0x10; + x2 = x1; + if (y1 < 0) { + x2 -= xStepAB * y1; + x1 -= xStepBC * y1; + y1 = 0; + } + x0 <<= 0x10; + if (y0 < 0) { + x0 -= xStepAC * y0; + y0 = 0; + } + if (xStepAB < xStepBC) { + y2 -= y0; + y0 -= y1; + y1 = y1 * width; + while (true) { + y0--; + if (y0 < 0) { + while (true) { + y2--; + if (y2 < 0) { + return; + } + rasterScanline(x0 >> 16, x1 >> 16, y1, color); + x0 += xStepAC; + x1 += xStepBC; + y1 += width; + } + } + rasterScanline(x2 >> 16, x1 >> 16, y1, color); + x2 += xStepAB; + x1 += xStepBC; + y1 += width; + } + } else { + y2 -= y0; + y0 -= y1; + y1 = y1 * width; + while (true) { + y0--; + if (y0 < 0) { + while (true) { + y2--; + if (y2 < 0) { + return; + } + rasterScanline(x1 >> 16, x0 >> 16, y1, color); + x0 += xStepAC; + x1 += xStepBC; + y1 += width; + } + } + rasterScanline(x1 >> 16, x2 >> 16, y1, color); + x2 += xStepAB; + x1 += xStepBC; + y1 += width; + } + } + } + } + } else if (y2 < boundBottom) { + if (y0 > boundBottom) { + y0 = boundBottom; + } + if (y1 > boundBottom) { + y1 = boundBottom; + } + if (y0 < y1) { + x2 <<= 0x10; + x1 = x2; + if (y2 < 0) { + x1 -= xStepBC * y2; + x2 -= xStepAC * y2; + y2 = 0; + } + x0 <<= 0x10; + if (y0 < 0) { + x0 -= xStepAB * y0; + y0 = 0; + } + if (xStepBC < xStepAC) { + y1 -= y0; + y0 -= y2; + y2 = y2 * width; + while (true) { + y0--; + if (y0 < 0) { + while (true) { + y1--; + if (y1 < 0) { + return; + } + rasterScanline(x1 >> 16, x0 >> 16, y2, color); + x1 += xStepBC; + x0 += xStepAB; + y2 += width; + } + } + rasterScanline(x1 >> 16, x2 >> 16, y2, color); + x1 += xStepBC; + x2 += xStepAC; + y2 += width; + } + } else { + y1 -= y0; + y0 -= y2; + y2 = y2 * width; + while (true) { + y0--; + if (y0 < 0) { + while (true) { + y1--; + if (y1 < 0) { + return; + } + rasterScanline(x0 >> 16, x1 >> 16, y2, color); + x1 += xStepBC; + x0 += xStepAB; + y2 += width; + } + } + rasterScanline(x2 >> 16, x1 >> 16, y2, color); + x1 += xStepBC; + x2 += xStepAC; + y2 += width; + } + } + } else { + x2 <<= 0x10; + x0 = x2; + if (y2 < 0) { + x0 -= xStepBC * y2; + x2 -= xStepAC * y2; + y2 = 0; + } + x1 <<= 0x10; + if (y1 < 0) { + x1 -= xStepAB * y1; + y1 = 0; + } + if (xStepBC < xStepAC) { + y0 -= y1; + y1 -= y2; + y2 = y2 * width; + while (true) { + y1--; + if (y1 < 0) { + while (true) { + y0--; + if (y0 < 0) { + return; + } + rasterScanline(x1 >> 16, x2 >> 16, y2, color); + x1 += xStepAB; + x2 += xStepAC; + y2 += width; + } + } + rasterScanline(x0 >> 16, x2 >> 16, y2, color); + x0 += xStepBC; + x2 += xStepAC; + y2 += width; + } + } else { + y0 -= y1; + y1 -= y2; + y2 = y2 * width; + while (true) { + y1--; + if (y1 < 0) { + while (true) { + y0--; + if (y0 < 0) { + return; + } + rasterScanline(x2 >> 16, x1 >> 16, y2, color); + x1 += xStepAB; + x2 += xStepAC; + y2 += width; + } + } + rasterScanline(x2 >> 16, x0 >> 16, y2, color); + x0 += xStepBC; + x2 += xStepAC; + y2 += width; + } + } + } + } +} + +fn rasterScanline(x0In: i32, x1In: i32, offsetIn: i32, rgbIn: u32) { + var x0 = x0In; + var x1 = x1In; + var offset = offsetIn; + var rgb = rgbIn; + if (clipX) { + if (x1 > boundX) { + x1 = boundX; + } + if (x0 < 0) { + x0 = 0; + } + } + if (x0 >= x1) { + return; + } + offset += x0; + var length = x1 - x0; + if (writeDepth) { + for (var x = 0; x < length; x++) { + atomicMax(&depthBuffer[offset + x], depth); + } + } else if (alpha == 0) { + for (var x = 0; x < length; x++) { + setPixel(offset + x, rgb); + } + } else { + length >>= 2; + let alpha = alpha; + let invAlpha = 256 - alpha; + rgb = ((((rgb & 0xff00ff) * invAlpha) >> 8) & 0xff00ff) + ((((rgb & 0xff00) * invAlpha) >> 8) & 0xff00); + var blendRgb: u32; + while (true) { + length--; + if (length < 0) { + length = (x1 - x0) & 0x3; + if (length > 0) { + while (true) { + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + length--; + if (length <= 0) { + break; + } + } + } + break; + } + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + } + } +} + +fn rasterGouraudTriangle(xAIn: i32, xBIn: i32, xCIn: i32, yAIn: i32, yBIn: i32, yCIn: i32, colorAIn: i32, colorBIn: i32, colorCIn: i32) { + var xA = xAIn; + var xB = xBIn; + var xC = xCIn; + var yA = yAIn; + var yB = yBIn; + var yC = yCIn; + var colorA = colorAIn; + var colorB = colorBIn; + var colorC = colorCIn; + clipX = xA < 0 || xB < 0 || xC < 0 || xA > boundX || xB > boundX || xC > boundX; + var xStepAB: i32; + var colorStepAB: i32; + if (yB != yA) { + xStepAB = ((xB - xA) << 16) / (yB - yA); + colorStepAB = ((colorB - colorA) << 15) / (yB - yA); + } + var xStepBC: i32; + var colorStepBC: i32; + if (yC != yB) { + xStepBC = ((xC - xB) << 16) / (yC - yB); + colorStepBC = ((colorC - colorB) << 15) / (yC - yB); + } + var xStepAC: i32; + var colorStepAC: i32; + if (yC != yA) { + xStepAC = ((xA - xC) << 16) / (yA - yC); + colorStepAC = ((colorA - colorC) << 15) / (yA - yC); + } + + if (yA <= yB && yA <= yC) { + if (yA < boundBottom) { + if (yB > boundBottom) { + yB = boundBottom; + } + if (yC > boundBottom) { + yC = boundBottom; + } + if (yB < yC) { + xA <<= 0x10; + xC = xA; + colorA <<= 0xf; + colorC = colorA; + if (yA < 0) { + xC -= xStepAC * yA; + xA -= xStepAB * yA; + colorC -= colorStepAC * yA; + colorA -= colorStepAB * yA; + yA = 0; + } + xB <<= 0x10; + colorB <<= 0xf; + if (yB < 0) { + xB -= xStepBC * yB; + colorB -= colorStepBC * yB; + yB = 0; + } + if ((yA != yB && xStepAC < xStepAB) || (yA == yB && xStepAC > xStepBC)) { + yC -= yB; + yB -= yA; + yA = yA * width; + while (true) { + yB--; + if (yB < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterGouraudScanline(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, yA); + xC += xStepAC; + xB += xStepBC; + colorC += colorStepAC; + colorB += colorStepBC; + yA += width; + } + } + rasterGouraudScanline(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, yA); + xC += xStepAC; + xA += xStepAB; + colorC += colorStepAC; + colorA += colorStepAB; + yA += width; + } + } else { + yC -= yB; + yB -= yA; + yA = yA * width; + while (true) { + yB--; + if (yB < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterGouraudScanline(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, yA); + xC += xStepAC; + xB += xStepBC; + colorC += colorStepAC; + colorB += colorStepBC; + yA += width; + } + } + rasterGouraudScanline(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, yA); + xC += xStepAC; + xA += xStepAB; + colorC += colorStepAC; + colorA += colorStepAB; + yA += width; + } + } + } else { + xA <<= 0x10; + xB = xA; + colorA <<= 0xf; + colorB = colorA; + if (yA < 0) { + xB -= xStepAC * yA; + xA -= xStepAB * yA; + colorB -= colorStepAC * yA; + colorA -= colorStepAB * yA; + yA = 0; + } + xC <<= 0x10; + colorC <<= 0xf; + if (yC < 0) { + xC -= xStepBC * yC; + colorC -= colorStepBC * yC; + yC = 0; + } + if ((yA != yC && xStepAC < xStepAB) || (yA == yC && xStepBC > xStepAB)) { + yB -= yC; + yC -= yA; + yA = yA * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterGouraudScanline(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, yA); + xC += xStepBC; + xA += xStepAB; + colorC += colorStepBC; + colorA += colorStepAB; + yA += width; + } + } + rasterGouraudScanline(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, yA); + xB += xStepAC; + xA += xStepAB; + colorB += colorStepAC; + colorA += colorStepAB; + yA += width; + } + } else { + yB -= yC; + yC -= yA; + yA = yA * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterGouraudScanline(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, yA); + xC += xStepBC; + xA += xStepAB; + colorC += colorStepBC; + colorA += colorStepAB; + yA += width; + } + } + rasterGouraudScanline(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, yA); + xB += xStepAC; + xA += xStepAB; + colorB += colorStepAC; + colorA += colorStepAB; + yA += width; + } + } + } + } + } else if (yB <= yC) { + if (yB < boundBottom) { + if (yC > boundBottom) { + yC = boundBottom; + } + if (yA > boundBottom) { + yA = boundBottom; + } + if (yC < yA) { + xB <<= 0x10; + xA = xB; + colorB <<= 0xf; + colorA = colorB; + if (yB < 0) { + xA -= xStepAB * yB; + xB -= xStepBC * yB; + colorA -= colorStepAB * yB; + colorB -= colorStepBC * yB; + yB = 0; + } + xC <<= 0x10; + colorC <<= 0xf; + if (yC < 0) { + xC -= xStepAC * yC; + colorC -= colorStepAC * yC; + yC = 0; + } + if ((yB != yC && xStepAB < xStepBC) || (yB == yC && xStepAB > xStepAC)) { + yA -= yC; + yC -= yB; + yB = yB * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterGouraudScanline(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, yB); + xA += xStepAB; + xC += xStepAC; + colorA += colorStepAB; + colorC += colorStepAC; + yB += width; + } + } + rasterGouraudScanline(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, yB); + xA += xStepAB; + xB += xStepBC; + colorA += colorStepAB; + colorB += colorStepBC; + yB += width; + } + } else { + yA -= yC; + yC -= yB; + yB = yB * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterGouraudScanline(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, yB); + xA += xStepAB; + xC += xStepAC; + colorA += colorStepAB; + colorC += colorStepAC; + yB += width; + } + } + rasterGouraudScanline(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, yB); + xA += xStepAB; + xB += xStepBC; + colorA += colorStepAB; + colorB += colorStepBC; + yB += width; + } + } + } else { + xB <<= 0x10; + xC = xB; + colorB <<= 0xf; + colorC = colorB; + if (yB < 0) { + xC -= xStepAB * yB; + xB -= xStepBC * yB; + colorC -= colorStepAB * yB; + colorB -= colorStepBC * yB; + yB = 0; + } + xA <<= 0x10; + colorA <<= 0xf; + if (yA < 0) { + xA -= xStepAC * yA; + colorA -= colorStepAC * yA; + yA = 0; + } + yC -= yA; + yA -= yB; + yB = yB * width; + if (xStepAB < xStepBC) { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterGouraudScanline(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, yB); + xA += xStepAC; + xB += xStepBC; + colorA += colorStepAC; + colorB += colorStepBC; + yB += width; + } + } + rasterGouraudScanline(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, yB); + xC += xStepAB; + xB += xStepBC; + colorC += colorStepAB; + colorB += colorStepBC; + yB += width; + } + } else { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterGouraudScanline(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, yB); + xA += xStepAC; + xB += xStepBC; + colorA += colorStepAC; + colorB += colorStepBC; + yB += width; + } + } + rasterGouraudScanline(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, yB); + xC += xStepAB; + xB += xStepBC; + colorC += colorStepAB; + colorB += colorStepBC; + yB += width; + } + } + } + } + } else if (yC < boundBottom) { + if (yA > boundBottom) { + yA = boundBottom; + } + if (yB > boundBottom) { + yB = boundBottom; + } + if (yA < yB) { + xC <<= 0x10; + xB = xC; + colorC <<= 0xf; + colorB = colorC; + if (yC < 0) { + xB -= xStepBC * yC; + xC -= xStepAC * yC; + colorB -= colorStepBC * yC; + colorC -= colorStepAC * yC; + yC = 0; + } + xA <<= 0x10; + colorA <<= 0xf; + if (yA < 0) { + xA -= xStepAB * yA; + colorA -= colorStepAB * yA; + yA = 0; + } + yB -= yA; + yA -= yC; + yC = yC * width; + if (xStepBC < xStepAC) { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterGouraudScanline(xB >> 16, xA >> 16, colorB >> 7, colorA >> 7, yC); + xB += xStepBC; + xA += xStepAB; + colorB += colorStepBC; + colorA += colorStepAB; + yC += width; + } + } + rasterGouraudScanline(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, yC); + xB += xStepBC; + xC += xStepAC; + colorB += colorStepBC; + colorC += colorStepAC; + yC += width; + } + } else { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterGouraudScanline(xA >> 16, xB >> 16, colorA >> 7, colorB >> 7, yC); + xB += xStepBC; + xA += xStepAB; + colorB += colorStepBC; + colorA += colorStepAB; + yC += width; + } + } + rasterGouraudScanline(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, yC); + xB += xStepBC; + xC += xStepAC; + colorB += colorStepBC; + colorC += colorStepAC; + yC += width; + } + } + } else { + xC <<= 0x10; + xA = xC; + colorC <<= 0xf; + colorA = colorC; + if (yC < 0) { + xA -= xStepBC * yC; + xC -= xStepAC * yC; + colorA -= colorStepBC * yC; + colorC -= colorStepAC * yC; + yC = 0; + } + xB <<= 0x10; + colorB <<= 0xf; + if (yB < 0) { + xB -= xStepAB * yB; + colorB -= colorStepAB * yB; + yB = 0; + } + yA -= yB; + yB -= yC; + yC = yC * width; + if (xStepBC < xStepAC) { + while (true) { + yB--; + if (yB < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterGouraudScanline(xB >> 16, xC >> 16, colorB >> 7, colorC >> 7, yC); + xB += xStepAB; + xC += xStepAC; + colorB += colorStepAB; + colorC += colorStepAC; + yC += width; + } + } + rasterGouraudScanline(xA >> 16, xC >> 16, colorA >> 7, colorC >> 7, yC); + xA += xStepBC; + xC += xStepAC; + colorA += colorStepBC; + colorC += colorStepAC; + yC += width; + } + } else { + while (true) { + yB--; + if (yB < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterGouraudScanline(xC >> 16, xB >> 16, colorC >> 7, colorB >> 7, yC); + xB += xStepAB; + xC += xStepAC; + colorB += colorStepAB; + colorC += colorStepAC; + yC += width; + } + } + rasterGouraudScanline(xC >> 16, xA >> 16, colorC >> 7, colorA >> 7, yC); + xA += xStepBC; + xC += xStepAC; + colorA += colorStepBC; + colorC += colorStepAC; + yC += width; + } + } + } + } +} + +fn rasterGouraudScanline(x0In: i32, x1In: i32, color0In: i32, color1: i32, offsetIn: i32) { + var x0 = x0In; + var x1 = x1In; + var color0 = color0In; + var offset = offsetIn; + + var rgb: u32; + if (jagged) { + var colorStep: i32; + var length: i32; + + if (clipX) { + if (x1 - x0 > 3) { + colorStep = ((color1 - color0) / (x1 - x0)); + } else { + colorStep = 0; + } + if (x1 > boundX) { + x1 = boundX; + } + if (x0 < 0) { + color0 -= x0 * colorStep; + x0 = 0; + } + if (x0 >= x1) { + return; + } + offset += x0; + length = (x1 - x0) >> 2; + colorStep <<= 0x2; + } else if (x0 < x1) { + offset += x0; + length = (x1 - x0) >> 2; + if (length > 0) { + colorStep = ((color1 - color0) * reciprocal15(length)) >> 15; + } else { + colorStep = 0; + } + } else { + return; + } + + if (alpha == 0) { + while (true) { + length--; + if (length < 0) { + length = (x1 - x0) & 0x3; + if (length > 0) { + rgb = luts.palette[color0 >> 8]; + while (true) { + setPixel(offset, rgb); + offset++; + length--; + if (length <= 0) { + break; + } + } + return; + } + break; + } + rgb = luts.palette[color0 >> 8]; + color0 += colorStep; + setPixel(offset, rgb); + offset++; + setPixel(offset, rgb); + offset++; + setPixel(offset, rgb); + offset++; + setPixel(offset, rgb); + offset++; + } + } else { + let alpha = alpha; + let invAlpha = 256 - alpha; + var blendRgb: u32; + while (true) { + length--; + if (length < 0) { + length = (x1 - x0) & 0x3; + if (length > 0) { + rgb = luts.palette[color0 >> 8]; + rgb = ((((rgb & 0xff00ff) * invAlpha) >> 8) & 0xff00ff) + ((((rgb & 0xff00) * invAlpha) >> 8) & 0xff00); + while (true) { + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + length--; + if (length <= 0) { + break; + } + } + } + break; + } + rgb = luts.palette[color0 >> 8]; + color0 += colorStep; + rgb = ((((rgb & 0xff00ff) * invAlpha) >> 8) & 0xff00ff) + ((((rgb & 0xff00) * invAlpha) >> 8) & 0xff00); + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + blendRgb = pixelBuffer.data[offset + i32(atomicLoad(&depthBuffer[offset + 1]) < depth)]; + setPixel(offset, rgb + ((((blendRgb & 0xff00ff) * alpha) >> 8) & 0xff00ff) + ((((blendRgb & 0xff00) * alpha) >> 8) & 0xff00)); + offset++; + } + } + } +} + +fn rasterTexturedTriangle( + xAIn: i32, + xBIn: i32, + xCIn: i32, + yAIn: i32, + yBIn: i32, + yCIn: i32, + shadeAIn: i32, + shadeBIn: i32, + shadeCIn: i32, + originXIn: i32, + originYIn: i32, + originZIn: i32, + txBIn: i32, + txCIn: i32, + tyBIn: i32, + tyCIn: i32, + tzBIn: i32, + tzCIn: i32, + textureId: i32 +) { + var xA = xAIn; + var xB = xBIn; + var xC = xCIn; + var yA = yAIn; + var yB = yBIn; + var yC = yCIn; + var shadeA = shadeAIn; + var shadeB = shadeBIn; + var shadeC = shadeCIn; + var originX = originXIn; + var originY = originYIn; + var originZ = originZIn; + var txB = txBIn; + var txC = txCIn; + var tyB = tyBIn; + var tyC = tyCIn; + var tzB = tzBIn; + var tzC = tzCIn; + let texels = &luts.textures[textureId]; + opaqueTexture = luts.texturesTranslucent[textureId] == 0; + clipX = xA < 0 || xB < 0 || xC < 0 || xA > boundX || xB > boundX || xC > boundX; + + let verticalX = originX - txB; + let verticalY = originY - tyB; + let verticalZ = originZ - tzB; + + let horizontalX = txC - originX; + let horizontalY = tyC - originY; + let horizontalZ = tzC - originZ; + + var u = (horizontalX * originY - horizontalY * originX) << 14; + let uStride = (horizontalY * originZ - horizontalZ * originY) << 8; + let uStepVertical = (horizontalZ * originX - horizontalX * originZ) << 5; + + var v = (verticalX * originY - verticalY * originX) << 14; + let vStride = (verticalY * originZ - verticalZ * originY) << 8; + let vStepVertical = (verticalZ * originX - verticalX * originZ) << 5; + + var w = (verticalY * horizontalX - verticalX * horizontalY) << 14; + let wStride = (verticalZ * horizontalY - verticalY * horizontalZ) << 8; + let wStepVertical = (verticalX * horizontalZ - verticalZ * horizontalX) << 5; + + var xStepAB = 0; + var shadeStepAB = 0; + if (yB != yA) { + xStepAB = (((xB - xA) << 16) / (yB - yA)); + shadeStepAB = (((shadeB - shadeA) << 16) / (yB - yA)); + } + + var xStepBC = 0; + var shadeStepBC = 0; + if (yC != yB) { + xStepBC = (((xC - xB) << 16) / (yC - yB)); + shadeStepBC = (((shadeC - shadeB) << 16) / (yC - yB)); + } + + var xStepAC = 0; + var shadeStepAC = 0; + if (yC != yA) { + xStepAC = (((xA - xC) << 16) / (yA - yC)); + shadeStepAC = (((shadeA - shadeC) << 16) / (yA - yC)); + } + + if (yA <= yB && yA <= yC) { + if (yA < boundBottom) { + if (yB > boundBottom) { + yB = boundBottom; + } + + if (yC > boundBottom) { + yC = boundBottom; + } + + if (yB < yC) { + xA <<= 0x10; + xC = xA; + shadeA <<= 0x10; + shadeC = shadeA; + if (yA < 0) { + xC -= xStepAC * yA; + xA -= xStepAB * yA; + shadeC -= shadeStepAC * yA; + shadeA -= shadeStepAB * yA; + yA = 0; + } + xB <<= 0x10; + shadeB <<= 0x10; + if (yB < 0) { + xB -= xStepBC * yB; + shadeB -= shadeStepBC * yB; + yB = 0; + } + let dy = yA - centerY; + u += uStepVertical * dy; + v += vStepVertical * dy; + w += wStepVertical * dy; + if ((yA != yB && xStepAC < xStepAB) || (yA == yB && xStepAC > xStepBC)) { + yC -= yB; + yB -= yA; + yA = yA * width; + while (true) { + yB--; + if (yB < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterTexturedScanline(xC >> 16, xB >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeB >> 8); + xC += xStepAC; + xB += xStepBC; + shadeC += shadeStepAC; + shadeB += shadeStepBC; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xC >> 16, xA >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeA >> 8); + xC += xStepAC; + xA += xStepAB; + shadeC += shadeStepAC; + shadeA += shadeStepAB; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } else { + yC -= yB; + yB -= yA; + yA = yA * width; + while (true) { + yB--; + if (yB < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterTexturedScanline(xB >> 16, xC >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeC >> 8); + xC += xStepAC; + xB += xStepBC; + shadeC += shadeStepAC; + shadeB += shadeStepBC; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xA >> 16, xC >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeC >> 8); + xC += xStepAC; + xA += xStepAB; + shadeC += shadeStepAC; + shadeA += shadeStepAB; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + } else { + xA <<= 0x10; + xB = xA; + shadeA <<= 0x10; + shadeB = shadeA; + if (yA < 0) { + xB -= xStepAC * yA; + xA -= xStepAB * yA; + shadeB -= shadeStepAC * yA; + shadeA -= shadeStepAB * yA; + yA = 0; + } + xC <<= 0x10; + shadeC <<= 0x10; + if (yC < 0) { + xC -= xStepBC * yC; + shadeC -= shadeStepBC * yC; + yC = 0; + } + let dy = yA - centerY; + u += uStepVertical * dy; + v += vStepVertical * dy; + w += wStepVertical * dy; + if ((yA == yC || xStepAC >= xStepAB) && (yA != yC || xStepBC <= xStepAB)) { + yB -= yC; + yC -= yA; + yA = yA * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterTexturedScanline(xA >> 16, xC >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeC >> 8); + xC += xStepBC; + xA += xStepAB; + shadeC += shadeStepBC; + shadeA += shadeStepAB; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xA >> 16, xB >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeB >> 8); + xB += xStepAC; + xA += xStepAB; + shadeB += shadeStepAC; + shadeA += shadeStepAB; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } else { + yB -= yC; + yC -= yA; + yA = yA * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterTexturedScanline(xC >> 16, xA >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeA >> 8); + xC += xStepBC; + xA += xStepAB; + shadeC += shadeStepBC; + shadeA += shadeStepAB; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xB >> 16, xA >> 16, yA, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeA >> 8); + xB += xStepAC; + xA += xStepAB; + shadeB += shadeStepAC; + shadeA += shadeStepAB; + yA += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + } + } + } else if (yB <= yC) { + if (yB < boundBottom) { + if (yC > boundBottom) { + yC = boundBottom; + } + if (yA > boundBottom) { + yA = boundBottom; + } + if (yC < yA) { + xB <<= 0x10; + xA = xB; + shadeB <<= 0x10; + shadeA = shadeB; + if (yB < 0) { + xA -= xStepAB * yB; + xB -= xStepBC * yB; + shadeA -= shadeStepAB * yB; + shadeB -= shadeStepBC * yB; + yB = 0; + } + xC <<= 0x10; + shadeC <<= 0x10; + if (yC < 0) { + xC -= xStepAC * yC; + shadeC -= shadeStepAC * yC; + yC = 0; + } + let dy = yB - centerY; + u += uStepVertical * dy; + v += vStepVertical * dy; + w += wStepVertical * dy; + if ((yB != yC && xStepAB < xStepBC) || (yB == yC && xStepAB > xStepAC)) { + yA -= yC; + yC -= yB; + yB = yB * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterTexturedScanline(xA >> 16, xC >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeC >> 8); + xA += xStepAB; + xC += xStepAC; + shadeA += shadeStepAB; + shadeC += shadeStepAC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xA >> 16, xB >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeB >> 8); + xA += xStepAB; + xB += xStepBC; + shadeA += shadeStepAB; + shadeB += shadeStepBC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } else { + yA -= yC; + yC -= yB; + yB = yB * width; + while (true) { + yC--; + if (yC < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterTexturedScanline(xC >> 16, xA >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeA >> 8); + xA += xStepAB; + xC += xStepAC; + shadeA += shadeStepAB; + shadeC += shadeStepAC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xB >> 16, xA >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeA >> 8); + xA += xStepAB; + xB += xStepBC; + shadeA += shadeStepAB; + shadeB += shadeStepBC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + } else { + xB <<= 0x10; + xC = xB; + shadeB <<= 0x10; + shadeC = shadeB; + if (yB < 0) { + xC -= xStepAB * yB; + xB -= xStepBC * yB; + shadeC -= shadeStepAB * yB; + shadeB -= shadeStepBC * yB; + yB = 0; + } + xA <<= 0x10; + shadeA <<= 0x10; + if (yA < 0) { + xA -= xStepAC * yA; + shadeA -= shadeStepAC * yA; + yA = 0; + } + let dy = yB - centerY; + u += uStepVertical * dy; + v += vStepVertical * dy; + w += wStepVertical * dy; + yC -= yA; + yA -= yB; + yB = yB * width; + if (xStepAB < xStepBC) { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterTexturedScanline(xA >> 16, xB >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeB >> 8); + xA += xStepAC; + xB += xStepBC; + shadeA += shadeStepAC; + shadeB += shadeStepBC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xC >> 16, xB >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeB >> 8); + xC += xStepAB; + xB += xStepBC; + shadeC += shadeStepAB; + shadeB += shadeStepBC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } else { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yC--; + if (yC < 0) { + return; + } + rasterTexturedScanline(xB >> 16, xA >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeA >> 8); + xA += xStepAC; + xB += xStepBC; + shadeA += shadeStepAC; + shadeB += shadeStepBC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xB >> 16, xC >> 16, yB, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeC >> 8); + xC += xStepAB; + xB += xStepBC; + shadeC += shadeStepAB; + shadeB += shadeStepBC; + yB += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + } + } + } else if (yC < boundBottom) { + if (yA > boundBottom) { + yA = boundBottom; + } + if (yB > boundBottom) { + yB = boundBottom; + } + if (yA < yB) { + xC <<= 0x10; + xB = xC; + shadeC <<= 0x10; + shadeB = shadeC; + if (yC < 0) { + xB -= xStepBC * yC; + xC -= xStepAC * yC; + shadeB -= shadeStepBC * yC; + shadeC -= shadeStepAC * yC; + yC = 0; + } + xA <<= 0x10; + shadeA <<= 0x10; + if (yA < 0) { + xA -= xStepAB * yA; + shadeA -= shadeStepAB * yA; + yA = 0; + } + let dy = yC - centerY; + u += uStepVertical * dy; + v += vStepVertical * dy; + w += wStepVertical * dy; + yB -= yA; + yA -= yC; + yC = yC * width; + if (xStepBC < xStepAC) { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterTexturedScanline(xB >> 16, xA >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeA >> 8); + xB += xStepBC; + xA += xStepAB; + shadeB += shadeStepBC; + shadeA += shadeStepAB; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xB >> 16, xC >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeC >> 8); + xB += xStepBC; + xC += xStepAC; + shadeB += shadeStepBC; + shadeC += shadeStepAC; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } else { + while (true) { + yA--; + if (yA < 0) { + while (true) { + yB--; + if (yB < 0) { + return; + } + rasterTexturedScanline(xA >> 16, xB >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeB >> 8); + xB += xStepBC; + xA += xStepAB; + shadeB += shadeStepBC; + shadeA += shadeStepAB; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xC >> 16, xB >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeB >> 8); + xB += xStepBC; + xC += xStepAC; + shadeB += shadeStepBC; + shadeC += shadeStepAC; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + } else { + xC <<= 0x10; + xA = xC; + shadeC <<= 0x10; + shadeA = shadeC; + if (yC < 0) { + xA -= xStepBC * yC; + xC -= xStepAC * yC; + shadeA -= shadeStepBC * yC; + shadeC -= shadeStepAC * yC; + yC = 0; + } + xB <<= 0x10; + shadeB <<= 0x10; + if (yB < 0) { + xB -= xStepAB * yB; + shadeB -= shadeStepAB * yB; + yB = 0; + } + let dy = yC - centerY; + u += uStepVertical * dy; + v += vStepVertical * dy; + w += wStepVertical * dy; + yA -= yB; + yB -= yC; + yC = yC * width; + if (xStepBC < xStepAC) { + while (true) { + yB--; + if (yB < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterTexturedScanline(xB >> 16, xC >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeB >> 8, shadeC >> 8); + xB += xStepAB; + xC += xStepAC; + shadeB += shadeStepAB; + shadeC += shadeStepAC; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xA >> 16, xC >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeA >> 8, shadeC >> 8); + xA += xStepBC; + xC += xStepAC; + shadeA += shadeStepBC; + shadeC += shadeStepAC; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } else { + while (true) { + yB--; + if (yB < 0) { + while (true) { + yA--; + if (yA < 0) { + return; + } + rasterTexturedScanline(xC >> 16, xB >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeB >> 8); + xB += xStepAB; + xC += xStepAC; + shadeB += shadeStepAB; + shadeC += shadeStepAC; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + rasterTexturedScanline(xC >> 16, xA >> 16, yC, texels, 0, 0, u, v, w, uStride, vStride, wStride, shadeC >> 8, shadeA >> 8); + xA += xStepBC; + xC += xStepAC; + shadeA += shadeStepBC; + shadeC += shadeStepAC; + yC += width; + u += uStepVertical; + v += vStepVertical; + w += wStepVertical; + } + } + } + } +} + +fn rasterTexturedScanline( + xAIn: i32, + xBIn: i32, + offsetIn: i32, + texels: ptr>, + curUIn: i32, + curVIn: i32, + uIn: i32, + vIn: i32, + wIn: i32, + uStride: i32, + vStride: i32, + wStride: i32, + shadeAIn: i32, + shadeBIn: i32 +) { + var xA = xAIn; + var xB = xBIn; + var offset = offsetIn; + var curU = curUIn; + var curV = curVIn; + var u = uIn; + var v = vIn; + var w = wIn; + var shadeA = shadeAIn; + var shadeB = shadeBIn; + if (xA >= xB) { + return; + } + var shadeStrides: i32; + var strides: i32; + if (clipX) { + shadeStrides = ((shadeB - shadeA) / (xB - xA)); + if (xB > boundX) { + xB = boundX; + } + if (xA < 0) { + shadeA -= xA * shadeStrides; + xA = 0; + } + if (xA >= xB) { + return; + } + strides = (xB - xA) >> 3; + shadeStrides <<= 0xc; + } else { + if (xB - xA > 7) { + strides = (xB - xA) >> 3; + shadeStrides = ((shadeB - shadeA) * reciprocal15(strides)) >> 6; + } else { + strides = 0; + shadeStrides = 0; + } + } + + shadeA <<= 0x9; + offset += xA; + + var nextU = 0; + var nextV = 0; + var dx = xA - centerX; + u = u + (uStride >> 3) * dx; + v = v + (vStride >> 3) * dx; + w = w + (wStride >> 3) * dx; + var curW = w >> 14; + if (curW != 0) { + curU = (u / curW); + curV = (v / curW); + if (curU < 0) { + curU = 0; + } else if (curU > 16256) { + curU = 16256; + } + } + u = u + uStride; + v = v + vStride; + w = w + wStride; + curW = w >> 14; + if (curW != 0) { + nextU = (u / curW); + nextV = (v / curW); + if (nextU < 7) { + nextU = 7; + } else if (nextU > 16256) { + nextU = 16256; + } + } + var stepU = (nextU - curU) >> 3; + var stepV = (nextV - curV) >> 3; + curU += shadeA & 0x600000; + var shadeShift = u32(shadeA >> 23); + if (writeDepth) { + while (strides > 0) { + strides--; + var rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset = offset + 1; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU = nextU; + curV = nextV; + u += uStride; + v += vStride; + w += wStride; + curW = w >> 14; + if (curW != 0) { + nextU = (u / curW); + nextV = (v / curW); + if (nextU < 7) { + nextU = 7; + } else if (nextU > 16256) { + nextU = 16256; + } + } + stepU = (nextU - curU) >> 3; + stepV = (nextV - curV) >> 3; + shadeA += shadeStrides; + curU += shadeA & 0x600000; + shadeShift = u32(shadeA >> 23); + } + strides = (xB - xA) & 0x7; + while (strides > 0) { + strides--; + var rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + atomicMax(&depthBuffer[offset], depth); + } + offset++; + curU += stepU; + curV += stepV; + } + } else if (opaqueTexture) { + while (strides > 0) { + strides--; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU = nextU; + curV = nextV; + u += uStride; + v += vStride; + w += wStride; + curW = w >> 14; + if (curW != 0) { + nextU = (u / curW); + nextV = (v / curW); + if (nextU < 7) { + nextU = 7; + } else if (nextU > 16256) { + nextU = 16256; + } + } + stepU = (nextU - curU) >> 3; + stepV = (nextV - curV) >> 3; + shadeA += shadeStrides; + curU += shadeA & 0x600000; + shadeShift = u32(shadeA >> 23); + } + strides = (xB - xA) & 0x7; + while (strides > 0) { + strides--; + setPixel(offset, texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift); + offset++; + curU += stepU; + curV += stepV; + } + } else { + while (strides > 0) { + strides--; + var rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset = offset + 1; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU += stepU; + curV += stepV; + rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU = nextU; + curV = nextV; + u += uStride; + v += vStride; + w += wStride; + curW = w >> 14; + if (curW != 0) { + nextU = (u / curW); + nextV = (v / curW); + if (nextU < 7) { + nextU = 7; + } else if (nextU > 16256) { + nextU = 16256; + } + } + stepU = (nextU - curU) >> 3; + stepV = (nextV - curV) >> 3; + shadeA += shadeStrides; + curU += shadeA & 0x600000; + shadeShift = u32(shadeA >> 23); + } + strides = (xB - xA) & 0x7; + while (strides > 0) { + strides--; + var rgb = texels[(curV & 0x3f80) + (curU >> 7)] >> shadeShift; + if (rgb != 0) { + setPixel(offset, rgb); + } + offset++; + curU += stepU; + curV += stepV; + } + } +} + +fn reciprocal15(value: i32) -> i32 { + return 32768 / value; +} + +fn reciprocal16(value: i32) -> i32 { + return 65536 / value; +} +`; diff --git a/src/graphics/renderer/webgpu/shaders/fullscreen-pixels.wgsl.ts b/src/graphics/renderer/webgpu/shaders/fullscreen-pixels.wgsl.ts new file mode 100644 index 0000000..400d0e5 --- /dev/null +++ b/src/graphics/renderer/webgpu/shaders/fullscreen-pixels.wgsl.ts @@ -0,0 +1,27 @@ +import { UNPACK_COLOR888 } from './commons.wgsl'; + +export const SHADER_CODE: string = ` +struct PixelBuffer { + data: array, +}; + +struct Uniforms { + screenWidth: f32, + screenHeight: f32, +}; + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var pixelBuffer: PixelBuffer; + +${UNPACK_COLOR888} + +@fragment +fn frag_main(@location(0) TexCoord: vec2f) -> @location(0) vec4f { + let coord = floor(vec2f(TexCoord.x * uniforms.screenWidth, TexCoord.y * uniforms.screenHeight)); + let index = u32(coord.y * uniforms.screenWidth + coord.x); + + let finalColor = vec4f(unpackColor888(pixelBuffer.data[index]), 1.0); + return finalColor; +} + +`; diff --git a/src/graphics/renderer/webgpu/shaders/fullscreen-pixmap.wgsl.ts b/src/graphics/renderer/webgpu/shaders/fullscreen-pixmap.wgsl.ts new file mode 100644 index 0000000..7ef630e --- /dev/null +++ b/src/graphics/renderer/webgpu/shaders/fullscreen-pixmap.wgsl.ts @@ -0,0 +1,13 @@ +export const SHADER_CODE: string = ` +@group(0) @binding(0) var textureSampler: sampler; +@group(0) @binding(1) var texture: texture_2d; + +@fragment +fn frag_main(@location(0) TexCoord: vec2f) -> @location(0) vec4f { + var color = textureSample(texture, textureSampler, TexCoord).bgra; + if (all(color == vec4f(1.0))) { + discard; + } + return color; +} +`; diff --git a/src/graphics/renderer/webgpu/shaders/fullscreen-texture.wgsl.ts b/src/graphics/renderer/webgpu/shaders/fullscreen-texture.wgsl.ts new file mode 100644 index 0000000..cda6c12 --- /dev/null +++ b/src/graphics/renderer/webgpu/shaders/fullscreen-texture.wgsl.ts @@ -0,0 +1,10 @@ +export const SHADER_CODE: string = ` +@group(0) @binding(0) var textureSampler: sampler; +@group(0) @binding(1) var texture: texture_2d; + +@fragment +fn frag_main(@location(0) TexCoord: vec2f) -> @location(0) vec4f { + var color = textureSample(texture, textureSampler, TexCoord); + return color; +} +`; diff --git a/src/graphics/renderer/webgpu/shaders/fullscreen-vertex.wgsl.ts b/src/graphics/renderer/webgpu/shaders/fullscreen-vertex.wgsl.ts new file mode 100644 index 0000000..7d9c9ab --- /dev/null +++ b/src/graphics/renderer/webgpu/shaders/fullscreen-vertex.wgsl.ts @@ -0,0 +1,21 @@ +export const SHADER_CODE: string = ` +struct VertexOutput { + @builtin(position) Position: vec4f, + @location(0) TexCoord: vec2f, +}; + +@vertex +fn vert_main(@builtin(vertex_index) VertexIndex: u32) -> VertexOutput { + var pos = array( + vec2f(-1, 3), + vec2f( 3, -1), + vec2f(-1, -1), + ); + + var output: VertexOutput; + output.Position = vec4f(pos[VertexIndex], 0.0, 1.0); + output.TexCoord = pos[VertexIndex] * 0.5 + 0.5; + output.TexCoord.y = 1.0 - output.TexCoord.y; + return output; +} +`; diff --git a/src/io/Packet.ts b/src/io/Packet.ts index 235e9f0..2da2ee3 100644 --- a/src/io/Packet.ts +++ b/src/io/Packet.ts @@ -4,6 +4,7 @@ import LinkList from '#/datastruct/LinkList.js'; import Isaac from '#/io/Isaac.js'; import { bigIntModPow, bigIntToBytes, bytesToBigInt } from '#/util/JsUtil.js'; +import { PacketType } from '#/io/PacketType.ts'; export default class Packet extends DoublyLinkable { private static readonly CRC32_POLYNOMIAL: number = 0xedb88320; @@ -80,13 +81,13 @@ export default class Packet extends DoublyLinkable { static alloc(type: number): Packet { let cached: Packet | null = null; - if (type === 0 && Packet.cacheMinCount > 0) { + if (type === PacketType.TYPE_100B && Packet.cacheMinCount > 0) { Packet.cacheMinCount--; cached = Packet.cacheMin.removeHead() as Packet | null; - } else if (type === 1 && Packet.cacheMidCount > 0) { + } else if (type === PacketType.TYPE_5KB && Packet.cacheMidCount > 0) { Packet.cacheMidCount--; cached = Packet.cacheMid.removeHead() as Packet | null; - } else if (type === 2 && Packet.cacheMaxCount > 0) { + } else if (type === PacketType.TYPE_30KB && Packet.cacheMaxCount > 0) { Packet.cacheMaxCount--; cached = Packet.cacheMax.removeHead() as Packet | null; } @@ -96,9 +97,9 @@ export default class Packet extends DoublyLinkable { return cached; } - if (type === 0) { + if (type === PacketType.TYPE_100B) { return new Packet(new Uint8Array(100)); - } else if (type === 1) { + } else if (type === PacketType.TYPE_5KB) { return new Packet(new Uint8Array(5000)); } return new Packet(new Uint8Array(30000)); diff --git a/src/io/PacketType.ts b/src/io/PacketType.ts new file mode 100644 index 0000000..fb9b43d --- /dev/null +++ b/src/io/PacketType.ts @@ -0,0 +1,5 @@ +export const enum PacketType { + TYPE_100B = 0, + TYPE_5KB = 1, + TYPE_30KB = 2, +} \ No newline at end of file diff --git a/src/mapview/MapView.ts b/src/mapview/MapView.ts index 67fb013..274a894 100644 --- a/src/mapview/MapView.ts +++ b/src/mapview/MapView.ts @@ -1,3 +1,5 @@ +// noinspection JSSuspiciousNameCombination + import GameShell from '#/client/GameShell.ts'; import Pix24 from '#/graphics/Pix24.ts'; import Pix2D from '#/graphics/Pix2D.ts'; @@ -8,6 +10,10 @@ import Jagfile from '#/io/Jagfile.ts'; import Packet from '#/io/Packet.ts'; import { TypedArray1d, TypedArray2d } from '#/util/Arrays.ts'; import { downloadUrl, sleep } from '#/util/JsUtil.ts'; +import { LocAngle } from '#/dash3d/LocAngle.ts'; +import LocShape from '#/dash3d/LocShape.ts'; +import Pix3D from '#/graphics/Pix3D.ts'; +import { MouseButton } from '#/client/MouseButton.ts'; export class MapView extends GameShell { static shouldDrawBorders: boolean = false; @@ -312,7 +318,7 @@ export class MapView extends GameShell { if (this.flashTimer > 0 && this.flashTimer % 10 < 5) { for (let i: number = 0; i < this.activeMapFunctionCount; i++) { - if (this.activeMapFunctions[i] == this.currentKey) { + if (this.activeMapFunctions[i] === this.currentKey) { const x: number = (this.overviewX + (this.imageOverviewWidth * this.activeMapFunctionX[i]) / this.sizeX) | 0; const y: number = (this.overviewY + (this.imageOverviewHeight * this.activeMapFunctionZ[i]) / this.sizeZ) | 0; Pix2D.fillCircle(x, y, 2, 0xffff00, 256); @@ -339,10 +345,10 @@ export class MapView extends GameShell { this.b12?.drawString(this.keyX + 21, y + 14, this.keyNames[row + this.lastKeyPage], 0); let rgb: number = 0xffffff; - if (this.currentKeyHover == row + this.lastKeyPage) { + if (this.currentKeyHover === row + this.lastKeyPage) { rgb = 0xbbaaaa; } - if (this.flashTimer > 0 && this.flashTimer % 10 < 5 && this.currentKey == row + this.lastKeyPage) { + if (this.flashTimer > 0 && this.flashTimer % 10 < 5 && this.currentKey === row + this.lastKeyPage) { rgb = 0xffff00; } @@ -357,25 +363,25 @@ export class MapView extends GameShell { this.drawString(this.keyX, this.keyY + this.keyHeight, this.keyWidth, 18, this.colorInactiveBorderTL, this.colorInactive, this.colorInactiveBorderBR, 'Key'); let y = this.height - this.keyY - 20 + 1; - if (this.targetZoom == 3.0) { + if (this.targetZoom === 3.0) { this.drawString(170, y, 50, 30, this.colorActiveBorderTL, this.colorActive, this.colorActiveBorderBR, '37%'); } else { this.drawString(170, y, 50, 30, this.colorInactiveBorderTL, this.colorInactive, this.colorInactiveBorderBR, '37%'); } - if (this.targetZoom == 4.0) { + if (this.targetZoom === 4.0) { this.drawString(230, y, 50, 30, this.colorActiveBorderTL, this.colorActive, this.colorActiveBorderBR, '50%'); } else { this.drawString(230, y, 50, 30, this.colorInactiveBorderTL, this.colorInactive, this.colorInactiveBorderBR, '50%'); } - if (this.targetZoom == 6.0) { + if (this.targetZoom === 6.0) { this.drawString(290, y, 50, 30, this.colorActiveBorderTL, this.colorActive, this.colorActiveBorderBR, '75%'); } else { this.drawString(290, y, 50, 30, this.colorInactiveBorderTL, this.colorInactive, this.colorInactiveBorderBR, '75%'); } - if (this.targetZoom == 8.0) { + if (this.targetZoom === 8.0) { this.drawString(350, y, 50, 30, this.colorActiveBorderTL, this.colorActive, this.colorActiveBorderBR, '100%'); } else { this.drawString(350, y, 50, 30, this.colorInactiveBorderTL, this.colorInactive, this.colorInactiveBorderBR, '100%'); @@ -394,19 +400,19 @@ export class MapView extends GameShell { } async update(): Promise { - if (this.actionKey[1] == 1) { + if (this.actionKey[1] === 1) { this.offsetX = (this.offsetX - 16.0 / this.zoom) | 0; this.redraw = true; } - if (this.actionKey[2] == 1) { + if (this.actionKey[2] === 1) { this.offsetX = (this.offsetX + 16.0 / this.zoom) | 0; this.redraw = true; } - if (this.actionKey[3] == 1) { + if (this.actionKey[3] === 1) { this.offsetZ = (this.offsetZ - 16.0 / this.zoom) | 0; this.redraw = true; } - if (this.actionKey[4] == 1) { + if (this.actionKey[4] === 1) { this.offsetZ = (this.offsetZ + 16.0 / this.zoom) | 0; this.redraw = true; } @@ -418,42 +424,42 @@ export class MapView extends GameShell { break; } - if (key == '1'.charCodeAt(0)) { + if (key === '1'.charCodeAt(0)) { this.targetZoom = 3.0; this.redraw = true; - } else if (key == '2'.charCodeAt(0)) { + } else if (key === '2'.charCodeAt(0)) { this.targetZoom = 4.0; this.redraw = true; - } else if (key == '3'.charCodeAt(0)) { + } else if (key === '3'.charCodeAt(0)) { this.targetZoom = 6.0; this.redraw = true; - } else if (key == '4'.charCodeAt(0)) { + } else if (key === '4'.charCodeAt(0)) { this.targetZoom = 8.0; this.redraw = true; - } else if (key == 'k'.charCodeAt(0) || key == 'K'.charCodeAt(0)) { + } else if (key === 'k'.charCodeAt(0) || key === 'K'.charCodeAt(0)) { this.showKey = !this.showKey; this.redraw = true; - } else if (key == 'o'.charCodeAt(0) || key == 'O'.charCodeAt(0)) { + } else if (key === 'o'.charCodeAt(0) || key === 'O'.charCodeAt(0)) { this.showOverview = !this.showOverview; this.redraw = true; - } else if (key == 'e'.charCodeAt(0) || key == 'E'.charCodeAt(0)) { + } else if (key === 'e'.charCodeAt(0) || key === 'E'.charCodeAt(0)) { // todo: export as png and prompt user to download file - } else if (key == 'n'.charCodeAt(0) || key == 'N'.charCodeAt(0)) { + } else if (key === 'n'.charCodeAt(0) || key === 'N'.charCodeAt(0)) { MapView.shouldDrawNpcs = !MapView.shouldDrawNpcs; this.redraw = true; - } else if (key == 'i'.charCodeAt(0) || key == 'I'.charCodeAt(0)) { + } else if (key === 'i'.charCodeAt(0) || key === 'I'.charCodeAt(0)) { MapView.shouldDrawItems = !MapView.shouldDrawItems; this.redraw = true; - } else if (key == 'l'.charCodeAt(0) || key == 'L'.charCodeAt(0)) { + } else if (key === 'l'.charCodeAt(0) || key === 'L'.charCodeAt(0)) { MapView.shouldDrawLabels = !MapView.shouldDrawLabels; this.redraw = true; - } else if (key == 'b'.charCodeAt(0) || key == 'B'.charCodeAt(0)) { + } else if (key === 'b'.charCodeAt(0) || key === 'B'.charCodeAt(0)) { MapView.shouldDrawBorders = !MapView.shouldDrawBorders; this.redraw = true; } } while (key > 0); - if (this.mouseClickButton == 1) { + if (this.mouseClickButton === MouseButton.LEFT) { this.lastMouseClickX = this.mouseClickX; this.lastMouseClickY = this.mouseClickY; this.lastOffsetX = this.offsetX; @@ -507,7 +513,7 @@ export class MapView extends GameShell { if (this.mouseY >= y && this.mouseY < y + 17) { this.currentKeyHover = row + this.lastKeyPage; - if (this.mouseClickButton == 1) { + if (this.mouseClickButton === MouseButton.LEFT) { this.currentKey = row + this.lastKeyPage; this.flashTimer = 50; } @@ -518,16 +524,16 @@ export class MapView extends GameShell { } } - if (this.currentKeyHover != this.lastKeyHover) { + if (this.currentKeyHover !== this.lastKeyHover) { this.lastKeyHover = this.currentKeyHover; this.redraw = true; } } - if ((this.mouseButton == 1 || this.mouseClickButton == 1) && this.showOverview) { + if ((this.mouseButton === MouseButton.LEFT || this.mouseClickButton === MouseButton.LEFT) && this.showOverview) { let mouseClickX: number = this.mouseClickX; let mouseClickY: number = this.mouseClickY; - if (this.mouseButton == 1) { + if (this.mouseButton === MouseButton.LEFT) { mouseClickX = this.mouseX; mouseClickY = this.mouseY; } @@ -540,7 +546,7 @@ export class MapView extends GameShell { } } - if (this.mouseButton == 1 && this.lastMouseClickX != -1) { + if (this.mouseButton === MouseButton.LEFT && this.lastMouseClickX !== -1) { this.offsetX = this.lastOffsetX + ((((this.lastMouseClickX - this.mouseX) * 2.0) / this.targetZoom) | 0); this.offsetZ = this.lastOffsetZ + ((((this.lastMouseClickY - this.mouseY) * 2.0) / this.targetZoom) | 0); this.redraw = true; @@ -653,7 +659,7 @@ export class MapView extends GameShell { clearEmptyTiles(): void { for (let x: number = 0; x < this.sizeX; x++) { for (let z: number = 0; z < this.sizeZ; z++) { - if (this.underlayTiles[x][z] == 0 && this.overlayTiles[x][z] == 0) { + if (this.underlayTiles[x][z] === 0 && this.overlayTiles[x][z] === 0) { this.floormapColors[x][z] = 0; } } @@ -685,7 +691,7 @@ export class MapView extends GameShell { b += (tileNorth & 0x3ff) - (tileSouth & 0x3ff); if (b > 0) { - this.floormapColors[x][z] = this.convertHsl(r / 8533.0, g / 8533.0, b / 8533.0); + this.floormapColors[x][z] = Pix3D.convertHsl(r / 8533.0, g / 8533.0, b / 8533.0); } } } @@ -734,7 +740,7 @@ export class MapView extends GameShell { } else { for (let i: number = -4096; i < 0; i++) { const opcode: number = data.g1(); - if (opcode != 0) { + if (opcode !== 0) { data.g1(); } } @@ -781,7 +787,7 @@ export class MapView extends GameShell { for (let z: number = -64; z < 0; z++) { do { opcode = data.g1(); - } while (opcode != 0); + } while (opcode !== 0); } } } @@ -798,7 +804,7 @@ export class MapView extends GameShell { let zIndex: number = this.sizeZ - mz - 1; for (let z: number = -64; z < 0; z++) { - this.objTiles[x + mx][zIndex--] = data.g1() == 1; + this.objTiles[x + mx][zIndex--] = data.g1() === 1; } } } else { @@ -817,7 +823,7 @@ export class MapView extends GameShell { let zIndex: number = this.sizeZ - mz - 1; for (let z: number = -64; z < 0; z++) { - this.npcTiles[x + mx][zIndex--] = data.g1() == 1; + this.npcTiles[x + mx][zIndex--] = data.g1() === 1; } } } else { @@ -827,66 +833,6 @@ export class MapView extends GameShell { } // ---- - convertHsl(hue: number, saturation: number, lightness: number): number { - let r: number = lightness; - let g: number = lightness; - let b: number = lightness; - - if (saturation !== 0.0) { - let q: number; - if (lightness < 0.5) { - q = lightness * (saturation + 1.0); - } else { - q = lightness + saturation - lightness * saturation; - } - - const p: number = lightness * 2.0 - q; - let t: number = hue + 0.3333333333333333; - if (t > 1.0) { - t--; - } - - let d11: number = hue - 0.3333333333333333; - if (d11 < 0.0) { - d11++; - } - - if (t * 6.0 < 1.0) { - r = p + (q - p) * 6.0 * t; - } else if (t * 2.0 < 1.0) { - r = q; - } else if (t * 3.0 < 2.0) { - r = p + (q - p) * (0.6666666666666666 - t) * 6.0; - } else { - r = p; - } - - if (hue * 6.0 < 1.0) { - g = p + (q - p) * 6.0 * hue; - } else if (hue * 2.0 < 1.0) { - g = q; - } else if (hue * 3.0 < 2.0) { - g = p + (q - p) * (0.6666666666666666 - hue) * 6.0; - } else { - g = p; - } - - if (d11 * 6.0 < 1.0) { - b = p + (q - p) * 6.0 * d11; - } else if (d11 * 2.0 < 1.0) { - b = q; - } else if (d11 * 3.0 < 2.0) { - b = p + (q - p) * (0.6666666666666666 - d11) * 6.0; - } else { - b = p; - } - } - - const intR: number = (r * 256.0) | 0; - const intG: number = (g * 256.0) | 0; - const intB: number = (b * 256.0) | 0; - return (intR << 16) + (intG << 8) + intB; - } drawMap(left: number, top: number, right: number, bottom: number, widthOffset: number, heightOffset: number, width: number, height: number): void { const visibleX: number = right - left; @@ -926,7 +872,7 @@ export class MapView extends GameShell { } else { const info: number = this.overlayInfo[x + left][y + top]; const shape: number = info & 0xfc; - if (shape == 0 || lengthX <= 1 || lengthY <= 1) { + if (shape === 0 || lengthX <= 1 || lengthY <= 1) { Pix2D.fillRect2d(startX, startY, lengthX, lengthY, overlay); } else { this.drawSmoothEdges(Pix2D.pixels, startY * Pix2D.width2d + startX, this.floormapColors[x + left][y + top], overlay, lengthX, lengthY, shape >> 2, info & 0x3); @@ -967,16 +913,16 @@ export class MapView extends GameShell { endY += heightOffset; let wall: number = this.locWalls[x + left][y + top] & 0xff; - if (wall != 0) { + if (wall !== 0) { let edgeX: number; - if (lengthX == 1) { + if (lengthX === 1) { edgeX = startX; } else { edgeX = endX - 1; } let edgeY: number; - if (lengthY == 1) { + if (lengthY === 1) { edgeY = startY; } else { edgeY = endY - 1; @@ -987,45 +933,45 @@ export class MapView extends GameShell { rgb = 0xcc0000; wall -= 4; } - if (wall == 27 || wall == 28) { + if (wall === 27 || wall === 28) { // bugfix: drawing diagonal doors rgb = 0xcc0000; wall -= 2; } - if (wall == 1) { + if (wall === 1) { Pix2D.drawVerticalLine(startX, startY, rgb, lengthY); - } else if (wall == 2) { + } else if (wall === 2) { Pix2D.drawHorizontalLine(startX, startY, rgb, lengthX); - } else if (wall == 3) { + } else if (wall === 3) { Pix2D.drawVerticalLine(edgeX, startY, rgb, lengthY); - } else if (wall == 4) { + } else if (wall === 4) { Pix2D.drawHorizontalLine(startX, edgeY, rgb, lengthX); - } else if (wall == 9) { + } else if (wall === 9) { Pix2D.drawVerticalLine(startX, startY, 0xffffff, lengthY); Pix2D.drawHorizontalLine(startX, startY, rgb, lengthX); - } else if (wall == 10) { + } else if (wall === 10) { Pix2D.drawVerticalLine(edgeX, startY, 0xffffff, lengthY); Pix2D.drawHorizontalLine(startX, startY, rgb, lengthX); - } else if (wall == 11) { + } else if (wall === 11) { Pix2D.drawVerticalLine(edgeX, startY, 0xffffff, lengthY); Pix2D.drawHorizontalLine(startX, edgeY, rgb, lengthX); - } else if (wall == 12) { + } else if (wall === 12) { Pix2D.drawVerticalLine(startX, startY, 0xffffff, lengthY); Pix2D.drawHorizontalLine(startX, edgeY, rgb, lengthX); - } else if (wall == 17) { + } else if (wall === 17) { Pix2D.drawHorizontalLine(startX, startY, rgb, 1); - } else if (wall == 18) { + } else if (wall === 18) { Pix2D.drawHorizontalLine(edgeX, startY, rgb, 1); - } else if (wall == 19) { + } else if (wall === 19) { Pix2D.drawHorizontalLine(edgeX, edgeY, rgb, 1); - } else if (wall == 20) { + } else if (wall === 20) { Pix2D.drawHorizontalLine(startX, edgeY, rgb, 1); - } else if (wall == 25) { + } else if (wall === 25) { for (let i: number = 0; i < lengthY; i++) { Pix2D.drawHorizontalLine(startX + i, edgeY - i, rgb, 1); } - } else if (wall == 26) { + } else if (wall === 26) { for (let i: number = 0; i < lengthY; i++) { Pix2D.drawHorizontalLine(startX + i, startY + i, rgb, 1); } @@ -1033,12 +979,12 @@ export class MapView extends GameShell { } const mapscene: number = this.locMapscenes[x + left][y + top]; - if (mapscene != 0) { + if (mapscene !== 0) { this.imageMapscene[mapscene - 1].clip(startX - lengthX / 2, startY - lengthY / 2, lengthX * 2, lengthY * 2); } const mapfunction: number = this.locMapfunction[x + left][y + top]; - if (mapfunction != 0) { + if (mapfunction !== 0) { this.visibleMapFunctions[visibleMapFunctionCount] = mapfunction - 1; this.visibleMapFunctionsX[visibleMapFunctionCount] = startX + lengthX / 2; this.visibleMapFunctionsY[visibleMapFunctionCount] = startY + lengthY / 2; @@ -1121,7 +1067,7 @@ export class MapView extends GameShell { if (this.flashTimer > 0) { for (let i: number = 0; i < visibleMapFunctionCount; i++) { - if (this.visibleMapFunctions[i] == this.currentKey) { + if (this.visibleMapFunctions[i] === this.currentKey) { this.imageMapfunction[this.visibleMapFunctions[i]].draw(this.visibleMapFunctionsX[i] - 7, this.visibleMapFunctionsY[i] - 7); if (this.flashTimer % 10 < 5) { @@ -1132,7 +1078,7 @@ export class MapView extends GameShell { } } - if (this.zoom == this.targetZoom && MapView.shouldDrawLabels) { + if (this.zoom === this.targetZoom && MapView.shouldDrawLabels) { for (let i: number = 0; i < this.labelCount; i++) { let x = this.labelX[i]; let y = this.labelY[i]; @@ -1209,7 +1155,7 @@ export class MapView extends GameShell { Pix2D.drawRect(drawLeft, drawTop, drawRight - drawLeft, drawBottom - drawTop, color); this.b12?.drawStringRight(drawRight - 5, drawBottom - 5, mx + '_' + mz, color, false); - if (mx == 33 && mz >= 71 && mz <= 73) { + if (mx === 33 && mz >= 71 && mz <= 73) { this.b12?.drawStringCenter((drawRight + drawLeft) / 2, (drawBottom + drawTop) / 2, 'u_pass', 0xff0000); } else if (mx >= 32 && mx <= 34 && mz >= 70 && mz <= 74) { this.b12?.drawStringCenter((drawRight + drawLeft) / 2, (drawBottom + drawTop) / 2, 'u_pass', 0xffff00); @@ -1219,21 +1165,21 @@ export class MapView extends GameShell { } } - drawSmoothEdges(data: Int32Array, off: number, color: number, overlay: number, width: number, height: number, shape: number, rotation: number): void { + drawSmoothEdges(data: Int32Array, off: number, color: number, overlay: number, width: number, height: number, shape: number, angle: number): void { const step: number = Pix2D.width2d - width; - if (shape == 9) { - shape = 1; - rotation = (rotation + 1) & 0x3; - } else if (shape == 10) { - shape = 1; - rotation = (rotation + 3) & 0x3; - } else if (shape == 11) { - shape = 8; - rotation = (rotation + 3) & 0x3; + if (shape === LocShape.WALL_DIAGONAL.id) { + shape = LocShape.WALL_DIAGONAL_CORNER.id; + angle = (angle + 1) & 0x3; + } else if (shape === LocShape.CENTREPIECE_STRAIGHT.id) { + shape = LocShape.WALL_DIAGONAL_CORNER.id; + angle = (angle + 3) & 0x3; + } else if (shape === LocShape.CENTREPIECE_DIAGONAL.id) { + shape = LocShape.WALLDECOR_DIAGONAL_BOTH.id; + angle = (angle + 3) & 0x3; } - if (shape == 1) { - if (rotation == 0) { + if (shape === LocShape.WALL_DIAGONAL_CORNER.id) { + if (angle === LocAngle.WEST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x <= y) { @@ -1244,7 +1190,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x <= y) { @@ -1255,7 +1201,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x >= y) { @@ -1266,7 +1212,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x >= y) { @@ -1278,8 +1224,8 @@ export class MapView extends GameShell { off += step; } } - } else if (shape == 2) { - if (rotation == 0) { + } else if (shape === LocShape.WALL_L.id) { + if (angle === LocAngle.WEST) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x <= y >> 1) { @@ -1290,7 +1236,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x >= y << 1) { @@ -1301,7 +1247,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = 0; y < height; y++) { for (let x: number = width - 1; x >= 0; x--) { if (x <= y >> 1) { @@ -1312,7 +1258,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = width - 1; x >= 0; x--) { if (x >= y << 1) { @@ -1324,8 +1270,8 @@ export class MapView extends GameShell { off += step; } } - } else if (shape == 3) { - if (rotation == 0) { + } else if (shape === LocShape.WALL_SQUARE_CORNER.id) { + if (angle === LocAngle.WEST) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = width - 1; x >= 0; x--) { if (x <= y >> 1) { @@ -1336,7 +1282,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x >= y << 1) { @@ -1347,7 +1293,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x <= y >> 1) { @@ -1358,7 +1304,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = 0; y < height; y++) { for (let x: number = width - 1; x >= 0; x--) { if (x >= y << 1) { @@ -1370,8 +1316,8 @@ export class MapView extends GameShell { off += step; } } - } else if (shape == 4) { - if (rotation == 0) { + } else if (shape === LocShape.WALLDECOR_STRAIGHT_NOOFFSET.id) { + if (angle === LocAngle.WEST) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x >= y >> 1) { @@ -1382,7 +1328,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x <= y << 1) { @@ -1393,7 +1339,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = 0; y < height; y++) { for (let x: number = width - 1; x >= 0; x--) { if (x >= y >> 1) { @@ -1404,7 +1350,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = width - 1; x >= 0; x--) { if (x <= y << 1) { @@ -1416,8 +1362,8 @@ export class MapView extends GameShell { off += step; } } - } else if (shape == 5) { - if (rotation == 0) { + } else if (shape === LocShape.WALLDECOR_STRAIGHT_OFFSET.id) { + if (angle === LocAngle.WEST) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = width - 1; x >= 0; x--) { if (x >= y >> 1) { @@ -1428,7 +1374,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x <= y << 1) { @@ -1439,7 +1385,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x >= y >> 1) { @@ -1450,7 +1396,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = 0; y < height; y++) { for (let x: number = width - 1; x >= 0; x--) { if (x <= y << 1) { @@ -1462,8 +1408,8 @@ export class MapView extends GameShell { off += step; } } - } else if (shape == 6) { - if (rotation == 0) { + } else if (shape === LocShape.WALLDECOR_DIAGONAL_OFFSET.id) { + if (angle === LocAngle.WEST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x <= width / 2) { @@ -1474,7 +1420,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (y <= height / 2) { @@ -1485,7 +1431,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x >= width / 2) { @@ -1496,7 +1442,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (y >= height / 2) { @@ -1508,8 +1454,8 @@ export class MapView extends GameShell { off += step; } } - } else if (shape == 7) { - if (rotation == 0) { + } else if (shape === LocShape.WALLDECOR_DIAGONAL_NOOFFSET.id) { + if (angle === LocAngle.WEST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x <= y - height / 2) { @@ -1520,7 +1466,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x <= y - height / 2) { @@ -1531,7 +1477,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = width - 1; x >= 0; x--) { if (x <= y - height / 2) { @@ -1542,7 +1488,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = 0; y < height; y++) { for (let x: number = width - 1; x >= 0; x--) { if (x <= y - height / 2) { @@ -1554,8 +1500,8 @@ export class MapView extends GameShell { off += step; } } - } else if (shape == 8) { - if (rotation == 0) { + } else if (shape === LocShape.WALLDECOR_DIAGONAL_BOTH.id) { + if (angle === LocAngle.WEST) { for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { if (x >= y - height / 2) { @@ -1566,7 +1512,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 1) { + } else if (angle === LocAngle.NORTH) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = 0; x < width; x++) { if (x >= y - height / 2) { @@ -1577,7 +1523,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 2) { + } else if (angle === LocAngle.EAST) { for (let y: number = height - 1; y >= 0; y--) { for (let x: number = width - 1; x >= 0; x--) { if (x >= y - height / 2) { @@ -1588,7 +1534,7 @@ export class MapView extends GameShell { } off += step; } - } else if (rotation == 3) { + } else if (angle === LocAngle.SOUTH) { for (let y: number = 0; y < height; y++) { for (let x: number = width - 1; x >= 0; x--) { if (x >= y - height / 2) { diff --git a/src/util/JsUtil.ts b/src/util/JsUtil.ts index d59956f..4aa6561 100644 --- a/src/util/JsUtil.ts +++ b/src/util/JsUtil.ts @@ -1,4 +1,4 @@ -export const sleep = async (ms: number): Promise => new Promise((resolve): NodeJS.Timeout => setTimeout(resolve, ms)); +export const sleep = async (ms: number): Promise => new Promise((resolve): ReturnType => setTimeout(resolve, ms)); export const downloadUrl = async (url: string): Promise => new Uint8Array(await (await fetch(url)).arrayBuffer()); diff --git a/tsconfig.json b/tsconfig.json index 83f7d5a..149a305 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { // Enable latest features "lib": ["ESNext", "DOM"], + "types": ["@webgpu/types"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force",