@quake2ts/engine 0.0.776 → 0.0.778

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5040,6 +5040,25 @@ function boxIntersectsFrustum(mins, maxs, planes) {
5040
5040
  }
5041
5041
  return true;
5042
5042
  }
5043
+ function transformAabb(mins, maxs, transform) {
5044
+ const cx = (mins.x + maxs.x) * 0.5;
5045
+ const cy = (mins.y + maxs.y) * 0.5;
5046
+ const cz = (mins.z + maxs.z) * 0.5;
5047
+ const ex = (maxs.x - mins.x) * 0.5;
5048
+ const ey = (maxs.y - mins.y) * 0.5;
5049
+ const ez = (maxs.z - mins.z) * 0.5;
5050
+ const m = transform;
5051
+ const tcx = m[0] * cx + m[4] * cy + m[8] * cz + m[12];
5052
+ const tcy = m[1] * cx + m[5] * cy + m[9] * cz + m[13];
5053
+ const tcz = m[2] * cx + m[6] * cy + m[10] * cz + m[14];
5054
+ const tex = Math.abs(m[0]) * ex + Math.abs(m[4]) * ey + Math.abs(m[8]) * ez;
5055
+ const tey = Math.abs(m[1]) * ex + Math.abs(m[5]) * ey + Math.abs(m[9]) * ez;
5056
+ const tez = Math.abs(m[2]) * ex + Math.abs(m[6]) * ey + Math.abs(m[10]) * ez;
5057
+ return {
5058
+ mins: { x: tcx - tex, y: tcy - tey, z: tcz - tez },
5059
+ maxs: { x: tcx + tex, y: tcy + tey, z: tcz + tez }
5060
+ };
5061
+ }
5043
5062
 
5044
5063
  // src/render/bspTraversal.ts
5045
5064
  function childIsLeaf(index) {
@@ -7383,6 +7402,562 @@ function spawnBfgExplosion(context) {
7383
7402
  }
7384
7403
  }
7385
7404
 
