7struct Light3D { posRadius: vec4f, color: vec4f, spot: vec4f };
9 mvp: mat4x4f, model: mat4x4f, lightDir: vec4f, lightColor: vec4f,
10 tint: vec4f, cameraPos: vec4f, ambient: vec4f,
11 lights: array<Light3D, 8>, texBomb: vec4f, parallax: vec4f,
12 surface: vec4f, view: mat4x4f, clipInfo: vec4f, cloud: vec4f, cloudWind: vec4f,
14struct Externals { data: array<vec4f, 8> };
15struct VSIn { @location(0) pos: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f };
17 @builtin(position) pos: vec4f,
18 @location(0) uv: vec2f, @location(1) worldPos: vec3f,
19 @location(2) viewPos: vec3f, @location(3) rootPos: vec3f,
20 @location(4) instanceId: f32, @location(5) tint: vec4f,
21 @location(6) alwaysDark: f32,
22 @location(7) instanceTint: vec3f,
24@group(0) @binding(0) var<uniform> frame: Frame;
25@group(0) @binding(15) var<uniform> params: Externals;
26fn p(i: u32) -> f32 { return params.data[i / 4u][i % 4u]; }
27fn inverse3(m: mat3x3f) -> mat3x3f {
28 let a=m[0].x; let b=m[1].x; let c=m[2].x;
29 let d=m[0].y; let e=m[1].y; let f=m[2].y;
30 let g=m[0].z; let h=m[1].z; let i=m[2].z;
31 let det=a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g);
32 return mat3x3f(vec3f(e*i-f*h,f*g-d*i,d*h-e*g)/det,
33 vec3f(c*h-b*i,a*i-c*g,b*g-a*h)/det,
34 vec3f(b*f-c*e,c*d-a*f,a*e-b*d)/det);
36fn windOffset(local: vec3f, root: vec3f, up: vec3f, baseDimensions: vec2f) -> vec3f {
37 var dimensions = baseDimensions;
38 var range = abs(p(23u));
40 if (range == 0.0 || main == 0.0) { return local; }
41 var distance = (root - frame.cameraPos.xyz) / range;
42 var attenuation = 1.0 - clamp(dot(distance, distance), 0.0, 1.0);
43 attenuation *= attenuation;
44 if (attenuation == 0.0) { return local; }
45 var upLength = length(up);
46 var scale = max(upLength, 0.4);
47 var upDot = clamp(up.y / upLength, 0.0, 1.0);
48 dimensions = mix(vec2f(dimensions.y * 2.0), dimensions, upDot * upDot) * scale;
49 var flex = vec3f(p(24u), p(25u), p(26u)) *
50 vec3f(clamp(main * 3.0, 0.0, 1.0), clamp(main * 2.0, 0.0, 1.0), 1.0 - main * main * 0.5) * main * attenuation * scale;
51 var frequency = vec3f(p(27u), p(28u), p(29u)) * scale;
52 var direction = vec3f(p(18u), p(19u), p(20u));
53 var time = -fract(p(22u) * 6.0) * 6.283185;
54 var norm = local / vec3f(dimensions.x, dimensions.y, dimensions.x);
55 var branch = dot(norm.xz, norm.xz);
57 var lengthA = dot(local, local);
58 var world = root + local;
59 var gust = ((sin(time + frequency.x * (root.x + root.y + root.z)) * 0.3 + main * 0.5) +
60 (p(30u) * 0.4 + main) * main) * (p(31u) * 0.3 + 0.7);
61 var tally = vec3f(direction.x, 0.0, direction.z) * stem * stem * gust * flex.x;
62 gust = gust * 0.7 + 0.3;
64 var displaced = world + tally * 0.25;
65 tally += direction * stem * stem * (sin(time * 2.0 +
66 (displaced.x + displaced.y + displaced.z) * frequency.y) * branch * 0.7 + 0.3) * gust * flex.y;
68 var offset = local + tally;
69 var denominator = dot(offset, offset);
70 var normalization = 0.0;
71 if (denominator != 0.0) { normalization = clamp(lengthA / denominator, 0.0, 1.0); }
72 tally = offset * normalization;
73 if (p(23u) > 0.0 && flex.z != 0.0) {
74 var wave = sin(vec3f(time * 5.0) + (world + tally) * frequency.z);
75 return tally + (wave * direction + direction) * vec3f(branch, branch * 0.75, branch) *
76 (stem * gust * flex.z) * (normalization + 0.5);
81fn vs_main(in: VSIn) -> VSOut {
82 let model3 = mat3x3f(frame.model[0].xyz, frame.model[1].xyz, frame.model[2].xyz);
83 let invModel3 = inverse3(model3);
84 var up = invModel3 * vec3f(0.0, 1.0, 0.0);
85 up = select(vec3f(0.0, 1.0, 0.0), normalize(up), length(up) > 0.00001);
86 let modelTranslation = frame.model[3].xyz;
87 let cameraObj = invModel3 * (frame.cameraPos.xyz - modelTranslation);
88 var toCamera = cameraObj - in.pos;
89 toCamera -= up * dot(toCamera, up);
90 let fallback = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), abs(up.y) < 0.95);
91 let right = select(normalize(cross(up, fallback)), normalize(cross(up, toCamera)),
92 length(toCamera) > 0.0001);
93 let scale = select(1.0, in.normal.y, in.normal.y > 0.0);
94 let widthScale = select(select(scale, in.normal.z - 1.0, in.normal.z > 1.0), -in.normal.z, in.normal.z < 0.0);
95 var objectPos = in.pos + right * (in.uv.x - 0.5) * select(0.01, p(2u), p(2u) > 0.0) * widthScale +
96 up * in.uv.y * select(0.01, p(3u), p(3u) > 0.0) * scale;
97 let root = frame.model * vec4f(in.pos, 1.0);
98 if (p(23u) != 0.0 && p(21u) != 0.0) {
99 objectPos = in.pos + invModel3 * windOffset(model3 * (objectPos - in.pos), root.xyz,
100 model3 * up * scale, vec2f(select(0.01, p(2u), p(2u) > 0.0), select(0.01, p(3u), p(3u) > 0.0)));
102 let world = frame.model * vec4f(objectPos, 1.0);
104 out.pos = frame.mvp * vec4f(objectPos, 1.0); out.pos.y = -out.pos.y;
105 out.uv = in.uv; out.worldPos = world.xyz; out.viewPos = (frame.view * world).xyz;
106 out.rootPos = root.xyz; out.tint = frame.tint;
107 if (in.normal.x < 0.0) {
108 let packed = -in.normal.x - 1.0;
109 let red = floor(packed / 65536.0);
110 let greenRest = packed - red * 65536.0;
111 let green = floor(greenRest / 256.0);
112 let blue = greenRest - green * 256.0;
113 out.instanceTint = vec3f(red, green, blue) / 255.0;
114 out.instanceId = fract(sin(dot(in.pos, vec3f(12.9898, 78.233, 37.719))) * 43758.5453) * 65535.0;
116 out.instanceTint = vec3f(1.0);
117 out.instanceId = in.normal.x;
119 out.alwaysDark = select(0.0, 1.0, in.normal.z > 0.0);
125struct Light3D { posRadius: vec4f, color: vec4f, spot: vec4f };
127 mvp: mat4x4f, model: mat4x4f, lightDir: vec4f, lightColor: vec4f,
128 tint: vec4f, cameraPos: vec4f, ambient: vec4f,
129 lights: array<Light3D, 8>, texBomb: vec4f, parallax: vec4f,
130 surface: vec4f, view: mat4x4f, clipInfo: vec4f, cloud: vec4f, cloudWind: vec4f,
133 lightVP: array<mat4x4f, 3>, splits: vec4f, bias: vec4f,
134 cascadeBias: vec4f, cascadeTexel: vec4f,
136struct Externals { data: array<vec4f, 8> };
138 @location(0) uv: vec2f, @location(1) worldPos: vec3f,
139 @location(2) viewPos: vec3f, @location(3) rootPos: vec3f,
140 @location(4) instanceId: f32, @location(5) tint: vec4f,
141 @location(6) alwaysDark: f32,
142 @location(7) instanceTint: vec3f,
144@group(0) @binding(0) var<uniform> frame: Frame;
145@group(0) @binding(1) var albedo: texture_2d<f32>;
146@group(0) @binding(2) var normalMap: texture_2d<f32>;
147@group(0) @binding(3) var maskMap: texture_2d<f32>;
148@group(0) @binding(4) var<uniform> shadow: ShadowFrame;
149@group(0) @binding(5) var shadowMap: texture_depth_2d_array;
150@group(0) @binding(7) var mainSampler: sampler;
151@group(0) @binding(8) var shadowSampler: sampler_comparison;
152@group(0) @binding(15) var<uniform> params: Externals;
153fn p(i: u32) -> f32 { return params.data[i / 4u][i % 4u]; }
154fn cascadeBias(cascade: i32) -> f32 {
155 var value = shadow.cascadeBias.z;
156 if (cascade == 0) { value = shadow.cascadeBias.x; }
157 if (cascade == 1) { value = shadow.cascadeBias.y; }
158 return select(shadow.bias.x, value, value > 0.00000001);
160fn slopeScaledBias(cascade: i32, nDotL: f32) -> f32 {
161 return cascadeBias(cascade) * mix(0.75, 1.0, clamp(nDotL, 0.0, 1.0));
163fn sampleCascade(worldPos: vec3f, cascade: i32, bias: f32) -> f32 {
164 let clip = shadow.lightVP[cascade] * vec4f(worldPos, 1.0);
165 let ndc = clip.xyz / max(clip.w, 0.000001);
166 // The WebGPU shadow writer mirrors clip Y; sampling must mirror it too.
167 let uv = vec2f(ndc.x, -ndc.y) * 0.5 + vec2f(0.5);
168 if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0)) || ndc.z < 0.0 || ndc.z > 1.0) {
172 let offsets = array<vec2f, 9>(vec2f(-0.5,-0.5),vec2f(0.0,-0.6),vec2f(0.5,-0.5),
173 vec2f(-0.6,0.0),vec2f(0.0),vec2f(0.6,0.0),vec2f(-0.5,0.5),vec2f(0.0,0.6),vec2f(0.5,0.5));
174 let dims = vec2f(textureDimensions(shadowMap));
175 for (var i = 0; i < 9; i++) {
176 let sampleUv = uv + offsets[i] / dims;
177 if (all(sampleUv >= vec2f(0.0)) && all(sampleUv <= vec2f(1.0))) {
178 sum += textureSampleCompareLevel(shadowMap, shadowSampler, sampleUv, cascade,
184fn grassShadow(worldPos: vec3f, viewDepth: f32) -> f32 {
185 if (shadow.bias.y < 0.5 || shadow.bias.z < 0.5 || shadow.splits.w < 0.0001) { return 1.0; }
187 if (viewDepth < shadow.splits.x) { cascade = 0; }
188 else if (viewDepth < shadow.splits.y) { cascade = 1; }
189 var texel = shadow.cascadeTexel.z;
190 if (cascade == 0) { texel = shadow.cascadeTexel.x; }
191 else if (cascade == 1) { texel = shadow.cascadeTexel.y; }
192 let normal = vec3f(0.0, 1.0, 0.0);
193 let nDotL = max(dot(normal, normalize(frame.lightDir.xyz)), 0.0);
194 let samplePos = worldPos + normal * ((2.0 * max(texel, 0.000001)) / max(nDotL, 0.2));
195 var visibility = sampleCascade(samplePos, cascade, slopeScaledBias(cascade, nDotL));
196 var hi = shadow.splits.z;
197 var lo = shadow.splits.y;
198 if (cascade == 0) { hi = shadow.splits.x; lo = 0.0; }
199 else if (cascade == 1) { hi = shadow.splits.y; lo = shadow.splits.x; }
200 let band = max(0.5, (hi - lo) * 0.1);
201 let toPrev = 1.0 - clamp((viewDepth - lo) / band, 0.0, 1.0);
202 let toNext = 1.0 - clamp((hi - viewDepth) / band, 0.0, 1.0);
203 if (toPrev > 0.0 && cascade > 0) {
204 visibility = mix(visibility, sampleCascade(samplePos, cascade - 1,
205 slopeScaledBias(cascade - 1, nDotL)), toPrev);
207 if (toNext > 0.0 && cascade < 2) {
208 visibility = mix(visibility, sampleCascade(samplePos, cascade + 1,
209 slopeScaledBias(cascade + 1, nDotL)), toNext);
211 visibility = mix(1.0, visibility, clamp(shadow.splits.w, 0.0, 1.0));
212 return mix(0.04, 1.0, visibility);
215fn fs_main(in: FSIn) -> @location(0) vec4f {
216 let foliage = p(12u) < 0.0;
217 let frames = max(p(12u), 1.0); let duration = max(p(1u), 0.0001);
218 let random = fract(sin(in.instanceId * 12.9898) * 43758.5453);
219 let frameNumber = i32(p(0u) / duration + random * frames) % i32(frames);
220 let cols = select(i32(max(p(13u), 1.0)), 1, foliage);
221 let rows = select(i32(max(p(14u), 1.0)), 1, foliage);
222 let leaf = in.alwaysDark > 0.5 || p(5u) > 0.5;
223 let variants = select(i32(max(p(15u), 1.0)), i32(max(p(16u), 1.0)), leaf);
224 let variant = i32(in.instanceId) % variants;
227 atlasUv = vec2f((in.uv.x + f32(frameNumber)) / frames, 1.0 - in.uv.y);
229 let column = variant * 2 + frameNumber % 2;
230 let row = select(0, i32(p(17u) + 0.5), leaf) + frameNumber / 2;
231 atlasUv = vec2f((f32(column) + in.uv.x) / f32(cols),
232 (f32(row) + 1.0 - in.uv.y) / f32(rows));
234 var mask = textureSample(albedo, mainSampler, atlasUv) * in.tint * vec4f(in.instanceTint, 1.0);
236 let detailDistance = distance(in.rootPos, frame.cameraPos.xyz);
237 let packedDetail = max(p(17u), 0.0);
238 let hardDistance = floor(packedDetail);
239 let density = clamp(fract(packedDetail) * 2.0, 0.0, 1.0);
240 if (detailDistance > hardDistance || hash > density) { discard; }
241 let fade = (1.0 - clamp((detailDistance - p(13u)) /
242 max(p(14u), 0.0001), 0.0, 1.0)) * 2.0;
245 if (mask.a < max(p(4u), 0.01)) { discard; }
246 // Dawn's depth precision self-shadows roots that lie exactly on the ground
247 // plane. Sample the visible blade point; the explicit normal offset in
248 // grassShadow still preserves occluder shadows without blanket acne.
249 var visibility = grassShadow(in.worldPos, max(-in.viewPos.z, 0.0));
251 var base = mask.rgb * vec3f(p(6u), p(7u), p(8u)) * 0.5;
252 let snowFade = clamp((in.worldPos.y - p(0u)) / max(p(15u), 0.0001), 0.0, 1.0);
253 let snowAmount = mix(0.0, 0.1, snowFade) * clamp(p(16u), 0.0, 1.0);
254 base = mix(base, clamp(base + vec3f(p(9u), p(10u), p(11u)), vec3f(0.0), vec3f(1.0)), snowAmount);
255 let surface = clamp(textureSample(maskMap, mainSampler, atlasUv), vec4f(0.0), vec4f(1.0));
256 var tangentNormal = textureSample(normalMap, mainSampler, atlasUv).xyz * 2.0 - vec3f(1.0);
257 tangentNormal = abs(normalize(vec3f(tangentNormal.xy * max(p(1u), 0.01), tangentNormal.z)));
258 let lightDir = normalize(frame.lightDir.xyz);
259 let viewDir = normalize(frame.cameraPos.xyz - in.worldPos);
260 let diffuse = max(dot(tangentNormal, lightDir), 0.0);
261 let backlight = max(dot(-lightDir, viewDir), 0.0) * (1.0 - surface.b);
262 let smoothness = surface.a * 0.25;
263 let halfDir = normalize(lightDir + viewDir);
264 let specular = pow(max(dot(tangentNormal, halfDir), 0.0), mix(4.0, 64.0, smoothness)) * surface.r;
265 let lighting = frame.ambient.rgb * surface.g + frame.lightColor.rgb * frame.lightDir.w *
266 (diffuse * visibility + backlight * 0.35 + specular * visibility);
267 return vec4f(base * lighting, mask.a);
269 if (p(5u) > 0.5 || in.alwaysDark > 0.5) { visibility = 0.0; }
270 let color = mix(vec3f(p(9u), p(10u), p(11u)), vec3f(p(6u), p(7u), p(8u)), visibility);
271 return vec4f(color * mask.rgb, mask.a);
constexpr const char * kGrassVertWgsl
constexpr const char * kGrassFragWgsl