@tinypoly/gpu-renderer 0.2.0 → 0.2.2
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/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +2 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/grass.d.ts +106 -0
- package/dist/grass.d.ts.map +1 -0
- package/dist/grass.js +350 -0
- package/dist/grass.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/materialLayout.d.ts +6 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +2 -0
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +11 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +4 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +25 -4
- package/dist/renderer.js.map +1 -1
- package/dist/shaderProgram.d.ts.map +1 -1
- package/dist/shaderProgram.js +10 -1
- package/dist/shaderProgram.js.map +1 -1
- package/dist/subsurface.d.ts +2 -0
- package/dist/subsurface.d.ts.map +1 -1
- package/dist/subsurface.js +3 -2
- package/dist/subsurface.js.map +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bsdf.wgsl.js +39 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/grass.wgsl.d.ts +14 -0
- package/dist/wgsl/grass.wgsl.d.ts.map +1 -0
- package/dist/wgsl/grass.wgsl.js +456 -0
- package/dist/wgsl/grass.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +5 -0
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +34 -5
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +2 -0
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/package.json +2 -2
- package/src/frameLayout.ts +2 -0
- package/src/grass.ts +523 -0
- package/src/index.ts +2 -0
- package/src/materialLayout.ts +7 -0
- package/src/pathTracePass.ts +11 -9
- package/src/renderer.ts +36 -7
- package/src/shaderProgram.ts +11 -1
- package/src/subsurface.ts +3 -2
- package/src/wgsl/bsdf.wgsl.ts +39 -3
- package/src/wgsl/grass.wgsl.ts +456 -0
- package/src/wgsl/lights.wgsl.ts +5 -0
- package/src/wgsl/pathTrace.wgsl.ts +34 -5
- package/src/wgsl/surface.wgsl.ts +2 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grass grown on a material's surface: see grass.ts for the layout of what the texture buffer holds.
|
|
3
|
+
*
|
|
4
|
+
* A triangle that grows grass is a shell, its footprint on the ground times the height of the lawn above its plane, and
|
|
5
|
+
* the shells have a hierarchy of their own. Inside a shell the ray walks the world's tiles front to back, and in each
|
|
6
|
+
* one it is taken into the space of that tile's patch, where the plane is the ground and blades stand on it: a shear
|
|
7
|
+
* that keeps the ray's distances as they are. The search in a tile is held to the stretch of the ray over that tile and
|
|
8
|
+
* that triangle, so a blade that leans over a border is seen once, from the patch it is drawn in, and the first tile
|
|
9
|
+
* with a hit holds the closest one in the shell.
|
|
10
|
+
*/
|
|
11
|
+
export const GRASS_WGSL = /* wgsl */ `
|
|
12
|
+
const GRASS_NONE: u32 = 0xffffffffu;
|
|
13
|
+
const GRASS_MAX_TILES: u32 = 48u;
|
|
14
|
+
// What a hit on a blade carries as its triangle, which no scene triangle can be.
|
|
15
|
+
const GRASS_TRIANGLE: u32 = 0xfffffffeu;
|
|
16
|
+
|
|
17
|
+
struct GrassHit {
|
|
18
|
+
found: bool,
|
|
19
|
+
distance: f32,
|
|
20
|
+
// The lawn's record and the patch triangle hit, both as offsets into the texture buffer.
|
|
21
|
+
record: u32,
|
|
22
|
+
triangle: u32,
|
|
23
|
+
u: f32,
|
|
24
|
+
v: f32,
|
|
25
|
+
// The ground plane's slope and where the tile starts, which take the patch back to the world.
|
|
26
|
+
slope: vec2<f32>,
|
|
27
|
+
tile: vec2<f32>,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
fn noGrass(maxDistance: f32) -> GrassHit {
|
|
31
|
+
var hit: GrassHit;
|
|
32
|
+
hit.found = false;
|
|
33
|
+
hit.distance = maxDistance;
|
|
34
|
+
|
|
35
|
+
return hit;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn grassNode(at: u32) -> BvhNode {
|
|
39
|
+
var node: BvhNode;
|
|
40
|
+
node.leftLow = vec3<f32>(extraFloat(at), extraFloat(at + 1u), extraFloat(at + 2u));
|
|
41
|
+
node.leftLink = bitcast<i32>(textures[at + 3u]);
|
|
42
|
+
node.leftHigh = vec3<f32>(extraFloat(at + 4u), extraFloat(at + 5u), extraFloat(at + 6u));
|
|
43
|
+
node.leftCount = bitcast<i32>(textures[at + 7u]);
|
|
44
|
+
node.rightLow = vec3<f32>(extraFloat(at + 8u), extraFloat(at + 9u), extraFloat(at + 10u));
|
|
45
|
+
node.rightLink = bitcast<i32>(textures[at + 11u]);
|
|
46
|
+
node.rightHigh = vec3<f32>(extraFloat(at + 12u), extraFloat(at + 13u), extraFloat(at + 14u));
|
|
47
|
+
node.rightCount = bitcast<i32>(textures[at + 15u]);
|
|
48
|
+
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn grassVec3(at: u32) -> vec3<f32> {
|
|
53
|
+
return vec3<f32>(extraFloat(at), extraFloat(at + 1u), extraFloat(at + 2u));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn grassHash(cell: vec2<i32>) -> u32 {
|
|
57
|
+
return pcgHash((bitcast<u32>(cell.x) * 1597334677u) ^ pcgHash(bitcast<u32>(cell.y) * 3812015801u));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Smooth value noise in zero to one, for what changes across the lawn rather than from blade to blade. */
|
|
61
|
+
fn grassNoise(p: vec2<f32>) -> f32 {
|
|
62
|
+
let cell = floor(p);
|
|
63
|
+
let f = p - cell;
|
|
64
|
+
let s = f * f * (3.0 - 2.0 * f);
|
|
65
|
+
let c = vec2<i32>(cell);
|
|
66
|
+
let corner = vec4<f32>(
|
|
67
|
+
f32(grassHash(c) >> 8u), f32(grassHash(c + vec2<i32>(1, 0)) >> 8u),
|
|
68
|
+
f32(grassHash(c + vec2<i32>(0, 1)) >> 8u), f32(grassHash(c + vec2<i32>(1, 1)) >> 8u)) / 16777216.0;
|
|
69
|
+
|
|
70
|
+
return mix(mix(corner.x, corner.y, s.x), mix(corner.z, corner.w, s.x), s.y);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether a blade grows where its root stands: the lawn thins out in patches, and each blade has its own number. */
|
|
74
|
+
fn grassKeeps(record: u32, root: vec2<f32>, rand: f32) -> bool {
|
|
75
|
+
let patchiness = extraFloat(record + 22u);
|
|
76
|
+
|
|
77
|
+
if (patchiness <= 0.0) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let thin = smoothstep(0.3, 0.75, grassNoise(root * extraFloat(record + 23u) + vec2<f32>(31.7, 11.3)));
|
|
82
|
+
|
|
83
|
+
return rand >= patchiness * thin;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The closest blade of one patch along the ray, between near and the hit so far: origin and direction already in the
|
|
88
|
+
* patch's space. Records the triangle in best and returns whether it found one.
|
|
89
|
+
*/
|
|
90
|
+
fn tracePatch(record: u32, root: u32, tile: vec2<f32>, origin: vec3<f32>, direction: vec3<f32>, near: f32,
|
|
91
|
+
far: f32, best: ptr<function, GrassHit>) -> bool {
|
|
92
|
+
let triangles = textures[record + 3u];
|
|
93
|
+
let blades = textures[record + 4u];
|
|
94
|
+
let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
95
|
+
let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
|
|
96
|
+
var closest = far;
|
|
97
|
+
var found = false;
|
|
98
|
+
var stack: array<u32, 32>;
|
|
99
|
+
var depth = 0u;
|
|
100
|
+
var current = 0u;
|
|
101
|
+
|
|
102
|
+
loop {
|
|
103
|
+
let node = grassNode(root + current * 16u);
|
|
104
|
+
let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, closest);
|
|
105
|
+
let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, closest);
|
|
106
|
+
var next = NO_NODE;
|
|
107
|
+
|
|
108
|
+
for (var side = 0u; side < 2u; side = side + 1u) {
|
|
109
|
+
let enter = select(leftEnter, rightEnter, side == 1u);
|
|
110
|
+
let link = select(node.leftLink, node.rightLink, side == 1u);
|
|
111
|
+
let count = select(node.leftCount, node.rightCount, side == 1u);
|
|
112
|
+
|
|
113
|
+
if (enter >= 1e30) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (link >= 0) {
|
|
118
|
+
if (next == NO_NODE) {
|
|
119
|
+
next = u32(link);
|
|
120
|
+
} else if (depth < 32u) {
|
|
121
|
+
stack[depth] = u32(link);
|
|
122
|
+
depth = depth + 1u;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let start = u32(-1 - link);
|
|
129
|
+
|
|
130
|
+
for (var i = 0u; i < u32(count); i = i + 1u) {
|
|
131
|
+
let at = triangles + (start + i) * 16u;
|
|
132
|
+
let a = grassVec3(at);
|
|
133
|
+
let edge1 = grassVec3(at + 3u) - a;
|
|
134
|
+
let edge2 = grassVec3(at + 6u) - a;
|
|
135
|
+
let p = cross(direction, edge2);
|
|
136
|
+
let determinant = dot(edge1, p);
|
|
137
|
+
|
|
138
|
+
if (abs(determinant) < 1e-14) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let invDet = 1.0 / determinant;
|
|
143
|
+
let offset = origin - a;
|
|
144
|
+
let u = dot(offset, p) * invDet;
|
|
145
|
+
let q = cross(offset, edge1);
|
|
146
|
+
let v = dot(direction, q) * invDet;
|
|
147
|
+
let t = dot(edge2, q) * invDet;
|
|
148
|
+
|
|
149
|
+
if (u < 0.0 || v < 0.0 || u + v > 1.0 || t <= near || t >= closest) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let blade = blades + textures[at + 15u] * 4u;
|
|
154
|
+
let rootAt = tile + vec2<f32>(extraFloat(blade), extraFloat(blade + 1u));
|
|
155
|
+
|
|
156
|
+
if (!grassKeeps(record, rootAt, extraFloat(blade + 2u))) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
closest = t;
|
|
161
|
+
found = true;
|
|
162
|
+
(*best).triangle = at;
|
|
163
|
+
(*best).u = u;
|
|
164
|
+
(*best).v = v;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (next == NO_NODE) {
|
|
169
|
+
if (depth == 0u) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
depth = depth - 1u;
|
|
174
|
+
next = stack[depth];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
current = next;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (found) {
|
|
181
|
+
(*best).distance = closest;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Keeps the part of [range.x, range.y] where offset + rate t stays at or above zero. */
|
|
188
|
+
fn grassClip(range: vec2<f32>, offset: f32, rate: f32) -> vec2<f32> {
|
|
189
|
+
if (abs(rate) < 1e-20) {
|
|
190
|
+
return select(range, vec2<f32>(1.0, 0.0), offset < 0.0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let at = -offset / rate;
|
|
194
|
+
|
|
195
|
+
return select(vec2<f32>(range.x, min(range.y, at)), vec2<f32>(max(range.x, at), range.y), rate > 0.0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The edge from p to q of an anticlockwise footprint, as the ray crosses it on the ground: inside is on the left. */
|
|
199
|
+
fn grassEdge(range: vec2<f32>, p: vec2<f32>, q: vec2<f32>, origin: vec2<f32>, direction: vec2<f32>) -> vec2<f32> {
|
|
200
|
+
let e = q - p;
|
|
201
|
+
|
|
202
|
+
return grassClip(range, e.x * (origin.y - p.y) - e.y * (origin.x - p.x), e.x * direction.y - e.y * direction.x);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The grass over one triangle, against the closest hit so far. */
|
|
206
|
+
fn grassInShell(entry: u32, origin: vec3<f32>, direction: vec3<f32>, best: ptr<function, GrassHit>) {
|
|
207
|
+
let record = textures[entry];
|
|
208
|
+
let a = vec2<f32>(extraFloat(entry + 1u), extraFloat(entry + 2u));
|
|
209
|
+
let b = vec2<f32>(extraFloat(entry + 3u), extraFloat(entry + 4u));
|
|
210
|
+
let c = vec2<f32>(extraFloat(entry + 5u), extraFloat(entry + 6u));
|
|
211
|
+
let p0 = extraFloat(entry + 7u);
|
|
212
|
+
let slope = vec2<f32>(extraFloat(entry + 8u), extraFloat(entry + 9u));
|
|
213
|
+
let tile = extraFloat(record);
|
|
214
|
+
let top = extraFloat(record + 1u);
|
|
215
|
+
// The ray's height over the plane, at its start and per unit of distance.
|
|
216
|
+
let h0 = origin.y - p0 - slope.x * origin.x - slope.y * origin.z;
|
|
217
|
+
let dh = direction.y - slope.x * direction.x - slope.y * direction.z;
|
|
218
|
+
let ground = origin.xz;
|
|
219
|
+
let along = direction.xz;
|
|
220
|
+
var range = vec2<f32>(RAY_EPSILON, (*best).distance);
|
|
221
|
+
|
|
222
|
+
range = grassClip(range, h0, dh);
|
|
223
|
+
range = grassClip(range, top - h0, -dh);
|
|
224
|
+
range = grassEdge(range, a, b, ground, along);
|
|
225
|
+
range = grassEdge(range, b, c, ground, along);
|
|
226
|
+
range = grassEdge(range, c, a, ground, along);
|
|
227
|
+
|
|
228
|
+
if (range.x >= range.y) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Amanatides and Woo over the tiles of the ground.
|
|
233
|
+
var cell = floor((ground + along * range.x) / tile);
|
|
234
|
+
let stepping = select(vec2<f32>(-1.0), vec2<f32>(1.0), along >= vec2<f32>(0.0));
|
|
235
|
+
let span = abs(vec2<f32>(tile) / select(along, vec2<f32>(1e-20), abs(along) < vec2<f32>(1e-20)));
|
|
236
|
+
var crossing = vec2<f32>(1e30);
|
|
237
|
+
|
|
238
|
+
if (abs(along.x) >= 1e-20) {
|
|
239
|
+
crossing.x = ((cell.x + select(0.0, 1.0, along.x > 0.0)) * tile - ground.x) / along.x;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (abs(along.y) >= 1e-20) {
|
|
243
|
+
crossing.y = ((cell.y + select(0.0, 1.0, along.y > 0.0)) * tile - ground.y) / along.y;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
var start = range.x;
|
|
247
|
+
let variants = textures[record + 2u];
|
|
248
|
+
|
|
249
|
+
for (var i = 0u; i < GRASS_MAX_TILES; i = i + 1u) {
|
|
250
|
+
let end = min(min(crossing.x, crossing.y), range.y);
|
|
251
|
+
let corner = cell * tile;
|
|
252
|
+
let variant = grassHash(vec2<i32>(cell)) % max(variants, 1u);
|
|
253
|
+
let patchOrigin = vec3<f32>(origin.x - corner.x, h0, origin.z - corner.y);
|
|
254
|
+
let patchDirection = vec3<f32>(direction.x, dh, direction.z);
|
|
255
|
+
|
|
256
|
+
if (end > start
|
|
257
|
+
&& tracePatch(record, textures[record + 5u + variant], corner, patchOrigin, patchDirection, start, end, best)) {
|
|
258
|
+
(*best).found = true;
|
|
259
|
+
(*best).record = record;
|
|
260
|
+
(*best).slope = slope;
|
|
261
|
+
(*best).tile = corner;
|
|
262
|
+
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (end >= range.y) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (crossing.x < crossing.y) {
|
|
271
|
+
cell.x = cell.x + stepping.x;
|
|
272
|
+
start = crossing.x;
|
|
273
|
+
crossing.x = crossing.x + span.x;
|
|
274
|
+
} else {
|
|
275
|
+
cell.y = cell.y + stepping.y;
|
|
276
|
+
start = crossing.y;
|
|
277
|
+
crossing.y = crossing.y + span.y;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The closest blade along a ray before maxDistance, in any lawn of the scene. */
|
|
283
|
+
fn traceGrass(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> GrassHit {
|
|
284
|
+
var best = noGrass(maxDistance);
|
|
285
|
+
|
|
286
|
+
if (frame.grassShells == GRASS_NONE) {
|
|
287
|
+
return best;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
291
|
+
let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
|
|
292
|
+
var stack: array<u32, 32>;
|
|
293
|
+
var enters: array<f32, 32>;
|
|
294
|
+
var depth = 0u;
|
|
295
|
+
var current = 0u;
|
|
296
|
+
|
|
297
|
+
loop {
|
|
298
|
+
if (current == NO_NODE) {
|
|
299
|
+
if (depth == 0u) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
depth = depth - 1u;
|
|
304
|
+
|
|
305
|
+
if (enters[depth] > best.distance) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
current = stack[depth];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let node = grassNode(frame.grassShells + current * 16u);
|
|
313
|
+
let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, best.distance);
|
|
314
|
+
let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, best.distance);
|
|
315
|
+
let leftFirst = leftEnter <= rightEnter;
|
|
316
|
+
|
|
317
|
+
current = NO_NODE;
|
|
318
|
+
|
|
319
|
+
for (var k = 0u; k < 2u; k = k + 1u) {
|
|
320
|
+
// The nearer child first; a leaf is searched at once, a node is visited next or waits.
|
|
321
|
+
let left = (k == 0u) == leftFirst;
|
|
322
|
+
let enter = select(rightEnter, leftEnter, left);
|
|
323
|
+
let link = select(node.rightLink, node.leftLink, left);
|
|
324
|
+
let count = select(node.rightCount, node.leftCount, left);
|
|
325
|
+
|
|
326
|
+
if (enter >= best.distance) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (link < 0) {
|
|
331
|
+
let first = u32(-1 - link);
|
|
332
|
+
|
|
333
|
+
for (var i = 0u; i < u32(count); i = i + 1u) {
|
|
334
|
+
grassInShell(frame.grassEntries + (first + i) * 12u, origin, direction, &best);
|
|
335
|
+
}
|
|
336
|
+
} else if (current == NO_NODE) {
|
|
337
|
+
current = u32(link);
|
|
338
|
+
} else if (depth < 32u) {
|
|
339
|
+
stack[depth] = u32(link);
|
|
340
|
+
enters[depth] = enter;
|
|
341
|
+
depth = depth + 1u;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return best;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* A blade as a surface: thin, waxy and translucent, a quarter of its light let through to the far side. The colour runs
|
|
351
|
+
* from the root to the tip, changes from blade to blade and across the lawn, and some blades are dry.
|
|
352
|
+
*/
|
|
353
|
+
fn grassSurface(hit: GrassHit, direction: vec3<f32>) -> Surface {
|
|
354
|
+
let at = hit.triangle;
|
|
355
|
+
let record = hit.record;
|
|
356
|
+
let w = 1.0 - hit.u - hit.v;
|
|
357
|
+
let height = w * extraFloat(at + 9u) + hit.u * extraFloat(at + 10u) + hit.v * extraFloat(at + 11u);
|
|
358
|
+
let side = w * extraFloat(at + 12u) + hit.u * extraFloat(at + 13u) + hit.v * extraFloat(at + 14u);
|
|
359
|
+
let blade = textures[record + 4u] + textures[at + 15u] * 4u;
|
|
360
|
+
let root = hit.tile + vec2<f32>(extraFloat(blade), extraFloat(blade + 1u));
|
|
361
|
+
let rand = extraFloat(blade + 2u);
|
|
362
|
+
let yaw = extraFloat(blade + 3u);
|
|
363
|
+
let a = grassVec3(at);
|
|
364
|
+
let local = cross(grassVec3(at + 3u) - a, grassVec3(at + 6u) - a);
|
|
365
|
+
// Normals go back through the inverse transpose of the shear.
|
|
366
|
+
var normal = normalize(vec3<f32>(local.x - hit.slope.x * local.y, local.y, local.z - hit.slope.y * local.y));
|
|
367
|
+
|
|
368
|
+
if (dot(normal, direction) > 0.0) {
|
|
369
|
+
normal = -normal;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let across = normalize(vec3<f32>(cos(yaw), hit.slope.x * cos(yaw) + hit.slope.y * sin(yaw), sin(yaw)));
|
|
373
|
+
// A blade is rounded across: its normal turns towards each edge.
|
|
374
|
+
var shading = normalize(normal + across * (side * 0.55));
|
|
375
|
+
|
|
376
|
+
if (dot(shading, direction) >= 0.0) {
|
|
377
|
+
shading = normal;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let variation = extraFloat(record + 21u);
|
|
381
|
+
let scale = extraFloat(record + 23u);
|
|
382
|
+
let wide = grassNoise(root * scale);
|
|
383
|
+
let fine = grassNoise(root * scale * 4.3 + vec2<f32>(5.2, 1.7));
|
|
384
|
+
var albedo = mix(grassVec3(record + 9u), grassVec3(record + 12u), smoothstep(0.0, 1.0, pow(height, 0.75)));
|
|
385
|
+
|
|
386
|
+
// Across the lawn, greener and yellower stretches; from blade to blade, lighter and darker.
|
|
387
|
+
albedo = albedo * mix(vec3<f32>(1.0), vec3<f32>(1.12, 1.0, 0.7), variation * (wide - 0.4));
|
|
388
|
+
albedo = albedo * (1.0 + variation * 0.6 * (fract(rand * 13.37) - 0.5));
|
|
389
|
+
|
|
390
|
+
let dryness = extraFloat(record + 18u);
|
|
391
|
+
let dry = grassVec3(record + 15u) * (0.85 + 0.3 * fract(rand * 7.77));
|
|
392
|
+
|
|
393
|
+
if (fract(rand * 31.1) < dryness * (0.4 + 1.2 * fine)) {
|
|
394
|
+
albedo = mix(dry * 0.8, dry, height);
|
|
395
|
+
} else {
|
|
396
|
+
// Tips dry first.
|
|
397
|
+
albedo = mix(albedo, dry, smoothstep(0.82, 1.0, height) * dryness * 1.5);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// The sheath at the root is paler and duller.
|
|
401
|
+
albedo = mix(albedo * 0.7 + vec3<f32>(0.03, 0.03, 0.01), albedo, smoothstep(0.0, 0.2, height));
|
|
402
|
+
albedo = clamp(albedo, vec3<f32>(0.0), vec3<f32>(0.9));
|
|
403
|
+
|
|
404
|
+
let translucency = extraFloat(record + 19u);
|
|
405
|
+
var surface: Surface;
|
|
406
|
+
|
|
407
|
+
surface.material = 0u;
|
|
408
|
+
surface.flags = MATERIAL_LIT | MATERIAL_PBR;
|
|
409
|
+
surface.kind = KIND_STANDARD;
|
|
410
|
+
surface.blending = BLEND_NORMAL;
|
|
411
|
+
surface.facing = normal;
|
|
412
|
+
surface.front = true;
|
|
413
|
+
surface.normal = shading;
|
|
414
|
+
surface.albedo = albedo * (1.0 - 0.5 * translucency);
|
|
415
|
+
// Light through a leaf is more saturated and yellower than off it.
|
|
416
|
+
surface.translucent = min(albedo * translucency * vec3<f32>(1.05, 1.2, 0.6), vec3<f32>(0.9));
|
|
417
|
+
surface.opacity = 1.0;
|
|
418
|
+
surface.alpha = 1.0;
|
|
419
|
+
surface.roughness = clamp(extraFloat(record + 20u), 0.0525, 1.0);
|
|
420
|
+
surface.metalness = 0.0;
|
|
421
|
+
surface.ior = 1.5;
|
|
422
|
+
surface.specularColor = vec3<f32>(0.04);
|
|
423
|
+
surface.specularF90 = 1.0;
|
|
424
|
+
surface.specularStrength = 1.0;
|
|
425
|
+
surface.envMap = NO_TEXTURE;
|
|
426
|
+
surface.envMapIntensity = 1.0;
|
|
427
|
+
surface.gradientMap = NO_TEXTURE;
|
|
428
|
+
|
|
429
|
+
return surface;
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
|
|
433
|
+
/** What the kernel calls when no material grows grass: the same names, doing nothing. */
|
|
434
|
+
export const GRASS_STUB_WGSL = /* wgsl */ `
|
|
435
|
+
const GRASS_NONE: u32 = 0xffffffffu;
|
|
436
|
+
const GRASS_TRIANGLE: u32 = 0xfffffffeu;
|
|
437
|
+
|
|
438
|
+
struct GrassHit {
|
|
439
|
+
found: bool,
|
|
440
|
+
distance: f32,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
fn traceGrass(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> GrassHit {
|
|
444
|
+
var hit: GrassHit;
|
|
445
|
+
hit.found = false;
|
|
446
|
+
hit.distance = maxDistance;
|
|
447
|
+
|
|
448
|
+
return hit;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fn grassSurface(hit: GrassHit, direction: vec3<f32>) -> Surface {
|
|
452
|
+
var surface: Surface;
|
|
453
|
+
|
|
454
|
+
return surface;
|
|
455
|
+
}
|
|
456
|
+
`;
|
package/src/wgsl/lights.wgsl.ts
CHANGED
|
@@ -660,6 +660,11 @@ fn lightTransmittance(position: vec3<f32>, chosen: LightSample, rng: ptr<functio
|
|
|
660
660
|
return vec3<f32>(1.0);
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
+
// A blade of grass on the way blocks the light.
|
|
664
|
+
if (GRASS && chosen.occlude && traceGrass(position, chosen.direction, chosen.distance).found) {
|
|
665
|
+
return vec3<f32>(0.0);
|
|
666
|
+
}
|
|
667
|
+
|
|
663
668
|
if (chosen.occlude) {
|
|
664
669
|
// Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the
|
|
665
670
|
// first one found instead of looking for the closest. Anything that lets light through goes the long way below.
|
|
@@ -132,7 +132,8 @@ fn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect:
|
|
|
132
132
|
* volume, a toon ramp read all round, a clearcoat that may face what its base turns from, and transmission.
|
|
133
133
|
*/
|
|
134
134
|
fn treeNormal(surface: Surface) -> vec3<f32> {
|
|
135
|
-
if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0
|
|
135
|
+
if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0
|
|
136
|
+
|| (GRASS && any(surface.translucent > vec3<f32>(0.0)))) {
|
|
136
137
|
return vec3<f32>(0.0);
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -171,7 +172,14 @@ fn directLight(
|
|
|
171
172
|
let toon = surface.kind == KIND_TOON;
|
|
172
173
|
// Three's shadow map leaves a toon mesh's own far side unshadowed; tracing through the mesh would not.
|
|
173
174
|
let skip = select(NO_MATERIAL, surface.material, toon && dot(surface.normal, chosen.direction) < 0.1);
|
|
174
|
-
|
|
175
|
+
var shadowFrom = position;
|
|
176
|
+
|
|
177
|
+
// Light from behind a translucent surface is looked for from its far side, past the surface itself.
|
|
178
|
+
if (GRASS && any(surface.translucent > vec3<f32>(0.0)) && dot(chosen.direction, surface.facing) < 0.0) {
|
|
179
|
+
shadowFrom = position - (surface.normal + surface.facing) * RAY_EPSILON;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let through = lightTransmittance(shadowFrom, chosen, rng, skip, medium);
|
|
175
183
|
|
|
176
184
|
if (all(through <= vec3<f32>(0.0))) {
|
|
177
185
|
return vec3<f32>(0.0);
|
|
@@ -457,7 +465,20 @@ fn traceRadiance(
|
|
|
457
465
|
}
|
|
458
466
|
|
|
459
467
|
for (var step = 0u; step < MAX_STEPS + MAX_WALK; step = step + 1u) {
|
|
460
|
-
|
|
468
|
+
var hit = traceScene(origin, direction, limit);
|
|
469
|
+
// A blade of grass in front of whatever the ray hit. A walk inside a material never meets one.
|
|
470
|
+
var grass: GrassHit;
|
|
471
|
+
|
|
472
|
+
if (GRASS && !(SUBSURFACE_WALK && inMedium)) {
|
|
473
|
+
grass = traceGrass(origin, direction, hit.distance);
|
|
474
|
+
|
|
475
|
+
if (grass.found) {
|
|
476
|
+
hit.distance = grass.distance;
|
|
477
|
+
hit.triangle = GRASS_TRIANGLE;
|
|
478
|
+
hit.u = 0.0;
|
|
479
|
+
hit.v = 0.0;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
461
482
|
|
|
462
483
|
// A step of the walk inside a material that ends in the medium: the light scatters there, keeping each colour's
|
|
463
484
|
// share of it. It needs three numbers, which a plain hash gives for a fraction of what the sequence costs.
|
|
@@ -570,7 +591,7 @@ fn traceRadiance(
|
|
|
570
591
|
let point = origin + direction * travel;
|
|
571
592
|
var m = 0u;
|
|
572
593
|
|
|
573
|
-
if (!scattering) {
|
|
594
|
+
if (!scattering && !(GRASS && grass.found)) {
|
|
574
595
|
m = materialIndexOf(hit.triangle);
|
|
575
596
|
}
|
|
576
597
|
|
|
@@ -582,6 +603,8 @@ fn traceRadiance(
|
|
|
582
603
|
direction = -surface.normal;
|
|
583
604
|
} else if (VOLUMES && scattering) {
|
|
584
605
|
surface = volumePoint(direction, volumeAlbedo, volumeG);
|
|
606
|
+
} else if (GRASS && grass.found) {
|
|
607
|
+
surface = grassSurface(grass, direction);
|
|
585
608
|
} else {
|
|
586
609
|
|
|
587
610
|
// A volume's boundary is not a surface: the ray goes in or out, and the medium is what it meets.
|
|
@@ -730,7 +753,8 @@ fn traceRadiance(
|
|
|
730
753
|
if (primary) {
|
|
731
754
|
alpha = 1.0;
|
|
732
755
|
|
|
733
|
-
if (!scattering && !
|
|
756
|
+
if (!scattering && !(GRASS && grass.found) && !fogged && afterMaterial == NO_MATERIAL
|
|
757
|
+
&& materials[m].afterProgram != NO_PROGRAM) {
|
|
734
758
|
afterMaterial = m;
|
|
735
759
|
afterTriangle = hit.triangle;
|
|
736
760
|
afterBary = bary;
|
|
@@ -893,6 +917,11 @@ fn traceRadiance(
|
|
|
893
917
|
escapeIntensity = surface.envMapIntensity;
|
|
894
918
|
next = scattered.direction;
|
|
895
919
|
nextOrigin = position;
|
|
920
|
+
|
|
921
|
+
// Through a translucent surface, the path carries on from its far side.
|
|
922
|
+
if (GRASS && dot(next, surface.facing) < 0.0 && any(surface.translucent > vec3<f32>(0.0))) {
|
|
923
|
+
nextOrigin = point - surface.facing * RAY_EPSILON;
|
|
924
|
+
}
|
|
896
925
|
primary = false;
|
|
897
926
|
straight = false;
|
|
898
927
|
bounce = bounce + 1u;
|
package/src/wgsl/surface.wgsl.ts
CHANGED
|
@@ -64,6 +64,8 @@ struct Surface {
|
|
|
64
64
|
material: u32,
|
|
65
65
|
// Share of the diffuse layer that scatters inside the material instead of at its surface.
|
|
66
66
|
subsurface: f32,
|
|
67
|
+
// What a translucent surface lets through to its far side, diffusely: grass blades. Zero for everything else.
|
|
68
|
+
translucent: vec3<f32>,
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
/** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */
|