ugly-game 0.5.9 → 0.5.10
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/assets/gpuProps.d.ts +29 -0
- package/dist/assets/gpuProps.js +147 -0
- package/dist/gpuShadow.d.ts +22 -1
- package/dist/gpuShadow.js +84 -13
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PropMesh {
|
|
2
|
+
positions: Float32Array;
|
|
3
|
+
normals: Float32Array;
|
|
4
|
+
colors: Float32Array;
|
|
5
|
+
indices: Uint32Array;
|
|
6
|
+
}
|
|
7
|
+
export interface PropInstance {
|
|
8
|
+
model: number;
|
|
9
|
+
mat: Float32Array;
|
|
10
|
+
}
|
|
11
|
+
export declare class GpuPropsRenderer {
|
|
12
|
+
private device;
|
|
13
|
+
private pipe;
|
|
14
|
+
private occPipe;
|
|
15
|
+
private uBuf;
|
|
16
|
+
private lightBuf;
|
|
17
|
+
private models;
|
|
18
|
+
private instBufs;
|
|
19
|
+
private bgs;
|
|
20
|
+
private occBGs;
|
|
21
|
+
private counts;
|
|
22
|
+
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
23
|
+
setModels(meshes: PropMesh[]): void;
|
|
24
|
+
/** Group instances by model and upload per-model transform buffers. */
|
|
25
|
+
setInstances(instances: PropInstance[]): void;
|
|
26
|
+
/** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
|
|
27
|
+
shadowPass(pass: GPURenderPassEncoder, lightVP: Float32Array): void;
|
|
28
|
+
frame(enc: GPUCommandEncoder, colorView: GPUTextureView, depthView: GPUTextureView, camVP: Float32Array, sunDir: [number, number, number], sunCol: [number, number, number], amb: [number, number, number], camPos: [number, number, number], fog: [number, number, number], fogDensity: number): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// GPU PROPS — instanced static-mesh renderer for scattered nature props (PLATFORM). Draws each cooked
|
|
3
|
+
// model (tree/rock/bush/…) many times at per-instance world transforms, lit + fogged to match the
|
|
4
|
+
// terrain. Runs in a load pass after the terrain so props depth-test against the surface.
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
const SHADER = /* wgsl */ `
|
|
7
|
+
struct U { mvp: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32> };
|
|
8
|
+
@group(0) @binding(0) var<uniform> u: U;
|
|
9
|
+
@group(0) @binding(1) var<storage,read> models: array<mat4x4<f32>>;
|
|
10
|
+
struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec3<f32> };
|
|
11
|
+
@vertex fn vs(@location(0) p: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) col: vec3<f32>, @builtin(instance_index) ii: u32) -> VO {
|
|
12
|
+
let m = models[ii];
|
|
13
|
+
let world = (m * vec4(p, 1.0)).xyz;
|
|
14
|
+
var o: VO; o.pos = u.mvp * vec4(world, 1.0); o.world = world; o.n = (m * vec4(nrm, 0.0)).xyz; o.c = col; return o;
|
|
15
|
+
}
|
|
16
|
+
@fragment fn fs(i: VO) -> @location(0) vec4<f32> {
|
|
17
|
+
let d = clamp(dot(normalize(i.n), -normalize(u.sun.xyz)), 0.0, 1.0);
|
|
18
|
+
let lit = i.c * (u.amb.xyz + d * u.sunCol.xyz);
|
|
19
|
+
let dist = length(i.world - u.cam.xyz);
|
|
20
|
+
let f = clamp(1.0 - exp(-dist * u.fog.w), 0.0, 1.0);
|
|
21
|
+
return vec4(mix(lit, u.fog.xyz, f), 1.0);
|
|
22
|
+
}`;
|
|
23
|
+
// Depth-only occluder pass: instanced prop depth from the sun's ortho view (so trees/rocks cast shadows).
|
|
24
|
+
const OCCLUDER = /* wgsl */ `
|
|
25
|
+
@group(0) @binding(0) var<uniform> lightVP: mat4x4<f32>;
|
|
26
|
+
@group(0) @binding(1) var<storage,read> models: array<mat4x4<f32>>;
|
|
27
|
+
@vertex fn vs(@location(0) p: vec3<f32>, @builtin(instance_index) ii: u32) -> @builtin(position) vec4<f32> {
|
|
28
|
+
return lightVP * (models[ii] * vec4(p, 1.0));
|
|
29
|
+
}`;
|
|
30
|
+
export class GpuPropsRenderer {
|
|
31
|
+
device;
|
|
32
|
+
pipe;
|
|
33
|
+
occPipe;
|
|
34
|
+
uBuf;
|
|
35
|
+
lightBuf;
|
|
36
|
+
models = [];
|
|
37
|
+
instBufs = [];
|
|
38
|
+
bgs = [];
|
|
39
|
+
occBGs = [];
|
|
40
|
+
counts = [];
|
|
41
|
+
constructor(device, format) {
|
|
42
|
+
this.device = device;
|
|
43
|
+
const sm = device.createShaderModule({ code: SHADER });
|
|
44
|
+
this.pipe = device.createRenderPipeline({
|
|
45
|
+
layout: 'auto',
|
|
46
|
+
vertex: { module: sm, entryPoint: 'vs', buffers: [
|
|
47
|
+
{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
|
|
48
|
+
{ arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }] },
|
|
49
|
+
{ arrayStride: 12, attributes: [{ shaderLocation: 2, offset: 0, format: 'float32x3' }] }
|
|
50
|
+
] },
|
|
51
|
+
fragment: { module: sm, entryPoint: 'fs', targets: [{ format }] },
|
|
52
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
53
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
|
|
54
|
+
});
|
|
55
|
+
const om = device.createShaderModule({ code: OCCLUDER });
|
|
56
|
+
this.occPipe = device.createRenderPipeline({
|
|
57
|
+
layout: 'auto',
|
|
58
|
+
vertex: { module: om, entryPoint: 'vs', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] },
|
|
59
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
60
|
+
depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' },
|
|
61
|
+
});
|
|
62
|
+
this.uBuf = device.createBuffer({ size: 96 + 16 * 5, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
63
|
+
this.lightBuf = device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
64
|
+
}
|
|
65
|
+
setModels(meshes) {
|
|
66
|
+
const dev = this.device, U = GPUBufferUsage;
|
|
67
|
+
for (const p of this.models) {
|
|
68
|
+
p.posB.destroy();
|
|
69
|
+
p.nrmB.destroy();
|
|
70
|
+
p.colB.destroy();
|
|
71
|
+
p.idxB.destroy();
|
|
72
|
+
}
|
|
73
|
+
const mk = (d, u) => { const b = dev.createBuffer({ size: d.byteLength, usage: u }); dev.queue.writeBuffer(b, 0, d); return b; };
|
|
74
|
+
this.models = meshes.map((m) => ({ posB: mk(m.positions, U.VERTEX | U.COPY_DST), nrmB: mk(m.normals, U.VERTEX | U.COPY_DST), colB: mk(m.colors, U.VERTEX | U.COPY_DST), idxB: mk(m.indices, U.INDEX | U.COPY_DST), idxCount: m.indices.length }));
|
|
75
|
+
this.instBufs = meshes.map(() => null);
|
|
76
|
+
this.bgs = meshes.map(() => null);
|
|
77
|
+
this.occBGs = meshes.map(() => null);
|
|
78
|
+
this.counts = meshes.map(() => 0);
|
|
79
|
+
}
|
|
80
|
+
/** Group instances by model and upload per-model transform buffers. */
|
|
81
|
+
setInstances(instances) {
|
|
82
|
+
const dev = this.device;
|
|
83
|
+
const byModel = this.models.map(() => []);
|
|
84
|
+
const data = this.models.map(() => []);
|
|
85
|
+
for (const it of instances) {
|
|
86
|
+
if (it.model < 0 || it.model >= this.models.length)
|
|
87
|
+
continue;
|
|
88
|
+
byModel[it.model].push(0);
|
|
89
|
+
for (let i = 0; i < 16; i++)
|
|
90
|
+
data[it.model].push(it.mat[i]);
|
|
91
|
+
}
|
|
92
|
+
for (let m = 0; m < this.models.length; m++) {
|
|
93
|
+
this.instBufs[m]?.destroy();
|
|
94
|
+
const arr = new Float32Array(data[m]);
|
|
95
|
+
this.counts[m] = byModel[m].length;
|
|
96
|
+
if (arr.length === 0) {
|
|
97
|
+
this.instBufs[m] = null;
|
|
98
|
+
this.bgs[m] = null;
|
|
99
|
+
this.occBGs[m] = null;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const b = dev.createBuffer({ size: arr.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
103
|
+
dev.queue.writeBuffer(b, 0, arr);
|
|
104
|
+
this.instBufs[m] = b;
|
|
105
|
+
this.bgs[m] = dev.createBindGroup({ layout: this.pipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.uBuf } }, { binding: 1, resource: { buffer: b } }] });
|
|
106
|
+
this.occBGs[m] = dev.createBindGroup({ layout: this.occPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.lightBuf } }, { binding: 1, resource: { buffer: b } }] });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
|
|
110
|
+
shadowPass(pass, lightVP) {
|
|
111
|
+
this.device.queue.writeBuffer(this.lightBuf, 0, lightVP);
|
|
112
|
+
pass.setPipeline(this.occPipe);
|
|
113
|
+
for (let m = 0; m < this.models.length; m++) {
|
|
114
|
+
const bg = this.occBGs[m], mdl = this.models[m];
|
|
115
|
+
if (!bg || this.counts[m] === 0)
|
|
116
|
+
continue;
|
|
117
|
+
pass.setBindGroup(0, bg);
|
|
118
|
+
pass.setVertexBuffer(0, mdl.posB);
|
|
119
|
+
pass.setIndexBuffer(mdl.idxB, 'uint32');
|
|
120
|
+
pass.drawIndexed(mdl.idxCount, this.counts[m]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity) {
|
|
124
|
+
const u = new Float32Array(24 + 20);
|
|
125
|
+
u.set(camVP, 0);
|
|
126
|
+
u.set([sunDir[0], sunDir[1], sunDir[2], 0], 16);
|
|
127
|
+
u.set([sunCol[0], sunCol[1], sunCol[2], 0], 20);
|
|
128
|
+
u.set([amb[0], amb[1], amb[2], 0], 24);
|
|
129
|
+
u.set([camPos[0], camPos[1], camPos[2], 0], 28);
|
|
130
|
+
u.set([fog[0], fog[1], fog[2], fogDensity], 32);
|
|
131
|
+
this.device.queue.writeBuffer(this.uBuf, 0, u);
|
|
132
|
+
const rp = enc.beginRenderPass({ colorAttachments: [{ view: colorView, loadOp: 'load', storeOp: 'store' }], depthStencilAttachment: { view: depthView, depthLoadOp: 'load', depthStoreOp: 'store' } });
|
|
133
|
+
rp.setPipeline(this.pipe);
|
|
134
|
+
for (let m = 0; m < this.models.length; m++) {
|
|
135
|
+
const bg = this.bgs[m], mdl = this.models[m];
|
|
136
|
+
if (!bg || this.counts[m] === 0)
|
|
137
|
+
continue;
|
|
138
|
+
rp.setBindGroup(0, bg);
|
|
139
|
+
rp.setVertexBuffer(0, mdl.posB);
|
|
140
|
+
rp.setVertexBuffer(1, mdl.nrmB);
|
|
141
|
+
rp.setVertexBuffer(2, mdl.colB);
|
|
142
|
+
rp.setIndexBuffer(mdl.idxB, 'uint32');
|
|
143
|
+
rp.drawIndexed(mdl.idxCount, this.counts[m]);
|
|
144
|
+
}
|
|
145
|
+
rp.end();
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/gpuShadow.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ export interface CubeInstance {
|
|
|
8
8
|
hue: number;
|
|
9
9
|
emissive?: number;
|
|
10
10
|
rot?: [number, number, number, number];
|
|
11
|
+
tile?: number;
|
|
12
|
+
up?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Atlas layout: which grid tiles a material's top/side/bottom faces sample. Grid indices into a cols×rows atlas. */
|
|
15
|
+
export interface AtlasLayout {
|
|
16
|
+
tileSize: number;
|
|
17
|
+
cols: number;
|
|
18
|
+
rows: number;
|
|
19
|
+
tiles: Record<number, {
|
|
20
|
+
top: number;
|
|
21
|
+
side: number;
|
|
22
|
+
bottom: number;
|
|
23
|
+
}>;
|
|
11
24
|
}
|
|
12
25
|
/** The 36-vertex cube as WGSL array-literal bodies (pos + per-face normal). Shared by shadow modules. */
|
|
13
26
|
export declare const CUBE: {
|
|
@@ -29,6 +42,10 @@ export declare class GpuShadowRenderer {
|
|
|
29
42
|
private shadowSampler;
|
|
30
43
|
private count;
|
|
31
44
|
private cam;
|
|
45
|
+
private atlasView;
|
|
46
|
+
private atlasSampler;
|
|
47
|
+
private tileRectBuf;
|
|
48
|
+
private useAtlas;
|
|
32
49
|
private alphaPipe;
|
|
33
50
|
private overlayBuf;
|
|
34
51
|
private overlayBG;
|
|
@@ -70,6 +87,10 @@ export declare class GpuShadowRenderer {
|
|
|
70
87
|
private scratch;
|
|
71
88
|
private fill;
|
|
72
89
|
private makeBindGroups;
|
|
90
|
+
/** Bind a block-texture atlas + its material→tile layout. Each material's top/side/bottom faces sample distinct
|
|
91
|
+
* atlas tiles; a `CubeInstance.tile` of that material id + its `up`-face code selects the right tile in-shader.
|
|
92
|
+
* Call once after the atlas texture is uploaded; instances with `tile < 0` still use the flat hue path. */
|
|
93
|
+
setAtlas(tex: GPUTexture, layout: AtlasLayout): void;
|
|
73
94
|
/** Static scene: sizes the buffer to exactly these instances. */
|
|
74
95
|
setScene(instances: CubeInstance[]): void;
|
|
75
96
|
/** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
|
|
@@ -86,7 +107,7 @@ export declare class GpuShadowRenderer {
|
|
|
86
107
|
depth: GPUTextureView;
|
|
87
108
|
w: number;
|
|
88
109
|
h: number;
|
|
89
|
-
}): void;
|
|
110
|
+
}, tint?: [number, number, number, number], extraShadow?: (sp: GPURenderPassEncoder, lightVP: Float32Array) => void): void;
|
|
90
111
|
/** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
|
|
91
112
|
* safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
|
|
92
113
|
readCullStats(): Promise<{
|
package/dist/gpuShadow.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { frustumPlanes } from './mat4';
|
|
15
15
|
const SHADOW_SIZE = 2048;
|
|
16
16
|
const CULL_WG = 64; // compute-cull workgroup size
|
|
17
|
+
const INST_F = 16; // floats per instance: pos.xyz+hue, scl.xyz+emissive, rot quat, aux (tile,up,_,_)
|
|
17
18
|
const OVERLAY_CAP = 512; // max translucent overlay cubes per frame (the place ghost + headroom)
|
|
18
19
|
// Build the 36-vertex cube (6 faces × 2 tris) in JS and inject as WGSL const arrays —
|
|
19
20
|
// bulletproof + readable vs. bit-twiddling from vertex_index. cullMode is 'none', so winding
|
|
@@ -48,8 +49,10 @@ struct Cam {
|
|
|
48
49
|
lightVP: mat4x4<f32>,
|
|
49
50
|
sun: vec4<f32>, // xyz = direction light travels, w = ambient
|
|
50
51
|
sunCol: vec4<f32>, // xyz = sun colour, w = shadow map size (texels)
|
|
52
|
+
tint: vec4<f32>, // rgb = per-planet tint · w = tint strength (0 = none)
|
|
53
|
+
atlas: vec4<f32>, // x = useAtlas (1/0)
|
|
51
54
|
};
|
|
52
|
-
struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32> }; // pos.w = hue (<0 ⇒ ground) · scl.w = emissive/alpha · rot = quaternion
|
|
55
|
+
struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32>, aux: vec4<f32> }; // pos.w = hue (<0 ⇒ ground) · scl.w = emissive/alpha · rot = quaternion · aux.x = atlas tile (<0 ⇒ hue) · aux.y = up-face code
|
|
53
56
|
@group(0) @binding(0) var<uniform> cam: Cam;
|
|
54
57
|
@group(0) @binding(1) var<storage, read> insts: array<Inst>;
|
|
55
58
|
|
|
@@ -85,7 +88,7 @@ fn hsv(hh: f32) -> vec3<f32> {
|
|
|
85
88
|
// under-culling for one frame, never a false cull. occ.x toggles it (camera pass only; the shadow pass never
|
|
86
89
|
// occludes — an off-screen occluder still casts a shadow into view). occ.yz = hiz dims, occ.w = pixels/texel.
|
|
87
90
|
const CULL_WGSL = /* wgsl */ `
|
|
88
|
-
struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32> };
|
|
91
|
+
struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32>, aux: vec4<f32> };
|
|
89
92
|
struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
|
|
90
93
|
struct CullCam {
|
|
91
94
|
planes: array<vec4<f32>, 6>, cnt: vec4<f32>, // cnt.x = instance count
|
|
@@ -181,8 +184,18 @@ const MAIN_WGSL = COMMON_WGSL + /* wgsl */ `
|
|
|
181
184
|
@group(0) @binding(2) var shadowMap: texture_depth_2d;
|
|
182
185
|
@group(0) @binding(3) var shadowSamp: sampler_comparison;
|
|
183
186
|
@group(0) @binding(4) var<storage, read> vis: array<u32>;
|
|
187
|
+
@group(0) @binding(5) var atlasTex: texture_2d<f32>;
|
|
188
|
+
@group(0) @binding(6) var atlasSamp: sampler;
|
|
189
|
+
@group(0) @binding(7) var<storage, read> tileRects: array<vec4<f32>>; // [u0,v0,du,dv] per (material*3 + face-variant)
|
|
184
190
|
|
|
185
|
-
struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32, @location(3) emis: f32, @location(4) blk: vec3<f32>, @location(5) objN: vec3<f32
|
|
191
|
+
struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32, @location(3) emis: f32, @location(4) blk: vec3<f32>, @location(5) objN: vec3<f32>, @location(6) tile: f32, @location(7) upc: f32 };
|
|
192
|
+
|
|
193
|
+
fn faceVec(code: f32) -> vec3<f32> {
|
|
194
|
+
let c = i32(code);
|
|
195
|
+
if (c == 0) { return vec3(1.,0.,0.); } if (c == 1) { return vec3(-1.,0.,0.); }
|
|
196
|
+
if (c == 2) { return vec3(0.,1.,0.); } if (c == 3) { return vec3(0.,-1.,0.); }
|
|
197
|
+
if (c == 4) { return vec3(0.,0.,1.); } return vec3(0.,0.,-1.);
|
|
198
|
+
}
|
|
186
199
|
|
|
187
200
|
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
|
|
188
201
|
let inst = insts[vis[ii]];
|
|
@@ -198,6 +211,7 @@ struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>,
|
|
|
198
211
|
// by inst.scl so a merged LOD cube (scl>1) still shows its sub-blocks. objN = pre-rotation face normal.
|
|
199
212
|
o.blk = (CUBE_POS[vi] + vec3(0.5, 0.5, 0.5)) * inst.scl.xyz;
|
|
200
213
|
o.objN = CUBE_NRM[vi];
|
|
214
|
+
o.tile = inst.aux.x; o.upc = inst.aux.y;
|
|
201
215
|
return o;
|
|
202
216
|
}
|
|
203
217
|
|
|
@@ -232,7 +246,6 @@ fn blockShade(fx: f32, fz: f32) -> f32 {
|
|
|
232
246
|
}
|
|
233
247
|
let N = normalize(i.nrm);
|
|
234
248
|
let ndl = max(dot(N, -normalize(cam.sun.xyz)), 0.0);
|
|
235
|
-
let base = select(mix(vec3(0.34), hsv(i.hue), 0.62), vec3(0.6, 0.62, 0.66), i.hue < 0.0);
|
|
236
249
|
let sh = shadowFactor(i.world, ndl);
|
|
237
250
|
// in-block coords = the two OBJECT axes not along the (object-space, axis-aligned) face normal — so the AO
|
|
238
251
|
// pattern is anchored to the model's own block grid and translates/rotates with the instance.
|
|
@@ -241,6 +254,16 @@ fn blockShade(fx: f32, fz: f32) -> f32 {
|
|
|
241
254
|
if (an.y >= an.x && an.y >= an.z) { fx = fract(i.blk.x); fz = fract(i.blk.z); }
|
|
242
255
|
else if (an.x >= an.z) { fx = fract(i.blk.y); fz = fract(i.blk.z); }
|
|
243
256
|
else { fx = fract(i.blk.x); fz = fract(i.blk.y); }
|
|
257
|
+
var base = select(mix(vec3(0.34), hsv(i.hue), 0.62), vec3(0.6, 0.62, 0.66), i.hue < 0.0);
|
|
258
|
+
if (cam.atlas.x > 0.5 && i.tile >= 0.0) { // ATLAS: pick top/side/bottom tile from the face vs the up axis, tile the AO in-face coords
|
|
259
|
+
let d = dot(i.objN, faceVec(i.upc));
|
|
260
|
+
var variant = 1u; // side
|
|
261
|
+
if (d > 0.5) { variant = 0u; } else if (d < -0.5) { variant = 2u; }
|
|
262
|
+
let rect = tileRects[u32(i.tile) * 3u + variant];
|
|
263
|
+
let uv = rect.xy + fract(vec2(fx, fz)) * rect.zw;
|
|
264
|
+
let texc = textureSampleLevel(atlasTex, atlasSamp, uv, 0.0).rgb; // explicit LOD (early-return above makes control flow non-uniform)
|
|
265
|
+
base = mix(texc, texc * cam.tint.rgb, cam.tint.w); // per-planet tint
|
|
266
|
+
}
|
|
244
267
|
let ao = blockShade(fx, fz);
|
|
245
268
|
let lit = base * (cam.sun.w + cam.sunCol.xyz * ndl * sh) * ao;
|
|
246
269
|
return vec4(pow(clamp(lit, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0); // gamma
|
|
@@ -277,11 +300,15 @@ export class GpuShadowRenderer {
|
|
|
277
300
|
mainBGL;
|
|
278
301
|
shadowSampler;
|
|
279
302
|
count = 0;
|
|
280
|
-
cam = new Float32Array(
|
|
303
|
+
cam = new Float32Array(48); // 2×mat4 + sun + sunCol + tint + atlas
|
|
304
|
+
atlasView;
|
|
305
|
+
atlasSampler;
|
|
306
|
+
tileRectBuf;
|
|
307
|
+
useAtlas = 0;
|
|
281
308
|
alphaPipe;
|
|
282
309
|
overlayBuf;
|
|
283
310
|
overlayBG;
|
|
284
|
-
overlayScratch = new Float32Array(OVERLAY_CAP *
|
|
311
|
+
overlayScratch = new Float32Array(OVERLAY_CAP * INST_F);
|
|
285
312
|
overlayCount = 0;
|
|
286
313
|
// GPU-driven culling: one compute pipeline; per-frustum (camera + light) indirect args + compacted visible lists.
|
|
287
314
|
cullPipe;
|
|
@@ -332,6 +359,9 @@ export class GpuShadowRenderer {
|
|
|
332
359
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
333
360
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
334
361
|
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // camera-visible list
|
|
362
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } }, // block atlas
|
|
363
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // atlas sampler
|
|
364
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }, // per-tile UV rects
|
|
335
365
|
] });
|
|
336
366
|
this.mainPipe = device.createRenderPipeline({
|
|
337
367
|
layout: device.createPipelineLayout({ bindGroupLayouts: [mainBGL] }),
|
|
@@ -390,6 +420,12 @@ export class GpuShadowRenderer {
|
|
|
390
420
|
this.hizParam = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
391
421
|
const dummy = device.createTexture({ size: [1, 1], format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING });
|
|
392
422
|
this.occView = dummy.createView();
|
|
423
|
+
// atlas dummies (bound until setAtlas): a 1×1 white texture + a 1-entry rect buffer + a plain sampler
|
|
424
|
+
const white = device.createTexture({ size: [1, 1], format: 'rgba8unorm', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
|
|
425
|
+
device.queue.writeTexture({ texture: white }, new Uint8Array([255, 255, 255, 255]), {}, [1, 1]);
|
|
426
|
+
this.atlasView = white.createView();
|
|
427
|
+
this.atlasSampler = device.createSampler({ magFilter: 'nearest', minFilter: 'linear', mipmapFilter: 'linear' });
|
|
428
|
+
this.tileRectBuf = device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
393
429
|
}
|
|
394
430
|
// One frustum's cull resources: indirect args (drawIndirect), a compacted visible-index list sized to the
|
|
395
431
|
// instance capacity, and a uniform holding the 6 planes + instance count + camVP + occlusion params.
|
|
@@ -437,7 +473,7 @@ export class GpuShadowRenderer {
|
|
|
437
473
|
scratch = new Float32Array(0);
|
|
438
474
|
fill(scratch, instances, n) {
|
|
439
475
|
for (let i = 0; i < n; i++) {
|
|
440
|
-
const c = instances[i], o = i *
|
|
476
|
+
const c = instances[i], o = i * INST_F, r = c.rot;
|
|
441
477
|
scratch[o] = c.x;
|
|
442
478
|
scratch[o + 1] = c.y;
|
|
443
479
|
scratch[o + 2] = c.z;
|
|
@@ -450,6 +486,10 @@ export class GpuShadowRenderer {
|
|
|
450
486
|
scratch[o + 9] = r ? r[1] : 0;
|
|
451
487
|
scratch[o + 10] = r ? r[2] : 0;
|
|
452
488
|
scratch[o + 11] = r ? r[3] : 1; // quaternion, default identity
|
|
489
|
+
scratch[o + 12] = c.tile ?? -1;
|
|
490
|
+
scratch[o + 13] = c.up ?? 1;
|
|
491
|
+
scratch[o + 14] = 0;
|
|
492
|
+
scratch[o + 15] = 0; // aux: atlas tile id + up-face code
|
|
453
493
|
}
|
|
454
494
|
}
|
|
455
495
|
makeBindGroups() {
|
|
@@ -462,13 +502,37 @@ export class GpuShadowRenderer {
|
|
|
462
502
|
{ binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
|
|
463
503
|
{ binding: 2, resource: this.shadowView }, { binding: 3, resource: this.shadowSampler },
|
|
464
504
|
{ binding: 4, resource: { buffer: this.camVis } },
|
|
505
|
+
{ binding: 5, resource: this.atlasView }, { binding: 6, resource: this.atlasSampler }, { binding: 7, resource: { buffer: this.tileRectBuf } },
|
|
465
506
|
] });
|
|
466
507
|
}
|
|
508
|
+
/** Bind a block-texture atlas + its material→tile layout. Each material's top/side/bottom faces sample distinct
|
|
509
|
+
* atlas tiles; a `CubeInstance.tile` of that material id + its `up`-face code selects the right tile in-shader.
|
|
510
|
+
* Call once after the atlas texture is uploaded; instances with `tile < 0` still use the flat hue path. */
|
|
511
|
+
setAtlas(tex, layout) {
|
|
512
|
+
this.atlasView = tex.createView();
|
|
513
|
+
const ids = Object.keys(layout.tiles).map(Number);
|
|
514
|
+
const maxMat = (ids.length ? Math.max(...ids) : 0) + 1;
|
|
515
|
+
const rects = new Float32Array(maxMat * 3 * 4);
|
|
516
|
+
const rectOf = (g) => {
|
|
517
|
+
const cx = g % layout.cols, cy = Math.floor(g / layout.cols);
|
|
518
|
+
return [cx / layout.cols, cy / layout.rows, 1 / layout.cols, 1 / layout.rows];
|
|
519
|
+
};
|
|
520
|
+
for (const [mat, f] of Object.entries(layout.tiles)) {
|
|
521
|
+
const b = Number(mat) * 3 * 4;
|
|
522
|
+
rects.set(rectOf(f.top), b);
|
|
523
|
+
rects.set(rectOf(f.side), b + 4);
|
|
524
|
+
rects.set(rectOf(f.bottom), b + 8);
|
|
525
|
+
}
|
|
526
|
+
this.tileRectBuf = this.device.createBuffer({ size: Math.max(16, rects.byteLength), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
527
|
+
this.device.queue.writeBuffer(this.tileRectBuf, 0, rects);
|
|
528
|
+
this.useAtlas = 1;
|
|
529
|
+
this.makeBindGroups(); // rebind the main pass with the real atlas + rects
|
|
530
|
+
}
|
|
467
531
|
/** Static scene: sizes the buffer to exactly these instances. */
|
|
468
532
|
setScene(instances) {
|
|
469
533
|
this.count = instances.length;
|
|
470
534
|
this.cap = this.count;
|
|
471
|
-
this.scratch = new Float32Array(this.count *
|
|
535
|
+
this.scratch = new Float32Array(this.count * INST_F);
|
|
472
536
|
this.fill(this.scratch, instances, this.count);
|
|
473
537
|
this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
474
538
|
this.device.queue.writeBuffer(this.instBuf, 0, this.scratch);
|
|
@@ -477,15 +541,15 @@ export class GpuShadowRenderer {
|
|
|
477
541
|
/** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
|
|
478
542
|
setCapacity(cap) {
|
|
479
543
|
this.cap = cap;
|
|
480
|
-
this.scratch = new Float32Array(cap *
|
|
544
|
+
this.scratch = new Float32Array(cap * INST_F);
|
|
481
545
|
this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
482
546
|
this.count = 0;
|
|
483
547
|
this.makeBindGroups();
|
|
484
548
|
}
|
|
485
549
|
updateInstances(instances) {
|
|
486
|
-
const n = Math.min(instances.length, this.scratch.length /
|
|
550
|
+
const n = Math.min(instances.length, this.scratch.length / INST_F);
|
|
487
551
|
this.fill(this.scratch, instances, n);
|
|
488
|
-
this.device.queue.writeBuffer(this.instBuf, 0, this.scratch, 0, n *
|
|
552
|
+
this.device.queue.writeBuffer(this.instBuf, 0, this.scratch, 0, n * INST_F);
|
|
489
553
|
this.count = n;
|
|
490
554
|
}
|
|
491
555
|
/** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
|
|
@@ -493,7 +557,7 @@ export class GpuShadowRenderer {
|
|
|
493
557
|
const n = Math.min(instances.length, OVERLAY_CAP);
|
|
494
558
|
this.fill(this.overlayScratch, instances, n);
|
|
495
559
|
if (n > 0)
|
|
496
|
-
this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n *
|
|
560
|
+
this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * INST_F);
|
|
497
561
|
this.overlayCount = n;
|
|
498
562
|
}
|
|
499
563
|
/** Write a frustum's cull uniform: 6 planes + count + this frustum's VP + occlusion params (occ on the camera pass). */
|
|
@@ -517,7 +581,7 @@ export class GpuShadowRenderer {
|
|
|
517
581
|
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. Pass `occlusion` (the depth target +
|
|
518
582
|
* its size) to enable Hi-Z occlusion culling on the camera pass — the renderer rebuilds a coarse Hi-Z from that
|
|
519
583
|
* depth each frame and the NEXT frame's cull uses it (hole-free: the first frame + any resize skip occlusion). */
|
|
520
|
-
frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false, occlusion) {
|
|
584
|
+
frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false, occlusion, tint = [1, 1, 1, 0], extraShadow) {
|
|
521
585
|
this.cam.set(camVP, 0);
|
|
522
586
|
this.cam.set(lightVP, 16);
|
|
523
587
|
this.cam[32] = sunDir[0];
|
|
@@ -528,6 +592,11 @@ export class GpuShadowRenderer {
|
|
|
528
592
|
this.cam[37] = sunCol[1];
|
|
529
593
|
this.cam[38] = sunCol[2];
|
|
530
594
|
this.cam[39] = SHADOW_SIZE;
|
|
595
|
+
this.cam[40] = tint[0];
|
|
596
|
+
this.cam[41] = tint[1];
|
|
597
|
+
this.cam[42] = tint[2];
|
|
598
|
+
this.cam[43] = tint[3];
|
|
599
|
+
this.cam[44] = this.useAtlas;
|
|
531
600
|
this.device.queue.writeBuffer(this.camBuf, 0, this.cam);
|
|
532
601
|
if (occlusion)
|
|
533
602
|
this.ensureHiZ(occlusion.w, occlusion.h);
|
|
@@ -556,6 +625,8 @@ export class GpuShadowRenderer {
|
|
|
556
625
|
sp.setPipeline(this.shadowPipe);
|
|
557
626
|
sp.setBindGroup(0, this.shadowBG);
|
|
558
627
|
sp.drawIndirect(this.lightArgs, 0);
|
|
628
|
+
if (extraShadow)
|
|
629
|
+
extraShadow(sp, lightVP); // let the app cast extra occluders (mesh props/trees) into the same sun shadow map
|
|
559
630
|
sp.end();
|
|
560
631
|
// Pass 2: camera, shadow-sampled (camera-frustum + Hi-Z-occlusion-culled).
|
|
561
632
|
const mp = enc.beginRenderPass({
|