ugly-game 0.5.15 → 0.5.17
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/gpuShadow.d.ts +1 -1
- package/dist/gpuShadow.js +6 -1
- package/dist/gpuSsao.d.ts +10 -0
- package/dist/gpuSsao.js +156 -0
- package/package.json +1 -1
package/dist/gpuShadow.d.ts
CHANGED
|
@@ -107,7 +107,7 @@ export declare class GpuShadowRenderer {
|
|
|
107
107
|
depth: GPUTextureView;
|
|
108
108
|
w: number;
|
|
109
109
|
h: number;
|
|
110
|
-
}, tint?: [number, number, number, number], extraShadow?: (sp: GPURenderPassEncoder, lightVP: Float32Array) => void, up?: [number, number, number]): void;
|
|
110
|
+
}, tint?: [number, number, number, number], extraShadow?: (sp: GPURenderPassEncoder, lightVP: Float32Array) => void, up?: [number, number, number], linearOut?: boolean): void;
|
|
111
111
|
/** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
|
|
112
112
|
* safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
|
|
113
113
|
readCullStats(): Promise<{
|
package/dist/gpuShadow.js
CHANGED
|
@@ -254,6 +254,7 @@ fn fixedFill(N: vec3<f32>, up: vec3<f32>) -> f32 {
|
|
|
254
254
|
@fragment fn fs(i: VOut) -> @location(0) vec4<f32> {
|
|
255
255
|
if (i.emis > 0.5) { // self-glowing effect cube: full-bright, unlit
|
|
256
256
|
let g = mix(hsv(i.hue), vec3(1.0), 0.4);
|
|
257
|
+
if (cam.up.w > 0.5) { return vec4(g * 2.6, 1.0); } // linear-HDR path: push emissive >1 so it blooms
|
|
257
258
|
return vec4(pow(clamp(g, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0);
|
|
258
259
|
}
|
|
259
260
|
let N = normalize(i.nrm);
|
|
@@ -287,6 +288,9 @@ fn fixedFill(N: vec3<f32>, up: vec3<f32>) -> f32 {
|
|
|
287
288
|
let fill = cam.sun.w * (0.5 + 0.85 * fixedFill(N, cam.up.xyz));
|
|
288
289
|
let key = cam.sunCol.xyz * ndl * sh; // direct sun (shadowed) — primary light
|
|
289
290
|
let lit = base * (fill + key) * ao;
|
|
291
|
+
// cam.up.w > 0.5 selects LINEAR-HDR output (unclamped, no gamma) for an HDR pipeline that tonemaps downstream;
|
|
292
|
+
// otherwise the classic gamma-encoded LDR path (all existing consumers). Keeps both looks from one shader.
|
|
293
|
+
if (cam.up.w > 0.5) { return vec4(max(lit, vec3(0.0)), 1.0); }
|
|
290
294
|
return vec4(pow(clamp(lit, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0); // gamma
|
|
291
295
|
}
|
|
292
296
|
`;
|
|
@@ -602,7 +606,7 @@ export class GpuShadowRenderer {
|
|
|
602
606
|
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. Pass `occlusion` (the depth target +
|
|
603
607
|
* its size) to enable Hi-Z occlusion culling on the camera pass — the renderer rebuilds a coarse Hi-Z from that
|
|
604
608
|
* depth each frame and the NEXT frame's cull uses it (hole-free: the first frame + any resize skip occlusion). */
|
|
605
|
-
frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false, occlusion, tint = [1, 1, 1, 0], extraShadow, up = [0, 1, 0]) {
|
|
609
|
+
frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false, occlusion, tint = [1, 1, 1, 0], extraShadow, up = [0, 1, 0], linearOut = false) {
|
|
606
610
|
this.cam.set(camVP, 0);
|
|
607
611
|
this.cam.set(lightVP, 16);
|
|
608
612
|
this.cam[32] = sunDir[0];
|
|
@@ -621,6 +625,7 @@ export class GpuShadowRenderer {
|
|
|
621
625
|
this.cam[48] = up[0];
|
|
622
626
|
this.cam[49] = up[1];
|
|
623
627
|
this.cam[50] = up[2];
|
|
628
|
+
this.cam[51] = linearOut ? 1 : 0; // up.w = linear-HDR output flag
|
|
624
629
|
this.device.queue.writeBuffer(this.camBuf, 0, this.cam);
|
|
625
630
|
if (occlusion)
|
|
626
631
|
this.ensureHiZ(occlusion.w, occlusion.h);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class GpuSSAO {
|
|
2
|
+
private device;
|
|
3
|
+
private pipe;
|
|
4
|
+
private uBuf;
|
|
5
|
+
private bg;
|
|
6
|
+
private u;
|
|
7
|
+
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
8
|
+
/** color = scene colour (input), depth = scene depth, out = darkened target the water pass then samples. */
|
|
9
|
+
frame(enc: GPUCommandEncoder, out: GPUTextureView, color: GPUTextureView, depth: GPUTextureView, camVP: Float32Array, w: number, h: number, radius?: number, bias?: number, strength?: number, maxDarken?: number): void;
|
|
10
|
+
}
|
package/dist/gpuSsao.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// SCREEN-SPACE CONTACT AO — the missing cue that makes voxel terrain read as 3D. Fixed directional fill shades a
|
|
2
|
+
// face by its ORIENTATION, but a large flat ground plane has one normal everywhere, so it still looks flat. What
|
|
3
|
+
// the eye actually uses to read shape is the DARK where surfaces meet: the base of a step, the inside corner of a
|
|
4
|
+
// pit, the ground under a tree. This pass reconstructs each pixel's world position from the scene depth (via the
|
|
5
|
+
// inverse view-projection, so it's immune to the GL-vs-WebGPU depth-range convention), estimates a surface normal
|
|
6
|
+
// from neighbours, samples a small normal-oriented hemisphere, and darkens where nearby geometry rises above the
|
|
7
|
+
// tangent plane. It writes scene*ao into a second colour target the caller then composites (blit / water / post).
|
|
8
|
+
//
|
|
9
|
+
// PLATFORM PASS. Reusable by any ugly-game renderer: run it after the opaque scene is drawn, feeding the scene
|
|
10
|
+
// colour + depth and the same camera view-projection, then sample the AO target instead of the raw scene. Pure
|
|
11
|
+
// WebGPU — no engine deps.
|
|
12
|
+
const SHADER = /* wgsl */ `
|
|
13
|
+
struct U {
|
|
14
|
+
invVP: mat4x4<f32>,
|
|
15
|
+
params: vec4<f32>, // x = world radius, y = bias, z = strength, w = max darken
|
|
16
|
+
dim: vec4<f32>, // xy = target size in px
|
|
17
|
+
};
|
|
18
|
+
@group(0) @binding(0) var<uniform> u: U;
|
|
19
|
+
@group(0) @binding(1) var colTex: texture_2d<f32>;
|
|
20
|
+
@group(0) @binding(2) var depthTex: texture_depth_2d;
|
|
21
|
+
|
|
22
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
|
|
23
|
+
var p = array<vec2<f32>, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
|
24
|
+
return vec4(p[vi], 0.0, 1.0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn worldAt(px: vec2<i32>, dim: vec2<f32>) -> vec4<f32> {
|
|
28
|
+
let c = clamp(px, vec2<i32>(0, 0), vec2<i32>(dim) - vec2<i32>(1, 1));
|
|
29
|
+
let d = textureLoad(depthTex, c, 0);
|
|
30
|
+
let uv = (vec2<f32>(c) + vec2(0.5, 0.5)) / dim;
|
|
31
|
+
let ndc = vec4(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0, d, 1.0);
|
|
32
|
+
let w = u.invVP * ndc;
|
|
33
|
+
return vec4(w.xyz / w.w, d); // xyz = rebased world pos, w = stored depth
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 8 unit directions on the screen; sampled at two pixel radii for multi-scale contact darkening.
|
|
37
|
+
const DIRS = array<vec2<f32>, 8>(
|
|
38
|
+
vec2( 1.0, 0.0), vec2(-1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, -1.0),
|
|
39
|
+
vec2( 0.7, 0.7), vec2(-0.7, 0.7), vec2(0.7, -0.7), vec2(-0.7, -0.7));
|
|
40
|
+
|
|
41
|
+
@fragment fn fs(@builtin(position) fc: vec4<f32>) -> @location(0) vec4<f32> {
|
|
42
|
+
let px = vec2<i32>(fc.xy);
|
|
43
|
+
let dim = u.dim.xy;
|
|
44
|
+
let base = textureLoad(colTex, px, 0).rgb;
|
|
45
|
+
let ctr = worldAt(px, dim);
|
|
46
|
+
if (ctr.w >= 0.9999) { return vec4(base, 1.0); } // sky — no AO
|
|
47
|
+
|
|
48
|
+
let P = ctr.xyz;
|
|
49
|
+
let Px = worldAt(px + vec2<i32>(1, 0), dim).xyz;
|
|
50
|
+
let Py = worldAt(px + vec2<i32>(0, 1), dim).xyz;
|
|
51
|
+
let N = normalize(cross(Px - P, Py - P));
|
|
52
|
+
|
|
53
|
+
let radius = u.params.x;
|
|
54
|
+
let bias = u.params.y;
|
|
55
|
+
var occ = 0.0;
|
|
56
|
+
var wsum = 0.0;
|
|
57
|
+
// two pixel radii → catches both tight corners and broader valleys
|
|
58
|
+
let rads = array<f32, 2>(3.0, 8.0);
|
|
59
|
+
for (var r = 0; r < 2; r = r + 1) {
|
|
60
|
+
for (var i = 0; i < 8; i = i + 1) {
|
|
61
|
+
let o = vec2<i32>(DIRS[i] * rads[r]);
|
|
62
|
+
let s = worldAt(px + o, dim);
|
|
63
|
+
if (s.w >= 0.9999) { wsum = wsum + 1.0; continue; } // neighbour is sky → not occluding
|
|
64
|
+
let v = s.xyz - P;
|
|
65
|
+
let dist = length(v);
|
|
66
|
+
if (dist < 1e-4) { continue; }
|
|
67
|
+
// occluded when the neighbour rises above the tangent plane (dot(N,dir) > bias), attenuated by world distance
|
|
68
|
+
let ao = max(dot(v / dist, N) - bias, 0.0) * (1.0 - smoothstep(0.0, radius, dist));
|
|
69
|
+
occ = occ + ao;
|
|
70
|
+
wsum = wsum + 1.0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
occ = select(0.0, occ / wsum, wsum > 0.0);
|
|
74
|
+
let darken = clamp(occ * u.params.z, 0.0, u.params.w);
|
|
75
|
+
return vec4(base * (1.0 - darken), 1.0);
|
|
76
|
+
}`;
|
|
77
|
+
/** Invert a column-major 4×4 (Float32Array). Returns a new Float32Array; identity if singular. */
|
|
78
|
+
function invert4(m) {
|
|
79
|
+
const a = m;
|
|
80
|
+
const b = new Float32Array(16);
|
|
81
|
+
const m00 = a[0], m01 = a[1], m02 = a[2], m03 = a[3];
|
|
82
|
+
const m10 = a[4], m11 = a[5], m12 = a[6], m13 = a[7];
|
|
83
|
+
const m20 = a[8], m21 = a[9], m22 = a[10], m23 = a[11];
|
|
84
|
+
const m30 = a[12], m31 = a[13], m32 = a[14], m33 = a[15];
|
|
85
|
+
const b00 = m00 * m11 - m01 * m10, b01 = m00 * m12 - m02 * m10;
|
|
86
|
+
const b02 = m00 * m13 - m03 * m10, b03 = m01 * m12 - m02 * m11;
|
|
87
|
+
const b04 = m01 * m13 - m03 * m11, b05 = m02 * m13 - m03 * m12;
|
|
88
|
+
const b06 = m20 * m31 - m21 * m30, b07 = m20 * m32 - m22 * m30;
|
|
89
|
+
const b08 = m20 * m33 - m23 * m30, b09 = m21 * m32 - m22 * m31;
|
|
90
|
+
const b10 = m21 * m33 - m23 * m31, b11 = m22 * m33 - m23 * m32;
|
|
91
|
+
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
|
|
92
|
+
if (!det) {
|
|
93
|
+
b[0] = b[5] = b[10] = b[15] = 1;
|
|
94
|
+
return b;
|
|
95
|
+
}
|
|
96
|
+
det = 1.0 / det;
|
|
97
|
+
b[0] = (m11 * b11 - m12 * b10 + m13 * b09) * det;
|
|
98
|
+
b[1] = (m02 * b10 - m01 * b11 - m03 * b09) * det;
|
|
99
|
+
b[2] = (m31 * b05 - m32 * b04 + m33 * b03) * det;
|
|
100
|
+
b[3] = (m22 * b04 - m21 * b05 - m23 * b03) * det;
|
|
101
|
+
b[4] = (m12 * b08 - m10 * b11 - m13 * b07) * det;
|
|
102
|
+
b[5] = (m00 * b11 - m02 * b08 + m03 * b07) * det;
|
|
103
|
+
b[6] = (m32 * b02 - m30 * b05 - m33 * b01) * det;
|
|
104
|
+
b[7] = (m20 * b05 - m22 * b02 + m23 * b01) * det;
|
|
105
|
+
b[8] = (m10 * b10 - m11 * b08 + m13 * b06) * det;
|
|
106
|
+
b[9] = (m01 * b08 - m00 * b10 - m03 * b06) * det;
|
|
107
|
+
b[10] = (m30 * b04 - m31 * b02 + m33 * b00) * det;
|
|
108
|
+
b[11] = (m21 * b02 - m20 * b04 - m23 * b00) * det;
|
|
109
|
+
b[12] = (m11 * b07 - m10 * b09 - m12 * b06) * det;
|
|
110
|
+
b[13] = (m00 * b09 - m01 * b07 + m02 * b06) * det;
|
|
111
|
+
b[14] = (m31 * b01 - m30 * b03 - m32 * b00) * det;
|
|
112
|
+
b[15] = (m20 * b03 - m21 * b01 + m22 * b00) * det;
|
|
113
|
+
return b;
|
|
114
|
+
}
|
|
115
|
+
export class GpuSSAO {
|
|
116
|
+
device;
|
|
117
|
+
pipe;
|
|
118
|
+
uBuf;
|
|
119
|
+
bg = null;
|
|
120
|
+
u = new Float32Array(24); // invVP(16) + params(4) + dim(4)
|
|
121
|
+
constructor(device, format) {
|
|
122
|
+
this.device = device;
|
|
123
|
+
const sm = device.createShaderModule({ code: SHADER });
|
|
124
|
+
this.pipe = device.createRenderPipeline({
|
|
125
|
+
layout: 'auto',
|
|
126
|
+
vertex: { module: sm, entryPoint: 'vs' },
|
|
127
|
+
fragment: { module: sm, entryPoint: 'fs', targets: [{ format }] },
|
|
128
|
+
primitive: { topology: 'triangle-list' },
|
|
129
|
+
});
|
|
130
|
+
this.uBuf = device.createBuffer({ size: 96, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
131
|
+
}
|
|
132
|
+
/** color = scene colour (input), depth = scene depth, out = darkened target the water pass then samples. */
|
|
133
|
+
frame(enc, out, color, depth, camVP, w, h, radius = 2.2, bias = 0.12, strength = 1.9, maxDarken = 0.72) {
|
|
134
|
+
this.u.set(invert4(camVP), 0);
|
|
135
|
+
this.u[16] = radius;
|
|
136
|
+
this.u[17] = bias;
|
|
137
|
+
this.u[18] = strength;
|
|
138
|
+
this.u[19] = maxDarken;
|
|
139
|
+
this.u[20] = w;
|
|
140
|
+
this.u[21] = h;
|
|
141
|
+
this.device.queue.writeBuffer(this.uBuf, 0, this.u);
|
|
142
|
+
this.bg = this.device.createBindGroup({
|
|
143
|
+
layout: this.pipe.getBindGroupLayout(0),
|
|
144
|
+
entries: [
|
|
145
|
+
{ binding: 0, resource: { buffer: this.uBuf } },
|
|
146
|
+
{ binding: 1, resource: color },
|
|
147
|
+
{ binding: 2, resource: depth },
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
const rp = enc.beginRenderPass({ colorAttachments: [{ view: out, loadOp: 'clear', clearValue: { r: 0, g: 0, b: 0, a: 1 }, storeOp: 'store' }] });
|
|
151
|
+
rp.setPipeline(this.pipe);
|
|
152
|
+
rp.setBindGroup(0, this.bg);
|
|
153
|
+
rp.draw(3);
|
|
154
|
+
rp.end();
|
|
155
|
+
}
|
|
156
|
+
}
|