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