1use crate::components::*;
8use crate::hierarchy;
9use crate::mesh::{MeshData, Vertex};
10use crate::procedural::{signature, Rng};
11use crate::reflect::Field;
12use glam::Vec3;
13use kvas_ecs::{Entity, World};
14use serde::{Deserialize, Serialize};
15use std::sync::Arc;
16
17#[derive(Clone, Serialize, Deserialize)]
20#[serde(default)]
21pub struct Grass {
22 pub count: i32,
24 pub width: f32,
26 pub depth: f32,
28 pub blade_height: f32,
30 pub height_variation: f32,
32 pub blade_width: f32,
34 pub lean: f32,
36 pub seed: i32,
38 pub base_color: [f32; 4],
40 pub tip_color: [f32; 4],
42 pub wind_strength: f32,
44 pub wind_speed: f32,
46 pub chunk_size: f32,
48 pub fade_start: f32,
50 pub fade_end: f32,
52 pub patchiness: f32,
54 pub round: bool,
56 pub flowers: f32,
58 #[serde(skip)]
59 #[doc(hidden)]
60 pub sig: u64,
61}
62
63impl Default for Grass {
64 fn default() -> Self {
65 Self {
66 count: 40_000,
67 width: 30.0,
68 depth: 30.0,
69 blade_height: 0.55,
70 height_variation: 0.6,
71 blade_width: 0.085,
72 lean: 0.3,
73 seed: 1,
74 base_color: [0.04, 0.2, 0.1, 1.0],
75 tip_color: [0.72, 0.9, 0.3, 1.0],
76 wind_strength: 0.5,
77 wind_speed: 1.5,
78 chunk_size: 8.0,
79 fade_start: 28.0,
80 fade_end: 70.0,
81 patchiness: 0.7,
82 round: false,
83 flowers: 0.0,
84 sig: 0,
85 }
86 }
87}
88
89impl Component for Grass {
90 fn fields(&mut self) -> Vec<Field<'_>> {
91 vec![
92 Field::i32("count", &mut self.count),
93 Field::f32("width", &mut self.width),
94 Field::f32("depth", &mut self.depth),
95 Field::f32("blade_height", &mut self.blade_height),
96 Field::f32("height_variation", &mut self.height_variation),
97 Field::f32("blade_width", &mut self.blade_width),
98 Field::f32("lean", &mut self.lean),
99 Field::i32("seed", &mut self.seed),
100 Field::color("base_color", &mut self.base_color),
101 Field::color("tip_color", &mut self.tip_color),
102 Field::f32("patchiness", &mut self.patchiness),
103 Field::bool("round", &mut self.round),
104 Field::f32("flowers", &mut self.flowers),
105 Field::f32("wind_strength", &mut self.wind_strength),
106 Field::f32("wind_speed", &mut self.wind_speed),
107 Field::f32("chunk_size", &mut self.chunk_size),
108 Field::f32("fade_start", &mut self.fade_start),
109 Field::f32("fade_end", &mut self.fade_end),
110 ]
111 }
112}
113
114pub const BLADE_VERTICES: usize = 9;
116pub const BLADE_TRIANGLES: usize = 14;
118
119pub fn chunk_grid(g: &Grass) -> (usize, usize) {
121 let size = g.chunk_size.max(1.0);
122 ((g.width / size).ceil().clamp(1.0, 64.0) as usize, (g.depth / size).ceil().clamp(1.0, 64.0) as usize)
123}
124
125pub fn chunk_mesh(g: &Grass, ix: usize, iz: usize) -> (MeshData, Vec3) {
127 let (nx, nz) = chunk_grid(g);
128 let (cw, cd) = (g.width / nx as f32, g.depth / nz as f32);
129 let center = Vec3::new(-g.width * 0.5 + (ix as f32 + 0.5) * cw, 0.0, -g.depth * 0.5 + (iz as f32 + 0.5) * cd);
130 let share = (cw * cd) / (g.width * g.depth).max(1e-3);
131 let count = ((g.count.clamp(1, 400_000) as f32 * share).round() as usize).max(1);
132 let mut rng = Rng::new((g.seed as u64) << 20 ^ (ix as u64) << 8 ^ iz as u64);
133 let mut m = MeshData::default();
134 m.vertices.reserve(count * BLADE_VERTICES);
135 m.indices.reserve(count * BLADE_TRIANGLES * 3);
136 for _ in 0..count {
137 let (x, z) = ((rng.f32() - 0.5) * cw, (rng.f32() - 0.5) * cd);
138 let yaw = rng.f32() * std::f32::consts::TAU;
139 let mut tint = rng.f32();
140 let tuft = if rng.f32() > 0.93 { 1.7 } else { 1.0 };
142 let mut h = g.blade_height * tuft * (1.0 + g.height_variation * (rng.f32() * 2.0 - 1.0)).max(0.15);
143 let edge_roll = rng.f32();
144 let flower_roll = rng.f32();
145 if g.round {
146 let (ex, ez) = ((center.x + x) / (g.width * 0.5).max(0.01), (center.z + z) / (g.depth * 0.5).max(0.01));
148 let r = (ex * ex + ez * ez).sqrt();
149 if r > 1.0 || (r > 0.8 && edge_roll < (r - 0.8) / 0.2) {
150 continue;
151 }
152 h *= 1.0 - ((r - 0.75) / 0.25).clamp(0.0, 1.0) * 0.55;
153 }
154 let flower = flower_roll < g.flowers.clamp(0.0, 0.3);
156 if flower {
157 h *= 1.25;
158 tint += 2.0;
159 }
160 let lean = g.lean * (0.3 + rng.f32());
161 let (sin, cos) = yaw.sin_cos();
162 let (right, forward) = (Vec3::new(cos, 0.0, -sin), Vec3::new(sin, 0.0, cos));
163 let root = Vec3::new(x, 0.0, z);
164 let base = m.vertices.len() as u32;
165 for (t, width) in [(0.0f32, 0.85f32), (0.28, 1.0), (0.58, 0.72), (0.84, 0.38)] {
167 let curve = lean * h * t * t;
168 let center = root + Vec3::Y * (h * t * (1.0 - 0.12 * t)) + forward * curve;
169 let half = g.blade_width * 0.5 * width * if flower { 0.6 } else { 1.0 };
170 let normal = (Vec3::Y * 2.0 + forward * 0.5).normalize();
171 for side in [-1.0f32, 1.0] {
172 m.vertices.push(Vertex { pos: (center + right * half * side).into(), normal: normal.into(), uv: [tint, t] });
173 }
174 }
175 let tip = root + Vec3::Y * h * 0.9 + forward * (lean * h * 1.05);
176 m.vertices.push(Vertex { pos: tip.into(), normal: Vec3::Y.into(), uv: [tint, 1.0] });
177 for (a, b, c) in [(0, 1, 2), (1, 3, 2), (2, 3, 4), (3, 5, 4), (4, 5, 6), (5, 7, 6), (6, 7, 8)] {
178 m.indices.extend([base + a, base + b, base + c]);
179 m.indices.extend([base + c, base + b, base + a]);
180 }
181 }
182 (m, center)
183}
184
185pub fn update(world: &mut World) {
187 let orphans: Vec<Entity> = world
189 .entities_with::<GrassChunk>()
190 .into_iter()
191 .filter(|&c| hierarchy::parent_of(world, c).is_none_or(|p| !world.is_alive(p) || !world.has::<Grass>(p)))
192 .collect();
193 for c in orphans {
194 world.despawn(c);
195 }
196 for e in world.entities_with::<Grass>() {
197 world.remove::<DynamicMesh>(e);
199 world.remove::<MeshRenderer>(e);
200 let g = world.get::<Grass>(e).unwrap().clone();
201 let sig = signature(&(
202 g.count,
203 g.width.to_bits(),
204 g.depth.to_bits(),
205 g.blade_height.to_bits(),
206 g.height_variation.to_bits(),
207 g.blade_width.to_bits(),
208 g.lean.to_bits(),
209 g.seed,
210 g.chunk_size.to_bits(),
211 g.round,
212 g.flowers.to_bits(),
213 ));
214 let chunks: Vec<Entity> = hierarchy::children_all(world, e).into_iter().filter(|&c| world.has::<GrassChunk>(c)).collect();
215 if world.get::<Grass>(e).unwrap().sig != sig || chunks.is_empty() {
216 for c in &chunks {
217 world.despawn(*c);
218 }
219 let (nx, nz) = chunk_grid(&g);
220 for iz in 0..nz {
221 for ix in 0..nx {
222 let (mesh, center) = chunk_mesh(&g, ix, iz);
223 let c = world.spawn();
224 world.insert(c, Name("Grass chunk".into()));
225 world.insert(c, Transform::at(center));
226 world.insert(c, Parent(e));
227 world.insert(c, Transient);
228 world.insert(c, GrassChunk);
229 world.insert(c, DynamicMesh { data: Arc::new(mesh), version: sig });
230 world.insert(c, MeshRenderer::default());
231 }
232 }
233 world.get_mut::<Grass>(e).unwrap().sig = sig;
234 }
235 let user = [
237 g.wind_strength,
238 g.wind_speed,
239 g.fade_start,
240 g.fade_end.max(g.fade_start + 1.0),
241 g.tip_color[0],
242 g.tip_color[1],
243 g.tip_color[2],
244 g.patchiness,
245 ];
246 for c in hierarchy::children_all(world, e) {
247 if let Some(m) = world.get_mut::<MeshRenderer>(c) {
248 m.shader = "builtin:grass".into();
249 m.color = g.base_color;
250 m.cast_shadows = false;
251 m.roughness = 1.0;
252 m.user = user;
253 }
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::scene::Scene;
262
263 #[test]
264 fn chunks_partition_the_patch_and_keep_the_blade_count() {
265 let g = Grass { count: 4000, width: 20.0, depth: 12.0, chunk_size: 4.0, ..Default::default() };
266 let (nx, nz) = chunk_grid(&g);
267 assert_eq!((nx, nz), (5, 3));
268 let mut blades = 0;
269 for iz in 0..nz {
270 for ix in 0..nx {
271 let (m, center) = chunk_mesh(&g, ix, iz);
272 blades += m.vertices.len() / BLADE_VERTICES;
273 let (min, max) = m.bounds();
274 assert!(
275 min.x > -2.8 && max.x < 2.8 && min.z > -2.8 && max.z < 2.8,
276 "chunk stays near its cell (blades may lean out a little): {min} {max}"
277 );
278 assert!(center.x.abs() <= 10.0 && center.z.abs() <= 6.0);
279 }
280 }
281 assert!((3900..=4100).contains(&blades), "{blades}");
282 }
283
284 #[test]
285 fn same_seed_same_field_and_triangle_budget() {
286 let g = Grass { count: 800, chunk_size: 100.0, ..Default::default() };
287 let (a, _) = chunk_mesh(&g, 0, 0);
288 assert_eq!(a.vertices[10].pos, chunk_mesh(&g, 0, 0).0.vertices[10].pos);
289 assert_ne!(a.vertices[10].pos, chunk_mesh(&Grass { seed: 2, ..g.clone() }, 0, 0).0.vertices[10].pos);
290 assert_eq!(a.indices.len() / 3, 800 * BLADE_TRIANGLES);
291 }
292
293 #[test]
294 fn chunks_are_transient_children_that_do_not_leak_into_scenes() {
295 let reg = crate::registry::Registry::default();
296 let mut scene = Scene::default();
297 let e = hierarchy::spawn_named(&mut scene.world, "Lawn");
298 scene.world.insert(e, Grass { count: 2000, width: 16.0, depth: 16.0, ..Default::default() });
299 update(&mut scene.world);
300 let all = hierarchy::children_all(&scene.world, e);
301 assert_eq!(all.len(), 4, "16x16 patch in 8 m chunks");
302 assert!(hierarchy::children(&scene.world, e).is_empty(), "hidden from the hierarchy");
303 let json = scene.to_json(®);
304 assert_eq!(json.matches("\"type\": \"Name\"").count(), 1, "only the Lawn is saved");
305 scene.world.get_mut::<Grass>(e).unwrap().wind_strength = 2.0;
307 update(&mut scene.world);
308 assert_eq!(hierarchy::children_all(&scene.world, e), all);
309 scene.world.get_mut::<Grass>(e).unwrap().chunk_size = 4.0;
310 update(&mut scene.world);
311 assert_eq!(hierarchy::children_all(&scene.world, e).len(), 16);
312 scene.world.despawn(e);
313 update(&mut scene.world);
314 assert_eq!(scene.world.count::<GrassChunk>(), 0, "orphan chunks are removed");
315 }
316}