7405
+ // src/render/postprocessing/pipeline.ts
7406
+ var VERTEX_SOURCE = `#version 300 es
7407
+ layout(location = 0) in vec2 a_position;
7408
+
7409
+ out vec2 v_texCoord;
7410
+
7411
+ void main() {
7412
+ v_texCoord = a_position * 0.5 + 0.5;
7413
+ gl_Position = vec4(a_position, 0.0, 1.0);
7414
+ }
7415
+ `;
7416
+ var FRAGMENT_SOURCE = `#version 300 es
7417
+ precision mediump float;
7418
+
7419
+ in vec2 v_texCoord;
7420
+
7421
+ uniform sampler2D u_texture;
7422
+ uniform float u_time;
7423
+ uniform float u_strength; // Distortion strength
7424
+
7425
+ out vec4 o_color;
7426
+
7427
+ void main() {
7428
+ vec2 uv = v_texCoord;
7429
+
7430
+ // Simple sine wave distortion
7431
+ // Corresponds to gl_warp.c TURBSCALE somewhat
7432
+ float xOffset = sin(uv.y * 10.0 + u_time * 2.0) * 0.01 * u_strength;
7433
+ float yOffset = cos(uv.x * 10.0 + u_time * 2.0) * 0.01 * u_strength;
7434
+
7435
+ uv += vec2(xOffset, yOffset);
7436
+
7437
+ // Clamp UVs to avoid edge artifacts
7438
+ uv = clamp(uv, 0.001, 0.999);
7439
+
7440
+ o_color = texture(u_texture, uv);
7441
+ }
7442
+ `;
7443
+ var PostProcessPipeline = class {
7444
+ constructor(gl) {
7445
+ this.gl = gl;
7446
+ this.program = ShaderProgram.create(
7447
+ gl,
7448
+ { vertex: VERTEX_SOURCE, fragment: FRAGMENT_SOURCE },
7449
+ { a_position: 0 }
7450
+ );
7451
+ this.uTime = this.program.getUniformLocation("u_time");
7452
+ this.uStrength = this.program.getUniformLocation("u_strength");
7453
+ this.uTexture = this.program.getUniformLocation("u_texture");
7454
+ this.vao = gl.createVertexArray();
7455
+ gl.bindVertexArray(this.vao);
7456
+ const vertices = new Float32Array([
7457
+ -1,
7458
+ -1,
7459
+ 1,
7460
+ -1,
7461
+ -1,
7462
+ 1,
7463
+ 1,
7464
+ 1
7465
+ ]);
7466
+ const vbo = gl.createBuffer();
7467
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
7468
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
7469
+ gl.enableVertexAttribArray(0);
7470
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
7471
+ gl.bindVertexArray(null);
7472
+ }
7473
+ render(texture, time, strength = 1) {
7474
+ this.program.use();
7475
+ this.gl.activeTexture(this.gl.TEXTURE0);
7476
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
7477
+ this.gl.uniform1i(this.uTexture, 0);
7478
+ this.gl.uniform1f(this.uTime, time);
7479
+ this.gl.uniform1f(this.uStrength, strength);
7480
+ this.gl.bindVertexArray(this.vao);
7481
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
7482
+ this.gl.bindVertexArray(null);
7483
+ }
7484
+ dispose() {
7485
+ this.program.dispose();
7486
+ this.gl.deleteVertexArray(this.vao);
7487
+ }
7488
+ };
7489
+
7490
+ // src/render/bloom.ts
7491
+ var QUAD_VERTEX_SOURCE = `#version 300 es
7492
+ layout(location = 0) in vec2 a_position;
7493
+
7494
+ out vec2 v_texCoord;
7495
+
7496
+ void main() {
7497
+ v_texCoord = a_position * 0.5 + 0.5;
7498
+ gl_Position = vec4(a_position, 0.0, 1.0);
7499
+ }
7500
+ `;
7501
+ var EXTRACT_FRAGMENT_SOURCE = `#version 300 es
7502
+ precision mediump float;
7503
+
7504
+ in vec2 v_texCoord;
7505
+ uniform sampler2D u_texture;
7506
+ uniform float u_threshold;
7507
+
7508
+ out vec4 o_color;
7509
+
7510
+ void main() {
7511
+ vec4 color = texture(u_texture, v_texCoord);
7512
+ float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
7513
+ if (brightness > u_threshold) {
7514
+ o_color = vec4(color.rgb, 1.0);
7515
+ } else {
7516
+ o_color = vec4(0.0, 0.0, 0.0, 1.0);
7517
+ }
7518
+ }
7519
+ `;
7520
+ var BLUR_FRAGMENT_SOURCE = `#version 300 es
7521
+ precision mediump float;
7522
+
7523
+ in vec2 v_texCoord;
7524
+ uniform sampler2D u_texture;
7525
+ uniform bool u_horizontal;
7526
+ uniform float u_weight[5];
7527
+
7528
+ out vec4 o_color;
7529
+
7530
+ void main() {
7531
+ vec2 tex_offset = 1.0 / vec2(textureSize(u_texture, 0)); // gets size of single texel
7532
+ vec3 result = texture(u_texture, v_texCoord).rgb * u_weight[0];
7533
+
7534
+ if (u_horizontal) {
7535
+ for(int i = 1; i < 5; ++i) {
7536
+ result += texture(u_texture, v_texCoord + vec2(tex_offset.x * float(i), 0.0)).rgb * u_weight[i];
7537
+ result += texture(u_texture, v_texCoord - vec2(tex_offset.x * float(i), 0.0)).rgb * u_weight[i];
7538
+ }
7539
+ } else {
7540
+ for(int i = 1; i < 5; ++i) {
7541
+ result += texture(u_texture, v_texCoord + vec2(0.0, tex_offset.y * float(i))).rgb * u_weight[i];
7542
+ result += texture(u_texture, v_texCoord - vec2(0.0, tex_offset.y * float(i))).rgb * u_weight[i];
7543
+ }
7544
+ }
7545
+ o_color = vec4(result, 1.0);
7546
+ }
7547
+ `;
7548
+ var COMPOSITE_FRAGMENT_SOURCE = `#version 300 es
7549
+ precision mediump float;
7550
+
7551
+ in vec2 v_texCoord;
7552
+ uniform sampler2D u_texture;
7553
+ uniform float u_intensity;
7554
+
7555
+ out vec4 o_color;
7556
+
7557
+ void main() {
7558
+ vec4 color = texture(u_texture, v_texCoord);
7559
+ o_color = vec4(color.rgb * u_intensity, 1.0);
7560
+ }
7561
+ `;
7562
+ var BloomPipeline = class {
7563
+ constructor(gl) {
7564
+ this.width = 0;
7565
+ this.height = 0;
7566
+ this.gl = gl;
7567
+ this.extractProgram = ShaderProgram.create(gl, { vertex: QUAD_VERTEX_SOURCE, fragment: EXTRACT_FRAGMENT_SOURCE }, { a_position: 0 });
7568
+ this.blurProgram = ShaderProgram.create(gl, { vertex: QUAD_VERTEX_SOURCE, fragment: BLUR_FRAGMENT_SOURCE }, { a_position: 0 });
7569
+ this.compositeProgram = ShaderProgram.create(gl, { vertex: QUAD_VERTEX_SOURCE, fragment: COMPOSITE_FRAGMENT_SOURCE }, { a_position: 0 });
7570
+ this.framebuffer1 = new Framebuffer(gl);
7571
+ this.framebuffer2 = new Framebuffer(gl);
7572
+ this.texture1 = new Texture2D(gl);
7573
+ this.texture2 = new Texture2D(gl);
7574
+ this.texture1.setParameters({ minFilter: gl.LINEAR, magFilter: gl.LINEAR, wrapS: gl.CLAMP_TO_EDGE, wrapT: gl.CLAMP_TO_EDGE });
7575
+ this.texture2.setParameters({ minFilter: gl.LINEAR, magFilter: gl.LINEAR, wrapS: gl.CLAMP_TO_EDGE, wrapT: gl.CLAMP_TO_EDGE });
7576
+ this.vao = gl.createVertexArray();
7577
+ gl.bindVertexArray(this.vao);
7578
+ const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
7579
+ const vbo = gl.createBuffer();
7580
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
7581
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
7582
+ gl.enableVertexAttribArray(0);
7583
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
7584
+ gl.bindVertexArray(null);
7585
+ }
7586
+ resize(width, height) {
7587
+ if (this.width !== width || this.height !== height) {
7588
+ this.width = width;
7589
+ this.height = height;
7590
+ const w = Math.floor(width / 2) || 1;
7591
+ const h = Math.floor(height / 2) || 1;
7592
+ this.texture1.upload(w, h, null);
7593
+ this.framebuffer1.attachTexture2D(this.gl.COLOR_ATTACHMENT0, this.texture1);
7594
+ this.texture2.upload(w, h, null);
7595
+ this.framebuffer2.attachTexture2D(this.gl.COLOR_ATTACHMENT0, this.texture2);
7596
+ }
7597
+ }
7598
+ render(sceneTexture, intensity = 0.5) {
7599
+ const gl = this.gl;
7600
+ const w = Math.floor(this.width / 2) || 1;
7601
+ const h = Math.floor(this.height / 2) || 1;
7602
+ gl.bindVertexArray(this.vao);
7603
+ this.framebuffer1.bind();
7604
+ gl.viewport(0, 0, w, h);
7605
+ gl.clear(gl.COLOR_BUFFER_BIT);
7606
+ this.extractProgram.use();
7607
+ gl.activeTexture(gl.TEXTURE0);
7608
+ gl.bindTexture(gl.TEXTURE_2D, sceneTexture.texture);
7609
+ gl.uniform1i(this.extractProgram.getUniformLocation("u_texture"), 0);
7610
+ gl.uniform1f(this.extractProgram.getUniformLocation("u_threshold"), 0.7);
7611
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
7612
+ this.blurProgram.use();
7613
+ gl.uniform1fv(this.blurProgram.getUniformLocation("u_weight"), [0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216]);
7614
+ const locHorizontal = this.blurProgram.getUniformLocation("u_horizontal");
7615
+ const locTexture = this.blurProgram.getUniformLocation("u_texture");
7616
+ const amount = 4;
7617
+ let horizontal = true;
7618
+ for (let i = 0; i < amount; i++) {
7619
+ this.framebuffer2.bind();
7620
+ if (horizontal) {
7621
+ this.framebuffer2.bind();
7622
+ gl.bindTexture(gl.TEXTURE_2D, this.texture1.texture);
7623
+ } else {
7624
+ this.framebuffer1.bind();
7625
+ gl.bindTexture(gl.TEXTURE_2D, this.texture2.texture);
7626
+ }
7627
+ gl.uniform1i(locHorizontal, horizontal ? 1 : 0);
7628
+ gl.uniform1i(locTexture, 0);
7629
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
7630
+ horizontal = !horizontal;
7631
+ }
7632
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
7633
+ gl.viewport(0, 0, this.width, this.height);
7634
+ gl.enable(gl.BLEND);
7635
+ gl.blendFunc(gl.ONE, gl.ONE);
7636
+ gl.disable(gl.DEPTH_TEST);
7637
+ this.compositeProgram.use();
7638
+ gl.activeTexture(gl.TEXTURE0);
7639
+ gl.bindTexture(gl.TEXTURE_2D, this.texture1.texture);
7640
+ gl.uniform1i(this.compositeProgram.getUniformLocation("u_texture"), 0);
7641
+ gl.uniform1f(this.compositeProgram.getUniformLocation("u_intensity"), intensity);
7642
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
7643
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
7644
+ gl.enable(gl.DEPTH_TEST);
7645
+ gl.bindVertexArray(null);
7646
+ }
7647
+ dispose() {
7648
+ this.extractProgram.dispose();
7649
+ this.blurProgram.dispose();
7650
+ this.compositeProgram.dispose();
7651
+ this.framebuffer1.dispose();
7652
+ this.framebuffer2.dispose();
7653
+ this.texture1.dispose();
7654
+ this.texture2.dispose();
7655
+ this.gl.deleteVertexArray(this.vao);
7656
+ }
7657
+ };
7658
+
7659
+ // src/render/frame.ts
7660
+ var DEFAULT_DEPS = {
7661
+ gatherVisibleFaces,
7662
+ extractFrustumPlanes,
7663
+ computeSkyScroll,
7664
+ removeViewTranslation
7665
+ };
7666
+ function renderSky(skyboxPipeline, camera, timeSeconds, options, deps) {
7667
+ if (!options) {
7668
+ return;
7669
+ }
7670
+ const viewNoTranslation = deps.removeViewTranslation(camera.viewMatrix);
7671
+ const skyViewProjection = glMatrix.mat4.create();
7672
+ glMatrix.mat4.multiply(skyViewProjection, camera.projectionMatrix, viewNoTranslation);
7673
+ const scroll = deps.computeSkyScroll(timeSeconds, options.scrollSpeeds ?? [0.01, 0.02]);
7674
+ skyboxPipeline.bind({
7675
+ viewProjection: skyViewProjection,
7676
+ scroll,
7677
+ textureUnit: options.textureUnit ?? 0
7678
+ });
7679
+ skyboxPipeline.draw();
7680
+ skyboxPipeline.gl.depthMask(true);
7681
+ }
7682
+ function sortVisibleFacesFrontToBack(faces) {
7683
+ return [...faces].sort((a, b) => b.sortKey - a.sortKey);
7684
+ }
7685
+ function sortVisibleFacesBackToFront(faces) {
7686
+ return [...faces].sort((a, b) => a.sortKey - b.sortKey);
7687
+ }
7688
+ function resolveSurfaceTextures(geometry, world, refractionTexture) {
7689
+ const material = world?.materials?.getMaterial(geometry.texture);
7690
+ let diffuse;
7691
+ if (material) {
7692
+ const matTex = material.texture;
7693
+ if (matTex) {
7694
+ diffuse = matTex;
7695
+ }
7696
+ }
7697
+ if (!diffuse) {
7698
+ diffuse = world?.textures?.get(geometry.texture);
7699
+ }
7700
+ const lightmapIndex = geometry.lightmap?.atlasIndex;
7701
+ const lightmap = lightmapIndex !== void 0 ? world?.lightmaps?.[lightmapIndex]?.texture : void 0;
7702
+ return { diffuse, lightmap, refraction: refractionTexture };
7703
+ }
7704
+ function bindSurfaceTextures(geometry, world, cache, resolved) {
7705
+ const diffuse = resolved.diffuse;
7706
+ if (diffuse && cache.diffuse !== diffuse) {
7707
+ diffuse.bind(0);
7708
+ cache.diffuse = diffuse;
7709
+ }
7710
+ const lightmap = resolved.lightmap;
7711
+ if (lightmap && cache.lightmap !== lightmap) {
7712
+ lightmap.bind(1);
7713
+ cache.lightmap = lightmap;
7714
+ }
7715
+ if (!lightmap) {
7716
+ cache.lightmap = void 0;
7717
+ }
7718
+ const refraction = resolved.refraction;
7719
+ let refractionSampler;
7720
+ if (refraction && geometry.surfaceFlags & shared.SURF_WARP) {
7721
+ if (cache.refraction !== refraction) {
7722
+ refraction.bind(2);
7723
+ cache.refraction = refraction;
7724
+ }
7725
+ refractionSampler = 2;
7726
+ } else {
7727
+ cache.refraction = void 0;
7728
+ }
7729
+ return { diffuse: 0, lightmap: lightmap ? 1 : void 0, refraction: refractionSampler };
7730
+ }
7731
+ function renderViewModel(gl, camera, viewModel, removeTranslation) {
7732
+ if (!viewModel) {
7733
+ return false;
7734
+ }
7735
+ const projection = viewModel.fov ? camera.getViewmodelProjectionMatrix(viewModel.fov) : camera.projectionMatrix;
7736
+ const view = removeTranslation(camera.viewMatrix);
7737
+ const viewProjection = glMatrix.mat4.create();
7738
+ glMatrix.mat4.multiply(viewProjection, projection, view);
7739
+ if (viewModel.depthRange) {
7740
+ gl.depthRange(viewModel.depthRange[0], viewModel.depthRange[1]);
7741
+ }
7742
+ viewModel.draw(new Float32Array(viewProjection));
7743
+ if (viewModel.depthRange) {
7744
+ gl.depthRange(0, 1);
7745
+ }
7746
+ return true;
7747
+ }
7748
+ function evaluateLightStyle(pattern, time) {
7749
+ if (!pattern) return 1;
7750
+ const frame = Math.floor(time * 10) % pattern.length;
7751
+ const charCode = pattern.charCodeAt(frame);
7752
+ return (charCode - 97) / 12;
7753
+ }
7754
+ var createFrameRenderer = (gl, bspPipeline, skyboxPipeline, deps = DEFAULT_DEPS) => {
7755
+ const postProcess = new PostProcessPipeline(gl);
7756
+ const bloomPipeline = new BloomPipeline(gl);
7757
+ let lastFrameTime = 0;
7758
+ let copyTexture;
7759
+ let copyTextureWidth = 0;
7760
+ let copyTextureHeight = 0;
7761
+ const ensureCopyTexture = (width, height) => {
7762
+ if (!copyTexture || copyTextureWidth !== width || copyTextureHeight !== height) {
7763
+ copyTexture = new Texture2D(gl);
7764
+ copyTexture.upload(width, height, null);
7765
+ copyTexture.setParameters({
7766
+ minFilter: gl.LINEAR,
7767
+ magFilter: gl.LINEAR,
7768
+ wrapS: gl.CLAMP_TO_EDGE,
7769
+ wrapT: gl.CLAMP_TO_EDGE
7770
+ });
7771
+ copyTextureWidth = width;
7772
+ copyTextureHeight = height;
7773
+ }
7774
+ return copyTexture;
7775
+ };
7776
+ const renderFrame = (options) => {
7777
+ const now = performance.now();
7778
+ const fps = lastFrameTime > 0 ? 1e3 / (now - lastFrameTime) : 0;
7779
+ lastFrameTime = now;
7780
+ const stats = {
7781
+ batches: 0,
7782
+ facesDrawn: 0,
7783
+ drawCalls: 0,
7784
+ skyDrawn: false,
7785
+ viewModelDrawn: false,
7786
+ fps: Math.round(fps),
7787
+ vertexCount: 0
7788
+ };
7789
+ const {
7790
+ camera,
7791
+ world,
7792
+ sky,
7793
+ clearColor = [0, 0, 0, 1],
7794
+ timeSeconds = 0,
7795
+ viewModel,
7796
+ dlights,
7797
+ renderMode,
7798
+ disableLightmaps,
7799
+ lightmapOnly,
7800
+ brightness,
7801
+ gamma,
7802
+ fullbright,
7803
+ ambient,
7804
+ lightStyleOverrides,
7805
+ waterTint,
7806
+ underwaterWarp,
7807
+ bloom,
7808
+ bloomIntensity,
7809
+ portalState
7810
+ } = options;
7811
+ const viewProjection = new Float32Array(camera.viewProjectionMatrix);
7812
+ gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
7813
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
7814
+ renderSky(skyboxPipeline, camera, timeSeconds, sky, deps);
7815
+ stats.skyDrawn = Boolean(sky);
7816
+ if (world) {
7817
+ world.materials?.update(timeSeconds);
7818
+ const frustum = deps.extractFrustumPlanes(Array.from(viewProjection));
7819
+ const cameraPosition = {
7820
+ x: camera.position[0] ?? 0,
7821
+ y: camera.position[1] ?? 0,
7822
+ z: camera.position[2] ?? 0
7823
+ };
7824
+ const visibleFaces = deps.gatherVisibleFaces(world.map, cameraPosition, frustum, portalState);
7825
+ const opaqueFaces = [];
7826
+ const transparentFaces = [];
7827
+ for (const face of visibleFaces) {
7828
+ const geometry = world.surfaces[face.faceIndex];
7829
+ if (!geometry) continue;
7830
+ const isTransparent = (geometry.surfaceFlags & (shared.SURF_TRANS33 | shared.SURF_TRANS66 | shared.SURF_WARP)) !== 0;
7831
+ if (isTransparent) {
7832
+ transparentFaces.push(face);
7833
+ } else {
7834
+ opaqueFaces.push(face);
7835
+ }
7836
+ }
7837
+ const sortedOpaque = sortVisibleFacesFrontToBack(opaqueFaces);
7838
+ let effectiveLightStyles = world.lightStyles || [];
7839
+ if (lightStyleOverrides && lightStyleOverrides.size > 0) {
7840
+ const styles = [...world.lightStyles || []];
7841
+ for (const [index, pattern] of lightStyleOverrides) {
7842
+ while (styles.length <= index) styles.push(1);
7843
+ styles[index] = evaluateLightStyle(pattern, timeSeconds);
7844
+ }
7845
+ effectiveLightStyles = styles;
7846
+ }
7847
+ const drawSurfaceBatch = (faces, useRefraction) => {
7848
+ let lastBatchKey;
7849
+ let cachedState;
7850
+ const cache = {};
7851
+ const currentRefractionTexture = useRefraction ? copyTexture : void 0;
7852
+ for (const { faceIndex } of faces) {
7853
+ const geometry = world.surfaces[faceIndex];
7854
+ if (!geometry) continue;
7855
+ if ((geometry.surfaceFlags & shared.SURF_SKY) !== 0) continue;
7856
+ const faceStyles = world.map.faces[faceIndex]?.styles;
7857
+ const material = world.materials?.getMaterial(geometry.texture);
7858
+ const resolvedTextures = resolveSurfaceTextures(geometry, world, currentRefractionTexture);
7859
+ let activeRenderMode = renderMode;
7860
+ if (renderMode && !renderMode.applyToAll && resolvedTextures.diffuse) {
7861
+ activeRenderMode = void 0;
7862
+ } else if (renderMode && !renderMode.applyToAll && !resolvedTextures.diffuse) {
7863
+ activeRenderMode = renderMode;
7864
+ }
7865
+ let effectiveLightmap = resolvedTextures.lightmap;
7866
+ if (disableLightmaps) {
7867
+ effectiveLightmap = void 0;
7868
+ }
7869
+ const batchKey = {
7870
+ diffuse: resolvedTextures.diffuse,
7871
+ lightmap: effectiveLightmap,
7872
+ surfaceFlags: geometry.surfaceFlags,
7873
+ styleKey: faceStyles?.join(",") ?? ""
7874
+ };
7875
+ const isSameBatch = lastBatchKey && lastBatchKey.diffuse === batchKey.diffuse && lastBatchKey.lightmap === batchKey.lightmap && lastBatchKey.surfaceFlags === batchKey.surfaceFlags && lastBatchKey.styleKey === batchKey.styleKey;
7876
+ if (!isSameBatch) {
7877
+ stats.batches += 1;
7878
+ cache.diffuse = void 0;
7879
+ cache.lightmap = void 0;
7880
+ cache.refraction = void 0;
7881
+ const effectiveTextures = { ...resolvedTextures, lightmap: effectiveLightmap };
7882
+ const textures = bindSurfaceTextures(geometry, world, cache, effectiveTextures);
7883
+ const texScroll = material ? material.scrollOffset : void 0;
7884
+ const warp = material ? material.warp : void 0;
7885
+ cachedState = bspPipeline.bind({
7886
+ modelViewProjection: viewProjection,
7887
+ styleIndices: faceStyles,
7888
+ styleValues: effectiveLightStyles,
7889
+ surfaceFlags: geometry.surfaceFlags,
7890
+ timeSeconds,
7891
+ diffuseSampler: textures.diffuse,
7892
+ lightmapSampler: textures.lightmap,
7893
+ refractionSampler: textures.refraction,
7894
+ texScroll,
7895
+ warp,
7896
+ dlights,
7897
+ renderMode: activeRenderMode,
7898
+ lightmapOnly,
7899
+ brightness,
7900
+ gamma,
7901
+ fullbright,
7902
+ ambient
7903
+ });
7904
+ applySurfaceState(gl, cachedState);
7905
+ lastBatchKey = batchKey;
7906
+ } else {
7907
+ const effectiveTextures = { ...resolvedTextures, lightmap: effectiveLightmap };
7908
+ bindSurfaceTextures(geometry, world, cache, effectiveTextures);
7909
+ if (cachedState) {
7910
+ applySurfaceState(gl, cachedState);
7911
+ }
7912
+ }
7913
+ bspPipeline.draw(geometry, activeRenderMode);
7914
+ stats.facesDrawn += 1;
7915
+ stats.drawCalls += 1;
7916
+ stats.vertexCount += geometry.vertexCount;
7917
+ }
7918
+ };
7919
+ drawSurfaceBatch(sortedOpaque, false);
7920
+ if (transparentFaces.length > 0) {
7921
+ const width = gl.canvas.width;
7922
+ const height = gl.canvas.height;
7923
+ const rt = ensureCopyTexture(width, height);
7924
+ rt.bind(2);
7925
+ gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, width, height, 0);
7926
+ }
7927
+ const sortedTransparent = sortVisibleFacesBackToFront(transparentFaces);
7928
+ drawSurfaceBatch(sortedTransparent, true);
7929
+ }
7930
+ if (renderViewModel(gl, camera, viewModel, deps.removeViewTranslation)) {
7931
+ stats.viewModelDrawn = true;
7932
+ }
7933
+ if (underwaterWarp) {
7934
+ const width = gl.canvas.width;
7935
+ const height = gl.canvas.height;
7936
+ const rt = ensureCopyTexture(width, height);
7937
+ rt.bind(0);
7938
+ gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, width, height, 0);
7939
+ gl.disable(gl.DEPTH_TEST);
7940
+ gl.depthMask(false);
7941
+ postProcess.render(rt.texture, timeSeconds);
7942
+ gl.enable(gl.DEPTH_TEST);
7943
+ gl.depthMask(true);
7944
+ }
7945
+ if (bloom) {
7946
+ const width = gl.canvas.width;
7947
+ const height = gl.canvas.height;
7948
+ bloomPipeline.resize(width, height);
7949
+ const rt = ensureCopyTexture(width, height);
7950
+ rt.bind(0);
7951
+ gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, width, height, 0);
7952
+ bloomPipeline.render(rt, bloomIntensity ?? 0.5);
7953
+ }
7954
+ return stats;
7955
+ };
7956
+ return {
7957
+ renderFrame
7958
+ };
7959
+ };
7960
+
7386
7961
  // src/render/sprite.ts
