From 2bff9c55d239c5c3a5586eb40b3b25d4d2c9d555 Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Fri, 31 Jan 2020 02:35:09 -0500 Subject: [PATCH] Render lots of particles with SSBOs! We generate particle batch meshes with a fixed chunk of quads at the origin, and then dispatch instances of that batch. The vertex shader fetches from a shader storage buffer of particle info by using the instance ID (between batches) and the vertex ID (within batches), using the batch size. We need to run these in batches because GPUs process instances somewhat serially, so if you have a tiny instance you're wasting a bunch of spare compute instead of running nicely in parallel. Having smaller instances than the full particle size helps with avoiding rendering lots of hidden particles if you're using dynamic particle counts, and also gives smaller data uploads (although that's not particularly significant, since they're static buffers of geometry). The shaders do billboarding based on head position rather than the projection matrices so they don't visibly move when you rotate your head: projection-based billboards look really bad in VR it turns out. --- main.lua | 82 ++++++++++++++++++++++++++++++++++-------- shaders/particles.frag | 6 +++- shaders/particles.vert | 17 +++++---- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/main.lua b/main.lua index 5ca544e..78b666d 100644 --- a/main.lua +++ b/main.lua @@ -1,5 +1,9 @@ local mat4 = lovr.math.mat4 +local nParticles = 2 ^ 16 +local batchSize = 128 +local batches = nParticles / batchSize + local features local shaders = {} local blocks = {} @@ -9,26 +13,76 @@ function lovr.load() features = lovr.graphics.getFeatures() shaders.particles = lovr.graphics.newShader("shaders/particles.vert", "shaders/particles.frag") blocks.particles = lovr.graphics.newShaderBlock("compute", { - particlePos = {"vec4", 1024}, + particleData = {"vec4", nParticles}, }, { readable = true, writable = true }) - shaders.particles:sendBlock('particleData', blocks.particles) - meshes.particle = lovr.graphics.newMesh( - { - {'vertPosition', 'float', 2}, - }, - {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}}, - "triangles", "static" - ) - meshes.particle:setVertexMap({3, 2, 1, 4, 3, 1}) + shaders.particles:sendBlock('particleDataBuffer', blocks.particles) + shaders.particles:send("batchSize", batchSize) + meshes.particle = makeParticleMesh(batchSize) + local initial = initialParticleData(nParticles, range(-2, 2), range(0, 4), range(-2, 2), range(0.005, 0.01)) + blocks.particles:send("particleData", initial) end function lovr.draw() lovr.graphics.clear() - lovr.graphics.setColor(0, 0.5, 1) local x, y, z = lovr.headset.getPose("head") shaders.particles:send("headPos", {x, y, z}) - blocks.particles:send("particlePos", {{0, 2, -2, 0}, {-1, 1, -4, 0}}) - lovr.graphics.setShader(shaders.particles) - meshes.particle:draw(mat4(), 2) + lovr.graphics.setColor(0.6, 0.1, 0) + lovr.graphics.setShader(nil) + for _, hand in ipairs(lovr.headset.getHands()) do + local x, y, z, a, ax, ay, az = lovr.headset.getPose(hand) + lovr.graphics.cube("fill", x, y, z, 0.1, a, ax, ay, az) + end + + withDepthTest("lequal", false, function() + lovr.graphics.setColor(0, 0.5, 1) + lovr.graphics.setShader(shaders.particles) + meshes.particle:draw(mat4(), batches) + end) +end + +function makeParticleMesh(size) + local format = { {'vertPosition', 'float', 2}, } + local verts = {} + local vertMap = {} + local insert = table.insert + for chunk = 1, size do + insert(verts, { 1, 1}) + insert(verts, { 1, -1}) + insert(verts, {-1, -1}) + insert(verts, {-1, 1}) + local base = 4 * (chunk - 1) + insert(vertMap, 3 + base) + insert(vertMap, 2 + base) + insert(vertMap, 1 + base) + insert(vertMap, 4 + base) + insert(vertMap, 3 + base) + insert(vertMap, 1 + base) + end + local mesh = lovr.graphics.newMesh(format, verts, "triangles", "static") + mesh:setVertexMap(vertMap) + return mesh +end + +function range(min, max) + local scale = max - min + return function() + return min + lovr.math.random() * scale + end +end + +function initialParticleData(size, xRange, yRange, zRange, sRange) + local positions = {} + local insert = table.insert + for i = 1, size do + insert(positions, {xRange(), yRange(), zRange(), sRange()}) + end + return positions +end + +function withDepthTest(compareMode, write, callback) + local oldComp, oldWrite = lovr.graphics.getDepthTest() + lovr.graphics.setDepthTest(compareMode, write) + callback() + lovr.graphics.setDepthTest(oldComp, oldWrite) end diff --git a/shaders/particles.frag b/shaders/particles.frag index fff0f2a..67f73bb 100644 --- a/shaders/particles.frag +++ b/shaders/particles.frag @@ -1,3 +1,7 @@ -vec4 color(vec4 graphicsColor, sampler2D image, vec2 uv) { +in vec2 uv; + +vec4 color(vec4 graphicsColor, sampler2D image, vec2 _uv) { + float alpha = 1.0 - (distance(uv, vec2(0.5)) * 2.0); + graphicsColor.a *= alpha; return graphicsColor; } diff --git a/shaders/particles.vert b/shaders/particles.vert index 3ff33f8..d5ca089 100644 --- a/shaders/particles.vert +++ b/shaders/particles.vert @@ -1,14 +1,19 @@ in vec2 vertPosition; +out vec2 uv; uniform vec3 headPos; -layout(std430) buffer particleData { vec4 particlePos[2]; }; +uniform int batchSize; +layout(std430) buffer particleDataBuffer { vec4 particleData[]; }; vec4 position(mat4 projection, mat4 transform, vec4 vertex) { - vec3 pos = particlePos[gl_InstanceID].xyz; + int index = gl_VertexID / 4 + gl_InstanceID * batchSize; + vec3 pos = particleData[index].xyz; + float size = particleData[index].w; + uv = vertPosition * 0.5 + 0.5; + vec3 forward = normalize(pos - headPos); vec3 right = normalize(cross(forward, vec3(0, 1, 0))); vec3 up = cross(right, forward); - mat2 frame = mat2(up, right); - // vec3 vert = pos + vec3(frame * vertPosition * 0.3, 0); - vec3 vert = pos + vec3(vertPosition, 0); - return projection * transform * vec4(vert, 1); + vec3 offset = mat2x3(up, right) * (vertPosition * size); + + return projection * transform * vec4(pos + offset, 1.0); } -- 2.47.0