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