7387
7962
  var SPRITE_VERTEX_SHADER = `#version 300 es
7388
7963
  precision highp float;
@@ -7474,6 +8049,1523 @@ var SpriteRenderer = class {
7474
8049
  this.draw(x, y, width, height, 0, 0, 1, 1, color);
7475
8050
  }
7476
8051
  };
8052
+
8053
+ // src/render/collisionVis.ts
8054
+ var LINE_VERTEX_SHADER = `#version 300 es
8055
+ precision highp float;
8056
+
8057
+ layout(location = 0) in vec3 a_position;
8058
+ layout(location = 1) in vec4 a_color;
8059
+
8060
+ uniform mat4 u_viewProjection;
8061
+
8062
+ out vec4 v_color;
8063
+
8064
+ void main() {
8065
+ v_color = a_color;
8066
+ gl_Position = u_viewProjection * vec4(a_position, 1.0);
8067
+ }`;
8068
+ var LINE_FRAGMENT_SHADER = `#version 300 es
8069
+ precision highp float;
8070
+
8071
+ in vec4 v_color;
8072
+ out vec4 o_color;
8073
+
8074
+ void main() {
8075
+ o_color = v_color;
8076
+ }`;
8077
+ var CollisionVisRenderer = class {
8078
+ constructor(gl) {
8079
+ this.vertices = [];
8080
+ this.gl = gl;
8081
+ this.program = ShaderProgram.create(
8082
+ gl,
8083
+ { vertex: LINE_VERTEX_SHADER, fragment: LINE_FRAGMENT_SHADER },
8084
+ { a_position: 0, a_color: 1 }
8085
+ );
8086
+ this.uniformViewProjection = this.program.getUniformLocation("u_viewProjection");
8087
+ this.vao = new VertexArray(gl);
8088
+ this.vbo = new VertexBuffer(gl, gl.STREAM_DRAW);
8089
+ this.vao.configureAttributes(
8090
+ [
8091
+ { index: 0, size: 3, type: gl.FLOAT, stride: 28, offset: 0 },
8092
+ { index: 1, size: 4, type: gl.FLOAT, stride: 28, offset: 12 }
8093
+ ],
8094
+ this.vbo
8095
+ );
8096
+ }
8097
+ get shaderSize() {
8098
+ return this.program.sourceSize;
8099
+ }
8100
+ addLine(start, end, color = { r: 1, g: 0, b: 0, a: 1 }) {
8101
+ this.vertices.push(
8102
+ start.x,
8103
+ start.y,
8104
+ start.z,
8105
+ color.r,
8106
+ color.g,
8107
+ color.b,
8108
+ color.a,
8109
+ end.x,
8110
+ end.y,
8111
+ end.z,
8112
+ color.r,
8113
+ color.g,
8114
+ color.b,
8115
+ color.a
8116
+ );
8117
+ }
8118
+ addBBox(mins, maxs, color = { r: 0, g: 1, b: 0, a: 1 }) {
8119
+ const p0 = { x: mins.x, y: mins.y, z: mins.z };
8120
+ const p1 = { x: maxs.x, y: mins.y, z: mins.z };
8121
+ const p2 = { x: maxs.x, y: maxs.y, z: mins.z };
8122
+ const p3 = { x: mins.x, y: maxs.y, z: mins.z };
8123
+ const p4 = { x: mins.x, y: mins.y, z: maxs.z };
8124
+ const p5 = { x: maxs.x, y: mins.y, z: maxs.z };
8125
+ const p6 = { x: maxs.x, y: maxs.y, z: maxs.z };
8126
+ const p7 = { x: mins.x, y: maxs.y, z: maxs.z };
8127
+ this.addLine(p0, p1, color);
8128
+ this.addLine(p1, p2, color);
8129
+ this.addLine(p2, p3, color);
8130
+ this.addLine(p3, p0, color);
8131
+ this.addLine(p4, p5, color);
8132
+ this.addLine(p5, p6, color);
8133
+ this.addLine(p6, p7, color);
8134
+ this.addLine(p7, p4, color);
8135
+ this.addLine(p0, p4, color);
8136
+ this.addLine(p1, p5, color);
8137
+ this.addLine(p2, p6, color);
8138
+ this.addLine(p3, p7, color);
8139
+ }
8140
+ clear() {
8141
+ this.vertices = [];
8142
+ }
8143
+ render(viewProjection) {
8144
+ if (this.vertices.length === 0) return;
8145
+ this.program.use();
8146
+ this.gl.uniformMatrix4fv(this.uniformViewProjection, false, viewProjection);
8147
+ this.vbo.upload(new Float32Array(this.vertices), this.gl.STREAM_DRAW);
8148
+ this.vao.bind();
8149
+ this.gl.drawArrays(this.gl.LINES, 0, this.vertices.length / 7);
8150
+ }
8151
+ };
8152
+
8153
+ // src/render/light.ts
8154
+ function findLeaf(map, point) {
8155
+ let nodeIndex = map.models[0].headNode;
8156
+ while (nodeIndex >= 0) {
8157
+ const node = map.nodes[nodeIndex];
8158
+ const plane = map.planes[node.planeIndex];
8159
+ const dist = plane.normal[0] * point.x + plane.normal[1] * point.y + plane.normal[2] * point.z - plane.dist;
8160
+ if (dist >= 0) {
8161
+ nodeIndex = node.children[0];
8162
+ } else {
8163
+ nodeIndex = node.children[1];
8164
+ }
8165
+ }
8166
+ const index = -(nodeIndex + 1);
8167
+ return { leaf: map.leafs[index], index };
8168
+ }
8169
+ function getMinLight(map) {
8170
+ if (map.entities.worldspawn?.properties["light"]) {
8171
+ const val = parseInt(map.entities.worldspawn.properties["light"], 10);
8172
+ if (!isNaN(val)) return val / 255;
8173
+ }
8174
+ if (map.entities.worldspawn?.properties["_minlight"]) {
8175
+ const val = parseInt(map.entities.worldspawn.properties["_minlight"], 10);
8176
+ if (!isNaN(val)) return val / 255;
8177
+ }
8178
+ return 0.2;
8179
+ }
8180
+ function sampleLightmap(map, faceIndex, u, v) {
8181
+ const face = map.faces[faceIndex];
8182
+ if (!face || face.lightOffset < 0) {
8183
+ return 0;
8184
+ }
8185
+ const info = map.lightMapInfo[faceIndex];
8186
+ if (!info) return 0;
8187
+ const texInfo = map.texInfo[face.texInfo];
8188
+ let minS = Infinity, maxS = -Infinity, minT = Infinity, maxT = -Infinity;
8189
+ let edgeIndex = face.firstEdge;
8190
+ for (let i = 0; i < face.numEdges; i++) {
8191
+ const eIndex = map.surfEdges[edgeIndex + i];
8192
+ const vIndex = eIndex >= 0 ? map.edges[eIndex].vertices[0] : map.edges[Math.abs(eIndex)].vertices[1];
8193
+ const vert = map.vertices[vIndex];
8194
+ const s = vert[0] * texInfo.s[0] + vert[1] * texInfo.s[1] + vert[2] * texInfo.s[2] + texInfo.sOffset;
8195
+ const t = vert[0] * texInfo.t[0] + vert[1] * texInfo.t[1] + vert[2] * texInfo.t[2] + texInfo.tOffset;
8196
+ if (s < minS) minS = s;
8197
+ if (s > maxS) maxS = s;
8198
+ if (t < minT) minT = t;
8199
+ if (t > maxT) maxT = t;
8200
+ }
8201
+ const floorMinS = Math.floor(minS / 16);
8202
+ const floorMinT = Math.floor(minT / 16);
8203
+ const lmWidth = Math.ceil(maxS / 16) - floorMinS + 1;
8204
+ const lmHeight = Math.ceil(maxT / 16) - floorMinT + 1;
8205
+ const ls = u / 16 - floorMinS;
8206
+ const lt = v / 16 - floorMinT;
8207
+ const x = Math.max(0, Math.min(lmWidth - 1, Math.floor(ls)));
8208
+ const y = Math.max(0, Math.min(lmHeight - 1, Math.floor(lt)));
8209
+ const offset = face.lightOffset + (y * lmWidth + x) * 3;
8210
+ if (offset + 2 >= map.lightMaps.length) return 0;
8211
+ const r = map.lightMaps[offset];
8212
+ const g = map.lightMaps[offset + 1];
8213
+ const b = map.lightMaps[offset + 2];
8214
+ return Math.max(r, g, b) / 255;
8215
+ }
8216
+ function calculateEntityLight(map, origin) {
8217
+ if (!map) return 1;
8218
+ const minLight = getMinLight(map);
8219
+ const { leaf, index: leafIndex } = findLeaf(map, origin);
8220
+ if (leaf.contents & 1) {
8221
+ return minLight;
8222
+ }
8223
+ const leafFaces = map.leafLists.leafFaces[leafIndex];
8224
+ if (leafFaces) {
8225
+ for (const faceIndex of leafFaces) {
8226
+ const face = map.faces[faceIndex];
8227
+ const plane = map.planes[face.planeIndex];
8228
+ const side = face.side;
8229
+ const nx = side ? -plane.normal[0] : plane.normal[0];
8230
+ const ny = side ? -plane.normal[1] : plane.normal[1];
8231
+ const nz = side ? -plane.normal[2] : plane.normal[2];
8232
+ const dist = side ? -plane.dist : plane.dist;
8233
+ if (nz > 0.7) {
8234
+ const d = nx * origin.x + ny * origin.y + nz * origin.z - dist;
8235
+ if (d >= 0 && d < 64) {
8236
+ const texInfo = map.texInfo[face.texInfo];
8237
+ const s = origin.x * texInfo.s[0] + origin.y * texInfo.s[1] + origin.z * texInfo.s[2] + texInfo.sOffset;
8238
+ const t = origin.x * texInfo.t[0] + origin.y * texInfo.t[1] + origin.z * texInfo.t[2] + texInfo.tOffset;
8239
+ const light = sampleLightmap(map, faceIndex, s, t);
8240
+ if (light > minLight) {
8241
+ return light;
8242
+ }
8243
+ }
8244
+ }
8245
+ }
8246
+ }
8247
+ return Math.max(minLight, 0.2);
8248
+ }
8249
+
8250
+ // src/render/gpuProfiler.ts
8251
+ var GpuProfiler = class {
8252
+ constructor(gl) {
8253
+ this.activeQueries = [];
8254
+ this.queryPool = [];
8255
+ this.currentQuery = null;
8256
+ this.lastGpuTimeMs = 0;
8257
+ // CPU-side counters
8258
+ this.frameStartTime = 0;
8259
+ this.lastCpuFrameTimeMs = 0;
8260
+ // Resource Tracking
8261
+ this.textureMemoryBytes = 0;
8262
+ this.bufferMemoryBytes = 0;
8263
+ this.shaderMemoryBytes = 0;
8264
+ this.gl = gl;
8265
+ this.ext = gl.getExtension("EXT_disjoint_timer_query_webgl2");
8266
+ }
8267
+ get available() {
8268
+ return !!this.ext;
8269
+ }
8270
+ // To be called by Renderer to construct the final report
8271
+ getPerformanceReport(frameStats) {
8272
+ const textureMB = this.textureMemoryBytes / (1024 * 1024);
8273
+ const geometryMB = this.bufferMemoryBytes / (1024 * 1024);
8274
+ return {
8275
+ frameTimeMs: this.lastCpuFrameTimeMs,
8276
+ // Main frame time (CPU)
8277
+ gpuTimeMs: this.lastGpuTimeMs,
8278
+ cpuFrameTimeMs: this.lastCpuFrameTimeMs,
8279
+ drawCalls: frameStats.drawCalls,
8280
+ triangles: Math.floor(frameStats.vertexCount / 3),
8281
+ // Approximation if mostly triangles
8282
+ vertices: frameStats.vertexCount,
8283
+ textureBinds: frameStats.batches,
8284
+ // Using batches as proxy for texture binds
8285
+ shaderSwitches: frameStats.shaderSwitches,
8286
+ visibleSurfaces: frameStats.visibleSurfaces,
8287
+ culledSurfaces: frameStats.culledSurfaces,
8288
+ visibleEntities: frameStats.visibleEntities,
8289
+ culledEntities: frameStats.culledEntities,
8290
+ memoryUsageMB: {
8291
+ textures: textureMB,
8292
+ geometry: geometryMB,
8293
+ total: textureMB + geometryMB
8294
+ }
8295
+ };
8296
+ }
8297
+ startFrame() {
8298
+ this.frameStartTime = performance.now();
8299
+ if (!this.ext) return;
8300
+ if (this.currentQuery) {
8301
+ return;
8302
+ }
8303
+ const query = this.getQuery();
8304
+ if (query) {
8305
+ this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query);
8306
+ this.currentQuery = query;
8307
+ }
8308
+ }
8309
+ endFrame() {
8310
+ this.lastCpuFrameTimeMs = performance.now() - this.frameStartTime;
8311
+ if (!this.ext || !this.currentQuery) return;
8312
+ this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
8313
+ this.activeQueries.push(this.currentQuery);
8314
+ this.currentQuery = null;
8315
+ this.pollQueries();
8316
+ }
8317
+ trackTextureMemory(bytes) {
8318
+ this.textureMemoryBytes += bytes;
8319
+ }
8320
+ trackBufferMemory(bytes) {
8321
+ this.bufferMemoryBytes += bytes;
8322
+ }
8323
+ trackShaderMemory(bytes) {
8324
+ this.shaderMemoryBytes += bytes;
8325
+ }
8326
+ getMemoryUsage() {
8327
+ const texturesBytes = this.textureMemoryBytes;
8328
+ const geometryBytes = this.bufferMemoryBytes;
8329
+ const shadersBytes = this.shaderMemoryBytes;
8330
+ const buffersBytes = this.bufferMemoryBytes;
8331
+ const totalBytes = texturesBytes + geometryBytes + shadersBytes;
8332
+ return {
8333
+ texturesBytes,
8334
+ geometryBytes,
8335
+ shadersBytes,
8336
+ buffersBytes,
8337
+ totalBytes
8338
+ };
8339
+ }
8340
+ getQuery() {
8341
+ if (this.queryPool.length > 0) {
8342
+ return this.queryPool.pop();
8343
+ }
8344
+ return this.gl.createQuery();
8345
+ }
8346
+ pollQueries() {
8347
+ if (this.activeQueries.length === 0) return;
8348
+ const query = this.activeQueries[0];
8349
+ const available = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE);
8350
+ const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT);
8351
+ if (available) {
8352
+ if (disjoint) {
8353
+ this.activeQueries.forEach((q) => this.queryPool.push(q));
8354
+ this.activeQueries.length = 0;
8355
+ } else {
8356
+ const timeElapsedNs = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT);
8357
+ this.lastGpuTimeMs = timeElapsedNs / 1e6;
8358
+ this.activeQueries.shift();
8359
+ this.queryPool.push(query);
8360
+ }
8361
+ }
8362
+ }
8363
+ dispose() {
8364
+ [...this.activeQueries, ...this.queryPool].forEach((q) => this.gl.deleteQuery(q));
8365
+ this.activeQueries.length = 0;
8366
+ this.queryPool.length = 0;
8367
+ }
8368
+ };
8369
+
8370
+ // src/render/colors.ts
8371
+ var QUAKE2_COLORS = {
8372
+ "1": [1, 0, 0, 1],
8373
+ // Red
8374
+ "2": [0, 1, 0, 1],
8375
+ // Green
8376
+ "3": [1, 1, 0, 1],
8377
+ // Yellow
8378
+ "4": [0, 0, 1, 1],
8379
+ // Blue
8380
+ "5": [0, 1, 1, 1],
8381
+ // Cyan
8382
+ "6": [1, 0, 1, 1],
8383
+ // Magenta
8384
+ "7": [1, 1, 1, 1],
8385
+ // White
8386
+ "0": [0, 0, 0, 1]
8387
+ // Black (or default?)
8388
+ };
8389
+ function parseColorString(text) {
8390
+ const segments = [];
8391
+ let currentText = "";
8392
+ let currentColor = void 0;
8393
+ for (let i = 0; i < text.length; i++) {
8394
+ if (text[i] === "^" && i + 1 < text.length) {
8395
+ const code = text[i + 1];
8396
+ if (QUAKE2_COLORS[code]) {
8397
+ if (currentText.length > 0) {
8398
+ segments.push({ text: currentText, color: currentColor });
8399
+ currentText = "";
8400
+ }
8401
+ currentColor = QUAKE2_COLORS[code];
8402
+ i++;
8403
+ continue;
8404
+ }
8405
+ }
8406
+ currentText += text[i];
8407
+ }
8408
+ if (currentText.length > 0) {
8409
+ segments.push({ text: currentText, color: currentColor });
8410
+ }
8411
+ return segments;
8412
+ }
8413
+ var VS_SOURCE = `
8414
+ attribute vec3 a_position;
8415
+ attribute vec3 a_color;
8416
+ uniform mat4 u_viewProjection;
8417
+ varying vec3 v_color;
8418
+
8419
+ void main() {
8420
+ gl_Position = u_viewProjection * vec4(a_position, 1.0);
8421
+ v_color = a_color;
8422
+ }
8423
+ `;
8424
+ var FS_SOURCE = `
8425
+ precision mediump float;
8426
+ varying vec3 v_color;
8427
+
8428
+ void main() {
8429
+ gl_FragColor = vec4(v_color, 1.0);
8430
+ }
8431
+ `;
8432
+ var VS_SOLID = `
8433
+ attribute vec3 a_position;
8434
+ attribute vec3 a_color;
8435
+ attribute vec3 a_normal;
8436
+
8437
+ uniform mat4 u_viewProjection;
8438
+ uniform bool u_lightingEnabled;
8439
+
8440
+ varying vec3 v_color;
8441
+ varying vec3 v_normal;
8442
+
8443
+ void main() {
8444
+ gl_Position = u_viewProjection * vec4(a_position, 1.0);
8445
+ v_color = a_color;
8446
+ v_normal = a_normal;
8447
+ }
8448
+ `;
8449
+ var FS_SOLID = `
8450
+ precision mediump float;
8451
+ varying vec3 v_color;
8452
+ varying vec3 v_normal;
8453
+
8454
+ uniform bool u_lightingEnabled;
8455
+
8456
+ void main() {
8457
+ vec3 color = v_color;
8458
+
8459
+ if (u_lightingEnabled) {
8460
+ // Simple directional light from top-left
8461
+ vec3 lightDir = normalize(vec3(0.5, 0.7, 1.0));
8462
+ float diff = max(dot(v_normal, lightDir), 0.3); // 0.3 ambient
8463
+ color = color * diff;
8464
+ }
8465
+
8466
+ gl_FragColor = vec4(color, 1.0);
8467
+ }
8468
+ `;
8469
+ function toGlVec3(v) {
8470
+ return glMatrix.vec3.fromValues(v.x, v.y, v.z);
8471
+ }
8472
+ function fromGlVec3(v) {
8473
+ return { x: v[0], y: v[1], z: v[2] };
8474
+ }
8475
+ var DebugRenderer = class {
8476
+ constructor(gl) {
8477
+ this.vertices = [];
8478
+ this.solidVertices = [];
8479
+ this.labels = [];
8480
+ this.gl = gl;
8481
+ this.shader = ShaderProgram.create(gl, { vertex: VS_SOURCE, fragment: FS_SOURCE });
8482
+ this.vao = new VertexArray(gl);
8483
+ this.vbo = new VertexBuffer(gl, gl.DYNAMIC_DRAW);
8484
+ this.vao.configureAttributes([
8485
+ { index: 0, size: 3, type: gl.FLOAT, stride: 24, offset: 0 },
8486
+ // position
8487
+ { index: 1, size: 3, type: gl.FLOAT, stride: 24, offset: 12 }
8488
+ // color
8489
+ ], this.vbo);
8490
+ this.shaderSolid = ShaderProgram.create(gl, { vertex: VS_SOLID, fragment: FS_SOLID });
8491
+ this.vaoSolid = new VertexArray(gl);
8492
+ this.vboSolid = new VertexBuffer(gl, gl.DYNAMIC_DRAW);
8493
+ this.vaoSolid.configureAttributes([
8494
+ { index: 0, size: 3, type: gl.FLOAT, stride: 36, offset: 0 },
8495
+ // position
8496
+ { index: 1, size: 3, type: gl.FLOAT, stride: 36, offset: 12 },
8497
+ // color
8498
+ { index: 2, size: 3, type: gl.FLOAT, stride: 36, offset: 24 }
8499
+ // normal
8500
+ ], this.vboSolid);
8501
+ }
8502
+ get shaderSize() {
8503
+ return this.shader.sourceSize + this.shaderSolid.sourceSize;
8504
+ }
8505
+ drawLine(start, end, color) {
8506
+ this.vertices.push(
8507
+ start.x,
8508
+ start.y,
8509
+ start.z,
8510
+ color.r,
8511
+ color.g,
8512
+ color.b,
8513
+ end.x,
8514
+ end.y,
8515
+ end.z,
8516
+ color.r,
8517
+ color.g,
8518
+ color.b
8519
+ );
8520
+ }
8521
+ drawBoundingBox(mins, maxs, color) {
8522
+ const { x: x1, y: y1, z: z1 } = mins;
8523
+ const { x: x2, y: y2, z: z2 } = maxs;
8524
+ this.drawLine({ x: x1, y: y1, z: z1 }, { x: x2, y: y1, z: z1 }, color);
8525
+ this.drawLine({ x: x2, y: y1, z: z1 }, { x: x2, y: y2, z: z1 }, color);
8526
+ this.drawLine({ x: x2, y: y2, z: z1 }, { x: x1, y: y2, z: z1 }, color);
8527
+ this.drawLine({ x: x1, y: y2, z: z1 }, { x: x1, y: y1, z: z1 }, color);
8528
+ this.drawLine({ x: x1, y: y1, z: z2 }, { x: x2, y: y1, z: z2 }, color);
8529
+ this.drawLine({ x: x2, y: y1, z: z2 }, { x: x2, y: y2, z: z2 }, color);
8530
+ this.drawLine({ x: x2, y: y2, z: z2 }, { x: x1, y: y2, z: z2 }, color);
8531
+ this.drawLine({ x: x1, y: y2, z: z2 }, { x: x1, y: y1, z: z2 }, color);
8532
+ this.drawLine({ x: x1, y: y1, z: z1 }, { x: x1, y: y1, z: z2 }, color);
8533
+ this.drawLine({ x: x2, y: y1, z: z1 }, { x: x2, y: y1, z: z2 }, color);
8534
+ this.drawLine({ x: x2, y: y2, z: z1 }, { x: x2, y: y2, z: z2 }, color);
8535
+ this.drawLine({ x: x1, y: y2, z: z1 }, { x: x1, y: y2, z: z2 }, color);
8536
+ }
8537
+ drawPoint(position, size, color) {
8538
+ const s = size / 2;
8539
+ this.drawBoundingBox(
8540
+ { x: position.x - s, y: position.y - s, z: position.z - s },
8541
+ { x: position.x + s, y: position.y + s, z: position.z + s },
8542
+ color
8543
+ );
8544
+ }
8545
+ drawAxes(position, size) {
8546
+ this.drawLine(position, { x: position.x + size, y: position.y, z: position.z }, { r: 1, g: 0, b: 0 });
8547
+ this.drawLine(position, { x: position.x, y: position.y + size, z: position.z }, { r: 0, g: 1, b: 0 });
8548
+ this.drawLine(position, { x: position.x, y: position.y, z: position.z + size }, { r: 0, g: 0, b: 1 });
8549
+ }
8550
+ drawText3D(text, position) {
8551
+ this.labels.push({ text, position });
8552
+ }
8553
+ addTriangle(v1, v2, v3, normal, color) {
8554
+ this.solidVertices.push(v1.x, v1.y, v1.z);
8555
+ this.solidVertices.push(color.r, color.g, color.b);
8556
+ this.solidVertices.push(normal.x, normal.y, normal.z);
8557
+ this.solidVertices.push(v2.x, v2.y, v2.z);
8558
+ this.solidVertices.push(color.r, color.g, color.b);
8559
+ this.solidVertices.push(normal.x, normal.y, normal.z);
8560
+ this.solidVertices.push(v3.x, v3.y, v3.z);
8561
+ this.solidVertices.push(color.r, color.g, color.b);
8562
+ this.solidVertices.push(normal.x, normal.y, normal.z);
8563
+ }
8564
+ addCone(apex, baseCenter, baseRadius, color) {
8565
+ const segments = 16;
8566
+ const apexGl = toGlVec3(apex);
8567
+ const baseCenterGl = toGlVec3(baseCenter);
8568
+ const axis = glMatrix.vec3.create();
8569
+ glMatrix.vec3.subtract(axis, apexGl, baseCenterGl);
8570
+ glMatrix.vec3.normalize(axis, axis);
8571
+ let up = glMatrix.vec3.fromValues(0, 0, 1);
8572
+ if (Math.abs(glMatrix.vec3.dot(axis, up)) > 0.99) {
8573
+ up = glMatrix.vec3.fromValues(0, 1, 0);
8574
+ }
8575
+ const right = glMatrix.vec3.create();
8576
+ glMatrix.vec3.cross(right, up, axis);
8577
+ glMatrix.vec3.normalize(right, right);
8578
+ const forward = glMatrix.vec3.create();
8579
+ glMatrix.vec3.cross(forward, axis, right);
8580
+ const basePoints = [];
8581
+ for (let i = 0; i < segments; i++) {
8582
+ const angle = i / segments * Math.PI * 2;
8583
+ const x = Math.cos(angle) * baseRadius;
8584
+ const y = Math.sin(angle) * baseRadius;
8585
+ const point = glMatrix.vec3.clone(baseCenterGl);
8586
+ glMatrix.vec3.scaleAndAdd(point, point, right, x);
8587
+ glMatrix.vec3.scaleAndAdd(point, point, forward, y);
8588
+ basePoints.push(point);
8589
+ }
8590
+ const baseNormal = glMatrix.vec3.clone(axis);
8591
+ glMatrix.vec3.scale(baseNormal, baseNormal, -1);
8592
+ const baseNormalVec3 = fromGlVec3(baseNormal);
8593
+ const center = baseCenter;
8594
+ for (let i = 0; i < segments; i++) {
8595
+ const p1 = fromGlVec3(basePoints[i]);
8596
+ const p2 = fromGlVec3(basePoints[(i + 1) % segments]);
8597
+ this.addTriangle(center, p2, p1, baseNormalVec3, color);
8598
+ }
8599
+ for (let i = 0; i < segments; i++) {
8600
+ const p1Gl = basePoints[i];
8601
+ const p2Gl = basePoints[(i + 1) % segments];
8602
+ const p1 = fromGlVec3(p1Gl);
8603
+ const p2 = fromGlVec3(p2Gl);
8604
+ const v1 = glMatrix.vec3.create();
8605
+ glMatrix.vec3.subtract(v1, p2Gl, p1Gl);
8606
+ const v2 = glMatrix.vec3.create();
8607
+ glMatrix.vec3.subtract(v2, apexGl, p1Gl);
8608
+ const normal = glMatrix.vec3.create();
8609
+ glMatrix.vec3.cross(normal, v1, v2);
8610
+ glMatrix.vec3.normalize(normal, normal);
8611
+ const normalVec3 = fromGlVec3(normal);
8612
+ this.addTriangle(p1, p2, apex, normalVec3, color);
8613
+ }
8614
+ }
8615
+ addTorus(center, radius, tubeRadius, color, axis = { x: 0, y: 0, z: 1 }) {
8616
+ const segments = 16;
8617
+ const tubeSegments = 8;
8618
+ const axisVec = toGlVec3(axis);
8619
+ glMatrix.vec3.normalize(axisVec, axisVec);
8620
+ const centerGl = toGlVec3(center);
8621
+ const zAxis = glMatrix.vec3.fromValues(0, 0, 1);
8622
+ const rotation = glMatrix.mat4.create();
8623
+ if (Math.abs(glMatrix.vec3.dot(axisVec, zAxis)) > 0.999) {
8624
+ if (axisVec[2] < 0) {
8625
+ glMatrix.mat4.rotateX(rotation, rotation, Math.PI);
8626
+ }
8627
+ } else {
8628
+ const rotAxis = glMatrix.vec3.create();
8629
+ glMatrix.vec3.cross(rotAxis, zAxis, axisVec);
8630
+ glMatrix.vec3.normalize(rotAxis, rotAxis);
8631
+ const angle = Math.acos(glMatrix.vec3.dot(zAxis, axisVec));
8632
+ glMatrix.mat4.fromRotation(rotation, angle, rotAxis);
8633
+ }
8634
+ const vertices = [];
8635
+ for (let i = 0; i <= segments; i++) {
8636
+ const u = i / segments * Math.PI * 2;
8637
+ const cosU = Math.cos(u);
8638
+ const sinU = Math.sin(u);
8639
+ const ringVerts = [];
8640
+ const ringNorms = [];
8641
+ for (let j = 0; j <= tubeSegments; j++) {
8642
+ const v = j / tubeSegments * Math.PI * 2;
8643
+ const cosV = Math.cos(v);
8644
+ const sinV = Math.sin(v);
8645
+ const cx = (radius + tubeRadius * cosV) * cosU;
8646
+ const cy = (radius + tubeRadius * cosV) * sinU;
8647
+ const cz = tubeRadius * sinV;
8648
+ const pt = glMatrix.vec3.fromValues(cx, cy, cz);
8649
+ const nx = cosV * cosU;
8650
+ const ny = cosV * sinU;
8651
+ const nz = sinV;
8652
+ const n = glMatrix.vec3.fromValues(nx, ny, nz);
8653
+ glMatrix.vec3.transformMat4(pt, pt, rotation);
8654
+ glMatrix.vec3.transformMat4(n, n, rotation);
8655
+ glMatrix.vec3.add(pt, pt, centerGl);
8656
+ ringVerts.push(pt);
8657
+ ringNorms.push(n);
8658
+ }
8659
+ vertices.push(ringVerts);
8660
+ }
8661
+ for (let i = 0; i < segments; i++) {
8662
+ for (let j = 0; j < tubeSegments; j++) {
8663
+ const p1 = vertices[i][j];
8664
+ const p2 = vertices[i + 1][j];
8665
+ const p3 = vertices[i + 1][j + 1];
8666
+ const p4 = vertices[i][j + 1];
8667
+ const p1V = fromGlVec3(p1);
8668
+ const p2V = fromGlVec3(p2);
8669
+ const p3V = fromGlVec3(p3);
8670
+ const p4V = fromGlVec3(p4);
8671
+ const v1 = glMatrix.vec3.create();
8672
+ glMatrix.vec3.subtract(v1, p2, p1);
8673
+ const v2 = glMatrix.vec3.create();
8674
+ glMatrix.vec3.subtract(v2, p3, p1);
8675
+ const faceN1 = glMatrix.vec3.create();
8676
+ glMatrix.vec3.cross(faceN1, v1, v2);
8677
+ glMatrix.vec3.normalize(faceN1, faceN1);
8678
+ this.addTriangle(p1V, p2V, p3V, fromGlVec3(faceN1), color);
8679
+ const v3 = glMatrix.vec3.create();
8680
+ glMatrix.vec3.subtract(v3, p3, p1);
8681
+ const v4 = glMatrix.vec3.create();
8682
+ glMatrix.vec3.subtract(v4, p4, p1);
8683
+ const faceN2 = glMatrix.vec3.create();
8684
+ glMatrix.vec3.cross(faceN2, v3, v4);
8685
+ glMatrix.vec3.normalize(faceN2, faceN2);
8686
+ this.addTriangle(p1V, p3V, p4V, fromGlVec3(faceN2), color);
8687
+ }
8688
+ }
8689
+ }
8690
+ // Updated render method
8691
+ render(viewProjection, alwaysOnTop = false) {
8692
+ if (alwaysOnTop) {
8693
+ this.gl.disable(this.gl.DEPTH_TEST);
8694
+ }
8695
+ if (this.vertices.length > 0) {
8696
+ this.shader.use();
8697
+ const loc = this.shader.getUniformLocation("u_viewProjection");
8698
+ if (loc) {
8699
+ this.gl.uniformMatrix4fv(loc, false, viewProjection);
8700
+ }
8701
+ this.vbo.upload(new Float32Array(this.vertices));
8702
+ this.vao.bind();
8703
+ this.gl.drawArrays(this.gl.LINES, 0, this.vertices.length / 6);
8704
+ }
8705
+ if (this.solidVertices.length > 0) {
8706
+ this.shaderSolid.use();
8707
+ const loc = this.shaderSolid.getUniformLocation("u_viewProjection");
8708
+ if (loc) {
8709
+ this.gl.uniformMatrix4fv(loc, false, viewProjection);
8710
+ }
8711
+ const locLight = this.shaderSolid.getUniformLocation("u_lightingEnabled");
8712
+ if (locLight) {
8713
+ this.gl.uniform1i(locLight, 1);
8714
+ }
8715
+ this.vboSolid.upload(new Float32Array(this.solidVertices));
8716
+ this.vaoSolid.bind();
8717
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, this.solidVertices.length / 9);
8718
+ }
8719
+ if (alwaysOnTop) {
8720
+ this.gl.enable(this.gl.DEPTH_TEST);
8721
+ }
8722
+ }
8723
+ // New method to retrieve 2D projected labels for external rendering (e.g. by Renderer using drawString)
8724
+ // Returns list of { text, x, y } where x,y are canvas coords
8725
+ getLabels(viewProjection, width, height) {
8726
+ const results = [];
8727
+ for (const label of this.labels) {
8728
+ const v = glMatrix.vec4.fromValues(label.position.x, label.position.y, label.position.z, 1);
8729
+ const clip = glMatrix.vec4.create();
8730
+ const vp = viewProjection;
8731
+ const m = vp;
8732
+ const x = v[0], y = v[1], z = v[2], w = v[3];
8733
+ clip[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
8734
+ clip[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
8735
+ clip[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
8736
+ clip[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
8737
+ if (clip[3] > 0) {
8738
+ const ndcX = clip[0] / clip[3];
8739
+ const ndcY = clip[1] / clip[3];
8740
+ const screenX = (ndcX + 1) * 0.5 * width;
8741
+ const screenY = (1 - ndcY) * 0.5 * height;
8742
+ if (screenX >= 0 && screenX <= width && screenY >= 0 && screenY <= height) {
8743
+ results.push({ text: label.text, x: screenX, y: screenY });
8744
+ }
8745
+ }
8746
+ }
8747
+ return results;
8748
+ }
8749
+ clear() {
8750
+ this.vertices = [];
8751
+ this.solidVertices = [];
8752
+ this.labels = [];
8753
+ }
8754
+ };
8755
+
8756
+ // src/render/lightCulling.ts
8757
+ function cullLights(lights, planes, cameraPosition, maxLights = 32) {
8758
+ const visibleLights = [];
8759
+ for (const light of lights) {
8760
+ let visible = true;
8761
+ for (const plane of planes) {
8762
+ const dist = plane.normal.x * light.origin.x + plane.normal.y * light.origin.y + plane.normal.z * light.origin.z + plane.distance;
8763
+ if (dist < -light.intensity) {
8764
+ visible = false;
8765
+ break;
8766
+ }
8767
+ }
8768
+ if (visible) {
8769
+ let distSq = 0;
8770
+ if (cameraPosition) {
8771
+ const dx = light.origin.x - cameraPosition.x;
8772
+ const dy = light.origin.y - cameraPosition.y;
8773
+ const dz = light.origin.z - cameraPosition.z;
8774
+ distSq = dx * dx + dy * dy + dz * dz;
8775
+ }
8776
+ visibleLights.push({ light, distSq });
8777
+ }
8778
+ }
8779
+ if (cameraPosition) {
8780
+ visibleLights.sort((a, b) => a.distSq - b.distSq);
8781
+ }
8782
+ const result = [];
8783
+ const count = Math.min(visibleLights.length, maxLights);
8784
+ for (let i = 0; i < count; i++) {
8785
+ result.push(visibleLights[i].light);
8786
+ }
8787
+ return result;
8788
+ }
8789
+
8790
+ // src/render/renderer.ts
8791
+ function colorFromId(id) {
8792
+ const r = (id * 1664525 + 1013904223 >>> 0) / 4294967296;
8793
+ const g = (id * 25214903917 + 11 >>> 0) / 4294967296;
8794
+ const b = (id * 69069 + 1 >>> 0) / 4294967296;
8795
+ return [r, g, b, 1];
8796
+ }
8797
+ var createRenderer = (gl) => {
8798
+ const bspPipeline = new BspSurfacePipeline(gl);
8799
+ const skyboxPipeline = new SkyboxPipeline(gl);
8800
+ const md2Pipeline = new Md2Pipeline(gl);
8801
+ const md3Pipeline = new Md3Pipeline(gl);
8802
+ const spriteRenderer = new SpriteRenderer(gl);
8803
+ const collisionVis = new CollisionVisRenderer(gl);
8804
+ const debugRenderer = new DebugRenderer(gl);
8805
+ const gpuProfiler = new GpuProfiler(gl);
8806
+ const particleRng = new shared.RandomGenerator({ seed: Date.now() });
8807
+ const particleSystem = new ParticleSystem(4096, particleRng);
8808
+ const particleRenderer = new ParticleRenderer(gl, particleSystem);
8809
+ const shaderBytes = bspPipeline.shaderSize + skyboxPipeline.shaderSize + md2Pipeline.shaderSize + md3Pipeline.shaderSize + spriteRenderer.shaderSize + collisionVis.shaderSize + debugRenderer.shaderSize + particleRenderer.shaderSize;
8810
+ gpuProfiler.trackShaderMemory(shaderBytes);
8811
+ const md3MeshCache = /* @__PURE__ */ new Map();
8812
+ const md2MeshCache = /* @__PURE__ */ new Map();
8813
+ const picCache = /* @__PURE__ */ new Map();
8814
+ const entityLeafCache = /* @__PURE__ */ new Map();
8815
+ let frameCounter = 0;
8816
+ const CACHE_CLEANUP_INTERVAL = 600;
8817
+ const CACHE_MAX_AGE = 300;
8818
+ let font = null;
8819
+ let lastFrameStats = {
8820
+ drawCalls: 0,
8821
+ vertexCount: 0,
8822
+ batches: 0,
8823
+ shaderSwitches: 0,
8824
+ visibleSurfaces: 0,
8825
+ culledSurfaces: 0,
8826
+ visibleEntities: 0,
8827
+ culledEntities: 0
8828
+ };
8829
+ const highlightedEntities = /* @__PURE__ */ new Map();
8830
+ const highlightedSurfaces = /* @__PURE__ */ new Map();
8831
+ let debugMode = 0 /* None */;
8832
+ let brightness = 1;
8833
+ let gamma = 1;
8834
+ let fullbright = false;
8835
+ let ambient = 0;
8836
+ const lightStyleOverrides = /* @__PURE__ */ new Map();
8837
+ let underwaterWarp = false;
8838
+ let bloom = false;
8839
+ let bloomIntensity = 0.5;
8840
+ let lodBias = 1;
8841
+ const portalState = new Array(1024).fill(true);
8842
+ const frameRenderer = createFrameRenderer(gl, bspPipeline, skyboxPipeline);
8843
+ const MAX_INSTANCES = 1e4;
8844
+ const instancePool = [];
8845
+ let instanceCount = 0;
8846
+ const tempQuat = glMatrix.quat.create();
8847
+ const tempVec3Pos = glMatrix.vec3.create();
8848
+ const tempVec3Scale = glMatrix.vec3.create();
8849
+ for (let i = 0; i < MAX_INSTANCES; i++) {
8850
+ const entity = {
8851
+ id: -1,
8852
+ model: void 0,
8853
+ transform: new Float32Array(16),
8854
+ type: "md3",
8855
+ blend: { frame0: 0, frame1: 0, lerp: 0 },
8856
+ tint: [1, 1, 1, 1],
8857
+ lighting: {
8858
+ ambient: [0.5, 0.5, 0.5],
8859
+ dynamicLights: [],
8860
+ modelMatrix: void 0
8861
+ }
8862
+ };
8863
+ if (entity.lighting) {
8864
+ entity.lighting.modelMatrix = entity.transform;
8865
+ }
8866
+ instancePool.push(entity);
8867
+ }
8868
+ const queuedInstances = [];
8869
+ let begin2D;
8870
+ let end2D;
8871
+ let drawfillRect;
8872
+ const selectLod = (entity, cameraPos) => {
8873
+ if (entity.type === "md2") {
8874
+ const model = entity.model;
8875
+ if (!model.lods || model.lods.length === 0) {
8876
+ return { model: entity.model, type: entity.type };
8877
+ }
8878
+ const dx = entity.transform[12] - cameraPos.x;
8879
+ const dy = entity.transform[13] - cameraPos.y;
8880
+ const dz = entity.transform[14] - cameraPos.z;
8881
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
8882
+ const lodIndex = Math.floor(distance * lodBias / 500);
8883
+ if (lodIndex <= 0) {
8884
+ return { model: entity.model, type: entity.type };
8885
+ }
8886
+ const availableLods = model.lods;
8887
+ const selectedLodIndex = Math.min(lodIndex - 1, availableLods.length - 1);
8888
+ return { model: availableLods[selectedLodIndex], type: entity.type };
8889
+ }
8890
+ return { model: entity.model, type: entity.type };
8891
+ };
8892
+ const renderFrame = (options, entities, renderOptions) => {
8893
+ gpuProfiler.startFrame();
8894
+ frameCounter++;
8895
+ const allEntities = queuedInstances.length > 0 ? [...entities, ...queuedInstances] : entities;
8896
+ queuedInstances.length = 0;
8897
+ instanceCount = 0;
8898
+ if (options.deltaTime) {
8899
+ particleSystem.update(options.deltaTime);
8900
+ }
8901
+ gl.disable(gl.BLEND);
8902
+ gl.enable(gl.DEPTH_TEST);
8903
+ gl.depthMask(true);
8904
+ const currentRenderMode = options.renderMode;
8905
+ let effectiveRenderMode = currentRenderMode;
8906
+ let lightmapOnly = false;
8907
+ if (renderOptions?.wireframe || debugMode === 6 /* Wireframe */) {
8908
+ effectiveRenderMode = {
8909
+ mode: "wireframe",
8910
+ applyToAll: true,
8911
+ color: [1, 1, 1, 1]
8912
+ };
8913
+ } else if (debugMode === 5 /* Lightmaps */) {
8914
+ lightmapOnly = true;
8915
+ }
8916
+ let effectiveSky = options.sky;
8917
+ if (renderOptions?.showSkybox === false) {
8918
+ effectiveSky = void 0;
8919
+ }
8920
+ const viewProjection = new Float32Array(options.camera.viewProjectionMatrix);
8921
+ const frustumPlanes = extractFrustumPlanes(viewProjection);
8922
+ const culledLights = options.dlights ? cullLights(
8923
+ options.dlights,
8924
+ frustumPlanes,
8925
+ { x: options.camera.position[0], y: options.camera.position[1], z: options.camera.position[2] },
8926
+ 32
8927
+ ) : void 0;
8928
+ const augmentedOptions = {
8929
+ ...options,
8930
+ sky: effectiveSky,
8931
+ renderMode: effectiveRenderMode,
8932
+ disableLightmaps: renderOptions?.showLightmaps === false && debugMode !== 5 /* Lightmaps */,
8933
+ dlights: culledLights,
8934
+ lightmapOnly,
8935
+ brightness,
8936
+ gamma,
8937
+ fullbright,
8938
+ ambient,
8939
+ lightStyleOverrides,
8940
+ underwaterWarp,
8941
+ bloom,
8942
+ bloomIntensity,
8943
+ portalState
8944
+ // Pass it here.
8945
+ };
8946
+ const stats = frameRenderer.renderFrame(augmentedOptions);
8947
+ let visibleSurfaces = stats.facesDrawn || 0;
8948
+ let totalSurfaces = 0;
8949
+ if (options.world && options.world.map && options.world.map.faces) {
8950
+ totalSurfaces = options.world.map.faces.length;
8951
+ }
8952
+ let culledSurfaces = totalSurfaces - visibleSurfaces;
8953
+ let viewCluster = -1;
8954
+ let viewArea = -1;
8955
+ let reachableAreas = null;
8956
+ if (options.world && renderOptions?.cullingEnabled !== false) {
8957
+ const cameraPosition = {
8958
+ x: options.camera.position[0],
8959
+ y: options.camera.position[1],
8960
+ z: options.camera.position[2]
8961
+ };
8962
+ const viewLeafIndex = findLeafForPoint(options.world.map, cameraPosition);
8963
+ if (viewLeafIndex >= 0) {
8964
+ const leaf = options.world.map.leafs[viewLeafIndex];
8965
+ viewCluster = leaf.cluster;
8966
+ viewArea = leaf.area;
8967
+ if (viewArea >= 0 && options.world.map.areas.length > 0) {
8968
+ reachableAreas = calculateReachableAreas(options.world.map, viewArea, portalState);
8969
+ }
8970
+ }
8971
+ }
8972
+ let lastTexture;
8973
+ let entityDrawCalls = 0;
8974
+ let entityVertices = 0;
8975
+ let visibleEntities = 0;
8976
+ let culledEntities = 0;
8977
+ const cameraPos = {
8978
+ x: options.camera.position[0],
8979
+ y: options.camera.position[1],
8980
+ z: options.camera.position[2]
8981
+ };
8982
+ if (frameCounter % CACHE_CLEANUP_INTERVAL === 0) {
8983
+ for (const [id, entry] of entityLeafCache) {
8984
+ if (frameCounter - entry.lastFrameSeen > CACHE_MAX_AGE) {
8985
+ entityLeafCache.delete(id);
8986
+ }
8987
+ }
8988
+ }
8989
+ for (const entity of allEntities) {
8990
+ if (options.world && viewCluster >= 0) {
8991
+ const origin = {
8992
+ x: entity.transform[12],
8993
+ y: entity.transform[13],
8994
+ z: entity.transform[14]
8995
+ };
8996
+ let entityLeafIndex = -1;
8997
+ if (entity.id !== void 0) {
8998
+ const cached = entityLeafCache.get(entity.id);
8999
+ if (cached && cached.position[12] === entity.transform[12] && cached.position[13] === entity.transform[13] && cached.position[14] === entity.transform[14]) {
9000
+ entityLeafIndex = cached.leafIndex;
9001
+ cached.lastFrameSeen = frameCounter;
9002
+ } else {
9003
+ entityLeafIndex = findLeafForPoint(options.world.map, origin);
9004
+ if (entityLeafIndex >= 0) {
9005
+ entityLeafCache.set(entity.id, {
9006
+ leafIndex: entityLeafIndex,
9007
+ position: new Float32Array(entity.transform),
9008
+ // Clone
9009
+ lastFrameSeen: frameCounter
9010
+ });
9011
+ }
9012
+ }
9013
+ } else {
9014
+ entityLeafIndex = findLeafForPoint(options.world.map, origin);
9015
+ }
9016
+ if (entityLeafIndex >= 0) {
9017
+ const leaf = options.world.map.leafs[entityLeafIndex];
9018
+ const entityCluster = leaf.cluster;
9019
+ const entityArea = leaf.area;
9020
+ if (reachableAreas && entityArea >= 0 && !reachableAreas.has(entityArea)) {
9021
+ culledEntities++;
9022
+ continue;
9023
+ }
9024
+ if (!isClusterVisible(options.world.map.visibility, viewCluster, entityCluster)) {
9025
+ culledEntities++;
9026
+ continue;
9027
+ }
9028
+ }
9029
+ }
9030
+ const { model: activeModel, type: activeType } = selectLod(entity, cameraPos);
9031
+ let minBounds;
9032
+ let maxBounds;
9033
+ if (activeType === "md2") {
9034
+ const md2Model = activeModel;
9035
+ const frame0 = md2Model.frames[entity.blend.frame0 % md2Model.frames.length];
9036
+ const frame1 = md2Model.frames[entity.blend.frame1 % md2Model.frames.length];
9037
+ if (frame0 && frame1) {
9038
+ minBounds = {
9039
+ x: Math.min(frame0.minBounds.x, frame1.minBounds.x),
9040
+ y: Math.min(frame0.minBounds.y, frame1.minBounds.y),
9041
+ z: Math.min(frame0.minBounds.z, frame1.minBounds.z)
9042
+ };
9043
+ maxBounds = {
9044
+ x: Math.max(frame0.maxBounds.x, frame1.maxBounds.x),
9045
+ y: Math.max(frame0.maxBounds.y, frame1.maxBounds.y),
9046
+ z: Math.max(frame0.maxBounds.z, frame1.maxBounds.z)
9047
+ };
9048
+ }
9049
+ } else if (activeType === "md3") {
9050
+ const md3Model = activeModel;
9051
+ const frame0 = md3Model.frames[entity.blend.frame0 % md3Model.frames.length];
9052
+ const frame1 = md3Model.frames[entity.blend.frame1 % md3Model.frames.length];
9053
+ if (frame0 && frame1) {
9054
+ minBounds = {
9055
+ x: Math.min(frame0.minBounds.x, frame1.minBounds.x),
9056
+ y: Math.min(frame0.minBounds.y, frame1.minBounds.y),
9057
+ z: Math.min(frame0.minBounds.z, frame1.minBounds.z)
9058
+ };
9059
+ maxBounds = {
9060
+ x: Math.max(frame0.maxBounds.x, frame1.maxBounds.x),
9061
+ y: Math.max(frame0.maxBounds.y, frame1.maxBounds.y),
9062
+ z: Math.max(frame0.maxBounds.z, frame1.maxBounds.z)
9063
+ };
9064
+ } else {
9065
+ minBounds = { x: -32, y: -32, z: -32 };
9066
+ maxBounds = { x: 32, y: 32, z: 32 };
9067
+ }
9068
+ }
9069
+ if (minBounds && maxBounds) {
9070
+ const worldBounds = transformAabb(minBounds, maxBounds, entity.transform);
9071
+ if (!boxIntersectsFrustum(worldBounds.mins, worldBounds.maxs, frustumPlanes)) {
9072
+ culledEntities++;
9073
+ continue;
9074
+ }
9075
+ }
9076
+ visibleEntities++;
9077
+ const position = {
9078
+ x: entity.transform[12],
9079
+ y: entity.transform[13],
9080
+ z: entity.transform[14]
9081
+ };
9082
+ const light = calculateEntityLight(options.world?.map, position);
9083
+ const highlightColor = entity.id !== void 0 ? highlightedEntities.get(entity.id) : void 0;
9084
+ switch (activeType) {
9085
+ case "md2":
9086
+ {
9087
+ const md2Model = activeModel;
9088
+ let mesh = md2MeshCache.get(md2Model);
9089
+ if (!mesh) {
9090
+ mesh = new Md2MeshBuffers(gl, md2Model, entity.blend);
9091
+ md2MeshCache.set(md2Model, mesh);
9092
+ const vertexBytes = mesh.geometry.vertices.length * 8 * 4;
9093
+ const indexBytes = mesh.geometry.indices.length * 2;
9094
+ gpuProfiler.trackBufferMemory(vertexBytes + indexBytes);
9095
+ } else {
9096
+ mesh.update(md2Model, entity.blend);
9097
+ }
9098
+ const modelViewProjection = shared.multiplyMat4(viewProjection, entity.transform);
9099
+ const skinName = entity.skin;
9100
+ const texture = skinName ? options.world?.textures?.get(skinName) : void 0;
9101
+ if (texture && texture !== lastTexture) {
9102
+ texture.bind(0);
9103
+ lastTexture = texture;
9104
+ }
9105
+ let activeRenderMode = effectiveRenderMode;
9106
+ if (activeRenderMode && !activeRenderMode.applyToAll && texture) {
9107
+ activeRenderMode = void 0;
9108
+ }
9109
+ if (activeRenderMode?.generateRandomColor && entity.id !== void 0) {
9110
+ const randColor = colorFromId(entity.id);
9111
+ activeRenderMode = { ...activeRenderMode, color: randColor };
9112
+ }
9113
+ md2Pipeline.bind({
9114
+ modelViewProjection,
9115
+ modelMatrix: entity.transform,
9116
+ ambientLight: light,
9117
+ dlights: options.dlights,
9118
+ renderMode: activeRenderMode,
9119
+ tint: entity.tint,
9120
+ brightness,
9121
+ gamma,
9122
+ fullbright,
9123
+ ambient
9124
+ });
9125
+ md2Pipeline.draw(mesh, activeRenderMode);
9126
+ entityDrawCalls++;
9127
+ entityVertices += mesh.geometry.vertices.length;
9128
+ if (highlightColor) {
9129
+ const highlightMode = {
9130
+ mode: "wireframe",
9131
+ applyToAll: true,
9132
+ color: highlightColor
9133
+ };
9134
+ md2Pipeline.bind({
9135
+ modelViewProjection,
9136
+ modelMatrix: entity.transform,
9137
+ ambientLight: 1,
9138
+ renderMode: highlightMode,
9139
+ tint: [1, 1, 1, 1],
9140
+ brightness: 1,
9141
+ gamma: 1,
9142
+ fullbright: true,
9143
+ ambient: 0
9144
+ });
9145
+ md2Pipeline.draw(mesh, highlightMode);
9146
+ entityDrawCalls++;
9147
+ }
9148
+ }
9149
+ break;
9150
+ case "md3":
9151
+ {
9152
+ const md3Model = activeModel;
9153
+ let mesh = md3MeshCache.get(md3Model);
9154
+ const md3Entity = entity;
9155
+ const md3Dlights = options.dlights ? options.dlights.map((d) => ({
9156
+ origin: d.origin,
9157
+ color: [d.color.x, d.color.y, d.color.z],
9158
+ radius: d.intensity
9159
+ })) : void 0;
9160
+ const lighting = {
9161
+ ...md3Entity.lighting,
9162
+ ambient: [light, light, light],
9163
+ dynamicLights: md3Dlights,
9164
+ modelMatrix: entity.transform
9165
+ };
9166
+ if (!mesh) {
9167
+ mesh = new Md3ModelMesh(gl, md3Model, entity.blend, lighting);
9168
+ md3MeshCache.set(md3Model, mesh);
9169
+ } else {
9170
+ mesh.update(entity.blend, lighting);
9171
+ }
9172
+ const modelViewProjection = shared.multiplyMat4(viewProjection, entity.transform);
9173
+ md3Pipeline.bind(modelViewProjection);
9174
+ for (const surface of md3Model.surfaces) {
9175
+ const surfaceMesh = mesh.surfaces.get(surface.name);
9176
+ if (surfaceMesh) {
9177
+ const textureName = md3Entity.skins?.get(surface.name);
9178
+ const texture = textureName ? options.world?.textures?.get(textureName) : void 0;
9179
+ if (texture && texture !== lastTexture) {
9180
+ texture.bind(0);
9181
+ lastTexture = texture;
9182
+ }
9183
+ let activeRenderMode = effectiveRenderMode;
9184
+ if (activeRenderMode && !activeRenderMode.applyToAll && texture) {
9185
+ activeRenderMode = void 0;
9186
+ }
9187
+ if (activeRenderMode?.generateRandomColor && entity.id !== void 0) {
9188
+ const randColor = colorFromId(entity.id);
9189
+ activeRenderMode = { ...activeRenderMode, color: randColor };
9190
+ }
9191
+ const material = {
9192
+ renderMode: activeRenderMode,
9193
+ brightness,
9194
+ gamma,
9195
+ fullbright,
9196
+ globalAmbient: ambient
9197
+ };
9198
+ md3Pipeline.drawSurface(surfaceMesh, material);
9199
+ entityDrawCalls++;
9200
+ entityVertices += surfaceMesh.geometry.vertices.length;
9201
+ if (highlightColor) {
9202
+ const highlightMode = {
9203
+ mode: "wireframe",
9204
+ applyToAll: true,
9205
+ color: highlightColor
9206
+ };
9207
+ const highlightMaterial = {
9208
+ renderMode: highlightMode,
9209
+ brightness: 1,
9210
+ gamma: 1,
9211
+ fullbright: true,
9212
+ globalAmbient: 0
9213
+ };
9214
+ md3Pipeline.drawSurface(surfaceMesh, highlightMaterial);
9215
+ entityDrawCalls++;
9216
+ }
9217
+ }
9218
+ }
9219
+ }
9220
+ break;
9221
+ }
9222
+ }
9223
+ const viewMatrix = options.camera.viewMatrix;
9224
+ if (viewMatrix) {
9225
+ const viewRight = { x: viewMatrix[0], y: viewMatrix[4], z: viewMatrix[8] };
9226
+ const viewUp = { x: viewMatrix[1], y: viewMatrix[5], z: viewMatrix[9] };
9227
+ particleRenderer.render({
9228
+ viewProjection,
9229
+ viewRight,
9230
+ viewUp
9231
+ });
9232
+ }
9233
+ if (augmentedOptions.waterTint) {
9234
+ begin2D();
9235
+ drawfillRect(0, 0, gl.canvas.width, gl.canvas.height, augmentedOptions.waterTint);
9236
+ end2D();
9237
+ }
9238
+ collisionVis.render(viewProjection);
9239
+ collisionVis.clear();
9240
+ if (options.world && (highlightedSurfaces.size > 0 || debugMode === 3 /* PVSClusters */)) {
9241
+ const surfacesToDraw = new Map(highlightedSurfaces);
9242
+ if (debugMode === 3 /* PVSClusters */ && options.world) {
9243
+ const frustum = extractFrustumPlanes(viewProjection);
9244
+ const cameraPosition = {
9245
+ x: options.camera.position[0],
9246
+ y: options.camera.position[1],
9247
+ z: options.camera.position[2]
9248
+ };
9249
+ const visibleFaces = gatherVisibleFaces(options.world.map, cameraPosition, frustum, portalState);
9250
+ for (const { faceIndex, leafIndex } of visibleFaces) {
9251
+ const leaf = options.world.map.leafs[leafIndex];
9252
+ if (leaf && !surfacesToDraw.has(faceIndex)) {
9253
+ surfacesToDraw.set(faceIndex, colorFromId(leaf.cluster));
9254
+ }
9255
+ }
9256
+ }
9257
+ for (const [faceIndex, color] of surfacesToDraw) {
9258
+ const geometry = options.world.surfaces[faceIndex];
9259
+ if (geometry && geometry.vertexCount > 0) {
9260
+ const vertices = [];
9261
+ const stride = 7;
9262
+ for (let i = 0; i < geometry.vertexCount; i++) {
9263
+ vertices.push({
9264
+ x: geometry.vertexData[i * stride],
9265
+ y: geometry.vertexData[i * stride + 1],
9266
+ z: geometry.vertexData[i * stride + 2]
9267
+ });
9268
+ }
9269
+ const c = { r: color[0], g: color[1], b: color[2] };
9270
+ for (let i = 0; i < vertices.length; i++) {
9271
+ const p0 = vertices[i];
9272
+ const p1 = vertices[(i + 1) % vertices.length];
9273
+ debugRenderer.drawLine(p0, p1, c);
9274
+ }
9275
+ debugRenderer.drawLine(vertices[0], vertices[vertices.length / 2 | 0], c);
9276
+ }
9277
+ }
9278
+ }
9279
+ if ((renderOptions?.showNormals || debugMode === 2 /* Normals */) && options.world) {
9280
+ const frustum = extractFrustumPlanes(viewProjection);
9281
+ const cameraPosition = {
9282
+ x: options.camera.position[0],
9283
+ y: options.camera.position[1],
9284
+ z: options.camera.position[2]
9285
+ };
9286
+ const visibleFaces = gatherVisibleFaces(options.world.map, cameraPosition, frustum, portalState);
9287
+ for (const { faceIndex } of visibleFaces) {
9288
+ const face = options.world.map.faces[faceIndex];
9289
+ const plane = options.world.map.planes[face.planeIndex];
9290
+ const geometry = options.world.surfaces[faceIndex];
9291
+ if (!geometry) continue;
9292
+ let cx = 0, cy = 0, cz = 0;
9293
+ const count = geometry.vertexCount;
9294
+ for (let i = 0; i < count; i++) {
9295
+ const idx = i * 7;
9296
+ cx += geometry.vertexData[idx];
9297
+ cy += geometry.vertexData[idx + 1];
9298
+ cz += geometry.vertexData[idx + 2];
9299
+ }
9300
+ if (count > 0) {
9301
+ cx /= count;
9302
+ cy /= count;
9303
+ cz /= count;
9304
+ const nx = face.side === 0 ? plane.normal[0] : -plane.normal[0];
9305
+ const ny = face.side === 0 ? plane.normal[1] : -plane.normal[1];
9306
+ const nz = face.side === 0 ? plane.normal[2] : -plane.normal[2];
9307
+ const center = { x: cx, y: cy, z: cz };
9308
+ const end = { x: cx + nx * 8, y: cy + ny * 8, z: cz + nz * 8 };
9309
+ debugRenderer.drawLine(center, end, { r: 1, g: 1, b: 0 });
9310
+ }
9311
+ }
9312
+ }
9313
+ debugRenderer.render(viewProjection);
9314
+ const labels = debugRenderer.getLabels(viewProjection, gl.canvas.width, gl.canvas.height);
9315
+ if (labels.length > 0) {
9316
+ begin2D();
9317
+ for (const label of labels) {
9318
+ drawString(label.x, label.y, label.text, [1, 1, 1, 1]);
9319
+ }
9320
+ end2D();
9321
+ }
9322
+ debugRenderer.clear();
9323
+ lastFrameStats = {
9324
+ drawCalls: stats.drawCalls + entityDrawCalls,
9325
+ vertexCount: stats.vertexCount + entityVertices,
9326
+ batches: stats.batches,
9327
+ shaderSwitches: 0,
9328
+ visibleSurfaces,
9329
+ culledSurfaces,
9330
+ visibleEntities,
9331
+ culledEntities
9332
+ };
9333
+ gpuProfiler.endFrame();
9334
+ };
9335
+ const registerPic = async (name, data) => {
9336
+ if (picCache.has(name)) {
9337
+ return picCache.get(name);
9338
+ }
9339
+ const blob = new Blob([data]);
9340
+ const imageBitmap = await createImageBitmap(blob);
9341
+ const texture = new Texture2D(gl);
9342
+ texture.upload(imageBitmap.width, imageBitmap.height, imageBitmap);
9343
+ picCache.set(name, texture);
9344
+ gpuProfiler.trackTextureMemory(imageBitmap.width * imageBitmap.height * 4);
9345
+ if (name.includes("conchars")) {
9346
+ font = texture;
9347
+ }
9348
+ return texture;
9349
+ };
9350
+ const registerTexture = (name, texture) => {
9351
+ if (picCache.has(name)) {
9352
+ return picCache.get(name);
9353
+ }
9354
+ const tex = new Texture2D(gl);
9355
+ const level = texture.levels[0];
9356
+ tex.upload(level.width, level.height, level.rgba);
9357
+ picCache.set(name, tex);
9358
+ gpuProfiler.trackTextureMemory(level.width * level.height * 4);
9359
+ if (name.includes("conchars")) {
9360
+ font = tex;
9361
+ }
9362
+ return tex;
9363
+ };
9364
+ begin2D = () => {
9365
+ gl.enable(gl.BLEND);
9366
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
9367
+ gl.disable(gl.DEPTH_TEST);
9368
+ gl.depthMask(false);
9369
+ const projection = glMatrix.mat4.create();
9370
+ glMatrix.mat4.ortho(projection, 0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
9371
+ spriteRenderer.begin(projection);
9372
+ };
9373
+ end2D = () => {
9374
+ gl.disable(gl.BLEND);
9375
+ gl.enable(gl.DEPTH_TEST);
9376
+ gl.depthMask(true);
9377
+ };
9378
+ const drawPic = (x, y, pic, color) => {
9379
+ pic.bind(0);
9380
+ spriteRenderer.draw(x, y, pic.width, pic.height, 0, 0, 1, 1, color);
9381
+ };
9382
+ const drawChar = (x, y, char, color) => {
9383
+ if (!font) {
9384
+ return;
9385
+ }
9386
+ const charWidth = 8;
9387
+ const charHeight = 8;
9388
+ const numCols = font.width / charWidth;
9389
+ const charIndex = char & 255;
9390
+ const u0 = charIndex % numCols * charWidth / font.width;
9391
+ const v0 = Math.floor(charIndex / numCols) * charHeight / font.height;
9392
+ const u1 = u0 + charWidth / font.width;
9393
+ const v1 = v0 + charHeight / font.height;
9394
+ font.bind(0);
9395
+ spriteRenderer.draw(x, y, charWidth, charHeight, u0, v0, u1, v1, color);
9396
+ };
9397
+ const drawString = (x, y, text, color) => {
9398
+ const segments = parseColorString(text);
9399
+ let currentX = x;
9400
+ const charWidth = 8;
9401
+ for (const segment of segments) {
9402
+ const segmentColor = segment.color || color;
9403
+ for (let i = 0; i < segment.text.length; i++) {
9404
+ drawChar(currentX, y, segment.text.charCodeAt(i), segmentColor);
9405
+ currentX += charWidth;
9406
+ }
9407
+ }
9408
+ };
9409
+ const drawCenterString = (y, text) => {
9410
+ const charWidth = 8;
9411
+ const stripped = text.replace(/\^[0-9]/g, "");
9412
+ const width = stripped.length * charWidth;
9413
+ const x = (gl.canvas.width - width) / 2;
9414
+ drawString(x, y, text);
9415
+ };
9416
+ drawfillRect = (x, y, width, height, color) => {
9417
+ spriteRenderer.drawRect(x, y, width, height, color);
9418
+ };
9419
+ const setEntityHighlight = (entityId, color) => {
9420
+ highlightedEntities.set(entityId, color);
9421
+ };
9422
+ const clearEntityHighlight = (entityId) => {
9423
+ highlightedEntities.delete(entityId);
9424
+ };
9425
+ const highlightSurface = (faceIndex, color) => {
9426
+ highlightedSurfaces.set(faceIndex, color);
9427
+ };
9428
+ const removeSurfaceHighlight = (faceIndex) => {
9429
+ highlightedSurfaces.delete(faceIndex);
9430
+ };
9431
+ const setDebugMode = (mode) => {
9432
+ debugMode = mode;
9433
+ };
9434
+ const setBrightness = (value) => {
9435
+ brightness = Math.max(0, Math.min(2, value));
9436
+ };
9437
+ const setGamma = (value) => {
9438
+ gamma = Math.max(0.5, Math.min(3, value));
9439
+ };
9440
+ const setFullbright = (enabled) => {
9441
+ fullbright = enabled;
9442
+ };
9443
+ const setAmbient = (value) => {
9444
+ ambient = Math.max(0, Math.min(1, value));
9445
+ };
9446
+ const setLightStyle = (index, pattern) => {
9447
+ if (pattern === null) {
9448
+ lightStyleOverrides.delete(index);
9449
+ } else {
9450
+ lightStyleOverrides.set(index, pattern);
9451
+ }
9452
+ };
9453
+ const setUnderwaterWarp = (enabled) => {
9454
+ underwaterWarp = enabled;
9455
+ };
9456
+ const setBloom = (enabled) => {
9457
+ bloom = enabled;
9458
+ };
9459
+ const setBloomIntensity = (value) => {
9460
+ bloomIntensity = value;
9461
+ };
9462
+ const setLodBias = (bias) => {
9463
+ lodBias = Math.max(0, Math.min(2, bias));
9464
+ };
9465
+ const setAreaPortalState = (portalNum, open) => {
9466
+ if (portalNum >= 0 && portalNum < portalState.length) {
9467
+ portalState[portalNum] = open;
9468
+ }
9469
+ };
9470
+ const renderInstanced = (model, instances) => {
9471
+ const isMd2 = "glCommands" in model;
9472
+ const type = isMd2 ? "md2" : "md3";
9473
+ for (const instance of instances) {
9474
+ if (instanceCount >= MAX_INSTANCES) {
9475
+ console.warn("Max instances reached");
9476
+ break;
9477
+ }
9478
+ const entity = instancePool[instanceCount++];
9479
+ entity.model = model;
9480
+ entity.type = type;
9481
+ const blend = entity.blend;
9482
+ if (instance.frame !== void 0) {
9483
+ blend.frame0 = instance.frame;
9484
+ blend.frame1 = instance.frame;
9485
+ blend.lerp = 0;
9486
+ } else {
9487
+ blend.frame0 = instance.frame0 || 0;
9488
+ blend.frame1 = instance.frame1 || 0;
9489
+ blend.lerp = instance.lerp || 0;
9490
+ }
9491
+ if (isMd2) {
9492
+ entity.skin = instance.skin !== void 0 ? "skin" + instance.skin : void 0;
9493
+ }
9494
+ instance.rotation;
9495
+ instance.position;
9496
+ instance.scale || { };
9497
+ glMatrix.mat4.fromRotationTranslationScale(
9498
+ entity.transform,
9499
+ tempQuat,
9500
+ tempVec3Pos,
9501
+ tempVec3Scale
9502
+ );
9503
+ const lighting = entity.lighting;
9504
+ if (lighting) {
9505
+ if (lighting.dynamicLights) {
9506
+ lighting.dynamicLights.length = 0;
9507
+ } else {
9508
+ lighting.dynamicLights = [];
9509
+ }
9510
+ if (!lighting.ambient) {
9511
+ lighting.ambient = [0.5, 0.5, 0.5];
9512
+ } else {
9513
+ lighting.ambient[0] = 0.5;
9514
+ lighting.ambient[1] = 0.5;
9515
+ lighting.ambient[2] = 0.5;
9516
+ }
9517
+ }
9518
+ queuedInstances.push(entity);
9519
+ }
9520
+ };
9521
+ return {
9522
+ get width() {
9523
+ return gl.canvas.width;
9524
+ },
9525
+ get height() {
9526
+ return gl.canvas.height;
9527
+ },
9528
+ get collisionVis() {
9529
+ return collisionVis;
9530
+ },
9531
+ get debug() {
9532
+ return debugRenderer;
9533
+ },
9534
+ get particleSystem() {
9535
+ return particleSystem;
9536
+ },
9537
+ getPerformanceReport: () => gpuProfiler.getPerformanceReport(lastFrameStats),
9538
+ getMemoryUsage: () => gpuProfiler.getMemoryUsage(),
9539
+ renderFrame,
9540
+ registerPic,
9541
+ registerTexture,
9542
+ begin2D,
9543
+ end2D,
9544
+ drawPic,
9545
+ drawString,
9546
+ drawCenterString,
9547
+ drawfillRect,
9548
+ setEntityHighlight,
9549
+ clearEntityHighlight,
9550
+ highlightSurface,
9551
+ removeSurfaceHighlight,
9552
+ setDebugMode,
9553
+ setBrightness,
9554
+ setGamma,
9555
+ setFullbright,
9556
+ setAmbient,
9557
+ setLightStyle,
9558
+ setUnderwaterWarp,
9559
+ setBloom,
9560
+ setBloomIntensity,
9561
+ setLodBias,
9562
+ setAreaPortalState,
9563
+ renderInstanced,
9564
+ dispose: () => {
9565
+ gpuProfiler.dispose();
9566
+ }
9567
+ };
9568
+ };
7477
9569
  var DemoReader = class {
7478
9570
  constructor(buffer) {
7479
9571
  this.messageOffsets = [];
@@ -15991,6 +18083,7 @@ exports.createHeadlessRenderTarget = createHeadlessRenderTarget;
15991
18083
  exports.createInitialChannels = createInitialChannels;
15992
18084
  exports.createOcclusionResolver = createOcclusionResolver;
15993
18085
  exports.createProgramFromSources = createProgramFromSources;
18086
+ exports.createRenderer = createRenderer;
15994
18087
  exports.createWebGLContext = createWebGLContext;
15995
18088
  exports.createWebGPUContext = createWebGPUContext;
15996
18089
  exports.decodeOgg = decodeOgg;