ugly-game 0.2.0 → 0.3.0
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/gpuDriven.d.ts +39 -0
- package/dist/gpuDriven.js +197 -0
- package/dist/gpuGI.d.ts +39 -0
- package/dist/gpuGI.js +253 -0
- package/dist/gpuGraph.d.ts +28 -0
- package/dist/gpuGraph.js +128 -0
- package/dist/gpuLit.d.ts +40 -0
- package/dist/gpuLit.js +193 -0
- package/dist/gpuPost.d.ts +33 -0
- package/dist/gpuPost.js +162 -0
- package/dist/gpuShadowMap.d.ts +11 -0
- package/dist/gpuShadowMap.js +41 -0
- package/dist/gpuSpot.d.ts +39 -0
- package/dist/gpuSpot.js +213 -0
- package/dist/gpuTaa.d.ts +34 -0
- package/dist/gpuTaa.js +247 -0
- package/dist/gpuTerrain.d.ts +25 -0
- package/dist/gpuTerrain.js +154 -0
- package/dist/gpuTiled.d.ts +55 -0
- package/dist/gpuTiled.js +247 -0
- package/dist/gpuWorld.d.ts +49 -0
- package/dist/gpuWorld.js +309 -0
- package/dist/shaderGraph.d.ts +84 -0
- package/dist/shaderGraph.js +231 -0
- package/package.json +1 -1
package/dist/gpuSpot.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Render IR increment 6 — POINT-LIGHT (spotlight) SHADOWS. Pairs increment 4 (many lights)
|
|
2
|
+
// with increment 5 (shadows): now the positional lights themselves cast real shadows. Full
|
|
3
|
+
// omnidirectional cube shadows for 1,600 lights is infeasible (6 faces each), so this is the
|
|
4
|
+
// tractable, honest form — a handful of shadow-casting SPOTLIGHTS, each with its own 2D shadow
|
|
5
|
+
// map (a perspective light view) packed into a depth-2d-ARRAY. Same PCF-comparison machinery
|
|
6
|
+
// as the sun, extended to a positional light with a cone + distance falloff, times L lights.
|
|
7
|
+
import { CUBE } from './gpuShadow';
|
|
8
|
+
export const NUM_SPOTS = 6;
|
|
9
|
+
const SHADOW_SIZE = 1536;
|
|
10
|
+
const COMMON = /* wgsl */ `
|
|
11
|
+
struct Inst { pos: vec4<f32>, scl: vec4<f32> }; // pos.w = hue (<0 ⇒ ground)
|
|
12
|
+
const CUBE_POS = array<vec3<f32>, 36>(${CUBE.pos});
|
|
13
|
+
const CUBE_NRM = array<vec3<f32>, 36>(${CUBE.nrm});
|
|
14
|
+
fn worldOf(inst: Inst, vi: u32) -> vec3<f32> { return inst.pos.xyz + CUBE_POS[vi] * inst.scl.xyz; }
|
|
15
|
+
`;
|
|
16
|
+
// Shadow pass: one light's perspective view-proj (bound per layer), depth only.
|
|
17
|
+
const SHADOW_WGSL = COMMON + /* wgsl */ `
|
|
18
|
+
@group(0) @binding(0) var<uniform> lightVP: mat4x4<f32>;
|
|
19
|
+
@group(0) @binding(1) var<storage, read> insts: array<Inst>;
|
|
20
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> @builtin(position) vec4<f32> {
|
|
21
|
+
return lightVP * vec4(worldOf(insts[ii], vi), 1.0);
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
const MAIN_WGSL = COMMON + /* wgsl */ `
|
|
25
|
+
struct U {
|
|
26
|
+
camVP: mat4x4<f32>,
|
|
27
|
+
lightVP: array<mat4x4<f32>, ${NUM_SPOTS}>,
|
|
28
|
+
pos: array<vec4<f32>, ${NUM_SPOTS}>, // xyz, w = range
|
|
29
|
+
dir: array<vec4<f32>, ${NUM_SPOTS}>, // xyz axis, w = cosOuter
|
|
30
|
+
col: array<vec4<f32>, ${NUM_SPOTS}>, // rgb, w = cosInner
|
|
31
|
+
params: vec4<f32>, // x = ambient, y = shadow size
|
|
32
|
+
};
|
|
33
|
+
@group(0) @binding(0) var<uniform> u: U;
|
|
34
|
+
@group(0) @binding(1) var<storage, read> insts: array<Inst>;
|
|
35
|
+
@group(0) @binding(2) var shadowMap: texture_depth_2d_array;
|
|
36
|
+
@group(0) @binding(3) var shadowSamp: sampler_comparison;
|
|
37
|
+
|
|
38
|
+
struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32 };
|
|
39
|
+
|
|
40
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
|
|
41
|
+
let inst = insts[ii];
|
|
42
|
+
let world = worldOf(inst, vi);
|
|
43
|
+
var o: VOut;
|
|
44
|
+
o.clip = u.camVP * vec4(world, 1.0);
|
|
45
|
+
o.world = world; o.nrm = normalize(CUBE_NRM[vi] / inst.scl.xyz); o.hue = inst.pos.w;
|
|
46
|
+
return o;
|
|
47
|
+
}
|
|
48
|
+
fn hsv(hh: f32) -> vec3<f32> {
|
|
49
|
+
let h = fract(hh) * 6.0; let x = 1.0 - abs((h % 2.0) - 1.0);
|
|
50
|
+
if (h < 1.0) { return vec3(1.0, x, 0.0); } if (h < 2.0) { return vec3(x, 1.0, 0.0); }
|
|
51
|
+
if (h < 3.0) { return vec3(0.0, 1.0, x); } if (h < 4.0) { return vec3(0.0, x, 1.0); }
|
|
52
|
+
if (h < 5.0) { return vec3(x, 0.0, 1.0); } return vec3(1.0, 0.0, x);
|
|
53
|
+
}
|
|
54
|
+
fn shadowPCF(i: i32, world: vec3<f32>, N: vec3<f32>, ndl: f32) -> f32 {
|
|
55
|
+
// Normal-offset bias: nudge the sample point along the surface normal (more the more grazing
|
|
56
|
+
// the light) so the occluder depth clears the receiver without peter-panning. Far more stable
|
|
57
|
+
// under a moving light than a pure depth bias — this is the main flicker fix.
|
|
58
|
+
let wo = world + N * (0.06 + 0.22 * (1.0 - ndl));
|
|
59
|
+
let lp = u.lightVP[i] * vec4(wo, 1.0);
|
|
60
|
+
let proj = lp.xyz / lp.w;
|
|
61
|
+
let uv = proj.xy * vec2(0.5, -0.5) + vec2(0.5, 0.5);
|
|
62
|
+
if (proj.z > 1.0 || proj.z < 0.0 || uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return 1.0; }
|
|
63
|
+
let bias = 0.0006;
|
|
64
|
+
let texel = 1.0 / u.params.y;
|
|
65
|
+
var s = 0.0;
|
|
66
|
+
for (var dy = -1; dy <= 1; dy = dy + 1) {
|
|
67
|
+
for (var dx = -1; dx <= 1; dx = dx + 1) {
|
|
68
|
+
s = s + textureSampleCompareLevel(shadowMap, shadowSamp, uv + vec2(f32(dx), f32(dy)) * texel, i, proj.z - bias);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return s / 9.0;
|
|
72
|
+
}
|
|
73
|
+
@fragment fn fs(v: VOut) -> @location(0) vec4<f32> {
|
|
74
|
+
let N = normalize(v.nrm);
|
|
75
|
+
let base = select(mix(vec3(0.32), hsv(v.hue), 0.55), vec3(0.5, 0.52, 0.56), v.hue < 0.0);
|
|
76
|
+
var acc = vec3(0.0);
|
|
77
|
+
for (var i = 0; i < ${NUM_SPOTS}; i = i + 1) {
|
|
78
|
+
let toL = u.pos[i].xyz - v.world;
|
|
79
|
+
let d = length(toL);
|
|
80
|
+
if (d > u.pos[i].w) { continue; }
|
|
81
|
+
let l = toL / d;
|
|
82
|
+
let ndl = max(dot(N, l), 0.0);
|
|
83
|
+
if (ndl <= 0.0) { continue; }
|
|
84
|
+
let cosA = dot(-l, u.dir[i].xyz); // frag direction vs cone axis
|
|
85
|
+
let cone = smoothstep(u.dir[i].w, u.col[i].w, cosA); // outer→inner
|
|
86
|
+
if (cone <= 0.0) { continue; }
|
|
87
|
+
let atten = cone * (1.0 - d / u.pos[i].w);
|
|
88
|
+
acc = acc + u.col[i].xyz * (ndl * atten * shadowPCF(i, v.world, N, ndl));
|
|
89
|
+
}
|
|
90
|
+
let lit = base * (u.params.x + acc);
|
|
91
|
+
return vec4(pow(clamp(lit, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0);
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
export class GpuSpotRenderer {
|
|
95
|
+
device;
|
|
96
|
+
instBuf;
|
|
97
|
+
uBuf;
|
|
98
|
+
lightVPBufs = [];
|
|
99
|
+
shadowTex;
|
|
100
|
+
layerViews = [];
|
|
101
|
+
shadowPipe;
|
|
102
|
+
mainPipe;
|
|
103
|
+
shadowBGL;
|
|
104
|
+
mainBGL;
|
|
105
|
+
shadowSampler;
|
|
106
|
+
shadowBGs = [];
|
|
107
|
+
mainBG;
|
|
108
|
+
count = 0;
|
|
109
|
+
u = new Float32Array(192);
|
|
110
|
+
vpScratch = new Float32Array(16);
|
|
111
|
+
constructor(device, format) {
|
|
112
|
+
this.device = device;
|
|
113
|
+
this.uBuf = device.createBuffer({ size: this.u.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
114
|
+
for (let i = 0; i < NUM_SPOTS; i++)
|
|
115
|
+
this.lightVPBufs.push(device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }));
|
|
116
|
+
this.shadowTex = device.createTexture({ size: [SHADOW_SIZE, SHADOW_SIZE, NUM_SPOTS], format: 'depth32float', usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING });
|
|
117
|
+
for (let i = 0; i < NUM_SPOTS; i++)
|
|
118
|
+
this.layerViews.push(this.shadowTex.createView({ dimension: '2d', baseArrayLayer: i, arrayLayerCount: 1 }));
|
|
119
|
+
const shadowMod = device.createShaderModule({ code: SHADOW_WGSL });
|
|
120
|
+
const mainMod = device.createShaderModule({ code: MAIN_WGSL });
|
|
121
|
+
this.shadowBGL = device.createBindGroupLayout({ entries: [
|
|
122
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
|
|
123
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
124
|
+
] });
|
|
125
|
+
this.shadowPipe = device.createRenderPipeline({
|
|
126
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.shadowBGL] }),
|
|
127
|
+
vertex: { module: shadowMod, entryPoint: 'vs' },
|
|
128
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
129
|
+
depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' },
|
|
130
|
+
});
|
|
131
|
+
this.mainBGL = device.createBindGroupLayout({ entries: [
|
|
132
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
133
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
134
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth', viewDimension: '2d-array' } },
|
|
135
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
136
|
+
] });
|
|
137
|
+
this.mainPipe = device.createRenderPipeline({
|
|
138
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.mainBGL] }),
|
|
139
|
+
vertex: { module: mainMod, entryPoint: 'vs' },
|
|
140
|
+
fragment: { module: mainMod, entryPoint: 'fs', targets: [{ format }] },
|
|
141
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
142
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
|
|
143
|
+
});
|
|
144
|
+
this.shadowSampler = device.createSampler({ compare: 'less' });
|
|
145
|
+
this.u[185] = SHADOW_SIZE; // params.y — texel size for PCF (params vec4 starts at float 184: x=184,y=185,z=186,w=187)
|
|
146
|
+
}
|
|
147
|
+
setScene(instances) {
|
|
148
|
+
this.count = instances.length;
|
|
149
|
+
const data = new Float32Array(this.count * 8);
|
|
150
|
+
for (let i = 0; i < this.count; i++) {
|
|
151
|
+
const c = instances[i], o = i * 8;
|
|
152
|
+
data[o] = c.x;
|
|
153
|
+
data[o + 1] = c.y;
|
|
154
|
+
data[o + 2] = c.z;
|
|
155
|
+
data[o + 3] = c.hue;
|
|
156
|
+
data[o + 4] = c.sx;
|
|
157
|
+
data[o + 5] = c.sy;
|
|
158
|
+
data[o + 6] = c.sz;
|
|
159
|
+
}
|
|
160
|
+
this.instBuf = this.device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
161
|
+
this.device.queue.writeBuffer(this.instBuf, 0, data);
|
|
162
|
+
this.shadowBGs = this.lightVPBufs.map((b) => this.device.createBindGroup({ layout: this.shadowBGL, entries: [
|
|
163
|
+
{ binding: 0, resource: { buffer: b } }, { binding: 1, resource: { buffer: this.instBuf } }
|
|
164
|
+
] }));
|
|
165
|
+
this.mainBG = this.device.createBindGroup({ layout: this.mainBGL, entries: [
|
|
166
|
+
{ binding: 0, resource: { buffer: this.uBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
|
|
167
|
+
{ binding: 2, resource: this.shadowTex.createView({ dimension: '2d-array' }) }, { binding: 3, resource: this.shadowSampler }
|
|
168
|
+
] });
|
|
169
|
+
}
|
|
170
|
+
/** camVP: Float32Array(16). spotVPs: NUM_SPOTS × Float32Array(16) (perspective light views). */
|
|
171
|
+
frame(enc, colorView, sceneDepth, camVP, spots, spotVPs, ambient) {
|
|
172
|
+
this.u.set(camVP, 0);
|
|
173
|
+
for (let i = 0; i < NUM_SPOTS; i++) {
|
|
174
|
+
this.u.set(spotVPs[i], 16 + i * 16);
|
|
175
|
+
const s = spots[i];
|
|
176
|
+
this.u[112 + i * 4] = s.px;
|
|
177
|
+
this.u[113 + i * 4] = s.py;
|
|
178
|
+
this.u[114 + i * 4] = s.pz;
|
|
179
|
+
this.u[115 + i * 4] = s.range;
|
|
180
|
+
this.u[136 + i * 4] = s.dx;
|
|
181
|
+
this.u[137 + i * 4] = s.dy;
|
|
182
|
+
this.u[138 + i * 4] = s.dz;
|
|
183
|
+
this.u[139 + i * 4] = s.cosOuter;
|
|
184
|
+
this.u[160 + i * 4] = s.r;
|
|
185
|
+
this.u[161 + i * 4] = s.g;
|
|
186
|
+
this.u[162 + i * 4] = s.b;
|
|
187
|
+
this.u[163 + i * 4] = s.cosInner;
|
|
188
|
+
this.vpScratch.set(spotVPs[i]);
|
|
189
|
+
this.device.queue.writeBuffer(this.lightVPBufs[i], 0, this.vpScratch);
|
|
190
|
+
}
|
|
191
|
+
this.u[184] = ambient;
|
|
192
|
+
this.device.queue.writeBuffer(this.uBuf, 0, this.u);
|
|
193
|
+
// L shadow passes — one per spotlight, into its own array layer.
|
|
194
|
+
for (let i = 0; i < NUM_SPOTS; i++) {
|
|
195
|
+
const sp = enc.beginRenderPass({ colorAttachments: [], depthStencilAttachment: {
|
|
196
|
+
view: this.layerViews[i], depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store'
|
|
197
|
+
} });
|
|
198
|
+
sp.setPipeline(this.shadowPipe);
|
|
199
|
+
sp.setBindGroup(0, this.shadowBGs[i]);
|
|
200
|
+
sp.draw(36, this.count);
|
|
201
|
+
sp.end();
|
|
202
|
+
}
|
|
203
|
+
// Main camera pass.
|
|
204
|
+
const mp = enc.beginRenderPass({
|
|
205
|
+
colorAttachments: [{ view: colorView, clearValue: { r: 0.02, g: 0.025, b: 0.035, a: 1 }, loadOp: 'clear', storeOp: 'store' }],
|
|
206
|
+
depthStencilAttachment: { view: sceneDepth, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store' }
|
|
207
|
+
});
|
|
208
|
+
mp.setPipeline(this.mainPipe);
|
|
209
|
+
mp.setBindGroup(0, this.mainBG);
|
|
210
|
+
mp.draw(36, this.count);
|
|
211
|
+
mp.end();
|
|
212
|
+
}
|
|
213
|
+
}
|
package/dist/gpuTaa.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CubeInstance } from './gpuShadow';
|
|
2
|
+
export type { CubeInstance } from './gpuShadow';
|
|
3
|
+
export declare class GpuTaaRenderer {
|
|
4
|
+
private device;
|
|
5
|
+
private w;
|
|
6
|
+
private h;
|
|
7
|
+
private instBuf;
|
|
8
|
+
private count;
|
|
9
|
+
private mBuf;
|
|
10
|
+
private rBuf;
|
|
11
|
+
private m;
|
|
12
|
+
private r;
|
|
13
|
+
private scenePipe;
|
|
14
|
+
private resolvePipe;
|
|
15
|
+
private sceneBGL;
|
|
16
|
+
private resolveBGL;
|
|
17
|
+
private sceneBG;
|
|
18
|
+
private sampler;
|
|
19
|
+
private colorTex?;
|
|
20
|
+
private colorView;
|
|
21
|
+
private motionTex?;
|
|
22
|
+
private motionView;
|
|
23
|
+
private depthTex?;
|
|
24
|
+
private depthView;
|
|
25
|
+
private hist;
|
|
26
|
+
private histView;
|
|
27
|
+
private resolveBG;
|
|
28
|
+
private ping;
|
|
29
|
+
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
30
|
+
resize(w: number, h: number): void;
|
|
31
|
+
setScene(instances: CubeInstance[]): void;
|
|
32
|
+
/** currVP/prevVP: Float32Array(16), UNjittered. jitter: clip-space sub-pixel offset. */
|
|
33
|
+
frame(enc: GPUCommandEncoder, swapView: GPUTextureView, currVP: Float32Array, prevVP: Float32Array, jitter: [number, number], sunDir: [number, number, number], ambient: number, taaOn: boolean, blend: number): void;
|
|
34
|
+
}
|
package/dist/gpuTaa.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Render IR increment 7 — TEMPORAL ANTI-ALIASING (TAA). The last of GAME.md §3.6's named
|
|
2
|
+
// "biggest look gaps", and the proper fix for the residual shadow/edge shimmer of a moving
|
|
3
|
+
// camera. The engine complexity here is the temporal pipeline, not the shading:
|
|
4
|
+
// 1. jitter the projection a sub-pixel amount each frame (Halton sequence),
|
|
5
|
+
// 2. render scene color + per-pixel MOTION VECTORS to offscreen targets (MRT),
|
|
6
|
+
// 3. reproject last frame's history through the motion vectors, clamp it to the current
|
|
7
|
+
// pixel's neighborhood colour box (kills ghosting), and blend ~90% history / 10% current.
|
|
8
|
+
// Averaging the jittered samples over time = anti-aliasing + temporal stability, ~free per frame.
|
|
9
|
+
//
|
|
10
|
+
// Reuses the cube geometry from gpuShadow. Sun-lit (no shadow map here) to keep the module
|
|
11
|
+
// focused on the temporal machinery. `?taa=0` on the page disables jitter + accumulation so the
|
|
12
|
+
// raw aliased image is visible for comparison.
|
|
13
|
+
import { CUBE } from './gpuShadow';
|
|
14
|
+
const COLOR_FMT = 'rgba16float';
|
|
15
|
+
const MOTION_FMT = 'rg16float';
|
|
16
|
+
const SCENE_WGSL = /* wgsl */ `
|
|
17
|
+
struct Inst { pos: vec4<f32>, scl: vec4<f32> };
|
|
18
|
+
struct MU {
|
|
19
|
+
currVP: mat4x4<f32>,
|
|
20
|
+
prevVP: mat4x4<f32>,
|
|
21
|
+
jitter: vec4<f32>, // xy = clip-space sub-pixel jitter
|
|
22
|
+
sun: vec4<f32>, // xyz = direction, w = ambient
|
|
23
|
+
};
|
|
24
|
+
@group(0) @binding(0) var<uniform> mu: MU;
|
|
25
|
+
@group(0) @binding(1) var<storage, read> insts: array<Inst>;
|
|
26
|
+
const CUBE_POS = array<vec3<f32>, 36>(${CUBE.pos});
|
|
27
|
+
const CUBE_NRM = array<vec3<f32>, 36>(${CUBE.nrm});
|
|
28
|
+
|
|
29
|
+
struct VOut {
|
|
30
|
+
@builtin(position) clip: vec4<f32>,
|
|
31
|
+
@location(0) curr: vec4<f32>, // unjittered current clip (for motion)
|
|
32
|
+
@location(1) prev: vec4<f32>, // previous-frame clip (for motion)
|
|
33
|
+
@location(2) nrm: vec3<f32>,
|
|
34
|
+
@location(3) hue: f32,
|
|
35
|
+
};
|
|
36
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
|
|
37
|
+
let inst = insts[ii];
|
|
38
|
+
let world = inst.pos.xyz + CUBE_POS[vi] * inst.scl.xyz;
|
|
39
|
+
let c = mu.currVP * vec4(world, 1.0);
|
|
40
|
+
var o: VOut;
|
|
41
|
+
o.curr = c;
|
|
42
|
+
o.prev = mu.prevVP * vec4(world, 1.0);
|
|
43
|
+
var cj = c; // jitter is added post-projection, scaled by w
|
|
44
|
+
cj.x = cj.x + mu.jitter.x * c.w;
|
|
45
|
+
cj.y = cj.y + mu.jitter.y * c.w;
|
|
46
|
+
o.clip = cj;
|
|
47
|
+
o.nrm = normalize(CUBE_NRM[vi] / inst.scl.xyz);
|
|
48
|
+
o.hue = inst.pos.w;
|
|
49
|
+
return o;
|
|
50
|
+
}
|
|
51
|
+
fn hsv(hh: f32) -> vec3<f32> {
|
|
52
|
+
let h = fract(hh) * 6.0; let x = 1.0 - abs((h % 2.0) - 1.0);
|
|
53
|
+
if (h < 1.0) { return vec3(1.0, x, 0.0); } if (h < 2.0) { return vec3(x, 1.0, 0.0); }
|
|
54
|
+
if (h < 3.0) { return vec3(0.0, 1.0, x); } if (h < 4.0) { return vec3(0.0, x, 1.0); }
|
|
55
|
+
if (h < 5.0) { return vec3(x, 0.0, 1.0); } return vec3(1.0, 0.0, x);
|
|
56
|
+
}
|
|
57
|
+
struct FOut { @location(0) color: vec4<f32>, @location(1) motion: vec2<f32> };
|
|
58
|
+
@fragment fn fs(v: VOut) -> FOut {
|
|
59
|
+
let N = normalize(v.nrm);
|
|
60
|
+
let ndl = max(dot(N, -normalize(mu.sun.xyz)), 0.0);
|
|
61
|
+
let base = select(mix(vec3(0.30), hsv(v.hue), 0.6), vec3(0.55, 0.57, 0.62), v.hue < 0.0);
|
|
62
|
+
let lit = base * (mu.sun.w + ndl);
|
|
63
|
+
let currUV = v.curr.xy / v.curr.w * vec2(0.5, -0.5) + vec2(0.5, 0.5);
|
|
64
|
+
let prevUV = v.prev.xy / v.prev.w * vec2(0.5, -0.5) + vec2(0.5, 0.5);
|
|
65
|
+
var o: FOut;
|
|
66
|
+
o.color = vec4(lit, 1.0);
|
|
67
|
+
o.motion = currUV - prevUV; // prevUV = currUV - motion
|
|
68
|
+
return o;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
const RESOLVE_WGSL = /* wgsl */ `
|
|
72
|
+
struct RU { texel: vec4<f32>, ctrl: vec4<f32> }; // texel.xy = 1/size; ctrl.x = history blend, ctrl.y = taa on
|
|
73
|
+
@group(0) @binding(0) var<uniform> ru: RU;
|
|
74
|
+
@group(0) @binding(1) var histSamp: sampler;
|
|
75
|
+
@group(0) @binding(2) var colorTex: texture_2d<f32>;
|
|
76
|
+
@group(0) @binding(3) var motionTex: texture_2d<f32>;
|
|
77
|
+
@group(0) @binding(4) var histTex: texture_2d<f32>;
|
|
78
|
+
|
|
79
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
|
|
80
|
+
var p = array<vec2<f32>, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
|
81
|
+
return vec4(p[vi], 0.0, 1.0);
|
|
82
|
+
}
|
|
83
|
+
struct ROut { @location(0) screen: vec4<f32>, @location(1) hist: vec4<f32> };
|
|
84
|
+
@fragment fn fs(@builtin(position) fc: vec4<f32>) -> ROut {
|
|
85
|
+
let px = vec2<i32>(floor(fc.xy));
|
|
86
|
+
let cur = textureLoad(colorTex, px, 0).rgb;
|
|
87
|
+
var o: ROut;
|
|
88
|
+
if (ru.ctrl.y < 0.5) { o.screen = vec4(cur, 1.0); o.hist = vec4(cur, 1.0); return o; }
|
|
89
|
+
// 3×3 neighborhood colour box of the current frame (ghosting clamp).
|
|
90
|
+
var mn = cur; var mx = cur;
|
|
91
|
+
for (var dy = -1; dy <= 1; dy = dy + 1) {
|
|
92
|
+
for (var dx = -1; dx <= 1; dx = dx + 1) {
|
|
93
|
+
let c = textureLoad(colorTex, px + vec2<i32>(dx, dy), 0).rgb;
|
|
94
|
+
mn = min(mn, c); mx = max(mx, c);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let motion = textureLoad(motionTex, px, 0).xy;
|
|
98
|
+
let uv = fc.xy * ru.texel.xy;
|
|
99
|
+
let prevUV = uv - motion;
|
|
100
|
+
var outc = cur;
|
|
101
|
+
if (prevUV.x >= 0.0 && prevUV.x <= 1.0 && prevUV.y >= 0.0 && prevUV.y <= 1.0) {
|
|
102
|
+
let hist = clamp(textureSampleLevel(histTex, histSamp, prevUV, 0.0).rgb, mn, mx);
|
|
103
|
+
outc = mix(cur, hist, ru.ctrl.x); // ctrl.x ≈ 0.9 ⇒ 10% current, 90% reprojected history
|
|
104
|
+
}
|
|
105
|
+
o.screen = vec4(outc, 1.0); o.hist = vec4(outc, 1.0);
|
|
106
|
+
return o;
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
export class GpuTaaRenderer {
|
|
110
|
+
device;
|
|
111
|
+
w = 0;
|
|
112
|
+
h = 0;
|
|
113
|
+
instBuf;
|
|
114
|
+
count = 0;
|
|
115
|
+
mBuf;
|
|
116
|
+
rBuf;
|
|
117
|
+
m = new Float32Array(44); // 2×mat4 + 2×vec4
|
|
118
|
+
r = new Float32Array(8);
|
|
119
|
+
scenePipe;
|
|
120
|
+
resolvePipe;
|
|
121
|
+
sceneBGL;
|
|
122
|
+
resolveBGL;
|
|
123
|
+
sceneBG;
|
|
124
|
+
sampler;
|
|
125
|
+
colorTex;
|
|
126
|
+
colorView;
|
|
127
|
+
motionTex;
|
|
128
|
+
motionView;
|
|
129
|
+
depthTex;
|
|
130
|
+
depthView;
|
|
131
|
+
hist = [];
|
|
132
|
+
histView = [];
|
|
133
|
+
resolveBG = [];
|
|
134
|
+
ping = 0;
|
|
135
|
+
constructor(device, format) {
|
|
136
|
+
this.device = device;
|
|
137
|
+
this.mBuf = device.createBuffer({ size: this.m.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
138
|
+
this.rBuf = device.createBuffer({ size: this.r.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
139
|
+
this.sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
|
140
|
+
const sceneMod = device.createShaderModule({ code: SCENE_WGSL });
|
|
141
|
+
const resolveMod = device.createShaderModule({ code: RESOLVE_WGSL });
|
|
142
|
+
this.sceneBGL = device.createBindGroupLayout({ entries: [
|
|
143
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
144
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
145
|
+
] });
|
|
146
|
+
this.scenePipe = device.createRenderPipeline({
|
|
147
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.sceneBGL] }),
|
|
148
|
+
vertex: { module: sceneMod, entryPoint: 'vs' },
|
|
149
|
+
fragment: { module: sceneMod, entryPoint: 'fs', targets: [{ format: COLOR_FMT }, { format: MOTION_FMT }] },
|
|
150
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
151
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
|
|
152
|
+
});
|
|
153
|
+
this.resolveBGL = device.createBindGroupLayout({ entries: [
|
|
154
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
155
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
156
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
157
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
158
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
159
|
+
] });
|
|
160
|
+
this.resolvePipe = device.createRenderPipeline({
|
|
161
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.resolveBGL] }),
|
|
162
|
+
vertex: { module: resolveMod, entryPoint: 'vs' },
|
|
163
|
+
fragment: { module: resolveMod, entryPoint: 'fs', targets: [{ format }, { format: COLOR_FMT }] },
|
|
164
|
+
primitive: { topology: 'triangle-list' },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
resize(w, h) {
|
|
168
|
+
this.w = w;
|
|
169
|
+
this.h = h;
|
|
170
|
+
for (const t of [this.colorTex, this.motionTex, this.depthTex, ...this.hist])
|
|
171
|
+
t?.destroy();
|
|
172
|
+
const mk = (format, usage) => this.device.createTexture({ size: [w, h], format, usage });
|
|
173
|
+
const RA = GPUTextureUsage.RENDER_ATTACHMENT, TB = GPUTextureUsage.TEXTURE_BINDING;
|
|
174
|
+
this.colorTex = mk(COLOR_FMT, RA | TB);
|
|
175
|
+
this.colorView = this.colorTex.createView();
|
|
176
|
+
this.motionTex = mk(MOTION_FMT, RA | TB);
|
|
177
|
+
this.motionView = this.motionTex.createView();
|
|
178
|
+
this.depthTex = mk('depth24plus', RA);
|
|
179
|
+
this.depthView = this.depthTex.createView();
|
|
180
|
+
this.hist = [mk(COLOR_FMT, RA | TB), mk(COLOR_FMT, RA | TB)];
|
|
181
|
+
this.histView = this.hist.map((t) => t.createView());
|
|
182
|
+
// Resolve bind group i READS history[i] and (this frame) WRITES history[1-i].
|
|
183
|
+
this.resolveBG = [0, 1].map((i) => this.device.createBindGroup({ layout: this.resolveBGL, entries: [
|
|
184
|
+
{ binding: 0, resource: { buffer: this.rBuf } }, { binding: 1, resource: this.sampler },
|
|
185
|
+
{ binding: 2, resource: this.colorView }, { binding: 3, resource: this.motionView }, { binding: 4, resource: this.histView[i] }
|
|
186
|
+
] }));
|
|
187
|
+
this.ping = 0;
|
|
188
|
+
}
|
|
189
|
+
setScene(instances) {
|
|
190
|
+
this.count = instances.length;
|
|
191
|
+
const data = new Float32Array(this.count * 8);
|
|
192
|
+
for (let i = 0; i < this.count; i++) {
|
|
193
|
+
const c = instances[i], o = i * 8;
|
|
194
|
+
data[o] = c.x;
|
|
195
|
+
data[o + 1] = c.y;
|
|
196
|
+
data[o + 2] = c.z;
|
|
197
|
+
data[o + 3] = c.hue;
|
|
198
|
+
data[o + 4] = c.sx;
|
|
199
|
+
data[o + 5] = c.sy;
|
|
200
|
+
data[o + 6] = c.sz;
|
|
201
|
+
}
|
|
202
|
+
this.instBuf = this.device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
203
|
+
this.device.queue.writeBuffer(this.instBuf, 0, data);
|
|
204
|
+
this.sceneBG = this.device.createBindGroup({ layout: this.sceneBGL, entries: [
|
|
205
|
+
{ binding: 0, resource: { buffer: this.mBuf } }, { binding: 1, resource: { buffer: this.instBuf } }
|
|
206
|
+
] });
|
|
207
|
+
}
|
|
208
|
+
/** currVP/prevVP: Float32Array(16), UNjittered. jitter: clip-space sub-pixel offset. */
|
|
209
|
+
frame(enc, swapView, currVP, prevVP, jitter, sunDir, ambient, taaOn, blend) {
|
|
210
|
+
this.m.set(currVP, 0);
|
|
211
|
+
this.m.set(prevVP, 16);
|
|
212
|
+
this.m[32] = taaOn ? jitter[0] : 0;
|
|
213
|
+
this.m[33] = taaOn ? jitter[1] : 0;
|
|
214
|
+
this.m[36] = sunDir[0];
|
|
215
|
+
this.m[37] = sunDir[1];
|
|
216
|
+
this.m[38] = sunDir[2];
|
|
217
|
+
this.m[39] = ambient;
|
|
218
|
+
this.device.queue.writeBuffer(this.mBuf, 0, this.m);
|
|
219
|
+
this.r[0] = 1 / this.w;
|
|
220
|
+
this.r[1] = 1 / this.h;
|
|
221
|
+
this.r[4] = blend;
|
|
222
|
+
this.r[5] = taaOn ? 1 : 0;
|
|
223
|
+
this.device.queue.writeBuffer(this.rBuf, 0, this.r);
|
|
224
|
+
// Scene → color + motion (MRT), jittered.
|
|
225
|
+
const sp = enc.beginRenderPass({
|
|
226
|
+
colorAttachments: [
|
|
227
|
+
{ view: this.colorView, clearValue: { r: 0.02, g: 0.025, b: 0.035, a: 1 }, loadOp: 'clear', storeOp: 'store' },
|
|
228
|
+
{ view: this.motionView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store' }
|
|
229
|
+
],
|
|
230
|
+
depthStencilAttachment: { view: this.depthView, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store' }
|
|
231
|
+
});
|
|
232
|
+
sp.setPipeline(this.scenePipe);
|
|
233
|
+
sp.setBindGroup(0, this.sceneBG);
|
|
234
|
+
sp.draw(36, this.count);
|
|
235
|
+
sp.end();
|
|
236
|
+
// Resolve: read history[ping], write screen + history[1-ping].
|
|
237
|
+
const rp = enc.beginRenderPass({ colorAttachments: [
|
|
238
|
+
{ view: swapView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: 'clear', storeOp: 'store' },
|
|
239
|
+
{ view: this.histView[1 - this.ping], loadOp: 'clear', storeOp: 'store' }
|
|
240
|
+
] });
|
|
241
|
+
rp.setPipeline(this.resolvePipe);
|
|
242
|
+
rp.setBindGroup(0, this.resolveBG[this.ping]);
|
|
243
|
+
rp.draw(3);
|
|
244
|
+
rp.end();
|
|
245
|
+
this.ping = 1 - this.ping;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type GpuShadowMap } from './gpuShadowMap';
|
|
2
|
+
export declare class GpuTerrainRenderer {
|
|
3
|
+
private device;
|
|
4
|
+
private terrainPipe;
|
|
5
|
+
private waterPipe;
|
|
6
|
+
private occPipe;
|
|
7
|
+
private uBuf;
|
|
8
|
+
private uBG;
|
|
9
|
+
private uBGw;
|
|
10
|
+
private lightBuf;
|
|
11
|
+
private occBG;
|
|
12
|
+
private posB;
|
|
13
|
+
private nrmB;
|
|
14
|
+
private colB;
|
|
15
|
+
private idxB;
|
|
16
|
+
private idxCount;
|
|
17
|
+
private waterB;
|
|
18
|
+
private texel;
|
|
19
|
+
constructor(device: GPUDevice, format: GPUTextureFormat, shadow: GpuShadowMap);
|
|
20
|
+
/** Replace the terrain mesh (positions/normals/colors are Nx3, indices triangle list). */
|
|
21
|
+
setTerrain(positions: Float32Array, normals: Float32Array, colors: Float32Array, indices: Uint32Array): void;
|
|
22
|
+
/** Occluder pass: draw the terrain depth into the shared shadow map from the sun's ortho view. */
|
|
23
|
+
shadowPass(pass: GPURenderPassEncoder, lightVP: Float32Array): void;
|
|
24
|
+
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, sea: number, extent: number, center: [number, number], lightVP: Float32Array, time?: number, load?: boolean): void;
|
|
25
|
+
}
|