@quake2ts/engine 0.0.837 → 0.0.838

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
@@ -9613,6 +9613,2111 @@ var createRenderer = (gl) => {
9613
9613
  }
9614
9614
  };
9615
9615
  };
9616
+
9617
+ // src/render/webgpu/context.ts
9618
+ async function createWebGPUContext(canvas, options) {
9619
+ if (!navigator.gpu) {
9620
+ throw new Error("WebGPU is not supported in this environment");
9621
+ }
9622
+ const adapter = await navigator.gpu.requestAdapter({
9623
+ powerPreference: options?.powerPreference || "high-performance"
9624
+ });
9625
+ if (!adapter) {
9626
+ throw new Error("No appropriate GPUAdapter found");
9627
+ }
9628
+ if (options?.requiredFeatures) {
9629
+ for (const feature of options.requiredFeatures) {
9630
+ if (!adapter.features.has(feature)) {
9631
+ throw new Error(`Required feature not available: ${feature}`);
9632
+ }
9633
+ }
9634
+ }
9635
+ const deviceDescriptor = {
9636
+ requiredFeatures: options?.requiredFeatures,
9637
+ requiredLimits: options?.requiredLimits
9638
+ };
9639
+ const device = await adapter.requestDevice(deviceDescriptor);
9640
+ let context;
9641
+ let format = "rgba8unorm";
9642
+ const depthFormat = "depth24plus";
9643
+ let isHeadless = true;
9644
+ let width = options?.width || 800;
9645
+ let height = options?.height || 600;
9646
+ if (canvas) {
9647
+ context = canvas.getContext("webgpu");
9648
+ if (!context) {
9649
+ throw new Error("Failed to get WebGPU context from canvas");
9650
+ }
9651
+ isHeadless = false;
9652
+ format = navigator.gpu.getPreferredCanvasFormat();
9653
+ width = canvas.width;
9654
+ height = canvas.height;
9655
+ context.configure({
9656
+ device,
9657
+ format,
9658
+ alphaMode: "opaque"
9659
+ // Standard for game rendering
9660
+ });
9661
+ }
9662
+ const features = /* @__PURE__ */ new Set();
9663
+ for (const feature of adapter.features) {
9664
+ features.add(feature);
9665
+ }
9666
+ return {
9667
+ adapter,
9668
+ device,
9669
+ context,
9670
+ format,
9671
+ depthFormat,
9672
+ features,
9673
+ limits: device.limits,
9674
+ isHeadless,
9675
+ width,
9676
+ height
9677
+ };
9678
+ }
9679
+ function queryCapabilities(state) {
9680
+ const { features, limits } = state;
9681
+ return {
9682
+ hasTimestampQuery: features.has("timestamp-query"),
9683
+ hasDepthClipControl: features.has("depth-clip-control"),
9684
+ hasTextureCompressionBC: features.has("texture-compression-bc"),
9685
+ hasTextureCompressionETC2: features.has("texture-compression-etc2"),
9686
+ hasTextureCompressionASTC: features.has("texture-compression-astc"),
9687
+ maxTextureDimension2D: limits.maxTextureDimension2D,
9688
+ maxBindGroups: limits.maxBindGroups,
9689
+ maxUniformBufferBindingSize: limits.maxUniformBufferBindingSize,
9690
+ maxStorageBufferBindingSize: limits.maxStorageBufferBindingSize
9691
+ };
9692
+ }
9693
+ function evaluateLightStyle2(pattern, time) {
9694
+ if (!pattern) return 1;
9695
+ const frame = Math.floor(time * 10) % pattern.length;
9696
+ const charCode = pattern.charCodeAt(frame);
9697
+ return (charCode - 97) / 12;
9698
+ }
9699
+ function sortVisibleFacesFrontToBack2(faces) {
9700
+ return [...faces].sort((a, b) => b.sortKey - a.sortKey);
9701
+ }
9702
+ function sortVisibleFacesBackToFront2(faces) {
9703
+ return [...faces].sort((a, b) => a.sortKey - b.sortKey);
9704
+ }
9705
+ var FrameRenderer = class {
9706
+ constructor(context, pipelines) {
9707
+ this.context = context;
9708
+ this.pipelines = pipelines;
9709
+ this.depthTexture = null;
9710
+ this.copyTexture = null;
9711
+ // Separate texture for headless output if no context exists
9712
+ this.headlessTarget = null;
9713
+ this.lastWidth = 0;
9714
+ this.lastHeight = 0;
9715
+ this.lastFrameTime = 0;
9716
+ // Current frame context (available during frame rendering)
9717
+ this.currentFrameContext = null;
9718
+ }
9719
+ ensureDepthTexture(width, height) {
9720
+ if (this.depthTexture && this.lastWidth === width && this.lastHeight === height) {
9721
+ return this.depthTexture.createView();
9722
+ }
9723
+ if (this.depthTexture) {
9724
+ this.depthTexture.destroy();
9725
+ }
9726
+ this.depthTexture = this.context.device.createTexture({
9727
+ size: [width, height],
9728
+ format: "depth24plus",
9729
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
9730
+ label: "depth-buffer"
9731
+ });
9732
+ if (this.copyTexture) {
9733
+ this.copyTexture.destroy();
9734
+ this.copyTexture = null;
9735
+ }
9736
+ this.lastWidth = width;
9737
+ this.lastHeight = height;
9738
+ return this.depthTexture.createView();
9739
+ }
9740
+ ensureCopyTexture(width, height) {
9741
+ if (this.copyTexture && this.lastWidth === width && this.lastHeight === height) {
9742
+ return this.copyTexture;
9743
+ }
9744
+ if (this.copyTexture) {
9745
+ this.copyTexture.destroy();
9746
+ }
9747
+ this.copyTexture = this.context.device.createTexture({
9748
+ size: [width, height],
9749
+ format: this.context.format,
9750
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
9751
+ label: "frame-copy-texture"
9752
+ });
9753
+ return this.copyTexture;
9754
+ }
9755
+ ensureHeadlessTarget(width, height) {
9756
+ if (this.headlessTarget && this.headlessTarget.width === width && this.headlessTarget.height === height) {
9757
+ return this.headlessTarget.createView();
9758
+ }
9759
+ if (this.headlessTarget) {
9760
+ this.headlessTarget.destroy();
9761
+ }
9762
+ this.headlessTarget = this.context.device.createTexture({
9763
+ size: [width, height],
9764
+ format: this.context.format,
9765
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING,
9766
+ label: "headless-render-target"
9767
+ });
9768
+ return this.headlessTarget.createView();
9769
+ }
9770
+ beginFrame() {
9771
+ const { device, context, width, height } = this.context;
9772
+ const commandEncoder = device.createCommandEncoder({ label: "frame-command-encoder" });
9773
+ let renderTarget;
9774
+ if (context) {
9775
+ renderTarget = context.getCurrentTexture().createView();
9776
+ } else {
9777
+ renderTarget = this.ensureHeadlessTarget(width, height);
9778
+ }
9779
+ const depthTexture = this.ensureDepthTexture(width, height);
9780
+ return {
9781
+ device,
9782
+ commandEncoder,
9783
+ renderTarget,
9784
+ depthTexture,
9785
+ width,
9786
+ height
9787
+ };
9788
+ }
9789
+ /**
9790
+ * Begin 2D rendering pass. Called by WebGPURenderer.begin2D()
9791
+ */
9792
+ begin2DPass() {
9793
+ if (!this.currentFrameContext) {
9794
+ throw new Error("begin2DPass called outside of renderFrame");
9795
+ }
9796
+ const { commandEncoder, renderTarget, width, height } = this.currentFrameContext;
9797
+ this.pipelines.sprite.setProjection(width, height);
9798
+ this.pipelines.sprite.begin(commandEncoder, renderTarget);
9799
+ }
9800
+ /**
9801
+ * End 2D rendering pass. Called by WebGPURenderer.end2D()
9802
+ */
9803
+ end2DPass() {
9804
+ this.pipelines.sprite.end();
9805
+ }
9806
+ renderFrame(options) {
9807
+ const now = performance.now();
9808
+ const fps = this.lastFrameTime > 0 ? 1e3 / (now - this.lastFrameTime) : 0;
9809
+ this.lastFrameTime = now;
9810
+ const stats = {
9811
+ batches: 0,
9812
+ facesDrawn: 0,
9813
+ drawCalls: 0,
9814
+ skyDrawn: false,
9815
+ viewModelDrawn: false,
9816
+ fps: Math.round(fps),
9817
+ vertexCount: 0
9818
+ };
9819
+ const frameCtx = this.beginFrame();
9820
+ this.currentFrameContext = frameCtx;
9821
+ const { commandEncoder, renderTarget, depthTexture } = frameCtx;
9822
+ const {
9823
+ clearColor = [0, 0, 0, 1],
9824
+ world,
9825
+ camera,
9826
+ timeSeconds = 0,
9827
+ dlights,
9828
+ renderMode,
9829
+ disableLightmaps,
9830
+ lightmapOnly,
9831
+ brightness,
9832
+ gamma,
9833
+ fullbright,
9834
+ ambient,
9835
+ lightStyleOverrides,
9836
+ portalState
9837
+ } = options;
9838
+ const viewProjection = new Float32Array(options.camera.viewProjectionMatrix);
9839
+ const opaquePassDescriptor = {
9840
+ colorAttachments: [{
9841
+ view: renderTarget,
9842
+ clearValue: clearColor,
9843
+ loadOp: "clear",
9844
+ storeOp: "store"
9845
+ }],
9846
+ depthStencilAttachment: {
9847
+ view: depthTexture,
9848
+ depthClearValue: 1,
9849
+ depthLoadOp: "clear",
9850
+ depthStoreOp: "store"
9851
+ },
9852
+ label: "opaque-render-pass"
9853
+ };
9854
+ const opaquePass = commandEncoder.beginRenderPass(opaquePassDescriptor);
9855
+ if (options.sky && options.sky.cubemap) {
9856
+ {
9857
+ const cameraState = options.cameraState ?? options.camera.toState();
9858
+ const scroll = computeSkyScroll(options.timeSeconds ?? 0, options.sky.scrollSpeeds ?? [0.01, 0.02]);
9859
+ this.pipelines.skybox.draw(opaquePass, {
9860
+ cameraState,
9861
+ // NEW: let pipeline build matrices
9862
+ scroll,
9863
+ cubemap: options.sky.cubemap
9864
+ });
9865
+ }
9866
+ stats.skyDrawn = true;
9867
+ }
9868
+ const opaqueFaces = [];
9869
+ const transparentFaces = [];
9870
+ if (world) {
9871
+ world.materials?.update(timeSeconds);
9872
+ const frustum = extractFrustumPlanes(Array.from(viewProjection));
9873
+ const cameraPosition = {
9874
+ x: camera.position[0] ?? 0,
9875
+ y: camera.position[1] ?? 0,
9876
+ z: camera.position[2] ?? 0
9877
+ };
9878
+ const visibleFaces = gatherVisibleFaces(world.map, cameraPosition, frustum, portalState);
9879
+ for (const face of visibleFaces) {
9880
+ const geometry = world.surfaces[face.faceIndex];
9881
+ if (!geometry) continue;
9882
+ const isTransparent = (geometry.surfaceFlags & (SURF_TRANS33 | SURF_TRANS66 | SURF_WARP)) !== 0;
9883
+ if (isTransparent) {
9884
+ transparentFaces.push(face);
9885
+ } else {
9886
+ opaqueFaces.push(face);
9887
+ }
9888
+ }
9889
+ const sortedOpaque = sortVisibleFacesFrontToBack2(opaqueFaces);
9890
+ let effectiveLightStyles = world.lightStyles || [];
9891
+ if (lightStyleOverrides && lightStyleOverrides.size > 0) {
9892
+ const styles = [...world.lightStyles || []];
9893
+ for (const [index, pattern] of lightStyleOverrides) {
9894
+ while (styles.length <= index) styles.push(1);
9895
+ styles[index] = evaluateLightStyle2(pattern, timeSeconds);
9896
+ }
9897
+ effectiveLightStyles = styles;
9898
+ }
9899
+ const drawSurfaceBatch = (faces, pass, lightStyles) => {
9900
+ for (const { faceIndex } of faces) {
9901
+ const geometry = world.surfaces[faceIndex];
9902
+ if (!geometry) continue;
9903
+ if ((geometry.surfaceFlags & SURF_SKY) !== 0) continue;
9904
+ const faceStyles = world.map.faces[faceIndex]?.styles;
9905
+ this.pipelines.bsp.bind(pass, {
9906
+ modelViewProjection: viewProjection,
9907
+ styleIndices: faceStyles,
9908
+ styleValues: lightStyles,
9909
+ surfaceFlags: geometry.surfaceFlags,
9910
+ timeSeconds,
9911
+ // diffuseTexture: diffuse?.gpuTexture.createView(), // Need view
9912
+ // diffuseSampler: ...
9913
+ // lightmapTexture: ...
9914
+ // lightmapSampler: ...
9915
+ dlights,
9916
+ renderMode,
9917
+ lightmapOnly,
9918
+ brightness,
9919
+ gamma,
9920
+ fullbright,
9921
+ ambient,
9922
+ cameraPosition: options.camera.position
9923
+ });
9924
+ this.pipelines.bsp.draw(pass, geometry, renderMode);
9925
+ stats.facesDrawn++;
9926
+ stats.drawCalls++;
9927
+ stats.vertexCount += geometry.vertexCount;
9928
+ }
9929
+ };
9930
+ drawSurfaceBatch(sortedOpaque, opaquePass, effectiveLightStyles);
9931
+ opaquePass.end();
9932
+ const transparentPassDescriptor = {
9933
+ colorAttachments: [{
9934
+ view: renderTarget,
9935
+ loadOp: "load",
9936
+ storeOp: "store"
9937
+ }],
9938
+ depthStencilAttachment: {
9939
+ view: depthTexture,
9940
+ depthLoadOp: "load",
9941
+ depthStoreOp: "store"
9942
+ },
9943
+ label: "transparent-render-pass"
9944
+ };
9945
+ const transparentPass = commandEncoder.beginRenderPass(transparentPassDescriptor);
9946
+ if (transparentFaces.length > 0) {
9947
+ const sortedTransparent = sortVisibleFacesBackToFront2(transparentFaces);
9948
+ drawSurfaceBatch(sortedTransparent, transparentPass, effectiveLightStyles);
9949
+ }
9950
+ transparentPass.end();
9951
+ } else {
9952
+ opaquePass.end();
9953
+ }
9954
+ if (options.underwaterWarp || options.bloom) ;
9955
+ if (options.onDraw2D) {
9956
+ options.onDraw2D();
9957
+ }
9958
+ if (this.pipelines.sprite.isActive) {
9959
+ console.warn("2D render pass was not properly closed - auto-closing to prevent resource leak");
9960
+ this.end2DPass();
9961
+ }
9962
+ this.context.device.queue.submit([commandEncoder.finish()]);
9963
+ this.currentFrameContext = null;
9964
+ return stats;
9965
+ }
9966
+ };
9967
+
9968
+ // src/render/webgpu/shaders/mipmapShader.ts
9969
+ var MIPMAP_SHADER = `
9970
+ struct VSOutput {
9971
+ @builtin(position) position: vec4f,
9972
+ @location(0) texcoord: vec2f,
9973
+ };
9974
+
9975
+ @vertex
9976
+ fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> VSOutput {
9977
+ var pos = array<vec2f, 6>(
9978
+ vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0),
9979
+ vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0)
9980
+ );
9981
+
9982
+ var output: VSOutput;
9983
+ output.position = vec4f(pos[vertexIndex], 0.0, 1.0);
9984
+ // Remap from [-1, 1] to [0, 1] for texcoord, Y is flipped in WebGPU clip space vs texture UV
9985
+ // Clip space: (-1,-1) is bottom-left. UV: (0,0) is top-left usually?
9986
+ // Wait, WebGPU clip space Y is up. UV (0,0) is top-left.
9987
+ // Quad vertices are full screen.
9988
+ output.texcoord = pos[vertexIndex] * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);
9989
+ return output;
9990
+ }
9991
+
9992
+ @group(0) @binding(0) var imgSampler: sampler;
9993
+ @group(0) @binding(1) var img: texture_2d<f32>;
9994
+
9995
+ @fragment
9996
+ fn fs_main(in: VSOutput) -> @location(0) vec4f {
9997
+ return textureSample(img, imgSampler, in.texcoord);
9998
+ }
9999
+ `;
10000
+ var GPUBufferResource = class {
10001
+ constructor(device, descriptor) {
10002
+ this.device = device;
10003
+ this.size = Math.ceil(descriptor.size / 4) * 4;
10004
+ this.usage = descriptor.usage;
10005
+ this.buffer = device.createBuffer({
10006
+ size: this.size,
10007
+ usage: this.usage,
10008
+ label: descriptor.label,
10009
+ mappedAtCreation: descriptor.mappedAtCreation
10010
+ });
10011
+ }
10012
+ write(data, offset = 0) {
10013
+ const dataSize = data.byteLength;
10014
+ if (offset + dataSize > this.size) {
10015
+ throw new Error(`Buffer write out of bounds: offset ${offset} + data ${dataSize} > buffer ${this.size}`);
10016
+ }
10017
+ this.device.queue.writeBuffer(
10018
+ this.buffer,
10019
+ offset,
10020
+ data,
10021
+ 0
10022
+ // dataOffset
10023
+ );
10024
+ }
10025
+ async mapAsync(mode, offset = 0, size) {
10026
+ await this.buffer.mapAsync(mode, offset, size);
10027
+ }
10028
+ getMappedRange(offset = 0, size) {
10029
+ return this.buffer.getMappedRange(offset, size);
10030
+ }
10031
+ unmap() {
10032
+ this.buffer.unmap();
10033
+ }
10034
+ destroy() {
10035
+ this.buffer.destroy();
10036
+ }
10037
+ };
10038
+ var VertexBuffer2 = class extends GPUBufferResource {
10039
+ constructor(device, descriptor) {
10040
+ super(device, {
10041
+ size: descriptor.size,
10042
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | (descriptor.usage || 0),
10043
+ label: descriptor.label
10044
+ });
10045
+ }
10046
+ };
10047
+ var IndexBuffer2 = class extends GPUBufferResource {
10048
+ constructor(device, descriptor) {
10049
+ super(device, {
10050
+ size: descriptor.size,
10051
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST | (descriptor.usage || 0),
10052
+ label: descriptor.label
10053
+ });
10054
+ }
10055
+ };
10056
+ var UniformBuffer = class extends GPUBufferResource {
10057
+ constructor(device, descriptor) {
10058
+ super(device, {
10059
+ size: descriptor.size,
10060
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | (descriptor.usage || 0),
10061
+ label: descriptor.label
10062
+ });
10063
+ }
10064
+ };
10065
+ function getBlockSize(format) {
10066
+ switch (format) {
10067
+ case "rgba8unorm":
10068
+ case "rgba8unorm-srgb":
10069
+ case "bgra8unorm":
10070
+ case "bgra8unorm-srgb":
10071
+ case "rgba8sint":
10072
+ case "rgba8uint":
10073
+ return 4;
10074
+ case "rg8unorm":
10075
+ case "rg8sint":
10076
+ case "rg8uint":
10077
+ case "r16float":
10078
+ case "r16sint":
10079
+ case "r16uint":
10080
+ return 2;
10081
+ case "r8unorm":
10082
+ case "r8sint":
10083
+ case "r8uint":
10084
+ return 1;
10085
+ case "rgba16float":
10086
+ case "rgba16sint":
10087
+ case "rgba16uint":
10088
+ case "rgba32float":
10089
+ return 8;
10090
+ // wait, rgba16 is 8 bytes. rgba32 is 16 bytes.
10091
+ case "rgba32float":
10092
+ case "rgba32sint":
10093
+ case "rgba32uint":
10094
+ return 16;
10095
+ case "depth24plus":
10096
+ case "depth24plus-stencil8":
10097
+ // approximate
10098
+ case "depth32float":
10099
+ return 4;
10100
+ default:
10101
+ return 4;
10102
+ }
10103
+ }
10104
+ var _Texture2D = class _Texture2D {
10105
+ constructor(device, descriptor) {
10106
+ this.device = device;
10107
+ this.width = descriptor.width;
10108
+ this.height = descriptor.height;
10109
+ this.format = descriptor.format;
10110
+ const usage = descriptor.usage ?? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
10111
+ this.texture = device.createTexture({
10112
+ size: [this.width, this.height, 1],
10113
+ format: this.format,
10114
+ usage,
10115
+ mipLevelCount: descriptor.mipLevelCount ?? 1,
10116
+ label: descriptor.label
10117
+ });
10118
+ }
10119
+ upload(data, options = {}) {
10120
+ const width = options.width ?? this.width;
10121
+ const height = options.height ?? this.height;
10122
+ const depthOrArrayLayers = options.depthOrArrayLayers ?? 1;
10123
+ const mipLevel = options.mipLevel ?? 0;
10124
+ const blockSize = getBlockSize(this.format);
10125
+ let bytesPerRow = options.bytesPerRow;
10126
+ if (!bytesPerRow) {
10127
+ bytesPerRow = width * blockSize;
10128
+ }
10129
+ this.device.queue.writeTexture(
10130
+ { texture: this.texture, mipLevel, origin: { x: 0, y: 0, z: 0 } },
10131
+ data,
10132
+ {
10133
+ offset: 0,
10134
+ bytesPerRow,
10135
+ rowsPerImage: options.rowsPerImage ?? height
10136
+ },
10137
+ { width, height, depthOrArrayLayers }
10138
+ );
10139
+ }
10140
+ getMipmapPipeline(format) {
10141
+ let devicePipelines = _Texture2D.mipmapPipelines.get(this.device);
10142
+ if (!devicePipelines) {
10143
+ devicePipelines = /* @__PURE__ */ new Map();
10144
+ _Texture2D.mipmapPipelines.set(this.device, devicePipelines);
10145
+ }
10146
+ let pipeline = devicePipelines.get(format);
10147
+ if (!pipeline) {
10148
+ const module = this.device.createShaderModule({
10149
+ code: MIPMAP_SHADER,
10150
+ label: "mipmap-shader"
10151
+ });
10152
+ pipeline = this.device.createRenderPipeline({
10153
+ layout: "auto",
10154
+ vertex: {
10155
+ module,
10156
+ entryPoint: "vs_main"
10157
+ },
10158
+ fragment: {
10159
+ module,
10160
+ entryPoint: "fs_main",
10161
+ targets: [{ format }]
10162
+ },
10163
+ primitive: {
10164
+ topology: "triangle-list"
10165
+ },
10166
+ label: `mipmap-pipeline-${format}`
10167
+ });
10168
+ devicePipelines.set(format, pipeline);
10169
+ }
10170
+ return pipeline;
10171
+ }
10172
+ getMipmapSampler() {
10173
+ let sampler = _Texture2D.mipmapSamplers.get(this.device);
10174
+ if (!sampler) {
10175
+ sampler = this.device.createSampler({
10176
+ minFilter: "linear",
10177
+ magFilter: "linear",
10178
+ label: "mipmap-sampler"
10179
+ });
10180
+ _Texture2D.mipmapSamplers.set(this.device, sampler);
10181
+ }
10182
+ return sampler;
10183
+ }
10184
+ generateMipmaps(commandEncoder) {
10185
+ const mipCount = this.texture.mipLevelCount;
10186
+ if (mipCount <= 1) return;
10187
+ const pipeline = this.getMipmapPipeline(this.format);
10188
+ const sampler = this.getMipmapSampler();
10189
+ for (let i = 1; i < mipCount; i++) {
10190
+ const srcView = this.texture.createView({
10191
+ baseMipLevel: i - 1,
10192
+ mipLevelCount: 1,
10193
+ label: `mipmap-src-${i - 1}`
10194
+ });
10195
+ const dstView = this.texture.createView({
10196
+ baseMipLevel: i,
10197
+ mipLevelCount: 1,
10198
+ label: `mipmap-dst-${i}`
10199
+ });
10200
+ const passEncoder = commandEncoder.beginRenderPass({
10201
+ colorAttachments: [{
10202
+ view: dstView,
10203
+ loadOp: "clear",
10204
+ storeOp: "store"
10205
+ }],
10206
+ label: `mipmap-pass-${i}`
10207
+ });
10208
+ const bindGroup = this.device.createBindGroup({
10209
+ layout: pipeline.getBindGroupLayout(0),
10210
+ entries: [
10211
+ { binding: 0, resource: sampler },
10212
+ { binding: 1, resource: srcView }
10213
+ ],
10214
+ label: `mipmap-bindgroup-${i}`
10215
+ });
10216
+ passEncoder.setPipeline(pipeline);
10217
+ passEncoder.setBindGroup(0, bindGroup);
10218
+ passEncoder.draw(6);
10219
+ passEncoder.end();
10220
+ }
10221
+ }
10222
+ createView(descriptor) {
10223
+ return this.texture.createView(descriptor);
10224
+ }
10225
+ destroy() {
10226
+ this.texture.destroy();
10227
+ }
10228
+ get memorySize() {
10229
+ const blockSize = getBlockSize(this.format);
10230
+ let size = 0;
10231
+ let w = this.width;
10232
+ let h = this.height;
10233
+ const mipCount = this.texture.mipLevelCount;
10234
+ for (let i = 0; i < mipCount; i++) {
10235
+ size += w * h * blockSize;
10236
+ w = Math.max(1, Math.floor(w / 2));
10237
+ h = Math.max(1, Math.floor(h / 2));
10238
+ }
10239
+ return size;
10240
+ }
10241
+ };
10242
+ // Cache pipelines per device and per format
10243
+ _Texture2D.mipmapPipelines = /* @__PURE__ */ new WeakMap();
10244
+ _Texture2D.mipmapSamplers = /* @__PURE__ */ new WeakMap();
10245
+ var Texture2D2 = _Texture2D;
10246
+ var Sampler = class {
10247
+ constructor(device, descriptor) {
10248
+ this.sampler = device.createSampler({
10249
+ ...descriptor,
10250
+ label: descriptor.label
10251
+ });
10252
+ }
10253
+ destroy() {
10254
+ }
10255
+ };
10256
+ function createLinearSampler(device) {
10257
+ return new Sampler(device, {
10258
+ minFilter: "linear",
10259
+ magFilter: "linear",
10260
+ mipmapFilter: "linear",
10261
+ label: "linear-sampler"
10262
+ });
10263
+ }
10264
+ var ShaderModule = class {
10265
+ constructor(device, descriptor) {
10266
+ this.device = device;
10267
+ this.module = device.createShaderModule({
10268
+ code: descriptor.code,
10269
+ label: descriptor.label
10270
+ });
10271
+ }
10272
+ get compilationInfo() {
10273
+ return this.module.getCompilationInfo();
10274
+ }
10275
+ };
10276
+ var RenderPipeline = class {
10277
+ constructor(device, descriptor) {
10278
+ this.device = device;
10279
+ const layout = descriptor.layout;
10280
+ this.pipeline = device.createRenderPipeline({
10281
+ layout,
10282
+ vertex: {
10283
+ module: descriptor.vertex.module.module,
10284
+ entryPoint: descriptor.vertex.entryPoint,
10285
+ buffers: descriptor.vertex.buffers
10286
+ },
10287
+ fragment: descriptor.fragment ? {
10288
+ module: descriptor.fragment.module.module,
10289
+ entryPoint: descriptor.fragment.entryPoint,
10290
+ targets: descriptor.fragment.targets
10291
+ } : void 0,
10292
+ primitive: descriptor.primitive,
10293
+ depthStencil: descriptor.depthStencil,
10294
+ multisample: descriptor.multisample,
10295
+ label: descriptor.label
10296
+ });
10297
+ }
10298
+ get layout() {
10299
+ throw new Error("Cannot retrieve layout from pipeline created with 'auto' layout");
10300
+ }
10301
+ destroy() {
10302
+ }
10303
+ };
10304
+ var BindGroupLayout = class {
10305
+ constructor(device, descriptor) {
10306
+ this.device = device;
10307
+ this.layout = device.createBindGroupLayout({
10308
+ entries: descriptor.entries,
10309
+ label: descriptor.label
10310
+ });
10311
+ }
10312
+ };
10313
+ var BindGroup = class {
10314
+ constructor(device, layout, entries, label) {
10315
+ this.device = device;
10316
+ const gpuEntries = entries.map((entry) => {
10317
+ let resource;
10318
+ if (entry.resource instanceof GPUBufferResource) {
10319
+ resource = { buffer: entry.resource.buffer };
10320
+ } else if (entry.resource instanceof Sampler) {
10321
+ resource = entry.resource.sampler;
10322
+ } else {
10323
+ resource = entry.resource;
10324
+ }
10325
+ return {
10326
+ binding: entry.binding,
10327
+ resource
10328
+ };
10329
+ });
10330
+ this.bindGroup = device.createBindGroup({
10331
+ layout: layout.layout,
10332
+ entries: gpuEntries,
10333
+ label
10334
+ });
10335
+ }
10336
+ destroy() {
10337
+ }
10338
+ };
10339
+ var BindGroupBuilder = class {
10340
+ constructor(label) {
10341
+ this.label = label;
10342
+ this.entries = [];
10343
+ }
10344
+ addUniformBuffer(binding, visibility) {
10345
+ this.entries.push({
10346
+ binding,
10347
+ visibility,
10348
+ buffer: { type: "uniform" }
10349
+ });
10350
+ return this;
10351
+ }
10352
+ addStorageBuffer(binding, visibility, type = "read-only-storage") {
10353
+ this.entries.push({
10354
+ binding,
10355
+ visibility,
10356
+ buffer: { type }
10357
+ });
10358
+ return this;
10359
+ }
10360
+ addTexture(binding, visibility, sampleType = "float", viewDimension = "2d") {
10361
+ this.entries.push({
10362
+ binding,
10363
+ visibility,
10364
+ texture: { sampleType, viewDimension }
10365
+ });
10366
+ return this;
10367
+ }
10368
+ addSampler(binding, visibility, type = "filtering") {
10369
+ this.entries.push({
10370
+ binding,
10371
+ visibility,
10372
+ sampler: { type }
10373
+ });
10374
+ return this;
10375
+ }
10376
+ build(device) {
10377
+ return new BindGroupLayout(device, {
10378
+ entries: this.entries,
10379
+ label: this.label
10380
+ });
10381
+ }
10382
+ };
10383
+
10384
+ // src/render/webgpu/shaders/spriteShader.ts
10385
+ var SPRITE_SHADER = `
10386
+ // Sprite Rendering Shader (WGSL)
10387
+
10388
+ struct VertexInput {
10389
+ @location(0) position: vec2f,
10390
+ @location(1) texcoord: vec2f,
10391
+ @location(2) color: vec4f,
10392
+ }
10393
+
10394
+ struct VertexOutput {
10395
+ @builtin(position) position: vec4f,
10396
+ @location(0) texcoord: vec2f,
10397
+ @location(1) color: vec4f,
10398
+ }
10399
+
10400
+ struct Uniforms {
10401
+ projection: mat4x4f,
10402
+ }
10403
+
10404
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
10405
+
10406
+ @vertex
10407
+ fn vs_main(input: VertexInput) -> VertexOutput {
10408
+ var output: VertexOutput;
10409
+ // Orthographic projection: Input is screen coords (pixels), Z is 0.0, W is 1.0
10410
+ output.position = uniforms.projection * vec4f(input.position, 0.0, 1.0);
10411
+ output.texcoord = input.texcoord;
10412
+ output.color = input.color;
10413
+ return output;
10414
+ }
10415
+
10416
+ @group(1) @binding(0) var texSampler: sampler;
10417
+ @group(1) @binding(1) var tex: texture_2d<f32>;
10418
+
10419
+ @fragment
10420
+ fn fs_main(input: VertexOutput) -> @location(0) vec4f {
10421
+ let texColor = textureSample(tex, texSampler, input.texcoord);
10422
+ return texColor * input.color;
10423
+ }
10424
+
10425
+ @fragment
10426
+ fn fs_solid(input: VertexOutput) -> @location(0) vec4f {
10427
+ return input.color;
10428
+ }
10429
+ `;
10430
+
10431
+ // src/render/webgpu/pipelines/sprite.ts
10432
+ var MAX_SPRITES = 2048;
10433
+ var VERTICES_PER_SPRITE = 4;
10434
+ var INDICES_PER_SPRITE = 6;
10435
+ var FLOATS_PER_VERTEX = 8;
10436
+ var SpriteRenderer2 = class {
10437
+ constructor(device, format) {
10438
+ this.device = device;
10439
+ this.format = format;
10440
+ this.currentVertexCount = 0;
10441
+ this.currentIndexCount = 0;
10442
+ this.currentTexture = null;
10443
+ this.drawCommands = [];
10444
+ this.textureBindGroups = /* @__PURE__ */ new Map();
10445
+ this._activeEncoder = null;
10446
+ this._activeRenderTarget = null;
10447
+ const shaderModule = new ShaderModule(device, {
10448
+ code: SPRITE_SHADER,
10449
+ label: "sprite-shader"
10450
+ });
10451
+ const vertexBufferLayout = {
10452
+ arrayStride: FLOATS_PER_VERTEX * 4,
10453
+ stepMode: "vertex",
10454
+ attributes: [
10455
+ { format: "float32x2", offset: 0, shaderLocation: 0 },
10456
+ // position
10457
+ { format: "float32x2", offset: 8, shaderLocation: 1 },
10458
+ // texcoord
10459
+ { format: "float32x4", offset: 16, shaderLocation: 2 }
10460
+ // color
10461
+ ]
10462
+ };
10463
+ const bindGroupBuilder = new BindGroupBuilder("sprite-uniform-layout");
10464
+ bindGroupBuilder.addUniformBuffer(0, GPUShaderStage.VERTEX);
10465
+ const uniformBindGroupLayout = bindGroupBuilder.build(device);
10466
+ const textureBindGroupBuilder = new BindGroupBuilder("sprite-texture-layout");
10467
+ textureBindGroupBuilder.addSampler(0, GPUShaderStage.FRAGMENT);
10468
+ textureBindGroupBuilder.addTexture(1, GPUShaderStage.FRAGMENT);
10469
+ this.textureBindGroupLayout = textureBindGroupBuilder.build(device);
10470
+ const pipelineLayout = device.createPipelineLayout({
10471
+ bindGroupLayouts: [
10472
+ uniformBindGroupLayout.layout,
10473
+ this.textureBindGroupLayout.layout
10474
+ ],
10475
+ label: "sprite-pipeline-layout"
10476
+ });
10477
+ const pipelineLayoutSolid = device.createPipelineLayout({
10478
+ bindGroupLayouts: [
10479
+ uniformBindGroupLayout.layout
10480
+ ],
10481
+ label: "sprite-pipeline-solid-layout"
10482
+ });
10483
+ const blendState = {
10484
+ color: {
10485
+ srcFactor: "src-alpha",
10486
+ dstFactor: "one-minus-src-alpha",
10487
+ operation: "add"
10488
+ },
10489
+ alpha: {
10490
+ srcFactor: "one",
10491
+ dstFactor: "one-minus-src-alpha",
10492
+ operation: "add"
10493
+ }
10494
+ };
10495
+ this.pipelineTextured = new RenderPipeline(device, {
10496
+ layout: pipelineLayout,
10497
+ vertex: {
10498
+ module: shaderModule,
10499
+ entryPoint: "vs_main",
10500
+ buffers: [vertexBufferLayout]
10501
+ },
10502
+ fragment: {
10503
+ module: shaderModule,
10504
+ entryPoint: "fs_main",
10505
+ targets: [{
10506
+ format: this.format,
10507
+ blend: blendState
10508
+ }]
10509
+ },
10510
+ primitive: {
10511
+ topology: "triangle-list",
10512
+ cullMode: "none"
10513
+ // Sprites are 2D, no culling usually needed
10514
+ },
10515
+ label: "sprite-pipeline-textured"
10516
+ });
10517
+ this.pipelineSolid = new RenderPipeline(device, {
10518
+ layout: pipelineLayoutSolid,
10519
+ vertex: {
10520
+ module: shaderModule,
10521
+ entryPoint: "vs_main",
10522
+ buffers: [vertexBufferLayout]
10523
+ },
10524
+ fragment: {
10525
+ module: shaderModule,
10526
+ entryPoint: "fs_solid",
10527
+ targets: [{
10528
+ format: this.format,
10529
+ blend: blendState
10530
+ }]
10531
+ },
10532
+ primitive: {
10533
+ topology: "triangle-list",
10534
+ cullMode: "none"
10535
+ },
10536
+ label: "sprite-pipeline-solid"
10537
+ });
10538
+ this.vertexBuffer = new VertexBuffer2(device, {
10539
+ size: MAX_SPRITES * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX * 4,
10540
+ label: "sprite-vertex-buffer",
10541
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
10542
+ });
10543
+ this.indexBuffer = new IndexBuffer2(device, {
10544
+ size: MAX_SPRITES * INDICES_PER_SPRITE * 2,
10545
+ // Uint16
10546
+ label: "sprite-index-buffer",
10547
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
10548
+ });
10549
+ this.uniformBuffer = new UniformBuffer(device, {
10550
+ size: 64,
10551
+ // mat4
10552
+ label: "sprite-uniform-buffer",
10553
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
10554
+ });
10555
+ this.uniformBindGroup = new BindGroup(
10556
+ device,
10557
+ uniformBindGroupLayout,
10558
+ [
10559
+ { binding: 0, resource: this.uniformBuffer }
10560
+ ],
10561
+ "sprite-uniform-bind-group"
10562
+ );
10563
+ this.vertexData = new Float32Array(MAX_SPRITES * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX);
10564
+ const indices = new Uint16Array(MAX_SPRITES * INDICES_PER_SPRITE);
10565
+ for (let i = 0; i < MAX_SPRITES; i++) {
10566
+ const v = i * 4;
10567
+ const ii = i * 6;
10568
+ indices[ii] = v;
10569
+ indices[ii + 1] = v + 1;
10570
+ indices[ii + 2] = v + 2;
10571
+ indices[ii + 3] = v;
10572
+ indices[ii + 4] = v + 2;
10573
+ indices[ii + 5] = v + 3;
10574
+ }
10575
+ this.indexBuffer.write(indices);
10576
+ this.projectionMatrix = mat4.create();
10577
+ this.defaultSampler = createLinearSampler(device);
10578
+ }
10579
+ setProjection(width, height) {
10580
+ mat4.ortho(this.projectionMatrix, 0, width, height, 0, -1, 1);
10581
+ this.uniformBuffer.write(this.projectionMatrix);
10582
+ }
10583
+ begin(commandEncoder, renderTarget) {
10584
+ this.currentVertexCount = 0;
10585
+ this.currentIndexCount = 0;
10586
+ this.drawCommands = [];
10587
+ this.currentTexture = null;
10588
+ this._activeEncoder = commandEncoder;
10589
+ this._activeRenderTarget = renderTarget;
10590
+ }
10591
+ drawTexturedQuad(x, y, w, h, texture, u1 = 0, v1 = 0, u2 = 1, v2 = 1, color = [1, 1, 1, 1]) {
10592
+ if (this.currentVertexCount + VERTICES_PER_SPRITE > MAX_SPRITES * VERTICES_PER_SPRITE) {
10593
+ this.flush();
10594
+ }
10595
+ if (this.drawCommands.length === 0 || this.currentTexture !== texture) {
10596
+ this.drawCommands.push({
10597
+ texture,
10598
+ start: this.currentIndexCount,
10599
+ count: 0
10600
+ });
10601
+ this.currentTexture = texture;
10602
+ }
10603
+ const offset = this.currentVertexCount * FLOATS_PER_VERTEX;
10604
+ const [r, g, b, a] = color;
10605
+ this.vertexData[offset] = x;
10606
+ this.vertexData[offset + 1] = y;
10607
+ this.vertexData[offset + 2] = u1;
10608
+ this.vertexData[offset + 3] = v1;
10609
+ this.vertexData[offset + 4] = r;
10610
+ this.vertexData[offset + 5] = g;
10611
+ this.vertexData[offset + 6] = b;
10612
+ this.vertexData[offset + 7] = a;
10613
+ this.vertexData[offset + 8] = x;
10614
+ this.vertexData[offset + 9] = y + h;
10615
+ this.vertexData[offset + 10] = u1;
10616
+ this.vertexData[offset + 11] = v2;
10617
+ this.vertexData[offset + 12] = r;
10618
+ this.vertexData[offset + 13] = g;
10619
+ this.vertexData[offset + 14] = b;
10620
+ this.vertexData[offset + 15] = a;
10621
+ this.vertexData[offset + 16] = x + w;
10622
+ this.vertexData[offset + 17] = y + h;
10623
+ this.vertexData[offset + 18] = u2;
10624
+ this.vertexData[offset + 19] = v2;
10625
+ this.vertexData[offset + 20] = r;
10626
+ this.vertexData[offset + 21] = g;
10627
+ this.vertexData[offset + 22] = b;
10628
+ this.vertexData[offset + 23] = a;
10629
+ this.vertexData[offset + 24] = x + w;
10630
+ this.vertexData[offset + 25] = y;
10631
+ this.vertexData[offset + 26] = u2;
10632
+ this.vertexData[offset + 27] = v1;
10633
+ this.vertexData[offset + 28] = r;
10634
+ this.vertexData[offset + 29] = g;
10635
+ this.vertexData[offset + 30] = b;
10636
+ this.vertexData[offset + 31] = a;
10637
+ this.currentVertexCount += 4;
10638
+ this.currentIndexCount += 6;
10639
+ this.drawCommands[this.drawCommands.length - 1].count += 6;
10640
+ }
10641
+ drawSolidRect(x, y, w, h, color) {
10642
+ if (this.currentTexture !== null) {
10643
+ this.drawCommands.push({
10644
+ texture: null,
10645
+ start: this.currentIndexCount,
10646
+ count: 0
10647
+ });
10648
+ this.currentTexture = null;
10649
+ }
10650
+ this.drawTexturedQuad(x, y, w, h, null, 0, 0, 0, 0, color);
10651
+ }
10652
+ flush() {
10653
+ if (this.currentVertexCount === 0) return;
10654
+ if (!this._activeEncoder || !this._activeRenderTarget) return;
10655
+ const usedFloats = this.currentVertexCount * FLOATS_PER_VERTEX;
10656
+ this.vertexBuffer.write(this.vertexData.subarray(0, usedFloats));
10657
+ const passEncoder = this._activeEncoder.beginRenderPass({
10658
+ colorAttachments: [{
10659
+ view: this._activeRenderTarget,
10660
+ loadOp: "load",
10661
+ storeOp: "store"
10662
+ }],
10663
+ label: "sprite-pass"
10664
+ });
10665
+ passEncoder.setBindGroup(0, this.uniformBindGroup.bindGroup);
10666
+ passEncoder.setVertexBuffer(0, this.vertexBuffer.buffer);
10667
+ passEncoder.setIndexBuffer(this.indexBuffer.buffer, "uint16");
10668
+ for (const cmd of this.drawCommands) {
10669
+ if (cmd.count === 0) continue;
10670
+ if (cmd.texture) {
10671
+ passEncoder.setPipeline(this.pipelineTextured.pipeline);
10672
+ let bindGroup = this.textureBindGroups.get(cmd.texture);
10673
+ if (!bindGroup) {
10674
+ bindGroup = new BindGroup(
10675
+ this.device,
10676
+ this.textureBindGroupLayout,
10677
+ [
10678
+ { binding: 0, resource: this.defaultSampler },
10679
+ { binding: 1, resource: cmd.texture.createView() }
10680
+ ],
10681
+ `sprite-texture-bind-group-${cmd.texture.texture.label}`
10682
+ );
10683
+ this.textureBindGroups.set(cmd.texture, bindGroup);
10684
+ }
10685
+ passEncoder.setBindGroup(1, bindGroup.bindGroup);
10686
+ } else {
10687
+ passEncoder.setPipeline(this.pipelineSolid.pipeline);
10688
+ }
10689
+ passEncoder.drawIndexed(cmd.count, 1, cmd.start, 0, 0);
10690
+ }
10691
+ passEncoder.end();
10692
+ this.currentVertexCount = 0;
10693
+ this.currentIndexCount = 0;
10694
+ this.drawCommands = [];
10695
+ this.currentTexture = null;
10696
+ }
10697
+ end() {
10698
+ this.flush();
10699
+ this._activeEncoder = null;
10700
+ this._activeRenderTarget = null;
10701
+ }
10702
+ /**
10703
+ * Check if sprite renderer is currently in an active render pass
10704
+ * Ref: Defensive check to prevent GPU resource leaks
10705
+ */
10706
+ get isActive() {
10707
+ return this._activeEncoder !== null;
10708
+ }
10709
+ destroy() {
10710
+ this.vertexBuffer.destroy();
10711
+ this.indexBuffer.destroy();
10712
+ this.uniformBuffer.destroy();
10713
+ }
10714
+ };
10715
+
10716
+ // raw-loader:/home/runner/work/quake2/quake2/quake2ts/packages/engine/src/render/webgpu/shaders/skybox.wgsl
10717
+ var skybox_default = "struct Uniforms {\n viewProjection: mat4x4<f32>,\n scroll: vec2<f32>,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var t_skybox: texture_cube<f32>;\n@group(0) @binding(2) var s_skybox: sampler;\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) direction: vec3<f32>,\n}\n\n@vertex\nfn vertexMain(@location(0) position: vec3<f32>) -> VertexOutput {\n var output: VertexOutput;\n\n // Normalize input position (Quake Coordinates)\n var qDir = normalize(position);\n\n // Apply scrolling in Quake Coordinates (Horizontal Plane X/Y)\n // This ensures clouds scroll horizontally regardless of the final mapping.\n qDir.x += uniforms.scroll.x;\n qDir.y += uniforms.scroll.y;\n\n // Transform Quake coordinates (X-Fwd, Y-Left, Z-Up)\n // to WebGPU/GL cubemap coordinates (Right-handed? -Z Fwd, +X Right, +Y Up)\n // Quake X -> GL -Z\n // Quake Y -> GL -X\n // Quake Z -> GL Y\n var dir = vec3<f32>(-qDir.y, qDir.z, -qDir.x);\n\n output.direction = dir;\n\n output.position = uniforms.viewProjection * vec4<f32>(position, 1.0);\n return output;\n}\n\n@fragment\nfn fragmentMain(@location(0) direction: vec3<f32>) -> @location(0) vec4<f32> {\n return textureSample(t_skybox, s_skybox, direction);\n}\n";
10718
+
10719
+ // src/render/types/coordinates.ts
10720
+ var CoordinateSystem = /* @__PURE__ */ ((CoordinateSystem2) => {
10721
+ CoordinateSystem2["QUAKE"] = "quake";
10722
+ CoordinateSystem2["OPENGL"] = "opengl";
10723
+ CoordinateSystem2["WEBGPU"] = "webgpu";
10724
+ return CoordinateSystem2;
10725
+ })(CoordinateSystem || {});
10726
+
10727
+ // src/render/matrix/webgpu.ts
10728
+ var WebGPUMatrixBuilder = class {
10729
+ constructor() {
10730
+ this.coordinateSystem = "webgpu" /* WEBGPU */;
10731
+ }
10732
+ buildProjectionMatrix(camera) {
10733
+ const projection = mat4.create();
10734
+ const f = 1 / Math.tan(camera.fov * DEG2RAD / 2);
10735
+ const rangeInv = 1 / (camera.near - camera.far);
10736
+ projection[0] = f / camera.aspect;
10737
+ projection[5] = f;
10738
+ projection[10] = camera.far * rangeInv;
10739
+ projection[11] = -1;
10740
+ projection[14] = camera.near * camera.far * rangeInv;
10741
+ return projection;
10742
+ }
10743
+ buildViewMatrix(camera) {
10744
+ const [pitch, yaw, roll] = camera.angles;
10745
+ const pitchRad = pitch * DEG2RAD;
10746
+ const yawRad = yaw * DEG2RAD;
10747
+ const rollRad = roll * DEG2RAD;
10748
+ const rotationQuake = mat4.create();
10749
+ mat4.identity(rotationQuake);
10750
+ mat4.rotateZ(rotationQuake, rotationQuake, -yawRad);
10751
+ mat4.rotateY(rotationQuake, rotationQuake, -pitchRad);
10752
+ mat4.rotateX(rotationQuake, rotationQuake, -rollRad);
10753
+ mat4.fromValues(
10754
+ 0,
10755
+ 0,
10756
+ -1,
10757
+ 0,
10758
+ // Column 0: Quake X maps to View Z? Wait.
10759
+ -1,
10760
+ 0,
10761
+ 0,
10762
+ 0,
10763
+ // Column 1: Quake Y maps to View X?
10764
+ 0,
10765
+ 1,
10766
+ 0,
10767
+ 0,
10768
+ // Column 2: Quake Z maps to View Y?
10769
+ 0,
10770
+ 0,
10771
+ 0,
10772
+ 1
10773
+ );
10774
+ const quakeToWgpu = mat4.fromValues(
10775
+ 0,
10776
+ 0,
10777
+ -1,
10778
+ 0,
10779
+ // Col 0: X -> -Z
10780
+ -1,
10781
+ 0,
10782
+ 0,
10783
+ 0,
10784
+ // Col 1: Y -> -X
10785
+ 0,
10786
+ 1,
10787
+ 0,
10788
+ 0,
10789
+ // Col 2: Z -> Y
10790
+ 0,
10791
+ 0,
10792
+ 0,
10793
+ 1
10794
+ );
10795
+ const rotationView = mat4.create();
10796
+ mat4.multiply(rotationView, quakeToWgpu, rotationQuake);
10797
+ const cameraPos = vec3.fromValues(
10798
+ camera.position[0],
10799
+ camera.position[1],
10800
+ camera.position[2]
10801
+ );
10802
+ const t = vec3.transformMat4(vec3.create(), cameraPos, rotationView);
10803
+ vec3.negate(t, t);
10804
+ const view = mat4.clone(rotationView);
10805
+ view[12] = t[0];
10806
+ view[13] = t[1];
10807
+ view[14] = t[2];
10808
+ return view;
10809
+ }
10810
+ };
10811
+ var SKYBOX_POSITIONS2 = new Float32Array([
10812
+ // Front face (+X) - looking forward
10813
+ 1,
10814
+ -1,
10815
+ -1,
10816
+ 1,
10817
+ 1,
10818
+ -1,
10819
+ 1,
10820
+ 1,
10821
+ 1,
10822
+ 1,
10823
+ -1,
10824
+ -1,
10825
+ 1,
10826
+ 1,
10827
+ 1,
10828
+ 1,
10829
+ -1,
10830
+ 1,
10831
+ // Back face (-X) - looking backward
10832
+ -1,
10833
+ 1,
10834
+ -1,
10835
+ -1,
10836
+ -1,
10837
+ -1,
10838
+ -1,
10839
+ -1,
10840
+ 1,
10841
+ -1,
10842
+ 1,
10843
+ -1,
10844
+ -1,
10845
+ -1,
10846
+ 1,
10847
+ -1,
10848
+ 1,
10849
+ 1,
10850
+ // Left face (+Y) - looking left
10851
+ -1,
10852
+ 1,
10853
+ -1,
10854
+ -1,
10855
+ 1,
10856
+ 1,
10857
+ 1,
10858
+ 1,
10859
+ 1,
10860
+ -1,
10861
+ 1,
10862
+ -1,
10863
+ 1,
10864
+ 1,
10865
+ 1,
10866
+ 1,
10867
+ 1,
10868
+ -1,
10869
+ // Right face (-Y) - looking right
10870
+ 1,
10871
+ -1,
10872
+ -1,
10873
+ 1,
10874
+ -1,
10875
+ 1,
10876
+ -1,
10877
+ -1,
10878
+ 1,
10879
+ 1,
10880
+ -1,
10881
+ -1,
10882
+ -1,
10883
+ -1,
10884
+ 1,
10885
+ -1,
10886
+ -1,
10887
+ -1,
10888
+ // Top face (+Z) - looking up
10889
+ -1,
10890
+ -1,
10891
+ 1,
10892
+ 1,
10893
+ -1,
10894
+ 1,
10895
+ 1,
10896
+ 1,
10897
+ 1,
10898
+ -1,
10899
+ -1,
10900
+ 1,
10901
+ 1,
10902
+ 1,
10903
+ 1,
10904
+ -1,
10905
+ 1,
10906
+ 1,
10907
+ // Bottom face (-Z) - looking down
10908
+ -1,
10909
+ 1,
10910
+ -1,
10911
+ 1,
10912
+ 1,
10913
+ -1,
10914
+ 1,
10915
+ -1,
10916
+ -1,
10917
+ -1,
10918
+ 1,
10919
+ -1,
10920
+ 1,
10921
+ -1,
10922
+ -1,
10923
+ -1,
10924
+ -1,
10925
+ -1
10926
+ ]);
10927
+ var SkyboxPipeline3 = class {
10928
+ constructor(device, format) {
10929
+ this.device = device;
10930
+ this.format = format;
10931
+ this.matrixBuilder = new WebGPUMatrixBuilder();
10932
+ const module = device.createShaderModule({
10933
+ label: "skybox-shader",
10934
+ code: skybox_default
10935
+ });
10936
+ this.vertexBuffer = device.createBuffer({
10937
+ label: "skybox-vertex-buffer",
10938
+ size: SKYBOX_POSITIONS2.byteLength,
10939
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
10940
+ mappedAtCreation: true
10941
+ });
10942
+ new Float32Array(this.vertexBuffer.getMappedRange()).set(SKYBOX_POSITIONS2);
10943
+ this.vertexBuffer.unmap();
10944
+ this.uniformBuffer = device.createBuffer({
10945
+ label: "skybox-uniform-buffer",
10946
+ size: 80,
10947
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
10948
+ });
10949
+ this.sampler = device.createSampler({
10950
+ label: "skybox-sampler",
10951
+ minFilter: "linear",
10952
+ magFilter: "linear",
10953
+ addressModeU: "clamp-to-edge",
10954
+ addressModeV: "clamp-to-edge",
10955
+ addressModeW: "clamp-to-edge"
10956
+ });
10957
+ const bindGroupLayout = device.createBindGroupLayout({
10958
+ label: "skybox-bind-group-layout",
10959
+ entries: [
10960
+ {
10961
+ binding: 0,
10962
+ visibility: GPUShaderStage.VERTEX,
10963
+ buffer: { type: "uniform" }
10964
+ },
10965
+ {
10966
+ binding: 1,
10967
+ visibility: GPUShaderStage.FRAGMENT,
10968
+ texture: { viewDimension: "cube" }
10969
+ },
10970
+ {
10971
+ binding: 2,
10972
+ visibility: GPUShaderStage.FRAGMENT,
10973
+ sampler: { type: "filtering" }
10974
+ }
10975
+ ]
10976
+ });
10977
+ this.pipeline = device.createRenderPipeline({
10978
+ label: "skybox-pipeline",
10979
+ layout: device.createPipelineLayout({
10980
+ bindGroupLayouts: [bindGroupLayout]
10981
+ }),
10982
+ vertex: {
10983
+ module,
10984
+ entryPoint: "vertexMain",
10985
+ buffers: [{
10986
+ arrayStride: 12,
10987
+ // vec3<f32>
10988
+ attributes: [{
10989
+ shaderLocation: 0,
10990
+ offset: 0,
10991
+ format: "float32x3"
10992
+ }]
10993
+ }]
10994
+ },
10995
+ fragment: {
10996
+ module,
10997
+ entryPoint: "fragmentMain",
10998
+ targets: [{
10999
+ format: this.format,
11000
+ blend: {
11001
+ // Standard alpha blending (though skybox is usually opaque)
11002
+ color: {
11003
+ srcFactor: "src-alpha",
11004
+ dstFactor: "one-minus-src-alpha",
11005
+ operation: "add"
11006
+ },
11007
+ alpha: {
11008
+ srcFactor: "one",
11009
+ dstFactor: "one-minus-src-alpha",
11010
+ operation: "add"
11011
+ }
11012
+ }
11013
+ }]
11014
+ },
11015
+ depthStencil: {
11016
+ format: "depth24plus",
11017
+ depthWriteEnabled: false,
11018
+ depthCompare: "always"
11019
+ // Skybox is usually drawn first or last. If first, always pass.
11020
+ },
11021
+ primitive: {
11022
+ topology: "triangle-list",
11023
+ cullMode: "none"
11024
+ // Inside the box
11025
+ }
11026
+ });
11027
+ this.bindGroupHelper = new SkyboxBindGroupHelper(device, bindGroupLayout, this.uniformBuffer, this.sampler);
11028
+ }
11029
+ draw(passEncoder, options) {
11030
+ let viewProjection;
11031
+ let useNative = 0;
11032
+ if (options.cameraState) {
11033
+ const view = this.matrixBuilder.buildViewMatrix(options.cameraState);
11034
+ const projection = this.matrixBuilder.buildProjectionMatrix(options.cameraState);
11035
+ view[12] = 0;
11036
+ view[13] = 0;
11037
+ view[14] = 0;
11038
+ const vp = mat4.create();
11039
+ mat4.multiply(vp, projection, view);
11040
+ viewProjection = vp;
11041
+ useNative = 1;
11042
+ } else if (options.viewProjection) {
11043
+ viewProjection = options.viewProjection;
11044
+ useNative = 0;
11045
+ } else {
11046
+ throw new Error("SkyboxPipeline: Either cameraState or viewProjection must be provided");
11047
+ }
11048
+ const uniformData = new Float32Array(20);
11049
+ uniformData.set(viewProjection);
11050
+ uniformData[16] = options.scroll[0];
11051
+ uniformData[17] = options.scroll[1];
11052
+ uniformData[18] = useNative;
11053
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
11054
+ const bindGroup = this.bindGroupHelper.getBindGroup(options.cubemap);
11055
+ passEncoder.setPipeline(this.pipeline);
11056
+ passEncoder.setBindGroup(0, bindGroup);
11057
+ passEncoder.setVertexBuffer(0, this.vertexBuffer);
11058
+ passEncoder.draw(SKYBOX_POSITIONS2.length / 3);
11059
+ }
11060
+ destroy() {
11061
+ this.vertexBuffer.destroy();
11062
+ this.uniformBuffer.destroy();
11063
+ }
11064
+ };
11065
+ var SkyboxBindGroupHelper = class {
11066
+ constructor(device, layout, uniformBuffer, sampler) {
11067
+ this.device = device;
11068
+ this.layout = layout;
11069
+ this.uniformBuffer = uniformBuffer;
11070
+ this.sampler = sampler;
11071
+ this.bindGroupCache = /* @__PURE__ */ new Map();
11072
+ }
11073
+ getBindGroup(cubemap) {
11074
+ if (this.bindGroupCache.has(cubemap)) {
11075
+ return this.bindGroupCache.get(cubemap);
11076
+ }
11077
+ const bindGroup = this.device.createBindGroup({
11078
+ layout: this.layout,
11079
+ entries: [
11080
+ {
11081
+ binding: 0,
11082
+ resource: { buffer: this.uniformBuffer }
11083
+ },
11084
+ {
11085
+ binding: 1,
11086
+ resource: cubemap.createView()
11087
+ },
11088
+ {
11089
+ binding: 2,
11090
+ resource: this.sampler
11091
+ }
11092
+ ]
11093
+ });
11094
+ this.bindGroupCache.set(cubemap, bindGroup);
11095
+ return bindGroup;
11096
+ }
11097
+ };
11098
+
11099
+ // raw-loader:/home/runner/work/quake2/quake2/quake2ts/packages/engine/src/render/webgpu/shaders/bsp.wgsl
11100
+ var bsp_default = "struct DLight {\n position: vec3<f32>,\n intensity: f32,\n color: vec3<f32>,\n padding: f32,\n}\n\nstruct FrameUniforms {\n viewProjection: mat4x4<f32>, // 0-64\n cameraPosition: vec3<f32>, // 64-76\n pad0: f32, // 76-80 (Explicit padding to align next field to 80)\n time: f32, // 80-84\n brightness: f32, // 84-88\n gamma: f32, // 88-92\n ambient: f32, // 92-96\n numDlights: u32, // 96-100\n fullbright: u32, // 100-104\n pad1: vec2<f32>, // 104-112 (Aligns next field to 112)\n pad2: vec4<f32>, // 112-128 (Aligns next field to 128)\n dlights: array<DLight, 32>, // Starts at 128\n}\n\nstruct SurfaceUniforms {\n texScroll: vec2<f32>,\n lightmapScroll: vec2<f32>,\n lightStyleFactors: vec4<f32>,\n styleLayerMapping: vec4<f32>,\n solidColor: vec4<f32>,\n alpha: f32,\n applyLightmap: u32,\n warp: u32,\n lightmapOnly: u32,\n renderMode: u32, // 0: Texture, 1: Solid, 2: Faceted\n padding: vec3<f32>,\n}\n\n@group(0) @binding(0) var<uniform> frame: FrameUniforms;\n@group(1) @binding(0) var<uniform> surface: SurfaceUniforms;\n@group(2) @binding(0) var diffuseMap: texture_2d<f32>;\n@group(2) @binding(1) var diffuseSampler: sampler;\n@group(2) @binding(2) var lightmapAtlas: texture_2d<f32>;\n@group(2) @binding(3) var lightmapSampler: sampler;\n\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) texCoord: vec2<f32>,\n @location(2) lightmapCoord: vec2<f32>,\n @location(3) lightmapStep: f32,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) texCoord: vec2<f32>,\n @location(1) lightmapCoord: vec2<f32>,\n @location(2) lightmapStep: f32,\n @location(3) worldPos: vec3<f32>,\n @location(4) screenPos: vec4<f32>,\n}\n\n@vertex\nfn vertexMain(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n\n var pos = input.position;\n var tex = input.texCoord;\n var lm = input.lightmapCoord;\n\n // Vertex Warping\n if (surface.warp != 0u) {\n let amp = 0.125;\n let s = tex.x + sin((tex.y * 0.125 + frame.time) * 1.0) * amp;\n let t = tex.y + sin((tex.x * 0.125 + frame.time) * 1.0) * amp;\n tex = vec2<f32>(s, t);\n }\n\n output.texCoord = tex + surface.texScroll;\n output.lightmapCoord = lm + surface.lightmapScroll;\n output.lightmapStep = input.lightmapStep;\n output.worldPos = pos;\n output.position = frame.viewProjection * vec4<f32>(pos, 1.0);\n output.screenPos = output.position;\n\n return output;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {\n var finalColor: vec4<f32>;\n\n if (surface.renderMode == 0u) {\n // TEXTURED MODE\n var base = vec4<f32>(1.0, 1.0, 1.0, 1.0);\n if (surface.lightmapOnly == 0u) {\n base = textureSample(diffuseMap, diffuseSampler, input.texCoord);\n }\n\n var totalLight = vec3<f32>(0.0, 0.0, 0.0);\n\n if (frame.fullbright != 0u) {\n totalLight = vec3<f32>(1.0, 1.0, 1.0);\n } else {\n // Apply Lightmaps\n if (surface.applyLightmap != 0u) {\n var hasLight = false;\n for (var i = 0; i < 4; i++) {\n let layer = surface.styleLayerMapping[i];\n let factor = surface.lightStyleFactors[i];\n\n if (layer >= -0.5) {\n let offset = vec2<f32>(0.0, layer * input.lightmapStep);\n totalLight += textureSample(lightmapAtlas, lightmapSampler, input.lightmapCoord + offset).rgb * factor;\n hasLight = true;\n }\n }\n\n // If lightmap enabled but no active layers, fallback to white?\n // Quake 2 logic usually implies at least one style is active if surfaced has SURF_LIGHT\n // But if we have no lightmap data, we start black.\n if (!hasLight) {\n // Fallback to avoid pitch black if lightmap intended but missing?\n // totalLight = vec3<f32>(1.0, 1.0, 1.0);\n }\n }\n\n // Apply Dynamic Lights\n for (var i = 0u; i < 32u; i++) {\n if (i >= frame.numDlights) {\n break;\n }\n let dlight = frame.dlights[i];\n let dist = distance(input.worldPos, dlight.position);\n\n if (dist < dlight.intensity) {\n let contribution = (dlight.intensity - dist) * (1.0 / 255.0);\n totalLight += dlight.color * contribution;\n }\n }\n }\n\n totalLight = max(totalLight, vec3<f32>(frame.ambient));\n totalLight *= frame.brightness;\n base = vec4<f32>(base.rgb * totalLight, base.a);\n\n // Gamma correction\n if (frame.gamma != 1.0) {\n base = vec4<f32>(pow(base.rgb, vec3<f32>(1.0 / frame.gamma)), base.a);\n }\n\n finalColor = vec4<f32>(base.rgb, base.a * surface.alpha);\n\n } else {\n // SOLID / WIREFRAME / FACETED\n var color = surface.solidColor.rgb;\n if (surface.renderMode == 2u) {\n // FACETED\n let fdx = dpdx(input.worldPos);\n let fdy = dpdy(input.worldPos);\n let faceNormal = normalize(cross(fdx, fdy));\n let lightDir = normalize(vec3<f32>(0.5, 0.5, 1.0));\n let diff = max(dot(faceNormal, lightDir), 0.2);\n color *= diff;\n }\n finalColor = vec4<f32>(color, surface.solidColor.a * surface.alpha);\n }\n\n // Alpha Test (Simple discard for fence textures if alpha is very low)\n if (finalColor.a < 0.01) {\n discard;\n }\n\n return finalColor;\n}\n";
11101
+
11102
+ // src/render/webgpu/pipelines/bspPipeline.ts
11103
+ var DEFAULT_STYLE_INDICES2 = [0, 255, 255, 255];
11104
+ var DEFAULT_STYLE_LAYERS2 = [0, -1, -1, -1];
11105
+ function resolveLightStyles2(styleIndices = DEFAULT_STYLE_INDICES2, styleValues = []) {
11106
+ const factors = new Float32Array(4);
11107
+ for (let i = 0; i < 4; i += 1) {
11108
+ const styleIndex = styleIndices[i] ?? 255;
11109
+ if (styleIndex === 255) {
11110
+ factors[i] = 0;
11111
+ continue;
11112
+ }
11113
+ const value = styleValues[styleIndex];
11114
+ factors[i] = value !== void 0 ? value : 1;
11115
+ }
11116
+ return factors;
11117
+ }
11118
+ function computeFlowOffset2(timeSeconds) {
11119
+ const cycle = timeSeconds * 0.25 % 1;
11120
+ return [-cycle, 0];
11121
+ }
11122
+ function deriveSurfaceRenderState2(surfaceFlags = SURF_NONE, timeSeconds = 0) {
11123
+ const flowing = (surfaceFlags & SURF_FLOWING) !== 0;
11124
+ const warp = (surfaceFlags & SURF_WARP) !== 0;
11125
+ const sky = (surfaceFlags & SURF_SKY) !== 0;
11126
+ const trans33 = (surfaceFlags & SURF_TRANS33) !== 0;
11127
+ const trans66 = (surfaceFlags & SURF_TRANS66) !== 0;
11128
+ const alpha = trans33 ? 0.33 : trans66 ? 0.66 : 1;
11129
+ const blend = trans33 || trans66 || warp;
11130
+ const depthWrite = !blend && !sky;
11131
+ const flowOffset = flowing ? computeFlowOffset2(timeSeconds) : [0, 0];
11132
+ return {
11133
+ alpha,
11134
+ blend,
11135
+ depthWrite,
11136
+ warp,
11137
+ flowOffset,
11138
+ sky
11139
+ };
11140
+ }
11141
+ var BspSurfacePipeline3 = class {
11142
+ constructor(device, format, depthFormat) {
11143
+ // Cache for texture bind groups
11144
+ this.textureBindGroupCache = /* @__PURE__ */ new Map();
11145
+ this.device = device;
11146
+ const frameBufferSize = 2048;
11147
+ const surfaceBufferSize = 256;
11148
+ this.frameUniformBuffer = device.createBuffer({
11149
+ size: frameBufferSize,
11150
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
11151
+ });
11152
+ this.surfaceUniformBuffer = device.createBuffer({
11153
+ size: surfaceBufferSize,
11154
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
11155
+ });
11156
+ this.frameBindGroupLayout = device.createBindGroupLayout({
11157
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]
11158
+ });
11159
+ this.surfaceBindGroupLayout = device.createBindGroupLayout({
11160
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]
11161
+ });
11162
+ this.textureBindGroupLayout = device.createBindGroupLayout({
11163
+ entries: [
11164
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
11165
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
11166
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
11167
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
11168
+ ]
11169
+ });
11170
+ this.defaultSampler = createLinearSampler(device);
11171
+ this.defaultWhiteTexture = new Texture2D2(device, {
11172
+ width: 1,
11173
+ height: 1,
11174
+ format: "rgba8unorm",
11175
+ label: "bsp-default-white"
11176
+ });
11177
+ this.defaultWhiteTexture.upload(new Uint8Array([255, 255, 255, 255]));
11178
+ this.defaultBindGroup = device.createBindGroup({
11179
+ layout: this.textureBindGroupLayout,
11180
+ entries: [
11181
+ { binding: 0, resource: this.defaultWhiteTexture.createView() },
11182
+ { binding: 1, resource: this.defaultSampler.sampler },
11183
+ { binding: 2, resource: this.defaultWhiteTexture.createView() },
11184
+ // Use white texture as lightmap fallback
11185
+ { binding: 3, resource: this.defaultSampler.sampler }
11186
+ ]
11187
+ });
11188
+ this.createPipelines(format, depthFormat);
11189
+ this.frameBindGroup = device.createBindGroup({
11190
+ layout: this.frameBindGroupLayout,
11191
+ entries: [{ binding: 0, resource: { buffer: this.frameUniformBuffer } }]
11192
+ });
11193
+ }
11194
+ createPipelines(format, depthFormat) {
11195
+ const module = this.device.createShaderModule({
11196
+ code: bsp_default
11197
+ });
11198
+ const pipelineLayout = this.device.createPipelineLayout({
11199
+ bindGroupLayouts: [
11200
+ this.frameBindGroupLayout,
11201
+ this.surfaceBindGroupLayout,
11202
+ this.textureBindGroupLayout
11203
+ ]
11204
+ });
11205
+ const vertexState = {
11206
+ module,
11207
+ entryPoint: "vertexMain",
11208
+ buffers: [
11209
+ {
11210
+ // Interleaved buffer: pos(3), tex(2), lm(2), step(1) -> 8 floats -> 32 bytes
11211
+ arrayStride: 32,
11212
+ attributes: [
11213
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
11214
+ // Position
11215
+ { shaderLocation: 1, offset: 12, format: "float32x2" },
11216
+ // TexCoord
11217
+ { shaderLocation: 2, offset: 20, format: "float32x2" },
11218
+ // LightmapCoord
11219
+ { shaderLocation: 3, offset: 28, format: "float32" }
11220
+ // LightmapStep
11221
+ ]
11222
+ }
11223
+ ]
11224
+ };
11225
+ this.pipeline = this.device.createRenderPipeline({
11226
+ layout: pipelineLayout,
11227
+ vertex: vertexState,
11228
+ fragment: {
11229
+ module,
11230
+ entryPoint: "fragmentMain",
11231
+ targets: [{
11232
+ format,
11233
+ blend: {
11234
+ color: {
11235
+ srcFactor: "src-alpha",
11236
+ dstFactor: "one-minus-src-alpha",
11237
+ operation: "add"
11238
+ },
11239
+ alpha: {
11240
+ srcFactor: "one",
11241
+ dstFactor: "one-minus-src-alpha",
11242
+ operation: "add"
11243
+ }
11244
+ }
11245
+ }]
11246
+ },
11247
+ primitive: {
11248
+ topology: "triangle-list",
11249
+ cullMode: "back"
11250
+ // Quake 2 is usually back-face culled
11251
+ },
11252
+ depthStencil: {
11253
+ format: depthFormat,
11254
+ depthWriteEnabled: true,
11255
+ depthCompare: "less"
11256
+ }
11257
+ });
11258
+ this.pipelineWireframe = this.device.createRenderPipeline({
11259
+ layout: pipelineLayout,
11260
+ vertex: vertexState,
11261
+ fragment: {
11262
+ module,
11263
+ entryPoint: "fragmentMain",
11264
+ targets: [{ format, writeMask: GPUColorWrite.ALL }]
11265
+ },
11266
+ primitive: {
11267
+ topology: "line-list"
11268
+ },
11269
+ depthStencil: {
11270
+ format: depthFormat,
11271
+ depthWriteEnabled: true,
11272
+ depthCompare: "less"
11273
+ }
11274
+ });
11275
+ }
11276
+ bind(passEncoder, options) {
11277
+ const {
11278
+ modelViewProjection,
11279
+ styleIndices = DEFAULT_STYLE_INDICES2,
11280
+ styleLayers = DEFAULT_STYLE_LAYERS2,
11281
+ styleValues = [],
11282
+ diffuseTexture,
11283
+ diffuseSampler,
11284
+ lightmapTexture,
11285
+ lightmapSampler,
11286
+ surfaceFlags = SURF_NONE,
11287
+ timeSeconds = 0,
11288
+ texScroll,
11289
+ alpha,
11290
+ warp,
11291
+ dlights = [],
11292
+ renderMode,
11293
+ lightmapOnly,
11294
+ brightness = 1,
11295
+ gamma = 1,
11296
+ fullbright = false,
11297
+ ambient = 0,
11298
+ cameraPosition = [0, 0, 0]
11299
+ } = options;
11300
+ const state = deriveSurfaceRenderState2(surfaceFlags, timeSeconds);
11301
+ const styles = resolveLightStyles2(styleIndices, styleValues);
11302
+ const finalScrollX = texScroll ? texScroll[0] : state.flowOffset[0];
11303
+ const finalScrollY = texScroll ? texScroll[1] : state.flowOffset[1];
11304
+ const finalAlpha = alpha !== void 0 ? alpha : state.alpha;
11305
+ const finalWarp = warp !== void 0 ? warp : state.warp;
11306
+ const frameData = new Float32Array(512);
11307
+ frameData.set(modelViewProjection, 0);
11308
+ frameData.set(cameraPosition, 16);
11309
+ frameData[19] = 0;
11310
+ frameData[20] = timeSeconds;
11311
+ frameData[21] = brightness;
11312
+ frameData[22] = gamma;
11313
+ frameData[23] = ambient;
11314
+ const numDlights = Math.min(dlights.length, MAX_DLIGHTS);
11315
+ const numDlightsView = new Uint32Array(frameData.buffer, 24 * 4, 1);
11316
+ numDlightsView[0] = numDlights;
11317
+ const fullbrightView = new Uint32Array(frameData.buffer, 25 * 4, 1);
11318
+ fullbrightView[0] = fullbright ? 1 : 0;
11319
+ let lightOffset = 32;
11320
+ for (let i = 0; i < numDlights; i++) {
11321
+ const l = dlights[i];
11322
+ frameData[lightOffset + 0] = l.origin.x;
11323
+ frameData[lightOffset + 1] = l.origin.y;
11324
+ frameData[lightOffset + 2] = l.origin.z;
11325
+ frameData[lightOffset + 3] = l.intensity;
11326
+ frameData[lightOffset + 4] = l.color.x;
11327
+ frameData[lightOffset + 5] = l.color.y;
11328
+ frameData[lightOffset + 6] = l.color.z;
11329
+ frameData[lightOffset + 7] = 0;
11330
+ lightOffset += 8;
11331
+ }
11332
+ this.device.queue.writeBuffer(this.frameUniformBuffer, 0, frameData, 0, lightOffset);
11333
+ const surfaceData = new Float32Array(32);
11334
+ surfaceData[0] = finalScrollX;
11335
+ surfaceData[1] = finalScrollY;
11336
+ surfaceData[2] = state.flowOffset[0];
11337
+ surfaceData[3] = state.flowOffset[1];
11338
+ surfaceData.set(styles, 4);
11339
+ surfaceData.set(styleLayers, 8);
11340
+ let modeInt = 0;
11341
+ let color = [1, 1, 1, 1];
11342
+ if (renderMode) {
11343
+ if (renderMode.mode === "solid" || renderMode.mode === "wireframe") {
11344
+ modeInt = 1;
11345
+ } else if (renderMode.mode === "solid-faceted") {
11346
+ modeInt = 2;
11347
+ }
11348
+ if (renderMode.color) {
11349
+ color = [...renderMode.color];
11350
+ } else if (renderMode.generateRandomColor) {
11351
+ color = [1, 1, 1, 1];
11352
+ }
11353
+ }
11354
+ surfaceData.set(color, 12);
11355
+ surfaceData[16] = finalAlpha;
11356
+ const surfaceUint = new Uint32Array(surfaceData.buffer);
11357
+ const applyLightmap = !state.sky && lightmapSampler !== void 0 && !finalWarp;
11358
+ surfaceUint[17] = applyLightmap ? 1 : 0;
11359
+ surfaceUint[18] = finalWarp ? 1 : 0;
11360
+ surfaceUint[19] = lightmapOnly ? 1 : 0;
11361
+ surfaceUint[20] = modeInt;
11362
+ this.device.queue.writeBuffer(this.surfaceUniformBuffer, 0, surfaceData);
11363
+ const surfaceBindGroup = this.device.createBindGroup({
11364
+ layout: this.surfaceBindGroupLayout,
11365
+ entries: [{ binding: 0, resource: { buffer: this.surfaceUniformBuffer } }]
11366
+ });
11367
+ if (diffuseTexture && diffuseSampler && lightmapTexture && lightmapSampler) {
11368
+ const textureBindGroup = this.device.createBindGroup({
11369
+ layout: this.textureBindGroupLayout,
11370
+ entries: [
11371
+ { binding: 0, resource: diffuseTexture },
11372
+ { binding: 1, resource: diffuseSampler },
11373
+ { binding: 2, resource: lightmapTexture },
11374
+ { binding: 3, resource: lightmapSampler }
11375
+ ]
11376
+ });
11377
+ passEncoder.setBindGroup(2, textureBindGroup);
11378
+ } else {
11379
+ passEncoder.setBindGroup(2, this.defaultBindGroup);
11380
+ }
11381
+ const pipeline = renderMode && renderMode.mode === "wireframe" ? this.pipelineWireframe : this.pipeline;
11382
+ passEncoder.setPipeline(pipeline);
11383
+ passEncoder.setBindGroup(0, this.frameBindGroup);
11384
+ passEncoder.setBindGroup(1, surfaceBindGroup);
11385
+ return state;
11386
+ }
11387
+ draw(passEncoder, geometry, renderMode) {
11388
+ if (!geometry.gpuIndexBuffer) return;
11389
+ if (renderMode && renderMode.mode === "wireframe") ; else {
11390
+ passEncoder.setVertexBuffer(0, geometry.gpuVertexBuffer);
11391
+ passEncoder.setIndexBuffer(geometry.gpuIndexBuffer, "uint16");
11392
+ passEncoder.drawIndexed(geometry.indexCount);
11393
+ }
11394
+ }
11395
+ destroy() {
11396
+ this.frameUniformBuffer.destroy();
11397
+ this.surfaceUniformBuffer.destroy();
11398
+ this.defaultWhiteTexture.destroy();
11399
+ }
11400
+ };
11401
+
11402
+ // src/render/webgpu/renderer.ts
11403
+ var WebGPURendererImpl = class {
11404
+ constructor(context, frameRenderer, pipelines) {
11405
+ this.context = context;
11406
+ this.frameRenderer = frameRenderer;
11407
+ this.pipelines = pipelines;
11408
+ this.type = "webgpu";
11409
+ // Texture cache for registered pics
11410
+ this.picCache = /* @__PURE__ */ new Map();
11411
+ this.font = null;
11412
+ // 2D rendering state
11413
+ this.is2DActive = false;
11414
+ this.whiteTexture = new Texture2D2(context.device, {
11415
+ width: 1,
11416
+ height: 1,
11417
+ format: context.format,
11418
+ label: "white-texture"
11419
+ });
11420
+ this.whiteTexture.upload(new Uint8Array([255, 255, 255, 255]));
11421
+ this.collisionVis = null;
11422
+ this.debug = null;
11423
+ this.particleSystem = null;
11424
+ }
11425
+ get device() {
11426
+ return this.context.device;
11427
+ }
11428
+ get width() {
11429
+ return this.context.width;
11430
+ }
11431
+ get height() {
11432
+ return this.context.height;
11433
+ }
11434
+ // =========================================================================
11435
+ // Frame Rendering
11436
+ // =========================================================================
11437
+ renderFrame(options, entities = [], renderOptions) {
11438
+ const localOptions = options;
11439
+ let culledLights = localOptions.dlights;
11440
+ if (localOptions.dlights && localOptions.dlights.length > 0) {
11441
+ const viewProjection = new Float32Array(localOptions.camera.viewProjectionMatrix);
11442
+ const frustumPlanes = extractFrustumPlanes(viewProjection);
11443
+ const cameraPos = { x: localOptions.camera.position[0], y: localOptions.camera.position[1], z: localOptions.camera.position[2] };
11444
+ culledLights = cullLights(
11445
+ localOptions.dlights,
11446
+ frustumPlanes,
11447
+ cameraPos,
11448
+ 32
11449
+ // Max lights
11450
+ );
11451
+ }
11452
+ const augmentedOptions = {
11453
+ ...localOptions,
11454
+ dlights: culledLights
11455
+ };
11456
+ this.frameRenderer.renderFrame(augmentedOptions);
11457
+ }
11458
+ // =========================================================================
11459
+ // Geometry Management
11460
+ // =========================================================================
11461
+ uploadBspGeometry(surfaces) {
11462
+ for (const surface of surfaces) {
11463
+ if (surface.gpuVertexBuffer && surface.gpuIndexBuffer) continue;
11464
+ const vb = new VertexBuffer2(this.device, {
11465
+ size: surface.vertexData.byteLength,
11466
+ label: `bsp-surface-vb-${surface.texture}`
11467
+ });
11468
+ vb.write(surface.vertexData);
11469
+ const ib = new IndexBuffer2(this.device, {
11470
+ size: surface.indexData.byteLength,
11471
+ label: `bsp-surface-ib-${surface.texture}`
11472
+ });
11473
+ ib.write(surface.indexData);
11474
+ const mutableSurface = surface;
11475
+ mutableSurface.gpuVertexBuffer = vb.buffer;
11476
+ mutableSurface.gpuIndexBuffer = ib.buffer;
11477
+ }
11478
+ }
11479
+ // =========================================================================
11480
+ // Texture Management
11481
+ // =========================================================================
11482
+ async registerPic(name, data) {
11483
+ if (this.picCache.has(name)) {
11484
+ return this.picCache.get(name);
11485
+ }
11486
+ const texture = new Texture2D2(this.device, {
11487
+ width: 256,
11488
+ height: 256,
11489
+ format: this.context.format,
11490
+ label: `pic-${name}`
11491
+ });
11492
+ texture.upload(data);
11493
+ this.picCache.set(name, texture);
11494
+ return texture;
11495
+ }
11496
+ registerTexture(name, texture) {
11497
+ if (this.picCache.has(name)) {
11498
+ return this.picCache.get(name);
11499
+ }
11500
+ const gpuTexture = new Texture2D2(this.device, {
11501
+ width: texture.width,
11502
+ height: texture.height,
11503
+ format: this.context.format,
11504
+ mipLevelCount: texture.levels.length,
11505
+ label: `texture-${name}`
11506
+ });
11507
+ for (const mipLevel of texture.levels) {
11508
+ gpuTexture.upload(mipLevel.rgba, {
11509
+ width: mipLevel.width,
11510
+ height: mipLevel.height,
11511
+ mipLevel: mipLevel.level
11512
+ });
11513
+ }
11514
+ this.picCache.set(name, gpuTexture);
11515
+ return gpuTexture;
11516
+ }
11517
+ // =========================================================================
11518
+ // 2D Drawing API
11519
+ // =========================================================================
11520
+ begin2D() {
11521
+ this.frameRenderer.begin2DPass();
11522
+ this.is2DActive = true;
11523
+ }
11524
+ end2D() {
11525
+ this.frameRenderer.end2DPass();
11526
+ this.is2DActive = false;
11527
+ }
11528
+ drawPic(x, y, pic, color = [1, 1, 1, 1]) {
11529
+ if (!this.is2DActive) {
11530
+ throw new Error("drawPic called outside begin2D/end2D");
11531
+ }
11532
+ this.pipelines.sprite.drawTexturedQuad(
11533
+ x,
11534
+ y,
11535
+ pic.width,
11536
+ pic.height,
11537
+ pic,
11538
+ 0,
11539
+ 0,
11540
+ 1,
11541
+ 1,
11542
+ color
11543
+ );
11544
+ }
11545
+ drawfillRect(x, y, width, height, color) {
11546
+ if (!this.is2DActive) {
11547
+ throw new Error("drawfillRect called outside begin2D/end2D");
11548
+ }
11549
+ this.pipelines.sprite.drawSolidRect(x, y, width, height, color);
11550
+ }
11551
+ drawString(x, y, text, color = [1, 1, 1, 1]) {
11552
+ if (!this.is2DActive) {
11553
+ throw new Error("drawString called outside begin2D/end2D");
11554
+ }
11555
+ if (!this.font) {
11556
+ return;
11557
+ }
11558
+ const segments = parseColorString(text);
11559
+ let currentX = x;
11560
+ const charWidth = 8;
11561
+ const charHeight = 8;
11562
+ for (const segment of segments) {
11563
+ const segmentColor = segment.color || color;
11564
+ for (const char of segment.text) {
11565
+ const code = char.charCodeAt(0);
11566
+ const col = code % 16;
11567
+ const row = Math.floor(code / 16);
11568
+ const u0 = col / 16;
11569
+ const v0 = row / 16;
11570
+ const u1 = (col + 1) / 16;
11571
+ const v1 = (row + 1) / 16;
11572
+ this.pipelines.sprite.drawTexturedQuad(
11573
+ currentX,
11574
+ y,
11575
+ charWidth,
11576
+ charHeight,
11577
+ this.font,
11578
+ u0,
11579
+ v0,
11580
+ u1,
11581
+ v1,
11582
+ segmentColor
11583
+ );
11584
+ currentX += charWidth;
11585
+ }
11586
+ }
11587
+ }
11588
+ drawCenterString(y, text) {
11589
+ if (!this.is2DActive) {
11590
+ throw new Error("drawCenterString called outside begin2D/end2D");
11591
+ }
11592
+ const stripped = text.replace(/\^[0-9]/g, "");
11593
+ const charWidth = 8;
11594
+ const width = stripped.length * charWidth;
11595
+ const x = (this.width - width) / 2;
11596
+ this.drawString(x, y, text);
11597
+ }
11598
+ // =========================================================================
11599
+ // Entity Highlighting (Stubs)
11600
+ // =========================================================================
11601
+ setEntityHighlight(entityId, color) {
11602
+ }
11603
+ clearEntityHighlight(entityId) {
11604
+ }
11605
+ highlightSurface(faceIndex, color) {
11606
+ }
11607
+ removeSurfaceHighlight(faceIndex) {
11608
+ }
11609
+ // =========================================================================
11610
+ // Render Settings (Stubs)
11611
+ // =========================================================================
11612
+ setDebugMode(mode) {
11613
+ }
11614
+ setBrightness(value) {
11615
+ }
11616
+ setGamma(value) {
11617
+ }
11618
+ setFullbright(enabled) {
11619
+ }
11620
+ setAmbient(value) {
11621
+ }
11622
+ setLightStyle(index, pattern) {
11623
+ }
11624
+ setUnderwaterWarp(enabled) {
11625
+ }
11626
+ setBloom(enabled) {
11627
+ }
11628
+ setBloomIntensity(value) {
11629
+ }
11630
+ setLodBias(bias) {
11631
+ }
11632
+ // =========================================================================
11633
+ // Instanced Rendering (Stub)
11634
+ // =========================================================================
11635
+ renderInstanced(model, instances) {
11636
+ }
11637
+ // =========================================================================
11638
+ // Performance and Memory
11639
+ // =========================================================================
11640
+ getPerformanceReport() {
11641
+ return {
11642
+ frameTimeMs: 0,
11643
+ gpuTimeMs: 0,
11644
+ cpuFrameTimeMs: 0,
11645
+ drawCalls: 0,
11646
+ triangles: 0,
11647
+ vertices: 0,
11648
+ textureBinds: 0,
11649
+ shaderSwitches: 0,
11650
+ visibleSurfaces: 0,
11651
+ culledSurfaces: 0,
11652
+ visibleEntities: 0,
11653
+ culledEntities: 0,
11654
+ memoryUsageMB: {
11655
+ textures: 0,
11656
+ geometry: 0,
11657
+ total: 0
11658
+ }
11659
+ };
11660
+ }
11661
+ getMemoryUsage() {
11662
+ return {
11663
+ texturesBytes: 0,
11664
+ geometryBytes: 0,
11665
+ shadersBytes: 0,
11666
+ buffersBytes: 0,
11667
+ totalBytes: 0
11668
+ };
11669
+ }
11670
+ // =========================================================================
11671
+ // Lifecycle
11672
+ // =========================================================================
11673
+ resize(width, height) {
11674
+ if (this.context.context && !this.context.isHeadless) {
11675
+ this.context.width = width;
11676
+ this.context.height = height;
11677
+ } else {
11678
+ this.context.width = width;
11679
+ this.context.height = height;
11680
+ }
11681
+ }
11682
+ dispose() {
11683
+ this.pipelines.sprite.destroy();
11684
+ this.pipelines.skybox.destroy();
11685
+ this.pipelines.bsp.destroy();
11686
+ for (const texture of this.picCache.values()) {
11687
+ texture.destroy();
11688
+ }
11689
+ this.picCache.clear();
11690
+ this.whiteTexture.destroy();
11691
+ this.context.device.destroy();
11692
+ }
11693
+ destroy() {
11694
+ this.dispose();
11695
+ }
11696
+ };
11697
+ async function createWebGPURenderer(canvas, options) {
11698
+ const context = await createWebGPUContext(canvas, options);
11699
+ const spriteRenderer = new SpriteRenderer2(context.device, context.format);
11700
+ const skyboxPipeline = new SkyboxPipeline3(context.device, context.format);
11701
+ const bspPipeline = new BspSurfacePipeline3(
11702
+ context.device,
11703
+ context.format,
11704
+ context.depthFormat || "depth24plus"
11705
+ );
11706
+ const pipelines = {
11707
+ sprite: spriteRenderer,
11708
+ skybox: skyboxPipeline,
11709
+ bsp: bspPipeline
11710
+ };
11711
+ if (canvas) {
11712
+ context.width = canvas.width;
11713
+ context.height = canvas.height;
11714
+ } else {
11715
+ context.width = 800;
11716
+ context.height = 600;
11717
+ }
11718
+ const frameRenderer = new FrameRenderer(context, pipelines);
11719
+ return new WebGPURendererImpl(context, frameRenderer, pipelines);
11720
+ }
9616
11721
  var DemoReader = class {
9617
11722
  constructor(buffer) {
9618
11723
  this.messageOffsets = [];
@@ -17429,83 +19534,6 @@ function validateEntity(entity) {
17429
19534
  };
17430
19535
  }
17431
19536
 
17432
- // src/render/webgpu/context.ts
17433
- async function createWebGPUContext(canvas, options) {
17434
- if (!navigator.gpu) {
17435
- throw new Error("WebGPU is not supported in this environment");
17436
- }
17437
- const adapter = await navigator.gpu.requestAdapter({
17438
- powerPreference: options?.powerPreference || "high-performance"
17439
- });
17440
- if (!adapter) {
17441
- throw new Error("No appropriate GPUAdapter found");
17442
- }
17443
- if (options?.requiredFeatures) {
17444
- for (const feature of options.requiredFeatures) {
17445
- if (!adapter.features.has(feature)) {
17446
- throw new Error(`Required feature not available: ${feature}`);
17447
- }
17448
- }
17449
- }
17450
- const deviceDescriptor = {
17451
- requiredFeatures: options?.requiredFeatures,
17452
- requiredLimits: options?.requiredLimits
17453
- };
17454
- const device = await adapter.requestDevice(deviceDescriptor);
17455
- let context;
17456
- let format = "rgba8unorm";
17457
- const depthFormat = "depth24plus";
17458
- let isHeadless = true;
17459
- let width = options?.width || 800;
17460
- let height = options?.height || 600;
17461
- if (canvas) {
17462
- context = canvas.getContext("webgpu");
17463
- if (!context) {
17464
- throw new Error("Failed to get WebGPU context from canvas");
17465
- }
17466
- isHeadless = false;
17467
- format = navigator.gpu.getPreferredCanvasFormat();
17468
- width = canvas.width;
17469
- height = canvas.height;
17470
- context.configure({
17471
- device,
17472
- format,
17473
- alphaMode: "opaque"
17474
- // Standard for game rendering
17475
- });
17476
- }
17477
- const features = /* @__PURE__ */ new Set();
17478
- for (const feature of adapter.features) {
17479
- features.add(feature);
17480
- }
17481
- return {
17482
- adapter,
17483
- device,
17484
- context,
17485
- format,
17486
- depthFormat,
17487
- features,
17488
- limits: device.limits,
17489
- isHeadless,
17490
- width,
17491
- height
17492
- };
17493
- }
17494
- function queryCapabilities(state) {
17495
- const { features, limits } = state;
17496
- return {
17497
- hasTimestampQuery: features.has("timestamp-query"),
17498
- hasDepthClipControl: features.has("depth-clip-control"),
17499
- hasTextureCompressionBC: features.has("texture-compression-bc"),
17500
- hasTextureCompressionETC2: features.has("texture-compression-etc2"),
17501
- hasTextureCompressionASTC: features.has("texture-compression-astc"),
17502
- maxTextureDimension2D: limits.maxTextureDimension2D,
17503
- maxBindGroups: limits.maxBindGroups,
17504
- maxUniformBufferBindingSize: limits.maxUniformBufferBindingSize,
17505
- maxStorageBufferBindingSize: limits.maxStorageBufferBindingSize
17506
- };
17507
- }
17508
-
17509
19537
  // src/render/webgpu/headless.ts
17510
19538
  function createHeadlessRenderTarget(device, width, height, format = "rgba8unorm") {
17511
19539
  const usage = typeof GPUTextureUsage !== "undefined" ? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC : 17;
@@ -17689,16 +19717,6 @@ var NullRenderer = class {
17689
19717
  return this.frameCount;
17690
19718
  }
17691
19719
  };
17692
-
17693
- // src/render/types/coordinates.ts
17694
- var CoordinateSystem = /* @__PURE__ */ ((CoordinateSystem2) => {
17695
- CoordinateSystem2["QUAKE"] = "quake";
17696
- CoordinateSystem2["OPENGL"] = "opengl";
17697
- CoordinateSystem2["WEBGPU"] = "webgpu";
17698
- return CoordinateSystem2;
17699
- })(CoordinateSystem || {});
17700
-
17701
- // src/render/matrix/webgl.ts
17702
19720
  var WebGLMatrixBuilder = class {
17703
19721
  constructor() {
17704
19722
  this.coordinateSystem = "opengl" /* OPENGL */;
@@ -17765,89 +19783,6 @@ var WebGLMatrixBuilder = class {
17765
19783
  return view;
17766
19784
  }
17767
19785
  };
17768
- var WebGPUMatrixBuilder = class {
17769
- constructor() {
17770
- this.coordinateSystem = "webgpu" /* WEBGPU */;
17771
- }
17772
- buildProjectionMatrix(camera) {
17773
- const projection = mat4.create();
17774
- const f = 1 / Math.tan(camera.fov * DEG2RAD / 2);
17775
- const rangeInv = 1 / (camera.near - camera.far);
17776
- projection[0] = f / camera.aspect;
17777
- projection[5] = f;
17778
- projection[10] = camera.far * rangeInv;
17779
- projection[11] = -1;
17780
- projection[14] = camera.near * camera.far * rangeInv;
17781
- return projection;
17782
- }
17783
- buildViewMatrix(camera) {
17784
- const [pitch, yaw, roll] = camera.angles;
17785
- const pitchRad = pitch * DEG2RAD;
17786
- const yawRad = yaw * DEG2RAD;
17787
- const rollRad = roll * DEG2RAD;
17788
- const rotationQuake = mat4.create();
17789
- mat4.identity(rotationQuake);
17790
- mat4.rotateZ(rotationQuake, rotationQuake, -yawRad);
17791
- mat4.rotateY(rotationQuake, rotationQuake, -pitchRad);
17792
- mat4.rotateX(rotationQuake, rotationQuake, -rollRad);
17793
- mat4.fromValues(
17794
- 0,
17795
- 0,
17796
- -1,
17797
- 0,
17798
- // Column 0: Quake X maps to View Z? Wait.
17799
- -1,
17800
- 0,
17801
- 0,
17802
- 0,
17803
- // Column 1: Quake Y maps to View X?
17804
- 0,
17805
- 1,
17806
- 0,
17807
- 0,
17808
- // Column 2: Quake Z maps to View Y?
17809
- 0,
17810
- 0,
17811
- 0,
17812
- 1
17813
- );
17814
- const quakeToWgpu = mat4.fromValues(
17815
- 0,
17816
- 0,
17817
- -1,
17818
- 0,
17819
- // Col 0: X -> -Z
17820
- -1,
17821
- 0,
17822
- 0,
17823
- 0,
17824
- // Col 1: Y -> -X
17825
- 0,
17826
- 1,
17827
- 0,
17828
- 0,
17829
- // Col 2: Z -> Y
17830
- 0,
17831
- 0,
17832
- 0,
17833
- 1
17834
- );
17835
- const rotationView = mat4.create();
17836
- mat4.multiply(rotationView, quakeToWgpu, rotationQuake);
17837
- const cameraPos = vec3.fromValues(
17838
- camera.position[0],
17839
- camera.position[1],
17840
- camera.position[2]
17841
- );
17842
- const t = vec3.transformMat4(vec3.create(), cameraPos, rotationView);
17843
- vec3.negate(t, t);
17844
- const view = mat4.clone(rotationView);
17845
- view[12] = t[0];
17846
- view[13] = t[1];
17847
- view[14] = t[2];
17848
- return view;
17849
- }
17850
- };
17851
19786
  var IdentityMatrixBuilder = class {
17852
19787
  constructor() {
17853
19788
  this.coordinateSystem = "quake" /* QUAKE */;
@@ -18382,6 +20317,6 @@ pako/dist/pako.esm.mjs:
18382
20317
  (*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) *)
18383
20318
  */
18384
20319
 
18385
- 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, CoordinateSystem, Cvar, CvarRegistry, DemoAnalyzer, DemoCameraMode, DemoClipper, DemoEventType, DemoPlaybackController, DemoReader, DemoRecorder, DemoValidator, DynamicLightManager, EngineHost, EngineRuntime, FileType, FixedTimestepLoop, Framebuffer, IndexBuffer, LoggingRenderer, 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, NullRenderer, 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 };
20320
+ 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, CoordinateSystem, Cvar, CvarRegistry, DemoAnalyzer, DemoCameraMode, DemoClipper, DemoEventType, DemoPlaybackController, DemoReader, DemoRecorder, DemoValidator, DynamicLightManager, EngineHost, EngineRuntime, FileType, FixedTimestepLoop, Framebuffer, IndexBuffer, LoggingRenderer, 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, NullRenderer, 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, createWebGPURenderer, 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 };
18386
20321
  //# sourceMappingURL=index.js.map
18387
20322
  //# sourceMappingURL=index.js.map