mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:10:46 +00:00
4cd22a9b5c
The render/ tree was the only place in the client still using kebab-case filenames. Brings ~80 files in line with the rest of src/client/ (BuildPreviewController, TransformHandler, etc.). Directories kept as they were (name-pass/, fx-pass/, passes/, utils/, debug/) since the codebase already mixes those. Two collisions surfaced and got resolved: render/types/ is a directory, not a file, so its imports kept the lowercase form; and the sed pass incidentally normalized core/pathfinding imports, which had to be reverted since that file is actually lowercase on disk despite some imports having referenced it as ./Types under macOS case-insensitive resolution.
515 lines
16 KiB
TypeScript
515 lines
16 KiB
TypeScript
/**
|
||
* UnitPass — GPU-rendered mobile unit sprites.
|
||
*
|
||
* Renders all mobile (non-structure) units: boats, nukes, shells, SAM
|
||
* missiles, and MIRV warheads. All unit types are rotationally symmetric
|
||
* — no rotation needed. Sprites are tiny grayscale PNGs colorized on the
|
||
* GPU using the standard 3-band gray replacement (180/130/70). Shell and
|
||
* MIRV Warhead use programmatic 3×3 white squares (colorized to border
|
||
* color).
|
||
*
|
||
* Two instanced draw calls per frame — ground units and missiles are
|
||
* split into separate buffers for correct layer ordering:
|
||
* Ground/sea (boats, trains) → rendered below structures
|
||
* Missiles (nukes, shells, SAM, MIRV warheads) → rendered above structures
|
||
*
|
||
* Atlas layout (12 columns × 13px cells, pre-built by generate-sprite-atlases.mjs):
|
||
* Col 0: Transport (5×5)
|
||
* Col 1: Trade Ship (5×5)
|
||
* Col 2: Warship (11×11)
|
||
* Col 3: Atom Bomb (7×7)
|
||
* Col 4: Hydrogen Bomb (9×9)
|
||
* Col 5: MIRV (13×13, grayscale colorized)
|
||
* Col 6: SAM Missile (3×3)
|
||
* Col 7: Shell (3×3 white square)
|
||
* Col 8: MIRV Warhead (3×3 white square)
|
||
* Col 9: Train Engine (5×5)
|
||
* Col 10: Train Carriage (5×5)
|
||
* Col 11: Train Carriage Loaded (5×5)
|
||
*
|
||
* Data flow:
|
||
* FrameSnapshot.units → filter by typeToAtlasIdx → instance VBO → GPU
|
||
* Shells emit 2 instances (pos + lastPos) to match live game's 2-pixel trail.
|
||
*/
|
||
|
||
import { assetUrl } from "src/core/AssetUrls";
|
||
import type { RendererConfig, UnitState } from "../../types";
|
||
import {
|
||
TrainType,
|
||
UT_ATOM_BOMB,
|
||
UT_HYDROGEN_BOMB,
|
||
UT_MIRV,
|
||
UT_MIRV_WARHEAD,
|
||
UT_SAM_MISSILE,
|
||
UT_SHELL,
|
||
UT_TRADE_SHIP,
|
||
UT_TRAIN,
|
||
UT_TRANSPORT,
|
||
UT_WARSHIP,
|
||
} from "../../types";
|
||
import { DynamicInstanceBuffer } from "../DynamicBuffer";
|
||
import type { RenderSettings } from "../RenderSettings";
|
||
import unitFragSrc from "../shaders/unit/unit.frag.glsl?raw";
|
||
import unitVertSrc from "../shaders/unit/unit.vert.glsl?raw";
|
||
import { getPaletteSize } from "../utils/ColorUtils";
|
||
import { createProgram, shaderSrc } from "../utils/GlUtils";
|
||
|
||
const unitAtlasUrl = assetUrl("atlases/unit-atlas.png");
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Constants
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** Unit types in atlas column order. Index = atlas column.
|
||
* TrainEngine/TrainCarriage/TrainCarriageLoaded are synthetic names —
|
||
* they don't match header.unitTypes directly. Train resolution is
|
||
* handled specially in updateUnits() via trainType + loaded fields.
|
||
*/
|
||
const UNIT_ORDER = [
|
||
UT_TRANSPORT,
|
||
UT_TRADE_SHIP,
|
||
UT_WARSHIP,
|
||
UT_ATOM_BOMB,
|
||
UT_HYDROGEN_BOMB,
|
||
UT_MIRV,
|
||
UT_SAM_MISSILE,
|
||
UT_SHELL,
|
||
UT_MIRV_WARHEAD,
|
||
"TrainEngine",
|
||
"TrainCarriage",
|
||
"TrainCarriageLoaded",
|
||
] as const;
|
||
|
||
const ATLAS_COLS = UNIT_ORDER.length;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Instance data layout
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Per-instance data (16 bytes):
|
||
* float x, y, ownerID — 12 bytes (3 floats)
|
||
* uint8 atlasIdx — 1 byte (atlas column 0–11)
|
||
* uint8 flags — 1 byte (0 = normal, 1 = flicker, 2 = angry)
|
||
* 2 bytes padding — aligns to 4-byte boundary
|
||
*/
|
||
const FLOATS_PER_INSTANCE = 4;
|
||
const BYTES_PER_INSTANCE = FLOATS_PER_INSTANCE * 4;
|
||
|
||
/** Flag values — passed as uint8, received as float in shader via normalized attribute */
|
||
const FLAG_NORMAL = 0;
|
||
const FLAG_FLICKER = 1;
|
||
const FLAG_ANGRY = 2;
|
||
const FLAG_TRADE_FRIENDLY = 3;
|
||
|
||
/** Atlas column indices for train sub-types (resolved from trainType + loaded) */
|
||
const TRAIN_ENGINE_COL = UNIT_ORDER.indexOf("TrainEngine");
|
||
const TRAIN_CARRIAGE_COL = UNIT_ORDER.indexOf("TrainCarriage");
|
||
const TRAIN_CARRIAGE_LOADED_COL = UNIT_ORDER.indexOf("TrainCarriageLoaded");
|
||
|
||
/** Nuke + warhead types — rendered with flickering hot colors */
|
||
const FLICKER_TYPES: ReadonlySet<string> = new Set([
|
||
UT_ATOM_BOMB,
|
||
UT_HYDROGEN_BOMB,
|
||
UT_MIRV,
|
||
UT_MIRV_WARHEAD,
|
||
UT_SAM_MISSILE,
|
||
UT_SHELL,
|
||
]);
|
||
|
||
/** Missile/projectile types — rendered on top of structures in the layer order.
|
||
* Ground/sea units (boats, trains) render below structures. */
|
||
const MISSILE_TYPES: ReadonlySet<string> = new Set([
|
||
UT_ATOM_BOMB,
|
||
UT_HYDROGEN_BOMB,
|
||
UT_MIRV,
|
||
UT_SAM_MISSILE,
|
||
UT_SHELL,
|
||
UT_MIRV_WARHEAD,
|
||
]);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helper: create a VAO for instanced unit rendering
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function createUnitVao(
|
||
gl: WebGL2RenderingContext,
|
||
quadBuf: WebGLBuffer,
|
||
instanceBuf: WebGLBuffer,
|
||
): WebGLVertexArrayObject {
|
||
const vao = gl.createVertexArray()!;
|
||
gl.bindVertexArray(vao);
|
||
|
||
// Attribute 0: unit quad [0,0]->[1,1]
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||
gl.enableVertexAttribArray(0);
|
||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||
|
||
// Attribute 1: per-instance vec3 (x, y, ownerID) — 3 floats at offset 0
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuf);
|
||
gl.enableVertexAttribArray(1);
|
||
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, BYTES_PER_INSTANCE, 0);
|
||
gl.vertexAttribDivisor(1, 1);
|
||
|
||
// Attribute 2: per-instance (atlasIdx, flags) — 2 uint8s at offset 12, converted to float
|
||
gl.enableVertexAttribArray(2);
|
||
gl.vertexAttribPointer(2, 2, gl.UNSIGNED_BYTE, false, BYTES_PER_INSTANCE, 12);
|
||
gl.vertexAttribDivisor(2, 1);
|
||
|
||
gl.bindVertexArray(null);
|
||
return vao;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// UnitPass
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export class UnitPass {
|
||
private gl: WebGL2RenderingContext;
|
||
private settings: RenderSettings;
|
||
private program: WebGLProgram;
|
||
|
||
private uCamera: WebGLUniformLocation;
|
||
private uTick: WebGLUniformLocation;
|
||
private uUnitSize: WebGLUniformLocation;
|
||
private uFlickerSpeed: WebGLUniformLocation;
|
||
private uAngryColor: WebGLUniformLocation;
|
||
private uAltView: WebGLUniformLocation;
|
||
|
||
private affiliationTex: WebGLTexture | null = null;
|
||
private altView = false;
|
||
|
||
// Ground/sea units (boats, trains) — render below structures
|
||
private groundVao: WebGLVertexArrayObject;
|
||
private groundBuf: DynamicInstanceBuffer;
|
||
private groundCount = 0;
|
||
|
||
// Missiles/projectiles (nukes, shells, SAM) — render above structures
|
||
private missileVao: WebGLVertexArrayObject;
|
||
private missileBuf: DynamicInstanceBuffer;
|
||
private missileCount = 0;
|
||
|
||
private quadBuf: WebGLBuffer;
|
||
private paletteTex: WebGLTexture;
|
||
private atlasTex: WebGLTexture;
|
||
|
||
/** Frame tick received from renderer — drives tick-based effects */
|
||
private frameTick = 0;
|
||
|
||
/** unitType string → atlas column (0-11) */
|
||
private typeToAtlasCol = new Map<string, number>();
|
||
private mapW: number;
|
||
|
||
// Trade-friendly detection: enemy trade ships heading to a self/allied port
|
||
private localPlayerID = 0;
|
||
private friendlyOwners = new Set<number>();
|
||
private structures: Map<number, UnitState> = new Map();
|
||
|
||
constructor(
|
||
gl: WebGL2RenderingContext,
|
||
header: RendererConfig,
|
||
paletteTex: WebGLTexture,
|
||
settings: RenderSettings,
|
||
) {
|
||
this.gl = gl;
|
||
this.settings = settings;
|
||
this.mapW = header.mapWidth;
|
||
this.paletteTex = paletteTex;
|
||
|
||
// Build unitType string → atlas column mapping
|
||
for (let i = 0; i < header.unitTypes.length; i++) {
|
||
const col = UNIT_ORDER.indexOf(
|
||
header.unitTypes[i] as (typeof UNIT_ORDER)[number],
|
||
);
|
||
if (col >= 0) {
|
||
this.typeToAtlasCol.set(header.unitTypes[i], col);
|
||
}
|
||
}
|
||
|
||
// Compile shaders
|
||
this.program = createProgram(
|
||
gl,
|
||
shaderSrc(unitVertSrc, { ATLAS_COLS }),
|
||
shaderSrc(unitFragSrc, { PALETTE_SIZE: getPaletteSize() }),
|
||
);
|
||
this.uCamera = gl.getUniformLocation(this.program, "uCamera")!;
|
||
this.uTick = gl.getUniformLocation(this.program, "uTick")!;
|
||
this.uUnitSize = gl.getUniformLocation(this.program, "uUnitSize")!;
|
||
this.uFlickerSpeed = gl.getUniformLocation(this.program, "uFlickerSpeed")!;
|
||
this.uAngryColor = gl.getUniformLocation(this.program, "uAngryColor")!;
|
||
|
||
this.uAltView = gl.getUniformLocation(this.program, "uAltView")!;
|
||
|
||
// Texture unit bindings
|
||
gl.useProgram(this.program);
|
||
gl.uniform1i(gl.getUniformLocation(this.program, "uPalette"), 0);
|
||
gl.uniform1i(gl.getUniformLocation(this.program, "uAtlas"), 1);
|
||
gl.uniform1i(gl.getUniformLocation(this.program, "uAffiliation"), 2);
|
||
|
||
// Create placeholder atlas texture (1x1 gray pixel)
|
||
this.atlasTex = gl.createTexture()!;
|
||
gl.activeTexture(gl.TEXTURE1);
|
||
gl.bindTexture(gl.TEXTURE_2D, this.atlasTex);
|
||
gl.texImage2D(
|
||
gl.TEXTURE_2D,
|
||
0,
|
||
gl.RGBA,
|
||
1,
|
||
1,
|
||
0,
|
||
gl.RGBA,
|
||
gl.UNSIGNED_BYTE,
|
||
new Uint8Array([128, 128, 128, 255]),
|
||
);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||
|
||
// Start async atlas build
|
||
this.loadAtlas();
|
||
|
||
// --- Shared quad buffer ---
|
||
this.quadBuf = gl.createBuffer()!;
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuf);
|
||
gl.bufferData(
|
||
gl.ARRAY_BUFFER,
|
||
new Float32Array([0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1]),
|
||
gl.STATIC_DRAW,
|
||
);
|
||
|
||
// --- Ground instance buffer + VAO ---
|
||
const groundGlBuf = gl.createBuffer()!;
|
||
this.groundBuf = new DynamicInstanceBuffer(
|
||
gl,
|
||
groundGlBuf,
|
||
1024,
|
||
FLOATS_PER_INSTANCE,
|
||
);
|
||
this.groundVao = createUnitVao(gl, this.quadBuf, groundGlBuf);
|
||
|
||
// --- Missile instance buffer + VAO ---
|
||
const missileGlBuf = gl.createBuffer()!;
|
||
this.missileBuf = new DynamicInstanceBuffer(
|
||
gl,
|
||
missileGlBuf,
|
||
512,
|
||
FLOATS_PER_INSTANCE,
|
||
);
|
||
this.missileVao = createUnitVao(gl, this.quadBuf, missileGlBuf);
|
||
}
|
||
|
||
private async loadAtlas(): Promise<void> {
|
||
const img = new Image();
|
||
img.crossOrigin = "anonymous";
|
||
img.src = unitAtlasUrl;
|
||
await img.decode();
|
||
const gl = this.gl;
|
||
gl.bindTexture(gl.TEXTURE_2D, this.atlasTex);
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||
}
|
||
|
||
private emitGround(
|
||
x: number,
|
||
y: number,
|
||
ownerID: number,
|
||
atlasIdx: number,
|
||
flags: number,
|
||
): void {
|
||
this.groundBuf.ensureCapacity(this.groundCount + 1);
|
||
const off = this.groundCount * FLOATS_PER_INSTANCE;
|
||
this.groundBuf.float32[off + 0] = x;
|
||
this.groundBuf.float32[off + 1] = y;
|
||
this.groundBuf.float32[off + 2] = ownerID;
|
||
const byteOff = this.groundCount * BYTES_PER_INSTANCE;
|
||
this.groundBuf.uint8[byteOff + 12] = atlasIdx;
|
||
this.groundBuf.uint8[byteOff + 13] = flags;
|
||
this.groundCount++;
|
||
}
|
||
|
||
private emitMissile(
|
||
x: number,
|
||
y: number,
|
||
ownerID: number,
|
||
atlasIdx: number,
|
||
flags: number,
|
||
): void {
|
||
this.missileBuf.ensureCapacity(this.missileCount + 1);
|
||
const off = this.missileCount * FLOATS_PER_INSTANCE;
|
||
this.missileBuf.float32[off + 0] = x;
|
||
this.missileBuf.float32[off + 1] = y;
|
||
this.missileBuf.float32[off + 2] = ownerID;
|
||
const byteOff = this.missileCount * BYTES_PER_INSTANCE;
|
||
this.missileBuf.uint8[byteOff + 12] = atlasIdx;
|
||
this.missileBuf.uint8[byteOff + 13] = flags;
|
||
this.missileCount++;
|
||
}
|
||
|
||
updateUnits(units: Map<number, UnitState>, tick: number): void {
|
||
this.frameTick = tick;
|
||
this.groundCount = 0;
|
||
this.missileCount = 0;
|
||
|
||
for (const unit of units.values()) {
|
||
if (!unit.isActive) continue;
|
||
|
||
let atlasIdx = this.typeToAtlasCol.get(unit.unitType);
|
||
|
||
// Train sub-type resolution: "Train" isn't in UNIT_ORDER.
|
||
// Resolve to engine/carriage/loaded carriage based on trainType + loaded fields.
|
||
if (atlasIdx === undefined && unit.unitType === UT_TRAIN) {
|
||
const tt = unit.trainType;
|
||
if (tt === TrainType.Engine || tt === TrainType.TailEngine) {
|
||
atlasIdx = TRAIN_ENGINE_COL;
|
||
} else {
|
||
atlasIdx = unit.loaded
|
||
? TRAIN_CARRIAGE_LOADED_COL
|
||
: TRAIN_CARRIAGE_COL;
|
||
}
|
||
}
|
||
|
||
if (atlasIdx === undefined) continue;
|
||
|
||
const isAngryWarship =
|
||
unit.unitType === UT_WARSHIP && unit.targetUnitId !== null;
|
||
const isFlicker = FLICKER_TYPES.has(unit.unitType);
|
||
|
||
// Enemy trade ships heading to a self/allied port get FLAG_TRADE_FRIENDLY
|
||
// so alt-view renders them yellow instead of red.
|
||
let isTradeFriendly = false;
|
||
if (
|
||
unit.unitType === UT_TRADE_SHIP &&
|
||
unit.targetUnitId !== null &&
|
||
this.localPlayerID > 0
|
||
) {
|
||
const targetPort = this.structures.get(unit.targetUnitId);
|
||
if (targetPort) {
|
||
const portOwner = targetPort.ownerID;
|
||
isTradeFriendly =
|
||
portOwner === this.localPlayerID ||
|
||
this.friendlyOwners.has(portOwner);
|
||
}
|
||
}
|
||
|
||
const flags = isTradeFriendly
|
||
? FLAG_TRADE_FRIENDLY
|
||
: isAngryWarship
|
||
? FLAG_ANGRY
|
||
: isFlicker
|
||
? FLAG_FLICKER
|
||
: FLAG_NORMAL;
|
||
const isMissile = MISSILE_TYPES.has(unit.unitType);
|
||
|
||
const x = unit.pos % this.mapW;
|
||
const y = (unit.pos - x) / this.mapW;
|
||
|
||
if (isMissile) {
|
||
this.emitMissile(x, y, unit.ownerID, atlasIdx, flags);
|
||
|
||
// Shells emit a second instance at lastPos (2-pixel trail effect)
|
||
if (unit.unitType === UT_SHELL && unit.lastPos !== unit.pos) {
|
||
const lx = unit.lastPos % this.mapW;
|
||
const ly = (unit.lastPos - lx) / this.mapW;
|
||
this.emitMissile(lx, ly, unit.ownerID, atlasIdx, flags);
|
||
}
|
||
} else {
|
||
this.emitGround(x, y, unit.ownerID, atlasIdx, flags);
|
||
}
|
||
}
|
||
|
||
const gl = this.gl;
|
||
if (this.groundCount > 0) {
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, this.groundBuf.buffer);
|
||
gl.bufferSubData(
|
||
gl.ARRAY_BUFFER,
|
||
0,
|
||
this.groundBuf.float32,
|
||
0,
|
||
this.groundCount * FLOATS_PER_INSTANCE,
|
||
);
|
||
}
|
||
if (this.missileCount > 0) {
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, this.missileBuf.buffer);
|
||
gl.bufferSubData(
|
||
gl.ARRAY_BUFFER,
|
||
0,
|
||
this.missileBuf.float32,
|
||
0,
|
||
this.missileCount * FLOATS_PER_INSTANCE,
|
||
);
|
||
}
|
||
}
|
||
|
||
setAltView(active: boolean): void {
|
||
this.altView = active;
|
||
}
|
||
setAffiliationTex(tex: WebGLTexture): void {
|
||
this.affiliationTex = tex;
|
||
}
|
||
setLocalPlayer(id: number): void {
|
||
this.localPlayerID = id;
|
||
}
|
||
setAllies(allies: Set<number>): void {
|
||
this.friendlyOwners = allies;
|
||
}
|
||
setStructures(structs: Map<number, UnitState>): void {
|
||
this.structures = structs;
|
||
}
|
||
|
||
/** Bind shared program state + uniforms (call before drawGround/drawMissiles). */
|
||
private bindProgram(cameraMatrix: Float32Array): void {
|
||
const gl = this.gl;
|
||
gl.useProgram(this.program);
|
||
|
||
const us = this.settings.unit;
|
||
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
|
||
gl.uniform1f(this.uTick, this.frameTick);
|
||
gl.uniform1f(this.uUnitSize, us.unitSize);
|
||
gl.uniform1f(this.uFlickerSpeed, us.flickerSpeed);
|
||
gl.uniform3f(this.uAngryColor, us.angryR, us.angryG, us.angryB);
|
||
gl.uniform1i(this.uAltView, this.altView ? 1 : 0);
|
||
|
||
gl.activeTexture(gl.TEXTURE0);
|
||
gl.bindTexture(gl.TEXTURE_2D, this.paletteTex);
|
||
|
||
gl.activeTexture(gl.TEXTURE1);
|
||
gl.bindTexture(gl.TEXTURE_2D, this.atlasTex);
|
||
|
||
if (this.affiliationTex) {
|
||
gl.activeTexture(gl.TEXTURE2);
|
||
gl.bindTexture(gl.TEXTURE_2D, this.affiliationTex);
|
||
}
|
||
}
|
||
|
||
/** Draw ground/sea units (boats, trains). Render below structures. */
|
||
drawGround(cameraMatrix: Float32Array): void {
|
||
if (this.groundCount === 0) return;
|
||
this.bindProgram(cameraMatrix);
|
||
const gl = this.gl;
|
||
gl.bindVertexArray(this.groundVao);
|
||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.groundCount);
|
||
}
|
||
|
||
/** Draw missiles/projectiles (nukes, shells, SAM, MIRV warheads). Render above structures. */
|
||
drawMissiles(cameraMatrix: Float32Array): void {
|
||
if (this.missileCount === 0) return;
|
||
this.bindProgram(cameraMatrix);
|
||
const gl = this.gl;
|
||
gl.bindVertexArray(this.missileVao);
|
||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.missileCount);
|
||
}
|
||
|
||
dispose(): void {
|
||
const gl = this.gl;
|
||
gl.deleteProgram(this.program);
|
||
this.groundBuf.dispose();
|
||
this.missileBuf.dispose();
|
||
gl.deleteBuffer(this.quadBuf);
|
||
gl.deleteVertexArray(this.groundVao);
|
||
gl.deleteVertexArray(this.missileVao);
|
||
gl.deleteTexture(this.atlasTex);
|
||
}
|
||
}
|