Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions include/threepp/objects/ParticleSystem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@

namespace threepp {

// Material-name marker identifying the ParticleSystem billboard mesh. GL/WGPU
// compile the custom particle ShaderMaterial directly, but the Vulkan renderer
// has no generic ShaderMaterial path — it recognizes the particle Mesh by
// material()->name == kParticleMaterialName and routes it to a dedicated
// world-space billboard overlay pass instead of the PT/G-buffer path (where
// the un-expanded quad would be a zero-area, invisible triangle).
inline constexpr const char* kParticleMaterialName = "__threepp_particle__";

class ParticleSystem: public Object3D {

public:
Expand Down
21 changes: 21 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,27 @@ if (THREEPP_WITH_VULKAN)
"${CMAKE_CURRENT_SOURCE_DIR}/threepp/renderers/vulkan/shaders/overlay_sprite.frag"
kOverlaySpriteFragSpv
_overlay_sprite_frag_spv_h)
# World-space particle billboards (ParticleSystem). The Vulkan backend has no
# generic ShaderMaterial path, so the particle Mesh is drawn by a dedicated
# billboard pass in the post-TAA overlay block — depth-tested against the
# G-buffer depth, composited onto the swapchain. Two blend variants (alpha /
# additive) selected per material at draw time.
compile_vulkan_shader(threepp
"${CMAKE_CURRENT_SOURCE_DIR}/threepp/renderers/vulkan/shaders/particle.vert"
kParticleVertSpv
_particle_vert_spv_h)
compile_vulkan_shader(threepp
"${CMAKE_CURRENT_SOURCE_DIR}/threepp/renderers/vulkan/shaders/particle.frag"
kParticleFragSpv
_particle_frag_spv_h)
# World-space Sprite billboards (screenSpace == false) — perspective,
# depth-tested counterpart to the ortho overlay_sprite HUD path. Shares
# overlay_sprite.frag; only the vertex shader differs (perspective reverse-Z,
# no GL z-remap). Drawn in the post-TAA overlay block.
compile_vulkan_shader(threepp
"${CMAKE_CURRENT_SOURCE_DIR}/threepp/renderers/vulkan/shaders/sprite3d.vert"
kSprite3dVertSpv
_sprite3d_vert_spv_h)
endif ()


Expand Down
3 changes: 3 additions & 0 deletions src/threepp/objects/ParticleSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ struct ParticleSystem::Impl {
particleCount = settings.particlesPerSecond * std::min(settings.particleDeathAge, settings.emitterDeathAge);

particleMaterial = ShaderMaterial::create();
// Tag so backends without a generic ShaderMaterial path (Vulkan) can
// recognize this as a particle billboard mesh — see kParticleMaterialName.
particleMaterial->name = kParticleMaterialName;
particleMaterial->vertexShader = particleVertexShader;
particleMaterial->fragmentShader = particleFragmentShader;
particleMaterial->transparent = true;
Expand Down
945 changes: 938 additions & 7 deletions src/threepp/renderers/VulkanRenderer.cpp

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions src/threepp/renderers/vulkan/shaders/particle.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#version 460

// Particle billboard fragment shader. Mirrors the GL ParticleSystem fragment
// shader (modulate per-particle vertex color × particle texture) and the HUD
// overlay_sprite.frag color convention.
//
// The vertex color (HSL→RGB) and the texture are LINEAR (an sRGB-tagged texture
// is hardware-decoded to linear on sample). The swapchain is B8G8R8A8_UNORM with
// no hardware sRGB write-out and the path-traced image is sRGB-encoded by
// denoise.comp, so we apply the same linear→sRGB OETF here. Without it particles
// composite darker/more-saturated than the rest of the frame. Blending (alpha vs
// additive) is set by the pipeline variant, not here. Untextured particle systems
// bind a 1×1 white default so the sampler is always valid.

layout(set = 0, binding = 0) uniform sampler2D tex;

layout(location = 0) in vec4 vColor;
layout(location = 1) in vec2 vUv;

layout(location = 0) out vec4 outColor;

vec3 linearToSRGB(vec3 x) {
const vec3 cutoff = vec3(lessThan(x, vec3(0.0031308)));
const vec3 lower = 12.92 * x;
const vec3 higher = 1.055 * pow(max(x, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
return mix(higher, lower, cutoff);
}

void main() {
vec4 t = texture(tex, vUv);
outColor = vec4(linearToSRGB(vColor.rgb * t.rgb), vColor.a * t.a);
}
64 changes: 64 additions & 0 deletions src/threepp/renderers/vulkan/shaders/particle.vert
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#version 460

// World-space particle billboard vertex shader. Vulkan port of the GL/WGPU
// ParticleSystem shader (src/threepp/objects/ParticleSystem.cpp): the renderer
// has no generic ShaderMaterial path, so the particle Mesh is drawn here by a
// dedicated billboard pass in the post-TAA overlay block rather than the
// PT/G-buffer path.
//
// The geometry stores 4 coincident verts per particle (all at the particle
// CENTER); the quad is expanded HERE from the per-vertex data:
// inPos = particle center (model space)
// inNormal = {size, angle, opacity}
// inUv = quad corner offset, -0.5..0.5
// inColor = particle RGB
//
// CPU pushes modelView (= viewUnjittered · meshWorld) and the reverse-Z
// projection separately because the distance-attenuated billboard scale needs
// view-space depth and proj[1][1] individually (it can't use a combined MVP).

layout(location = 0) in vec3 inPos;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inUv;
layout(location = 3) in vec3 inColor;

layout(location = 0) out vec4 vColor;
layout(location = 1) out vec2 vUv;

layout(push_constant) uniform Pc {
mat4 modelView; // 64 — viewUnjittered · meshWorld
mat4 proj; // 64 — reverse-Z Vulkan projection (unjittered)
} pc;

void main() {
float pSize = inNormal.x;
float pAngle = inNormal.y;
float pOpacity = inNormal.z;

vColor = vec4(inColor, pOpacity);

// Rotate the corner offset by the particle angle (screen-aligned).
float c = cos(pAngle);
float s = sin(pAngle);
vec2 rotated = vec2(c * inUv.x - s * inUv.y,
s * inUv.x + c * inUv.y);

vec4 mvPosition = pc.modelView * vec4(inPos, 1.0);
vec4 clipPos = pc.proj * mvPosition;

// proj[1][1] is the Y scale (≈ 1/tan(fov/2)); reverse-Z only rewrites the
// depth row, so this matches the GL billboard scale term exactly. Multiply
// by clipPos.w to offset in clip space (perspective-correct billboard size).
float scale = pSize * pc.proj[1][1] / length(mvPosition.xyz);
clipPos.xy += rotated * scale * clipPos.w;

// threepp projection follows the GL NDC convention (Y up); Vulkan NDC is Y
// down, so flip Y at the clip boundary — same correction overlay.vert
// applies. The reverse-Z projection already maps z to [0,1], so no z-remap
// is needed (unlike the ortho overlay_sprite path).
clipPos.y = -clipPos.y;
gl_Position = clipPos;

// Texture UV derived from the corner offset (matches GL: uv + 0.5).
vUv = inUv + vec2(0.5);
}
51 changes: 51 additions & 0 deletions src/threepp/renderers/vulkan/shaders/sprite3d.vert
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#version 460

// World-space Sprite billboard vertex shader. Vulkan port of the GL/WGPU sprite
// path for sprites with screenSpace == false (3D-positioned camera-facing quads —
// e.g. the TPS shooter's impact "particles"). The screen-space HUD sprites go
// through overlay_sprite.vert (ortho); this is the perspective, depth-tested
// counterpart drawn in the post-TAA overlay block.
//
// Same push-constant layout as overlay_sprite (SpritePC) so the fragment shader
// (overlay_sprite.frag) is shared. The ONLY difference from overlay_sprite.vert:
// the projection here is the perspective reverse-Z matrix, which already maps z
// to Vulkan's [0,w] — so we DROP the GL→Vulkan z-remap (that remap is only
// correct for the GL-convention ortho HUD projection).

layout(location = 0) in vec3 inPos; // local quad position, -0.5..0.5
layout(location = 1) in vec2 inUv; // 0..1 corner UVs

layout(location = 0) out vec2 vUv;

layout(push_constant) uniform Pc {
mat4 projection; // 64 — perspective reverse-Z projection (unjittered)
vec4 mvPos; // 16 — sprite center in view space (xyz used)
vec2 scale; // 8 — world-space scale extracted from modelMatrix
vec2 center; // 8 — pivot offset (0..1), matches Sprite::center
vec4 color; // 16 — tint rgb + opacity (.a)
float rotation; // 4 — radians, SpriteMaterial.rotation
float _pad0; // 4
float _pad1; // 4
float _pad2; // 4
} pc;

void main() {
vec2 aligned = ((inPos.xy - pc.center) + vec2(0.5)) * pc.scale;
float s = sin(pc.rotation);
float c = cos(pc.rotation);
vec2 rotated = vec2(c * aligned.x - s * aligned.y,
s * aligned.x + c * aligned.y);
// Camera-facing: offset the view-space center in view XY, then project. The
// offset is in view (≈ world) units, so the sprite keeps a constant world
// size and shrinks with distance under perspective (sizeAttenuation).
vec4 view = vec4(pc.mvPos.xyz, 1.0);
view.xy += rotated;
vec4 clip = pc.projection * view;
// threepp projection follows the GL NDC convention (Y up); Vulkan NDC is Y
// down, so flip Y at the clip boundary (same as overlay.vert / particle.vert).
// The reverse-Z projection already maps z to [0,w], so — unlike the ortho
// overlay_sprite path — NO z-remap is applied.
clip.y = -clip.y;
gl_Position = clip;
vUv = inUv;
}
Loading