1use glam::Mat4;
24use glow::HasContext;
25
26pub const MAX_BONES: usize = 16;
29
30pub const MAX_PARTICLES_PER_ENTITY: u32 = 4_000_000;
34
35#[repr(C)]
37#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
38pub struct GpuBone {
39 pub start_end: [f32; 4],
41 pub params: [f32; 4],
44 pub color: [f32; 4],
46}
47
48impl Default for GpuBone {
49 fn default() -> Self {
50 GpuBone { start_end: [0.0; 4], params: [0.0; 4], color: [0.0; 4] }
51 }
52}
53
54#[repr(C)]
56#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
57pub struct GpuDensityEntityData {
58 pub position_scale: [f32; 4],
60 pub color: [f32; 4],
62 pub params: [f32; 4],
64 pub params2: [f32; 4],
66 pub bones: [GpuBone; MAX_BONES],
67}
68
69impl GpuDensityEntityData {
70 pub fn bone_count(&self) -> usize {
72 (self.params2[0].max(0.0) as usize).min(MAX_BONES)
73 }
74
75 pub fn requested_particles(&self) -> u32 {
77 self.params2[1].max(0.0).min(u32::MAX as f32) as u32
78 }
79
80 pub fn particle_count(&self, budget: u32) -> u32 {
82 self.requested_particles()
83 .min(budget)
84 .min(MAX_PARTICLES_PER_ENTITY)
85 }
86
87 pub fn weights(&self) -> ([f32; MAX_BONES], f32) {
90 let mut w = [0.0f32; MAX_BONES];
91 let mut total = 0.0;
92 for (i, b) in self.bones.iter().enumerate().take(self.bone_count()) {
93 w[i] = b.params[2].max(0.0);
94 total += w[i];
95 }
96 (w, total)
97 }
98}
99
100pub fn particle_pixels(count: u32) -> f32 {
103 let n = count.max(1) as f32;
104 (1.35 * (2_000_000.0 / n).sqrt()).clamp(1.0, 6.0)
105}
106
107pub fn particle_alpha(count: u32) -> f32 {
110 let n = count.max(1) as f32;
111 (400_000.0 / n).clamp(0.03, 0.85)
112}
113
114const VERT: &str = r#"
115#version 330 core
116layout(location = 0) in vec2 v_pos;
117layout(location = 1) in vec2 v_uv;
118
119uniform mat4 u_view_proj;
120uniform vec4 u_pos_scale;
121uniform vec4 u_color;
122uniform vec4 u_params; // hp, breath_phase, breath_amp, falloff
123uniform vec4 u_params2; // bone_count, particle_count, jitter, binding
124uniform vec4 u_bones[48]; // per bone: start_end, params, color
125uniform float u_time;
126uniform vec2 u_px; // particle size in clip units per w
127uniform float u_alpha;
128
129out vec2 f_uv;
130out vec4 f_color;
131out float f_emission;
132
133// Same hash the glyph pass uses, so the two kinds of matter share a grain.
134float hf(uint seed, uint v) {
135 uint n = seed * 374761393u + v * 668265263u;
136 n ^= (n >> 13u);
137 n *= 0x5851F42Du;
138 n ^= (n >> 16u);
139 return float(n & 0x00FFFFFFu) / float(0x01000000u);
140}
141
142void main() {
143 uint id = uint(gl_InstanceID);
144 int nb = int(clamp(u_params2.x, 1.0, 16.0));
145
146 // Which bone: in proportion to the weights.
147 float total = 0.0;
148 for (int i = 0; i < nb; ++i) total += max(u_bones[i * 3 + 1].z, 0.0);
149 float r = hf(id, 1u) * max(total, 1e-6);
150 int b = nb - 1;
151 float acc = 0.0;
152 for (int i = 0; i < nb; ++i) {
153 acc += max(u_bones[i * 3 + 1].z, 0.0);
154 if (r <= acc) { b = i; break; }
155 }
156 vec4 se = u_bones[b * 3];
157 vec4 bp = u_bones[b * 3 + 1];
158 vec4 bc = u_bones[b * 3 + 2];
159
160 // Where in the capsule. Uniform along the axis, a disc across it, the
161 // disc pulled toward the core by the falloff so the surface is a
162 // gradient rather than a hard edge.
163 float t = hf(id, 2u);
164 vec2 axis = se.zw - se.xy;
165 vec2 c = se.xy + axis * t;
166 float falloff = max(u_params.w, 0.5);
167 float rr = bp.x * pow(hf(id, 3u), 0.5 + 0.12 * (falloff - 2.0));
168 rr *= 1.0 + (hf(id, 7u) - 0.5) * 0.12;
169 float ang = hf(id, 4u) * 6.28318530;
170 vec3 local = vec3(c.x + cos(ang) * rr, c.y, sin(ang) * rr * 0.7);
171
172 // Breathing: the chest swells and the rest follows less.
173 local.xz *= 1.0 + u_params.z * sin(u_params.y + local.y * 2.0);
174
175 // Damage: the share of matter past hp has come loose. It drifts out
176 // from the body's middle and fades, then is reseeded.
177 float hp = clamp(u_params.x, 0.0, 1.0);
178 float loose = step(hp, hf(id, 5u));
179 float age = fract(u_time * 0.12 + hf(id, 6u));
180 vec3 away = normalize(vec3(local.x, local.y + 0.4, local.z) + vec3(1e-4));
181 local += away * loose * age * 1.6;
182 float alpha = u_alpha * mix(1.0, (1.0 - age) * (1.0 - age), loose);
183
184 // Jitter: matter held by springs is never still. The stronger the
185 // binding, the smaller the wander.
186 float jit = u_params2.z / max(u_params2.w, 1.0) * 6.0;
187 float ph = u_time * 2.6 + hf(id, 11u) * 6.28318530;
188 local += (vec3(hf(id, 8u), hf(id, 9u), hf(id, 10u)) - 0.5) * jit * (0.6 + 0.4 * sin(ph));
189
190 // Entity space is y-down like the screen; world is y-up.
191 vec3 world = u_pos_scale.xyz + vec3(local.x, -local.y, local.z) * u_pos_scale.w;
192 vec4 clip = u_view_proj * vec4(world, 1.0);
193 clip.xy += v_pos * u_px * clip.w;
194 clip.y = -clip.y;
195 gl_Position = clip;
196
197 vec3 rgb = mix(bc.rgb, bc.rgb * u_color.rgb, 0.35);
198 float bright = max(rgb.r, max(rgb.g, rgb.b));
199 f_uv = v_uv;
200 f_color = vec4(rgb, alpha * bc.a);
201 f_emission = smoothstep(0.72, 1.0, bright) * 0.9 + 0.04;
202}
203"#;
204
205const FRAG: &str = r#"
206#version 330 core
207in vec2 f_uv;
208in vec4 f_color;
209in float f_emission;
210layout(location = 0) out vec4 o_color;
211layout(location = 1) out vec4 o_emission;
212void main() {
213 float d = length(f_uv - 0.5) * 2.0;
214 float a = (1.0 - smoothstep(0.55, 1.0, d)) * f_color.a;
215 if (a < 0.002) discard;
216 // Premultiplied, added: a million faint discs sum to a solid body.
217 o_color = vec4(f_color.rgb * a, a);
218 o_emission = vec4(f_color.rgb * f_emission * a, a);
219}
220"#;
221
222#[rustfmt::skip]
223const QUAD: [f32; 24] = [
224 -0.5, 0.5, 0.0, 1.0,
225 -0.5, -0.5, 0.0, 0.0,
226 0.5, 0.5, 1.0, 1.0,
227 -0.5, -0.5, 0.0, 0.0,
228 0.5, -0.5, 1.0, 0.0,
229 0.5, 0.5, 1.0, 1.0,
230];
231
232pub struct GpuDensityRenderer {
234 program: glow::Program,
235 vao: glow::VertexArray,
236 quad_vbo: glow::Buffer,
237 loc_view_proj: Option<glow::UniformLocation>,
238 loc_pos_scale: Option<glow::UniformLocation>,
239 loc_color: Option<glow::UniformLocation>,
240 loc_params: Option<glow::UniformLocation>,
241 loc_params2: Option<glow::UniformLocation>,
242 loc_bones: Option<glow::UniformLocation>,
243 loc_time: Option<glow::UniformLocation>,
244 loc_px: Option<glow::UniformLocation>,
245 loc_alpha: Option<glow::UniformLocation>,
246 pub drawn: u32,
248}
249
250impl GpuDensityRenderer {
251 pub unsafe fn new(gl: &glow::Context) -> Self {
252 let program = compile(gl, VERT, FRAG);
253 let vao = gl.create_vertex_array().expect("density vao");
254 gl.bind_vertex_array(Some(vao));
255 let quad_vbo = gl.create_buffer().expect("density quad");
256 gl.bind_buffer(glow::ARRAY_BUFFER, Some(quad_vbo));
257 gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, bytemuck::cast_slice(&QUAD), glow::STATIC_DRAW);
258 gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 16, 0);
259 gl.enable_vertex_attrib_array(0);
260 gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 16, 8);
261 gl.enable_vertex_attrib_array(1);
262 gl.bind_vertex_array(None);
263 let loc = |n: &str| gl.get_uniform_location(program, n);
264 Self {
265 loc_view_proj: loc("u_view_proj"),
266 loc_pos_scale: loc("u_pos_scale"),
267 loc_color: loc("u_color"),
268 loc_params: loc("u_params"),
269 loc_params2: loc("u_params2"),
270 loc_bones: loc("u_bones[0]"),
271 loc_time: loc("u_time"),
272 loc_px: loc("u_px"),
273 loc_alpha: loc("u_alpha"),
274 program,
275 vao,
276 quad_vbo,
277 drawn: 0,
278 }
279 }
280
281 pub unsafe fn draw(
284 &mut self,
285 gl: &glow::Context,
286 entities: &[GpuDensityEntityData],
287 budget: u32,
288 view_proj: &Mat4,
289 viewport: (u32, u32),
290 time: f32,
291 ) -> u32 {
292 self.drawn = 0;
293 if entities.is_empty() || budget == 0 {
294 return 0;
295 }
296 let mut draws = 0;
297 gl.use_program(Some(self.program));
298 gl.bind_vertex_array(Some(self.vao));
299 gl.enable(glow::BLEND);
300 gl.blend_func(glow::ONE, glow::ONE_MINUS_SRC_ALPHA);
301 gl.uniform_matrix_4_f32_slice(self.loc_view_proj.as_ref(), false, &view_proj.to_cols_array());
302 gl.uniform_1_f32(self.loc_time.as_ref(), time);
303 let (vw, vh) = (viewport.0.max(1) as f32, viewport.1.max(1) as f32);
304
305 for e in entities {
306 let n = e.particle_count(budget);
307 if n == 0 || e.bone_count() == 0 {
308 continue;
309 }
310 let px = particle_pixels(n);
311 gl.uniform_4_f32_slice(self.loc_pos_scale.as_ref(), &e.position_scale);
312 gl.uniform_4_f32_slice(self.loc_color.as_ref(), &e.color);
313 gl.uniform_4_f32_slice(self.loc_params.as_ref(), &e.params);
314 gl.uniform_4_f32_slice(self.loc_params2.as_ref(), &e.params2);
315 let flat: &[f32] = bytemuck::cast_slice(&e.bones);
316 gl.uniform_4_f32_slice(self.loc_bones.as_ref(), flat);
317 gl.uniform_2_f32(self.loc_px.as_ref(), px * 2.0 / vw, px * 2.0 / vh);
318 gl.uniform_1_f32(self.loc_alpha.as_ref(), particle_alpha(n));
319 gl.draw_arrays_instanced(glow::TRIANGLES, 0, 6, n as i32);
320 self.drawn += n;
321 draws += 1;
322 }
323
324 gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
325 gl.bind_vertex_array(None);
326 draws
327 }
328
329 pub unsafe fn destroy(&self, gl: &glow::Context) {
330 gl.delete_program(self.program);
331 gl.delete_vertex_array(self.vao);
332 gl.delete_buffer(self.quad_vbo);
333 }
334}
335
336unsafe fn compile(gl: &glow::Context, vs_src: &str, fs_src: &str) -> glow::Program {
337 let vs = gl.create_shader(glow::VERTEX_SHADER).expect("density vs");
338 gl.shader_source(vs, vs_src);
339 gl.compile_shader(vs);
340 if !gl.get_shader_compile_status(vs) {
341 panic!("density vertex shader:\n{}", gl.get_shader_info_log(vs));
342 }
343 let fs = gl.create_shader(glow::FRAGMENT_SHADER).expect("density fs");
344 gl.shader_source(fs, fs_src);
345 gl.compile_shader(fs);
346 if !gl.get_shader_compile_status(fs) {
347 panic!("density fragment shader:\n{}", gl.get_shader_info_log(fs));
348 }
349 let p = gl.create_program().expect("density program");
350 gl.attach_shader(p, vs);
351 gl.attach_shader(p, fs);
352 gl.link_program(p);
353 if !gl.get_program_link_status(p) {
354 panic!("density link:\n{}", gl.get_program_info_log(p));
355 }
356 gl.detach_shader(p, vs);
357 gl.detach_shader(p, fs);
358 gl.delete_shader(vs);
359 gl.delete_shader(fs);
360 p
361}