ugly-game 0.5.9 → 0.5.11
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 +87 -14
- 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,7 +254,19 @@ 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); }
|
|
244
|
-
|
|
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
|
+
}
|
|
267
|
+
// per-block edge AO makes each voxel visible on FLAT-hue cubes, but reads as a hard GRID over textures — so the
|
|
268
|
+
// atlas path uses a soft, mostly-flat AO (texture already supplies surface detail) instead of the strong grid.
|
|
269
|
+
let ao = select(blockShade(fx, fz), mix(blockShade(fx, fz), 1.0, 0.8), cam.atlas.x > 0.5 && i.tile >= 0.0);
|
|
245
270
|
let lit = base * (cam.sun.w + cam.sunCol.xyz * ndl * sh) * ao;
|
|
246
271
|
return vec4(pow(clamp(lit, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0); // gamma
|
|
247
272
|
}
|
|
@@ -277,11 +302,15 @@ export class GpuShadowRenderer {
|
|
|
277
302
|
mainBGL;
|
|
278
303
|
shadowSampler;
|
|
279
304
|
count = 0;
|
|
280
|
-
cam = new Float32Array(
|
|
305
|
+
cam = new Float32Array(48); // 2×mat4 + sun + sunCol + tint + atlas
|
|
306
|
+
atlasView;
|
|
307
|
+
atlasSampler;
|
|
308
|
+
tileRectBuf;
|
|
309
|
+
useAtlas = 0;
|
|
281
310
|
alphaPipe;
|
|
282
311
|
overlayBuf;
|
|
283
312
|
overlayBG;
|
|
284
|
-
overlayScratch = new Float32Array(OVERLAY_CAP *
|
|
313
|
+
overlayScratch = new Float32Array(OVERLAY_CAP * INST_F);
|
|
285
314
|
overlayCount = 0;
|
|
286
315
|
// GPU-driven culling: one compute pipeline; per-frustum (camera + light) indirect args + compacted visible lists.
|
|
287
316
|
cullPipe;
|
|
@@ -332,6 +361,9 @@ export class GpuShadowRenderer {
|
|
|
332
361
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
333
362
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
334
363
|
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // camera-visible list
|
|
364
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } }, // block atlas
|
|
365
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // atlas sampler
|
|
366
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }, // per-tile UV rects
|
|
335
367
|
] });
|
|
336
368
|
this.mainPipe = device.createRenderPipeline({
|
|
337
369
|
layout: device.createPipelineLayout({ bindGroupLayouts: [mainBGL] }),
|
|
@@ -390,6 +422,12 @@ export class GpuShadowRenderer {
|
|
|
390
422
|
this.hizParam = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
391
423
|
const dummy = device.createTexture({ size: [1, 1], format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING });
|
|
392
424
|
this.occView = dummy.createView();
|
|
425
|
+
// atlas dummies (bound until setAtlas): a 1×1 white texture + a 1-entry rect buffer + a plain sampler
|
|
426
|
+
const white = device.createTexture({ size: [1, 1], format: 'rgba8unorm', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
|
|
427
|
+
device.queue.writeTexture({ texture: white }, new Uint8Array([255, 255, 255, 255]), {}, [1, 1]);
|
|
428
|
+
this.atlasView = white.createView();
|
|
429
|
+
this.atlasSampler = device.createSampler({ magFilter: 'nearest', minFilter: 'linear', mipmapFilter: 'linear' });
|
|
430
|
+
this.tileRectBuf = device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
393
431
|
}
|
|
394
432
|
// One frustum's cull resources: indirect args (drawIndirect), a compacted visible-index list sized to the
|
|
395
433
|
// instance capacity, and a uniform holding the 6 planes + instance count + camVP + occlusion params.
|
|
@@ -437,7 +475,7 @@ export class GpuShadowRenderer {
|
|
|
437
475
|
scratch = new Float32Array(0);
|
|
438
476
|
fill(scratch, instances, n) {
|
|
439
477
|
for (let i = 0; i < n; i++) {
|
|
440
|
-
const c = instances[i], o = i *
|
|
478
|
+
const c = instances[i], o = i * INST_F, r = c.rot;
|
|
441
479
|
scratch[o] = c.x;
|
|
442
480
|
scratch[o + 1] = c.y;
|
|
443
481
|
scratch[o + 2] = c.z;
|
|
@@ -450,6 +488,10 @@ export class GpuShadowRenderer {
|
|
|
450
488
|
scratch[o + 9] = r ? r[1] : 0;
|
|
451
489
|
scratch[o + 10] = r ? r[2] : 0;
|
|
452
490
|
scratch[o + 11] = r ? r[3] : 1; // quaternion, default identity
|
|
491
|
+
scratch[o + 12] = c.tile ?? -1;
|
|
492
|
+
scratch[o + 13] = c.up ?? 1;
|
|
493
|
+
scratch[o + 14] = 0;
|
|
494
|
+
scratch[o + 15] = 0; // aux: atlas tile id + up-face code
|
|
453
495
|
}
|
|
454
496
|
}
|
|
455
497
|
makeBindGroups() {
|
|
@@ -462,13 +504,37 @@ export class GpuShadowRenderer {
|
|
|
462
504
|
{ binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
|
|
463
505
|
{ binding: 2, resource: this.shadowView }, { binding: 3, resource: this.shadowSampler },
|
|
464
506
|
{ binding: 4, resource: { buffer: this.camVis } },
|
|
507
|
+
{ binding: 5, resource: this.atlasView }, { binding: 6, resource: this.atlasSampler }, { binding: 7, resource: { buffer: this.tileRectBuf } },
|
|
465
508
|
] });
|
|
466
509
|
}
|
|
510
|
+
/** Bind a block-texture atlas + its material→tile layout. Each material's top/side/bottom faces sample distinct
|
|
511
|
+
* atlas tiles; a `CubeInstance.tile` of that material id + its `up`-face code selects the right tile in-shader.
|
|
512
|
+
* Call once after the atlas texture is uploaded; instances with `tile < 0` still use the flat hue path. */
|
|
513
|
+
setAtlas(tex, layout) {
|
|
514
|
+
this.atlasView = tex.createView();
|
|
515
|
+
const ids = Object.keys(layout.tiles).map(Number);
|
|
516
|
+
const maxMat = (ids.length ? Math.max(...ids) : 0) + 1;
|
|
517
|
+
const rects = new Float32Array(maxMat * 3 * 4);
|
|
518
|
+
const rectOf = (g) => {
|
|
519
|
+
const cx = g % layout.cols, cy = Math.floor(g / layout.cols);
|
|
520
|
+
return [cx / layout.cols, cy / layout.rows, 1 / layout.cols, 1 / layout.rows];
|
|
521
|
+
};
|
|
522
|
+
for (const [mat, f] of Object.entries(layout.tiles)) {
|
|
523
|
+
const b = Number(mat) * 3 * 4;
|
|
524
|
+
rects.set(rectOf(f.top), b);
|
|
525
|
+
rects.set(rectOf(f.side), b + 4);
|
|
526
|
+
rects.set(rectOf(f.bottom), b + 8);
|
|
527
|
+
}
|
|
528
|
+
this.tileRectBuf = this.device.createBuffer({ size: Math.max(16, rects.byteLength), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
529
|
+
this.device.queue.writeBuffer(this.tileRectBuf, 0, rects);
|
|
530
|
+
this.useAtlas = 1;
|
|
531
|
+
this.makeBindGroups(); // rebind the main pass with the real atlas + rects
|
|
532
|
+
}
|
|
467
533
|
/** Static scene: sizes the buffer to exactly these instances. */
|
|
468
534
|
setScene(instances) {
|
|
469
535
|
this.count = instances.length;
|
|
470
536
|
this.cap = this.count;
|
|
471
|
-
this.scratch = new Float32Array(this.count *
|
|
537
|
+
this.scratch = new Float32Array(this.count * INST_F);
|
|
472
538
|
this.fill(this.scratch, instances, this.count);
|
|
473
539
|
this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
474
540
|
this.device.queue.writeBuffer(this.instBuf, 0, this.scratch);
|
|
@@ -477,15 +543,15 @@ export class GpuShadowRenderer {
|
|
|
477
543
|
/** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
|
|
478
544
|
setCapacity(cap) {
|
|
479
545
|
this.cap = cap;
|
|
480
|
-
this.scratch = new Float32Array(cap *
|
|
546
|
+
this.scratch = new Float32Array(cap * INST_F);
|
|
481
547
|
this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
482
548
|
this.count = 0;
|
|
483
549
|
this.makeBindGroups();
|
|
484
550
|
}
|
|
485
551
|
updateInstances(instances) {
|
|
486
|
-
const n = Math.min(instances.length, this.scratch.length /
|
|
552
|
+
const n = Math.min(instances.length, this.scratch.length / INST_F);
|
|
487
553
|
this.fill(this.scratch, instances, n);
|
|
488
|
-
this.device.queue.writeBuffer(this.instBuf, 0, this.scratch, 0, n *
|
|
554
|
+
this.device.queue.writeBuffer(this.instBuf, 0, this.scratch, 0, n * INST_F);
|
|
489
555
|
this.count = n;
|
|
490
556
|
}
|
|
491
557
|
/** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
|
|
@@ -493,7 +559,7 @@ export class GpuShadowRenderer {
|
|
|
493
559
|
const n = Math.min(instances.length, OVERLAY_CAP);
|
|
494
560
|
this.fill(this.overlayScratch, instances, n);
|
|
495
561
|
if (n > 0)
|
|
496
|
-
this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n *
|
|
562
|
+
this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * INST_F);
|
|
497
563
|
this.overlayCount = n;
|
|
498
564
|
}
|
|
499
565
|
/** Write a frustum's cull uniform: 6 planes + count + this frustum's VP + occlusion params (occ on the camera pass). */
|
|
@@ -517,7 +583,7 @@ export class GpuShadowRenderer {
|
|
|
517
583
|
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. Pass `occlusion` (the depth target +
|
|
518
584
|
* its size) to enable Hi-Z occlusion culling on the camera pass — the renderer rebuilds a coarse Hi-Z from that
|
|
519
585
|
* 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) {
|
|
586
|
+
frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false, occlusion, tint = [1, 1, 1, 0], extraShadow) {
|
|
521
587
|
this.cam.set(camVP, 0);
|
|
522
588
|
this.cam.set(lightVP, 16);
|
|
523
589
|
this.cam[32] = sunDir[0];
|
|
@@ -528,6 +594,11 @@ export class GpuShadowRenderer {
|
|
|
528
594
|
this.cam[37] = sunCol[1];
|
|
529
595
|
this.cam[38] = sunCol[2];
|
|
530
596
|
this.cam[39] = SHADOW_SIZE;
|
|
597
|
+
this.cam[40] = tint[0];
|
|
598
|
+
this.cam[41] = tint[1];
|
|
599
|
+
this.cam[42] = tint[2];
|
|
600
|
+
this.cam[43] = tint[3];
|
|
601
|
+
this.cam[44] = this.useAtlas;
|
|
531
602
|
this.device.queue.writeBuffer(this.camBuf, 0, this.cam);
|
|
532
603
|
if (occlusion)
|
|
533
604
|
this.ensureHiZ(occlusion.w, occlusion.h);
|
|
@@ -556,6 +627,8 @@ export class GpuShadowRenderer {
|
|
|
556
627
|
sp.setPipeline(this.shadowPipe);
|
|
557
628
|
sp.setBindGroup(0, this.shadowBG);
|
|
558
629
|
sp.drawIndirect(this.lightArgs, 0);
|
|
630
|
+
if (extraShadow)
|
|
631
|
+
extraShadow(sp, lightVP); // let the app cast extra occluders (mesh props/trees) into the same sun shadow map
|
|
559
632
|
sp.end();
|
|
560
633
|
// Pass 2: camera, shadow-sampled (camera-frustum + Hi-Z-occlusion-culled).
|
|
561
634
|
const mp = enc.beginRenderPass({
|