use glam::Mat4;
use glow::HasContext;
pub const MAX_BONES: usize = 16;
pub const MAX_PARTICLES_PER_ENTITY: u32 = 4_000_000;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuBone {
pub start_end: [f32; 4],
pub params: [f32; 4],
pub color: [f32; 4],
}
impl Default for GpuBone {
fn default() -> Self {
GpuBone { start_end: [0.0; 4], params: [0.0; 4], color: [0.0; 4] }
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuDensityEntityData {
pub position_scale: [f32; 4],
pub color: [f32; 4],
pub params: [f32; 4],
pub params2: [f32; 4],
pub bones: [GpuBone; MAX_BONES],
}
impl GpuDensityEntityData {
pub fn bone_count(&self) -> usize {
(self.params2[0].max(0.0) as usize).min(MAX_BONES)
}
pub fn requested_particles(&self) -> u32 {
self.params2[1].max(0.0).min(u32::MAX as f32) as u32
}
pub fn particle_count(&self, budget: u32) -> u32 {
self.requested_particles()
.min(budget)
.min(MAX_PARTICLES_PER_ENTITY)
}
pub fn weights(&self) -> ([f32; MAX_BONES], f32) {
let mut w = [0.0f32; MAX_BONES];
let mut total = 0.0;
for (i, b) in self.bones.iter().enumerate().take(self.bone_count()) {
w[i] = b.params[2].max(0.0);
total += w[i];
}
(w, total)
}
}
pub fn particle_pixels(count: u32) -> f32 {
let n = count.max(1) as f32;
(1.35 * (2_000_000.0 / n).sqrt()).clamp(1.0, 6.0)
}
pub fn particle_alpha(count: u32) -> f32 {
let n = count.max(1) as f32;
(400_000.0 / n).clamp(0.03, 0.85)
}
const VERT: &str = r#"
#version 330 core
layout(location = 0) in vec2 v_pos;
layout(location = 1) in vec2 v_uv;
uniform mat4 u_view_proj;
uniform vec4 u_pos_scale;
uniform vec4 u_color;
uniform vec4 u_params; // hp, breath_phase, breath_amp, falloff
uniform vec4 u_params2; // bone_count, particle_count, jitter, binding
uniform vec4 u_bones[48]; // per bone: start_end, params, color
uniform float u_time;
uniform vec2 u_px; // particle size in clip units per w
uniform float u_alpha;
out vec2 f_uv;
out vec4 f_color;
out float f_emission;
// Same hash the glyph pass uses, so the two kinds of matter share a grain.
float hf(uint seed, uint v) {
uint n = seed * 374761393u + v * 668265263u;
n ^= (n >> 13u);
n *= 0x5851F42Du;
n ^= (n >> 16u);
return float(n & 0x00FFFFFFu) / float(0x01000000u);
}
void main() {
uint id = uint(gl_InstanceID);
int nb = int(clamp(u_params2.x, 1.0, 16.0));
// Which bone: in proportion to the weights.
float total = 0.0;
for (int i = 0; i < nb; ++i) total += max(u_bones[i * 3 + 1].z, 0.0);
float r = hf(id, 1u) * max(total, 1e-6);
int b = nb - 1;
float acc = 0.0;
for (int i = 0; i < nb; ++i) {
acc += max(u_bones[i * 3 + 1].z, 0.0);
if (r <= acc) { b = i; break; }
}
vec4 se = u_bones[b * 3];
vec4 bp = u_bones[b * 3 + 1];
vec4 bc = u_bones[b * 3 + 2];
// Where in the capsule. Uniform along the axis, a disc across it, the
// disc pulled toward the core by the falloff so the surface is a
// gradient rather than a hard edge.
float t = hf(id, 2u);
vec2 axis = se.zw - se.xy;
vec2 c = se.xy + axis * t;
float falloff = max(u_params.w, 0.5);
float rr = bp.x * pow(hf(id, 3u), 0.5 + 0.12 * (falloff - 2.0));
rr *= 1.0 + (hf(id, 7u) - 0.5) * 0.12;
float ang = hf(id, 4u) * 6.28318530;
vec3 local = vec3(c.x + cos(ang) * rr, c.y, sin(ang) * rr * 0.7);
// Breathing: the chest swells and the rest follows less.
local.xz *= 1.0 + u_params.z * sin(u_params.y + local.y * 2.0);
// Damage: the share of matter past hp has come loose. It drifts out
// from the body's middle and fades, then is reseeded.
float hp = clamp(u_params.x, 0.0, 1.0);
float loose = step(hp, hf(id, 5u));
float age = fract(u_time * 0.12 + hf(id, 6u));
vec3 away = normalize(vec3(local.x, local.y + 0.4, local.z) + vec3(1e-4));
local += away * loose * age * 1.6;
float alpha = u_alpha * mix(1.0, (1.0 - age) * (1.0 - age), loose);
// Jitter: matter held by springs is never still. The stronger the
// binding, the smaller the wander.
float jit = u_params2.z / max(u_params2.w, 1.0) * 6.0;
float ph = u_time * 2.6 + hf(id, 11u) * 6.28318530;
local += (vec3(hf(id, 8u), hf(id, 9u), hf(id, 10u)) - 0.5) * jit * (0.6 + 0.4 * sin(ph));
// Entity space is y-down like the screen; world is y-up.
vec3 world = u_pos_scale.xyz + vec3(local.x, -local.y, local.z) * u_pos_scale.w;
vec4 clip = u_view_proj * vec4(world, 1.0);
clip.xy += v_pos * u_px * clip.w;
clip.y = -clip.y;
gl_Position = clip;
vec3 rgb = mix(bc.rgb, bc.rgb * u_color.rgb, 0.35);
float bright = max(rgb.r, max(rgb.g, rgb.b));
f_uv = v_uv;
f_color = vec4(rgb, alpha * bc.a);
f_emission = smoothstep(0.72, 1.0, bright) * 0.9 + 0.04;
}
"#;
const FRAG: &str = r#"
#version 330 core
in vec2 f_uv;
in vec4 f_color;
in float f_emission;
layout(location = 0) out vec4 o_color;
layout(location = 1) out vec4 o_emission;
void main() {
float d = length(f_uv - 0.5) * 2.0;
float a = (1.0 - smoothstep(0.55, 1.0, d)) * f_color.a;
if (a < 0.002) discard;
// Premultiplied, added: a million faint discs sum to a solid body.
o_color = vec4(f_color.rgb * a, a);
o_emission = vec4(f_color.rgb * f_emission * a, a);
}
"#;
#[rustfmt::skip]
const QUAD: [f32; 24] = [
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, -0.5, 1.0, 0.0,
0.5, 0.5, 1.0, 1.0,
];
pub struct GpuDensityRenderer {
program: glow::Program,
vao: glow::VertexArray,
quad_vbo: glow::Buffer,
loc_view_proj: Option<glow::UniformLocation>,
loc_pos_scale: Option<glow::UniformLocation>,
loc_color: Option<glow::UniformLocation>,
loc_params: Option<glow::UniformLocation>,
loc_params2: Option<glow::UniformLocation>,
loc_bones: Option<glow::UniformLocation>,
loc_time: Option<glow::UniformLocation>,
loc_px: Option<glow::UniformLocation>,
loc_alpha: Option<glow::UniformLocation>,
pub drawn: u32,
}
impl GpuDensityRenderer {
pub unsafe fn new(gl: &glow::Context) -> Self {
let program = compile(gl, VERT, FRAG);
let vao = gl.create_vertex_array().expect("density vao");
gl.bind_vertex_array(Some(vao));
let quad_vbo = gl.create_buffer().expect("density quad");
gl.bind_buffer(glow::ARRAY_BUFFER, Some(quad_vbo));
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, bytemuck::cast_slice(&QUAD), glow::STATIC_DRAW);
gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 16, 0);
gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 16, 8);
gl.enable_vertex_attrib_array(1);
gl.bind_vertex_array(None);
let loc = |n: &str| gl.get_uniform_location(program, n);
Self {
loc_view_proj: loc("u_view_proj"),
loc_pos_scale: loc("u_pos_scale"),
loc_color: loc("u_color"),
loc_params: loc("u_params"),
loc_params2: loc("u_params2"),
loc_bones: loc("u_bones[0]"),
loc_time: loc("u_time"),
loc_px: loc("u_px"),
loc_alpha: loc("u_alpha"),
program,
vao,
quad_vbo,
drawn: 0,
}
}
pub unsafe fn draw(
&mut self,
gl: &glow::Context,
entities: &[GpuDensityEntityData],
budget: u32,
view_proj: &Mat4,
viewport: (u32, u32),
time: f32,
) -> u32 {
self.drawn = 0;
if entities.is_empty() || budget == 0 {
return 0;
}
let mut draws = 0;
gl.use_program(Some(self.program));
gl.bind_vertex_array(Some(self.vao));
gl.enable(glow::BLEND);
gl.blend_func(glow::ONE, glow::ONE_MINUS_SRC_ALPHA);
gl.uniform_matrix_4_f32_slice(self.loc_view_proj.as_ref(), false, &view_proj.to_cols_array());
gl.uniform_1_f32(self.loc_time.as_ref(), time);
let (vw, vh) = (viewport.0.max(1) as f32, viewport.1.max(1) as f32);
for e in entities {
let n = e.particle_count(budget);
if n == 0 || e.bone_count() == 0 {
continue;
}
let px = particle_pixels(n);
gl.uniform_4_f32_slice(self.loc_pos_scale.as_ref(), &e.position_scale);
gl.uniform_4_f32_slice(self.loc_color.as_ref(), &e.color);
gl.uniform_4_f32_slice(self.loc_params.as_ref(), &e.params);
gl.uniform_4_f32_slice(self.loc_params2.as_ref(), &e.params2);
let flat: &[f32] = bytemuck::cast_slice(&e.bones);
gl.uniform_4_f32_slice(self.loc_bones.as_ref(), flat);
gl.uniform_2_f32(self.loc_px.as_ref(), px * 2.0 / vw, px * 2.0 / vh);
gl.uniform_1_f32(self.loc_alpha.as_ref(), particle_alpha(n));
gl.draw_arrays_instanced(glow::TRIANGLES, 0, 6, n as i32);
self.drawn += n;
draws += 1;
}
gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
gl.bind_vertex_array(None);
draws
}
pub unsafe fn destroy(&self, gl: &glow::Context) {
gl.delete_program(self.program);
gl.delete_vertex_array(self.vao);
gl.delete_buffer(self.quad_vbo);
}
}
unsafe fn compile(gl: &glow::Context, vs_src: &str, fs_src: &str) -> glow::Program {
let vs = gl.create_shader(glow::VERTEX_SHADER).expect("density vs");
gl.shader_source(vs, vs_src);
gl.compile_shader(vs);
if !gl.get_shader_compile_status(vs) {
panic!("density vertex shader:\n{}", gl.get_shader_info_log(vs));
}
let fs = gl.create_shader(glow::FRAGMENT_SHADER).expect("density fs");
gl.shader_source(fs, fs_src);
gl.compile_shader(fs);
if !gl.get_shader_compile_status(fs) {
panic!("density fragment shader:\n{}", gl.get_shader_info_log(fs));
}
let p = gl.create_program().expect("density program");
gl.attach_shader(p, vs);
gl.attach_shader(p, fs);
gl.link_program(p);
if !gl.get_program_link_status(p) {
panic!("density link:\n{}", gl.get_program_info_log(p));
}
gl.detach_shader(p, vs);
gl.detach_shader(p, fs);
gl.delete_shader(vs);
gl.delete_shader(fs);
p
}