ugly-game 0.5.14 → 0.5.16
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 +1 -1
- package/dist/assets/gpuProps.js +19 -6
- package/dist/gpuShadow.js +13 -15
- package/dist/gpuSsao.d.ts +10 -0
- package/dist/gpuSsao.js +156 -0
- package/package.json +1 -1
|
@@ -25,5 +25,5 @@ export declare class GpuPropsRenderer {
|
|
|
25
25
|
setInstances(instances: PropInstance[]): void;
|
|
26
26
|
/** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
|
|
27
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;
|
|
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, up?: [number, number, number]): void;
|
|
29
29
|
}
|
package/dist/assets/gpuProps.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// terrain. Runs in a load pass after the terrain so props depth-test against the surface.
|
|
5
5
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
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> };
|
|
7
|
+
struct U { mvp: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32>, up: vec4<f32> };
|
|
8
8
|
@group(0) @binding(0) var<uniform> u: U;
|
|
9
9
|
@group(0) @binding(1) var<storage,read> models: array<mat4x4<f32>>;
|
|
10
10
|
struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec3<f32> };
|
|
@@ -13,9 +13,20 @@ struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @l
|
|
|
13
13
|
let world = (m * vec4(p, 1.0)).xyz;
|
|
14
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
15
|
}
|
|
16
|
+
// same fixed directional FILL as the terrain cubes (gpuShadow.fixedFill) so plants/rocks read the same 3D shape,
|
|
17
|
+
// day or night, under one shared light. up.w > 0.5 signals a valid local-up was supplied (else fall back to flat).
|
|
18
|
+
fn fixedFill(N: vec3<f32>, up: vec3<f32>) -> f32 {
|
|
19
|
+
var t = cross(up, vec3(0.0, 0.0, 1.0));
|
|
20
|
+
if (dot(t, t) < 0.01) { t = cross(up, vec3(1.0, 0.0, 0.0)); }
|
|
21
|
+
t = normalize(t);
|
|
22
|
+
let L = normalize(up * 0.82 + t * 0.57);
|
|
23
|
+
return max(dot(N, L), 0.0);
|
|
24
|
+
}
|
|
16
25
|
@fragment fn fs(i: VO) -> @location(0) vec4<f32> {
|
|
17
|
-
let
|
|
18
|
-
let
|
|
26
|
+
let N = normalize(i.n);
|
|
27
|
+
let ff = select(1.0, 0.5 + 0.85 * fixedFill(N, u.up.xyz), u.up.w > 0.5); // fixed fill (fall back to flat amb if no up)
|
|
28
|
+
let d = clamp(dot(N, -normalize(u.sun.xyz)), 0.0, 1.0);
|
|
29
|
+
let lit = i.c * (u.amb.xyz * ff + d * u.sunCol.xyz);
|
|
19
30
|
let dist = length(i.world - u.cam.xyz);
|
|
20
31
|
let f = clamp(1.0 - exp(-dist * u.fog.w), 0.0, 1.0);
|
|
21
32
|
return vec4(mix(lit, u.fog.xyz, f), 1.0);
|
|
@@ -59,7 +70,7 @@ export class GpuPropsRenderer {
|
|
|
59
70
|
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
60
71
|
depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' },
|
|
61
72
|
});
|
|
62
|
-
this.uBuf = device.createBuffer({ size: 96 + 16 *
|
|
73
|
+
this.uBuf = device.createBuffer({ size: 96 + 16 * 6, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
63
74
|
this.lightBuf = device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
64
75
|
}
|
|
65
76
|
setModels(meshes) {
|
|
@@ -120,14 +131,16 @@ export class GpuPropsRenderer {
|
|
|
120
131
|
pass.drawIndexed(mdl.idxCount, this.counts[m]);
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
|
-
frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity) {
|
|
124
|
-
const u = new Float32Array(24 +
|
|
134
|
+
frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity, up) {
|
|
135
|
+
const u = new Float32Array(24 + 24);
|
|
125
136
|
u.set(camVP, 0);
|
|
126
137
|
u.set([sunDir[0], sunDir[1], sunDir[2], 0], 16);
|
|
127
138
|
u.set([sunCol[0], sunCol[1], sunCol[2], 0], 20);
|
|
128
139
|
u.set([amb[0], amb[1], amb[2], 0], 24);
|
|
129
140
|
u.set([camPos[0], camPos[1], camPos[2], 0], 28);
|
|
130
141
|
u.set([fog[0], fog[1], fog[2], fogDensity], 32);
|
|
142
|
+
if (up)
|
|
143
|
+
u.set([up[0], up[1], up[2], 1], 36); // up.w=1 → enable the fixed directional fill (matches the terrain)
|
|
131
144
|
this.device.queue.writeBuffer(this.uBuf, 0, u);
|
|
132
145
|
const rp = enc.beginRenderPass({ colorAttachments: [{ view: colorView, loadOp: 'load', storeOp: 'store' }], depthStencilAttachment: { view: depthView, depthLoadOp: 'load', depthStoreOp: 'store' } });
|
|
133
146
|
rp.setPipeline(this.pipe);
|
package/dist/gpuShadow.js
CHANGED
|
@@ -240,17 +240,16 @@ fn blockShade(fx: f32, fz: f32) -> f32 {
|
|
|
240
240
|
let t = clamp(edge / 0.17, 0.0, 1.0);
|
|
241
241
|
return 0.6 + 0.4 * (t * t * (3.0 - 2.0 * t));
|
|
242
242
|
}
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
fn
|
|
243
|
+
// ONE fixed, subtle directional FILL light in the local gravity frame — always on, independent of the sun, so
|
|
244
|
+
// terrain + props keep their 3D shape even in darkness (the sun stays the primary, shadow-casting key light). The
|
|
245
|
+
// fill comes from up + a fixed horizontal tangent (~35 deg off vertical), so a cube's three visible faces all get
|
|
246
|
+
// distinct brightness and corners/terrace risers read at a glance. Cubes and props (gpuProps) share this formula.
|
|
247
|
+
fn fixedFill(N: vec3<f32>, up: vec3<f32>) -> f32 {
|
|
248
248
|
var t = cross(up, vec3(0.0, 0.0, 1.0));
|
|
249
249
|
if (dot(t, t) < 0.01) { t = cross(up, vec3(1.0, 0.0, 0.0)); }
|
|
250
250
|
t = normalize(t);
|
|
251
|
-
let
|
|
252
|
-
|
|
253
|
-
return clamp(0.68 + 0.32 * nu + 0.14 * nt, 0.36, 1.0);
|
|
251
|
+
let L = normalize(up * 0.82 + t * 0.57); // fixed key-fill direction
|
|
252
|
+
return max(dot(N, L), 0.0); // 0 (facing away) .. 1 (facing the fill)
|
|
254
253
|
}
|
|
255
254
|
@fragment fn fs(i: VOut) -> @location(0) vec4<f32> {
|
|
256
255
|
if (i.emis > 0.5) { // self-glowing effect cube: full-bright, unlit
|
|
@@ -282,13 +281,12 @@ fn faceDirShade(N: vec3<f32>, up: vec3<f32>) -> f32 {
|
|
|
282
281
|
// per-block edge AO makes each voxel visible on FLAT-hue cubes, but reads as a hard GRID over textures — so the
|
|
283
282
|
// atlas path drops it entirely (the texture already supplies surface detail).
|
|
284
283
|
let ao = select(blockShade(fx, fz), 1.0, cam.atlas.x > 0.5 && i.tile >= 0.0);
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
let
|
|
289
|
-
let
|
|
290
|
-
let
|
|
291
|
-
let lit = base * (fill + key) * ao * (0.72 + 0.28 * dir); // 0.72..1.0 fixed face-light keeps corners legible under full sun
|
|
284
|
+
// LIGHTING = fixed directional FILL (always on, reads 3D shape in the dark) + the sun KEY (primary, shadowed).
|
|
285
|
+
// cam.sun.w is the fill/ambient brightness knob; the fill's directional term (0.5..1.35) keeps every face's
|
|
286
|
+
// orientation legible day or night, while the sun adds bright, shadow-casting key light on top during the day.
|
|
287
|
+
let fill = cam.sun.w * (0.5 + 0.85 * fixedFill(N, cam.up.xyz));
|
|
288
|
+
let key = cam.sunCol.xyz * ndl * sh; // direct sun (shadowed) — primary light
|
|
289
|
+
let lit = base * (fill + key) * ao;
|
|
292
290
|
return vec4(pow(clamp(lit, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0); // gamma
|
|
293
291
|
}
|
|
294
292
|
`;
|
|
@@ -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
|
+
}
|