mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: implement M2 ribbon emitter rendering for spell trail effects
Parse M2RibbonEmitter data (WotLK format) from M2 files — bone index, position, color/alpha/height tracks, edgesPerSecond, edgeLifetime, gravity. Add CPU-side trail simulation per instance (edge birth at bone world position, lifetime expiry, gravity droop). New m2_ribbon.vert/frag shaders render a triangle-strip quad per emitter using the existing particleTexLayout_ descriptor set. Supports both alpha-blend and additive pipeline variants based on material blend mode. Fixes invisible spell trail effects (~5-10%% of spell visuals) that were silently skipped.
This commit is contained in:
parent
022d387d95
commit
1108aa9ae6
9 changed files with 604 additions and 1 deletions
25
assets/shaders/m2_ribbon.frag.glsl
Normal file
25
assets/shaders/m2_ribbon.frag.glsl
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
// M2 ribbon emitter fragment shader.
|
||||||
|
// Samples the ribbon texture, multiplied by vertex color and alpha.
|
||||||
|
// Uses additive blending (pipeline-level) for magic/spell trails.
|
||||||
|
|
||||||
|
layout(set = 1, binding = 0) uniform sampler2D uTexture;
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 vColor;
|
||||||
|
layout(location = 1) in float vAlpha;
|
||||||
|
layout(location = 2) in vec2 vUV;
|
||||||
|
layout(location = 3) in float vFogFactor;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 tex = texture(uTexture, vUV);
|
||||||
|
// For additive ribbons alpha comes from texture luminance; multiply by vertex alpha.
|
||||||
|
float a = tex.a * vAlpha;
|
||||||
|
if (a < 0.01) discard;
|
||||||
|
vec3 rgb = tex.rgb * vColor;
|
||||||
|
// Ribbons fade slightly with fog (additive blend attenuated toward black = invisible in fog).
|
||||||
|
rgb *= vFogFactor;
|
||||||
|
outColor = vec4(rgb, a);
|
||||||
|
}
|
||||||
BIN
assets/shaders/m2_ribbon.frag.spv
Normal file
BIN
assets/shaders/m2_ribbon.frag.spv
Normal file
Binary file not shown.
43
assets/shaders/m2_ribbon.vert.glsl
Normal file
43
assets/shaders/m2_ribbon.vert.glsl
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
// M2 ribbon emitter vertex shader.
|
||||||
|
// Ribbon geometry is generated CPU-side as a triangle strip.
|
||||||
|
// Vertex format: pos(3) + color(3) + alpha(1) + uv(2) = 9 floats.
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform PerFrame {
|
||||||
|
mat4 view;
|
||||||
|
mat4 projection;
|
||||||
|
mat4 lightSpaceMatrix;
|
||||||
|
vec4 lightDir;
|
||||||
|
vec4 lightColor;
|
||||||
|
vec4 ambientColor;
|
||||||
|
vec4 viewPos;
|
||||||
|
vec4 fogColor;
|
||||||
|
vec4 fogParams;
|
||||||
|
vec4 shadowParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 aPos;
|
||||||
|
layout(location = 1) in vec3 aColor;
|
||||||
|
layout(location = 2) in float aAlpha;
|
||||||
|
layout(location = 3) in vec2 aUV;
|
||||||
|
|
||||||
|
layout(location = 0) out vec3 vColor;
|
||||||
|
layout(location = 1) out float vAlpha;
|
||||||
|
layout(location = 2) out vec2 vUV;
|
||||||
|
layout(location = 3) out float vFogFactor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 worldPos = vec4(aPos, 1.0);
|
||||||
|
vec4 viewPos4 = view * worldPos;
|
||||||
|
gl_Position = projection * viewPos4;
|
||||||
|
|
||||||
|
float dist = length(viewPos4.xyz);
|
||||||
|
float fogStart = fogParams.x;
|
||||||
|
float fogEnd = fogParams.y;
|
||||||
|
vFogFactor = clamp((fogEnd - dist) / max(fogEnd - fogStart, 0.001), 0.0, 1.0);
|
||||||
|
|
||||||
|
vColor = aColor;
|
||||||
|
vAlpha = aAlpha;
|
||||||
|
vUV = aUV;
|
||||||
|
}
|
||||||
BIN
assets/shaders/m2_ribbon.vert.spv
Normal file
BIN
assets/shaders/m2_ribbon.vert.spv
Normal file
Binary file not shown.
|
|
@ -165,6 +165,29 @@ struct M2ParticleEmitter {
|
||||||
bool enabled = true;
|
bool enabled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ribbon emitter definition parsed from M2 (WotLK format)
|
||||||
|
struct M2RibbonEmitter {
|
||||||
|
int32_t ribbonId = 0;
|
||||||
|
uint32_t bone = 0; // Bone that drives the ribbon spine
|
||||||
|
glm::vec3 position{0.0f}; // Offset from bone pivot
|
||||||
|
|
||||||
|
uint16_t textureIndex = 0; // First texture lookup index
|
||||||
|
uint16_t materialIndex = 0; // First material lookup index (blend mode)
|
||||||
|
|
||||||
|
// Animated tracks
|
||||||
|
M2AnimationTrack colorTrack; // RGB 0..1
|
||||||
|
M2AnimationTrack alphaTrack; // float 0..1 (stored as fixed16 on disk)
|
||||||
|
M2AnimationTrack heightAboveTrack; // Half-width above bone
|
||||||
|
M2AnimationTrack heightBelowTrack; // Half-width below bone
|
||||||
|
M2AnimationTrack visibilityTrack; // 0=hidden, 1=visible
|
||||||
|
|
||||||
|
float edgesPerSecond = 15.0f; // How many edge points are generated per second
|
||||||
|
float edgeLifetime = 0.5f; // Seconds before edges expire
|
||||||
|
float gravity = 0.0f; // Downward pull on edges per s²
|
||||||
|
uint16_t textureRows = 1;
|
||||||
|
uint16_t textureCols = 1;
|
||||||
|
};
|
||||||
|
|
||||||
// Complete M2 model structure
|
// Complete M2 model structure
|
||||||
struct M2Model {
|
struct M2Model {
|
||||||
// Model metadata
|
// Model metadata
|
||||||
|
|
@ -213,6 +236,9 @@ struct M2Model {
|
||||||
// Particle emitters
|
// Particle emitters
|
||||||
std::vector<M2ParticleEmitter> particleEmitters;
|
std::vector<M2ParticleEmitter> particleEmitters;
|
||||||
|
|
||||||
|
// Ribbon emitters
|
||||||
|
std::vector<M2RibbonEmitter> ribbonEmitters;
|
||||||
|
|
||||||
// Collision mesh (simplified geometry for physics)
|
// Collision mesh (simplified geometry for physics)
|
||||||
std::vector<glm::vec3> collisionVertices;
|
std::vector<glm::vec3> collisionVertices;
|
||||||
std::vector<uint16_t> collisionIndices; // 3 per triangle
|
std::vector<uint16_t> collisionIndices; // 3 per triangle
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
@ -130,6 +131,11 @@ struct M2ModelGPU {
|
||||||
std::vector<VkTexture*> particleTextures; // Resolved Vulkan textures per emitter
|
std::vector<VkTexture*> particleTextures; // Resolved Vulkan textures per emitter
|
||||||
std::vector<VkDescriptorSet> particleTexSets; // Pre-allocated descriptor sets per emitter (stable, avoids per-frame alloc)
|
std::vector<VkDescriptorSet> particleTexSets; // Pre-allocated descriptor sets per emitter (stable, avoids per-frame alloc)
|
||||||
|
|
||||||
|
// Ribbon emitter data (kept from M2Model)
|
||||||
|
std::vector<pipeline::M2RibbonEmitter> ribbonEmitters;
|
||||||
|
std::vector<VkTexture*> ribbonTextures; // Resolved texture per ribbon emitter
|
||||||
|
std::vector<VkDescriptorSet> ribbonTexSets; // Descriptor sets per ribbon emitter
|
||||||
|
|
||||||
// Texture transform data for UV animation
|
// Texture transform data for UV animation
|
||||||
std::vector<pipeline::M2TextureTransform> textureTransforms;
|
std::vector<pipeline::M2TextureTransform> textureTransforms;
|
||||||
std::vector<uint16_t> textureTransformLookup;
|
std::vector<uint16_t> textureTransformLookup;
|
||||||
|
|
@ -180,6 +186,19 @@ struct M2Instance {
|
||||||
std::vector<float> emitterAccumulators; // fractional particle counter per emitter
|
std::vector<float> emitterAccumulators; // fractional particle counter per emitter
|
||||||
std::vector<M2Particle> particles;
|
std::vector<M2Particle> particles;
|
||||||
|
|
||||||
|
// Ribbon emitter state
|
||||||
|
struct RibbonEdge {
|
||||||
|
glm::vec3 worldPos; // Spine world position when this edge was born
|
||||||
|
glm::vec3 color; // Interpolated color at birth
|
||||||
|
float alpha; // Interpolated alpha at birth
|
||||||
|
float heightAbove;// Half-width above spine
|
||||||
|
float heightBelow;// Half-width below spine
|
||||||
|
float age; // Seconds since spawned
|
||||||
|
};
|
||||||
|
// One deque of edges per ribbon emitter on this instance
|
||||||
|
std::vector<std::deque<RibbonEdge>> ribbonEdges;
|
||||||
|
std::vector<float> ribbonEdgeAccumulators; // fractional edge counter per emitter
|
||||||
|
|
||||||
// Cached model flags (set at creation to avoid per-frame hash lookups)
|
// Cached model flags (set at creation to avoid per-frame hash lookups)
|
||||||
bool cachedHasAnimation = false;
|
bool cachedHasAnimation = false;
|
||||||
bool cachedDisableAnimation = false;
|
bool cachedDisableAnimation = false;
|
||||||
|
|
@ -295,6 +314,11 @@ public:
|
||||||
*/
|
*/
|
||||||
void renderSmokeParticles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
|
void renderSmokeParticles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render M2 ribbon emitters (spell trails / wing effects)
|
||||||
|
*/
|
||||||
|
void renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
|
||||||
|
|
||||||
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
||||||
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
|
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
|
||||||
void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen);
|
void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen);
|
||||||
|
|
@ -374,6 +398,11 @@ private:
|
||||||
VkPipeline smokePipeline_ = VK_NULL_HANDLE; // Smoke particles
|
VkPipeline smokePipeline_ = VK_NULL_HANDLE; // Smoke particles
|
||||||
VkPipelineLayout smokePipelineLayout_ = VK_NULL_HANDLE;
|
VkPipelineLayout smokePipelineLayout_ = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Ribbon pipelines (additive + alpha-blend)
|
||||||
|
VkPipeline ribbonPipeline_ = VK_NULL_HANDLE; // Alpha-blend ribbons
|
||||||
|
VkPipeline ribbonAdditivePipeline_ = VK_NULL_HANDLE; // Additive ribbons
|
||||||
|
VkPipelineLayout ribbonPipelineLayout_ = VK_NULL_HANDLE;
|
||||||
|
|
||||||
// Descriptor set layouts
|
// Descriptor set layouts
|
||||||
VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; // set 1
|
VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; // set 1
|
||||||
VkDescriptorSetLayout boneSetLayout_ = VK_NULL_HANDLE; // set 2
|
VkDescriptorSetLayout boneSetLayout_ = VK_NULL_HANDLE; // set 2
|
||||||
|
|
@ -385,6 +414,12 @@ private:
|
||||||
static constexpr uint32_t MAX_MATERIAL_SETS = 8192;
|
static constexpr uint32_t MAX_MATERIAL_SETS = 8192;
|
||||||
static constexpr uint32_t MAX_BONE_SETS = 8192;
|
static constexpr uint32_t MAX_BONE_SETS = 8192;
|
||||||
|
|
||||||
|
// Dynamic ribbon vertex buffer (CPU-written triangle strip)
|
||||||
|
static constexpr size_t MAX_RIBBON_VERTS = 2048; // 9 floats each
|
||||||
|
::VkBuffer ribbonVB_ = VK_NULL_HANDLE;
|
||||||
|
VmaAllocation ribbonVBAlloc_ = VK_NULL_HANDLE;
|
||||||
|
void* ribbonVBMapped_ = nullptr;
|
||||||
|
|
||||||
// Dynamic particle buffers
|
// Dynamic particle buffers
|
||||||
::VkBuffer smokeVB_ = VK_NULL_HANDLE;
|
::VkBuffer smokeVB_ = VK_NULL_HANDLE;
|
||||||
VmaAllocation smokeVBAlloc_ = VK_NULL_HANDLE;
|
VmaAllocation smokeVBAlloc_ = VK_NULL_HANDLE;
|
||||||
|
|
@ -535,6 +570,7 @@ private:
|
||||||
glm::vec3 interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeRatio);
|
glm::vec3 interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeRatio);
|
||||||
void emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt);
|
void emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt);
|
||||||
void updateParticles(M2Instance& inst, float dt);
|
void updateParticles(M2Instance& inst, float dt);
|
||||||
|
void updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt);
|
||||||
|
|
||||||
// Helper to allocate descriptor sets
|
// Helper to allocate descriptor sets
|
||||||
VkDescriptorSet allocateMaterialSet();
|
VkDescriptorSet allocateMaterialSet();
|
||||||
|
|
|
||||||
|
|
@ -1258,6 +1258,125 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
||||||
} // end size check
|
} // end size check
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse ribbon emitters (WotLK only; vanilla format TBD).
|
||||||
|
// WotLK M2RibbonEmitter = 0xAC (172) bytes per entry.
|
||||||
|
static constexpr uint32_t RIBBON_SIZE_WOTLK = 0xAC;
|
||||||
|
if (header.nRibbonEmitters > 0 && header.ofsRibbonEmitters > 0 &&
|
||||||
|
header.nRibbonEmitters < 64 && header.version >= 264) {
|
||||||
|
|
||||||
|
if (static_cast<size_t>(header.ofsRibbonEmitters) +
|
||||||
|
static_cast<size_t>(header.nRibbonEmitters) * RIBBON_SIZE_WOTLK <= m2Data.size()) {
|
||||||
|
|
||||||
|
// Build sequence flags for parseAnimTrack
|
||||||
|
std::vector<uint32_t> ribSeqFlags;
|
||||||
|
ribSeqFlags.reserve(model.sequences.size());
|
||||||
|
for (const auto& seq : model.sequences) {
|
||||||
|
ribSeqFlags.push_back(seq.flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t ri = 0; ri < header.nRibbonEmitters; ri++) {
|
||||||
|
uint32_t base = header.ofsRibbonEmitters + ri * RIBBON_SIZE_WOTLK;
|
||||||
|
|
||||||
|
M2RibbonEmitter rib;
|
||||||
|
rib.ribbonId = readValue<int32_t>(m2Data, base + 0x00);
|
||||||
|
rib.bone = readValue<uint32_t>(m2Data, base + 0x04);
|
||||||
|
rib.position.x = readValue<float>(m2Data, base + 0x08);
|
||||||
|
rib.position.y = readValue<float>(m2Data, base + 0x0C);
|
||||||
|
rib.position.z = readValue<float>(m2Data, base + 0x10);
|
||||||
|
|
||||||
|
// textureIndices M2Array (0x14): count + offset → first element = texture lookup index
|
||||||
|
{
|
||||||
|
uint32_t nTex = readValue<uint32_t>(m2Data, base + 0x14);
|
||||||
|
uint32_t ofsTex = readValue<uint32_t>(m2Data, base + 0x18);
|
||||||
|
if (nTex > 0 && ofsTex + sizeof(uint16_t) <= m2Data.size()) {
|
||||||
|
rib.textureIndex = readValue<uint16_t>(m2Data, ofsTex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// materialIndices M2Array (0x1C): count + offset → first element = material index
|
||||||
|
{
|
||||||
|
uint32_t nMat = readValue<uint32_t>(m2Data, base + 0x1C);
|
||||||
|
uint32_t ofsMat = readValue<uint32_t>(m2Data, base + 0x20);
|
||||||
|
if (nMat > 0 && ofsMat + sizeof(uint16_t) <= m2Data.size()) {
|
||||||
|
rib.materialIndex = readValue<uint16_t>(m2Data, ofsMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorTrack M2TrackDisk at 0x24 (vec3 RGB 0..1)
|
||||||
|
if (base + 0x24 + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||||
|
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + 0x24);
|
||||||
|
parseAnimTrack(m2Data, disk, rib.colorTrack, TrackType::VEC3, ribSeqFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// alphaTrack M2TrackDisk at 0x38 (fixed16: int16/32767)
|
||||||
|
// Same nested-array layout as parseAnimTrack but keys are int16.
|
||||||
|
if (base + 0x38 + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||||
|
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + 0x38);
|
||||||
|
auto& track = rib.alphaTrack;
|
||||||
|
track.interpolationType = disk.interpolationType;
|
||||||
|
track.globalSequence = disk.globalSequence;
|
||||||
|
uint32_t nSeqs = disk.nTimestamps;
|
||||||
|
if (nSeqs > 0 && nSeqs <= 4096) {
|
||||||
|
track.sequences.resize(nSeqs);
|
||||||
|
for (uint32_t s = 0; s < nSeqs; s++) {
|
||||||
|
if (s < ribSeqFlags.size() && !(ribSeqFlags[s] & 0x20)) continue;
|
||||||
|
uint32_t tsHdr = disk.ofsTimestamps + s * 8;
|
||||||
|
uint32_t keyHdr = disk.ofsKeys + s * 8;
|
||||||
|
if (tsHdr + 8 > m2Data.size() || keyHdr + 8 > m2Data.size()) continue;
|
||||||
|
uint32_t tsCount = readValue<uint32_t>(m2Data, tsHdr);
|
||||||
|
uint32_t tsOfs = readValue<uint32_t>(m2Data, tsHdr + 4);
|
||||||
|
uint32_t kCount = readValue<uint32_t>(m2Data, keyHdr);
|
||||||
|
uint32_t kOfs = readValue<uint32_t>(m2Data, keyHdr + 4);
|
||||||
|
if (tsCount == 0 || kCount == 0) continue;
|
||||||
|
if (tsOfs + tsCount * 4 > m2Data.size()) continue;
|
||||||
|
if (kOfs + kCount * sizeof(int16_t) > m2Data.size()) continue;
|
||||||
|
track.sequences[s].timestamps = readArray<uint32_t>(m2Data, tsOfs, tsCount);
|
||||||
|
auto raw = readArray<int16_t>(m2Data, kOfs, kCount);
|
||||||
|
track.sequences[s].floatValues.reserve(raw.size());
|
||||||
|
for (auto v : raw) {
|
||||||
|
track.sequences[s].floatValues.push_back(
|
||||||
|
static_cast<float>(v) / 32767.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// heightAboveTrack M2TrackDisk at 0x4C (float)
|
||||||
|
if (base + 0x4C + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||||
|
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + 0x4C);
|
||||||
|
parseAnimTrack(m2Data, disk, rib.heightAboveTrack, TrackType::FLOAT, ribSeqFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// heightBelowTrack M2TrackDisk at 0x60 (float)
|
||||||
|
if (base + 0x60 + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||||
|
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + 0x60);
|
||||||
|
parseAnimTrack(m2Data, disk, rib.heightBelowTrack, TrackType::FLOAT, ribSeqFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
rib.edgesPerSecond = readValue<float>(m2Data, base + 0x74);
|
||||||
|
rib.edgeLifetime = readValue<float>(m2Data, base + 0x78);
|
||||||
|
rib.gravity = readValue<float>(m2Data, base + 0x7C);
|
||||||
|
rib.textureRows = readValue<uint16_t>(m2Data, base + 0x80);
|
||||||
|
rib.textureCols = readValue<uint16_t>(m2Data, base + 0x82);
|
||||||
|
if (rib.textureRows == 0) rib.textureRows = 1;
|
||||||
|
if (rib.textureCols == 0) rib.textureCols = 1;
|
||||||
|
|
||||||
|
// Clamp to sane values
|
||||||
|
if (rib.edgesPerSecond < 1.0f || rib.edgesPerSecond > 200.0f) rib.edgesPerSecond = 15.0f;
|
||||||
|
if (rib.edgeLifetime < 0.05f || rib.edgeLifetime > 10.0f) rib.edgeLifetime = 0.5f;
|
||||||
|
|
||||||
|
// visibilityTrack M2TrackDisk at 0x98 (uint8, treat as float 0/1)
|
||||||
|
if (base + 0x98 + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||||
|
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + 0x98);
|
||||||
|
parseAnimTrack(m2Data, disk, rib.visibilityTrack, TrackType::FLOAT, ribSeqFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.ribbonEmitters.push_back(std::move(rib));
|
||||||
|
}
|
||||||
|
core::Logger::getInstance().debug(" Ribbon emitters: ", model.ribbonEmitters.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read collision mesh (bounding triangles/vertices/normals)
|
// Read collision mesh (bounding triangles/vertices/normals)
|
||||||
if (header.nBoundingVertices > 0 && header.ofsBoundingVertices > 0) {
|
if (header.nBoundingVertices > 0 && header.ofsBoundingVertices > 0) {
|
||||||
struct Vec3Disk { float x, y, z; };
|
struct Vec3Disk { float x, y, z; };
|
||||||
|
|
|
||||||
|
|
@ -540,6 +540,54 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
||||||
.build(device);
|
.build(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Build ribbon pipelines ---
|
||||||
|
// Vertex format: pos(3) + color(3) + alpha(1) + uv(2) = 9 floats = 36 bytes
|
||||||
|
{
|
||||||
|
rendering::VkShaderModule ribVert, ribFrag;
|
||||||
|
ribVert.loadFromFile(device, "assets/shaders/m2_ribbon.vert.spv");
|
||||||
|
ribFrag.loadFromFile(device, "assets/shaders/m2_ribbon.frag.spv");
|
||||||
|
if (ribVert.isValid() && ribFrag.isValid()) {
|
||||||
|
// Reuse particleTexLayout_ for set 1 (single texture sampler)
|
||||||
|
VkDescriptorSetLayout ribLayouts[] = {perFrameLayout, particleTexLayout_};
|
||||||
|
VkPipelineLayoutCreateInfo lci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||||
|
lci.setLayoutCount = 2;
|
||||||
|
lci.pSetLayouts = ribLayouts;
|
||||||
|
vkCreatePipelineLayout(device, &lci, nullptr, &ribbonPipelineLayout_);
|
||||||
|
|
||||||
|
VkVertexInputBindingDescription rBind{};
|
||||||
|
rBind.binding = 0;
|
||||||
|
rBind.stride = 9 * sizeof(float);
|
||||||
|
rBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
|
|
||||||
|
std::vector<VkVertexInputAttributeDescription> rAttrs = {
|
||||||
|
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // pos
|
||||||
|
{1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * sizeof(float)}, // color
|
||||||
|
{2, 0, VK_FORMAT_R32_SFLOAT, 6 * sizeof(float)}, // alpha
|
||||||
|
{3, 0, VK_FORMAT_R32G32_SFLOAT, 7 * sizeof(float)}, // uv
|
||||||
|
};
|
||||||
|
|
||||||
|
auto buildRibbonPipeline = [&](VkPipelineColorBlendAttachmentState blend) -> VkPipeline {
|
||||||
|
return PipelineBuilder()
|
||||||
|
.setShaders(ribVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||||
|
ribFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||||
|
.setVertexInput({rBind}, rAttrs)
|
||||||
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)
|
||||||
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||||
|
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
|
||||||
|
.setColorBlendAttachment(blend)
|
||||||
|
.setMultisample(vkCtx_->getMsaaSamples())
|
||||||
|
.setLayout(ribbonPipelineLayout_)
|
||||||
|
.setRenderPass(mainPass)
|
||||||
|
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||||
|
.build(device);
|
||||||
|
};
|
||||||
|
|
||||||
|
ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha());
|
||||||
|
ribbonAdditivePipeline_ = buildRibbonPipeline(PipelineBuilder::blendAdditive());
|
||||||
|
}
|
||||||
|
ribVert.destroy(); ribFrag.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up shader modules
|
// Clean up shader modules
|
||||||
m2Vert.destroy(); m2Frag.destroy();
|
m2Vert.destroy(); m2Frag.destroy();
|
||||||
particleVert.destroy(); particleFrag.destroy();
|
particleVert.destroy(); particleFrag.destroy();
|
||||||
|
|
@ -570,6 +618,11 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
||||||
bci.size = MAX_GLOW_SPRITES * 9 * sizeof(float);
|
bci.size = MAX_GLOW_SPRITES * 9 * sizeof(float);
|
||||||
vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &glowVB_, &glowVBAlloc_, &allocInfo);
|
vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &glowVB_, &glowVBAlloc_, &allocInfo);
|
||||||
glowVBMapped_ = allocInfo.pMappedData;
|
glowVBMapped_ = allocInfo.pMappedData;
|
||||||
|
|
||||||
|
// Ribbon vertex buffer — triangle strip: pos(3)+color(3)+alpha(1)+uv(2)=9 floats/vert
|
||||||
|
bci.size = MAX_RIBBON_VERTS * 9 * sizeof(float);
|
||||||
|
vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &ribbonVB_, &ribbonVBAlloc_, &allocInfo);
|
||||||
|
ribbonVBMapped_ = allocInfo.pMappedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Create white fallback texture ---
|
// --- Create white fallback texture ---
|
||||||
|
|
@ -666,10 +719,11 @@ void M2Renderer::shutdown() {
|
||||||
whiteTexture_.reset();
|
whiteTexture_.reset();
|
||||||
glowTexture_.reset();
|
glowTexture_.reset();
|
||||||
|
|
||||||
// Clean up particle buffers
|
// Clean up particle/ribbon buffers
|
||||||
if (smokeVB_) { vmaDestroyBuffer(alloc, smokeVB_, smokeVBAlloc_); smokeVB_ = VK_NULL_HANDLE; }
|
if (smokeVB_) { vmaDestroyBuffer(alloc, smokeVB_, smokeVBAlloc_); smokeVB_ = VK_NULL_HANDLE; }
|
||||||
if (m2ParticleVB_) { vmaDestroyBuffer(alloc, m2ParticleVB_, m2ParticleVBAlloc_); m2ParticleVB_ = VK_NULL_HANDLE; }
|
if (m2ParticleVB_) { vmaDestroyBuffer(alloc, m2ParticleVB_, m2ParticleVBAlloc_); m2ParticleVB_ = VK_NULL_HANDLE; }
|
||||||
if (glowVB_) { vmaDestroyBuffer(alloc, glowVB_, glowVBAlloc_); glowVB_ = VK_NULL_HANDLE; }
|
if (glowVB_) { vmaDestroyBuffer(alloc, glowVB_, glowVBAlloc_); glowVB_ = VK_NULL_HANDLE; }
|
||||||
|
if (ribbonVB_) { vmaDestroyBuffer(alloc, ribbonVB_, ribbonVBAlloc_); ribbonVB_ = VK_NULL_HANDLE; }
|
||||||
smokeParticles.clear();
|
smokeParticles.clear();
|
||||||
|
|
||||||
// Destroy pipelines
|
// Destroy pipelines
|
||||||
|
|
@ -681,10 +735,13 @@ void M2Renderer::shutdown() {
|
||||||
destroyPipeline(particlePipeline_);
|
destroyPipeline(particlePipeline_);
|
||||||
destroyPipeline(particleAdditivePipeline_);
|
destroyPipeline(particleAdditivePipeline_);
|
||||||
destroyPipeline(smokePipeline_);
|
destroyPipeline(smokePipeline_);
|
||||||
|
destroyPipeline(ribbonPipeline_);
|
||||||
|
destroyPipeline(ribbonAdditivePipeline_);
|
||||||
|
|
||||||
if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; }
|
if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; }
|
||||||
if (particlePipelineLayout_) { vkDestroyPipelineLayout(device, particlePipelineLayout_, nullptr); particlePipelineLayout_ = VK_NULL_HANDLE; }
|
if (particlePipelineLayout_) { vkDestroyPipelineLayout(device, particlePipelineLayout_, nullptr); particlePipelineLayout_ = VK_NULL_HANDLE; }
|
||||||
if (smokePipelineLayout_) { vkDestroyPipelineLayout(device, smokePipelineLayout_, nullptr); smokePipelineLayout_ = VK_NULL_HANDLE; }
|
if (smokePipelineLayout_) { vkDestroyPipelineLayout(device, smokePipelineLayout_, nullptr); smokePipelineLayout_ = VK_NULL_HANDLE; }
|
||||||
|
if (ribbonPipelineLayout_) { vkDestroyPipelineLayout(device, ribbonPipelineLayout_, nullptr); ribbonPipelineLayout_ = VK_NULL_HANDLE; }
|
||||||
|
|
||||||
// Destroy descriptor pools and layouts
|
// Destroy descriptor pools and layouts
|
||||||
if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; }
|
if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; }
|
||||||
|
|
@ -719,6 +776,11 @@ void M2Renderer::destroyModelGPU(M2ModelGPU& model) {
|
||||||
if (pSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &pSet); pSet = VK_NULL_HANDLE; }
|
if (pSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &pSet); pSet = VK_NULL_HANDLE; }
|
||||||
}
|
}
|
||||||
model.particleTexSets.clear();
|
model.particleTexSets.clear();
|
||||||
|
// Free ribbon texture descriptor sets
|
||||||
|
for (auto& rSet : model.ribbonTexSets) {
|
||||||
|
if (rSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &rSet); rSet = VK_NULL_HANDLE; }
|
||||||
|
}
|
||||||
|
model.ribbonTexSets.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void M2Renderer::destroyInstanceBones(M2Instance& inst) {
|
void M2Renderer::destroyInstanceBones(M2Instance& inst) {
|
||||||
|
|
@ -1345,6 +1407,43 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy ribbon emitter data and resolve textures
|
||||||
|
gpuModel.ribbonEmitters = model.ribbonEmitters;
|
||||||
|
if (!model.ribbonEmitters.empty()) {
|
||||||
|
VkDevice device = vkCtx_->getDevice();
|
||||||
|
gpuModel.ribbonTextures.resize(model.ribbonEmitters.size(), whiteTexture_.get());
|
||||||
|
gpuModel.ribbonTexSets.resize(model.ribbonEmitters.size(), VK_NULL_HANDLE);
|
||||||
|
for (size_t ri = 0; ri < model.ribbonEmitters.size(); ri++) {
|
||||||
|
// Resolve texture via textureLookup table
|
||||||
|
uint16_t texLookupIdx = model.ribbonEmitters[ri].textureIndex;
|
||||||
|
uint32_t texIdx = (texLookupIdx < model.textureLookup.size())
|
||||||
|
? model.textureLookup[texLookupIdx] : UINT32_MAX;
|
||||||
|
if (texIdx < allTextures.size() && allTextures[texIdx] != nullptr) {
|
||||||
|
gpuModel.ribbonTextures[ri] = allTextures[texIdx];
|
||||||
|
}
|
||||||
|
// Allocate descriptor set (reuse particleTexLayout_ = single sampler)
|
||||||
|
if (particleTexLayout_ && materialDescPool_) {
|
||||||
|
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||||
|
ai.descriptorPool = materialDescPool_;
|
||||||
|
ai.descriptorSetCount = 1;
|
||||||
|
ai.pSetLayouts = &particleTexLayout_;
|
||||||
|
if (vkAllocateDescriptorSets(device, &ai, &gpuModel.ribbonTexSets[ri]) == VK_SUCCESS) {
|
||||||
|
VkTexture* tex = gpuModel.ribbonTextures[ri];
|
||||||
|
VkDescriptorImageInfo imgInfo = tex->descriptorInfo();
|
||||||
|
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||||
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
write.dstSet = gpuModel.ribbonTexSets[ri];
|
||||||
|
write.dstBinding = 0;
|
||||||
|
write.descriptorCount = 1;
|
||||||
|
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
write.pImageInfo = &imgInfo;
|
||||||
|
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_DEBUG(" Ribbon emitters loaded: ", model.ribbonEmitters.size());
|
||||||
|
}
|
||||||
|
|
||||||
// Copy texture transform data for UV animation
|
// Copy texture transform data for UV animation
|
||||||
gpuModel.textureTransforms = model.textureTransforms;
|
gpuModel.textureTransforms = model.textureTransforms;
|
||||||
gpuModel.textureTransformLookup = model.textureTransformLookup;
|
gpuModel.textureTransformLookup = model.textureTransformLookup;
|
||||||
|
|
@ -2241,6 +2340,9 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
if (!instance.cachedModel) continue;
|
if (!instance.cachedModel) continue;
|
||||||
emitParticles(instance, *instance.cachedModel, deltaTime);
|
emitParticles(instance, *instance.cachedModel, deltaTime);
|
||||||
updateParticles(instance, deltaTime);
|
updateParticles(instance, deltaTime);
|
||||||
|
if (!instance.cachedModel->ribbonEmitters.empty()) {
|
||||||
|
updateRibbons(instance, *instance.cachedModel, deltaTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -3375,6 +3477,214 @@ void M2Renderer::updateParticles(M2Instance& inst, float dt) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ribbon emitter simulation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void M2Renderer::updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt) {
|
||||||
|
const auto& emitters = gpu.ribbonEmitters;
|
||||||
|
if (emitters.empty()) return;
|
||||||
|
|
||||||
|
// Grow per-instance state arrays if needed
|
||||||
|
if (inst.ribbonEdges.size() != emitters.size()) {
|
||||||
|
inst.ribbonEdges.resize(emitters.size());
|
||||||
|
}
|
||||||
|
if (inst.ribbonEdgeAccumulators.size() != emitters.size()) {
|
||||||
|
inst.ribbonEdgeAccumulators.resize(emitters.size(), 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t ri = 0; ri < emitters.size(); ri++) {
|
||||||
|
const auto& em = emitters[ri];
|
||||||
|
auto& edges = inst.ribbonEdges[ri];
|
||||||
|
auto& accum = inst.ribbonEdgeAccumulators[ri];
|
||||||
|
|
||||||
|
// Determine bone world position for spine
|
||||||
|
glm::vec3 spineWorld = inst.position;
|
||||||
|
if (em.bone < inst.boneMatrices.size()) {
|
||||||
|
glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f);
|
||||||
|
spineWorld = glm::vec3(inst.modelMatrix * inst.boneMatrices[em.bone] * local);
|
||||||
|
} else {
|
||||||
|
glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f);
|
||||||
|
spineWorld = glm::vec3(inst.modelMatrix * local);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate animated tracks (use first available sequence key, or fallback value)
|
||||||
|
auto getFloatVal = [&](const pipeline::M2AnimationTrack& track, float fallback) -> float {
|
||||||
|
for (const auto& seq : track.sequences) {
|
||||||
|
if (!seq.floatValues.empty()) return seq.floatValues[0];
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
auto getVec3Val = [&](const pipeline::M2AnimationTrack& track, glm::vec3 fallback) -> glm::vec3 {
|
||||||
|
for (const auto& seq : track.sequences) {
|
||||||
|
if (!seq.vec3Values.empty()) return seq.vec3Values[0];
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
float visibility = getFloatVal(em.visibilityTrack, 1.0f);
|
||||||
|
float heightAbove = getFloatVal(em.heightAboveTrack, 0.5f);
|
||||||
|
float heightBelow = getFloatVal(em.heightBelowTrack, 0.5f);
|
||||||
|
glm::vec3 color = getVec3Val(em.colorTrack, glm::vec3(1.0f));
|
||||||
|
float alpha = getFloatVal(em.alphaTrack, 1.0f);
|
||||||
|
|
||||||
|
// Age existing edges and remove expired ones
|
||||||
|
for (auto& e : edges) {
|
||||||
|
e.age += dt;
|
||||||
|
// Apply gravity
|
||||||
|
if (em.gravity != 0.0f) {
|
||||||
|
e.worldPos.z -= em.gravity * dt * dt * 0.5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!edges.empty() && edges.front().age >= em.edgeLifetime) {
|
||||||
|
edges.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit new edges based on edgesPerSecond
|
||||||
|
if (visibility > 0.5f) {
|
||||||
|
accum += em.edgesPerSecond * dt;
|
||||||
|
while (accum >= 1.0f) {
|
||||||
|
accum -= 1.0f;
|
||||||
|
M2Instance::RibbonEdge e;
|
||||||
|
e.worldPos = spineWorld;
|
||||||
|
e.color = color;
|
||||||
|
e.alpha = alpha;
|
||||||
|
e.heightAbove = heightAbove;
|
||||||
|
e.heightBelow = heightBelow;
|
||||||
|
e.age = 0.0f;
|
||||||
|
edges.push_back(e);
|
||||||
|
// Cap trail length
|
||||||
|
if (edges.size() > 128) edges.pop_front();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
accum = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ribbon rendering
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void M2Renderer::renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
||||||
|
if (!ribbonPipeline_ || !ribbonVB_ || !ribbonVBMapped_) return;
|
||||||
|
|
||||||
|
// Build camera right vector for billboard orientation
|
||||||
|
// For ribbons we orient the quad strip along the spine with screen-space up.
|
||||||
|
// Simple approach: use world-space Z=up for the ribbon cross direction.
|
||||||
|
const glm::vec3 upWorld(0.0f, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
float* dst = static_cast<float*>(ribbonVBMapped_);
|
||||||
|
size_t written = 0;
|
||||||
|
|
||||||
|
struct DrawCall {
|
||||||
|
VkDescriptorSet texSet;
|
||||||
|
VkPipeline pipeline;
|
||||||
|
uint32_t firstVertex;
|
||||||
|
uint32_t vertexCount;
|
||||||
|
};
|
||||||
|
std::vector<DrawCall> draws;
|
||||||
|
|
||||||
|
for (const auto& inst : instances) {
|
||||||
|
if (!inst.cachedModel) continue;
|
||||||
|
const auto& gpu = *inst.cachedModel;
|
||||||
|
if (gpu.ribbonEmitters.empty()) continue;
|
||||||
|
|
||||||
|
for (size_t ri = 0; ri < gpu.ribbonEmitters.size(); ri++) {
|
||||||
|
if (ri >= inst.ribbonEdges.size()) continue;
|
||||||
|
const auto& edges = inst.ribbonEdges[ri];
|
||||||
|
if (edges.size() < 2) continue;
|
||||||
|
|
||||||
|
const auto& em = gpu.ribbonEmitters[ri];
|
||||||
|
|
||||||
|
// Select blend pipeline based on material blend mode
|
||||||
|
bool additive = false;
|
||||||
|
if (em.materialIndex < gpu.batches.size()) {
|
||||||
|
additive = (gpu.batches[em.materialIndex].blendMode >= 3);
|
||||||
|
}
|
||||||
|
VkPipeline pipe = additive ? ribbonAdditivePipeline_ : ribbonPipeline_;
|
||||||
|
|
||||||
|
// Descriptor set for texture
|
||||||
|
VkDescriptorSet texSet = (ri < gpu.ribbonTexSets.size())
|
||||||
|
? gpu.ribbonTexSets[ri] : VK_NULL_HANDLE;
|
||||||
|
if (!texSet) continue;
|
||||||
|
|
||||||
|
uint32_t firstVert = static_cast<uint32_t>(written);
|
||||||
|
|
||||||
|
// Emit triangle strip: 2 verts per edge (top + bottom)
|
||||||
|
for (size_t ei = 0; ei < edges.size(); ei++) {
|
||||||
|
if (written + 2 > MAX_RIBBON_VERTS) break;
|
||||||
|
const auto& e = edges[ei];
|
||||||
|
float t = (em.edgeLifetime > 0.0f)
|
||||||
|
? 1.0f - (e.age / em.edgeLifetime) : 1.0f;
|
||||||
|
float a = e.alpha * t;
|
||||||
|
float u = static_cast<float>(ei) / static_cast<float>(edges.size() - 1);
|
||||||
|
|
||||||
|
// Top vertex (above spine along upWorld)
|
||||||
|
glm::vec3 top = e.worldPos + upWorld * e.heightAbove;
|
||||||
|
dst[written * 9 + 0] = top.x;
|
||||||
|
dst[written * 9 + 1] = top.y;
|
||||||
|
dst[written * 9 + 2] = top.z;
|
||||||
|
dst[written * 9 + 3] = e.color.r;
|
||||||
|
dst[written * 9 + 4] = e.color.g;
|
||||||
|
dst[written * 9 + 5] = e.color.b;
|
||||||
|
dst[written * 9 + 6] = a;
|
||||||
|
dst[written * 9 + 7] = u;
|
||||||
|
dst[written * 9 + 8] = 0.0f; // v = top
|
||||||
|
written++;
|
||||||
|
|
||||||
|
// Bottom vertex (below spine)
|
||||||
|
glm::vec3 bot = e.worldPos - upWorld * e.heightBelow;
|
||||||
|
dst[written * 9 + 0] = bot.x;
|
||||||
|
dst[written * 9 + 1] = bot.y;
|
||||||
|
dst[written * 9 + 2] = bot.z;
|
||||||
|
dst[written * 9 + 3] = e.color.r;
|
||||||
|
dst[written * 9 + 4] = e.color.g;
|
||||||
|
dst[written * 9 + 5] = e.color.b;
|
||||||
|
dst[written * 9 + 6] = a;
|
||||||
|
dst[written * 9 + 7] = u;
|
||||||
|
dst[written * 9 + 8] = 1.0f; // v = bottom
|
||||||
|
written++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t vertCount = static_cast<uint32_t>(written) - firstVert;
|
||||||
|
if (vertCount >= 4) {
|
||||||
|
draws.push_back({texSet, pipe, firstVert, vertCount});
|
||||||
|
} else {
|
||||||
|
// Rollback if too few verts
|
||||||
|
written = firstVert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draws.empty() || written == 0) return;
|
||||||
|
|
||||||
|
VkExtent2D ext = vkCtx_->getSwapchainExtent();
|
||||||
|
VkViewport vp{};
|
||||||
|
vp.x = 0; vp.y = 0;
|
||||||
|
vp.width = static_cast<float>(ext.width);
|
||||||
|
vp.height = static_cast<float>(ext.height);
|
||||||
|
vp.minDepth = 0.0f; vp.maxDepth = 1.0f;
|
||||||
|
VkRect2D sc{};
|
||||||
|
sc.offset = {0, 0};
|
||||||
|
sc.extent = ext;
|
||||||
|
vkCmdSetViewport(cmd, 0, 1, &vp);
|
||||||
|
vkCmdSetScissor(cmd, 0, 1, &sc);
|
||||||
|
|
||||||
|
VkPipeline lastPipe = VK_NULL_HANDLE;
|
||||||
|
for (const auto& dc : draws) {
|
||||||
|
if (dc.pipeline != lastPipe) {
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, dc.pipeline);
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
ribbonPipelineLayout_, 0, 1, &perFrameSet, 0, nullptr);
|
||||||
|
lastPipe = dc.pipeline;
|
||||||
|
}
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
ribbonPipelineLayout_, 1, 1, &dc.texSet, 0, nullptr);
|
||||||
|
VkDeviceSize offset = 0;
|
||||||
|
vkCmdBindVertexBuffers(cmd, 0, 1, &ribbonVB_, &offset);
|
||||||
|
vkCmdDraw(cmd, dc.vertexCount, 1, dc.firstVertex, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
|
||||||
if (!particlePipeline_ || !m2ParticleVB_) return;
|
if (!particlePipeline_ || !m2ParticleVB_) return;
|
||||||
|
|
||||||
|
|
@ -4505,6 +4815,8 @@ void M2Renderer::recreatePipelines() {
|
||||||
if (particlePipeline_) { vkDestroyPipeline(device, particlePipeline_, nullptr); particlePipeline_ = VK_NULL_HANDLE; }
|
if (particlePipeline_) { vkDestroyPipeline(device, particlePipeline_, nullptr); particlePipeline_ = VK_NULL_HANDLE; }
|
||||||
if (particleAdditivePipeline_) { vkDestroyPipeline(device, particleAdditivePipeline_, nullptr); particleAdditivePipeline_ = VK_NULL_HANDLE; }
|
if (particleAdditivePipeline_) { vkDestroyPipeline(device, particleAdditivePipeline_, nullptr); particleAdditivePipeline_ = VK_NULL_HANDLE; }
|
||||||
if (smokePipeline_) { vkDestroyPipeline(device, smokePipeline_, nullptr); smokePipeline_ = VK_NULL_HANDLE; }
|
if (smokePipeline_) { vkDestroyPipeline(device, smokePipeline_, nullptr); smokePipeline_ = VK_NULL_HANDLE; }
|
||||||
|
if (ribbonPipeline_) { vkDestroyPipeline(device, ribbonPipeline_, nullptr); ribbonPipeline_ = VK_NULL_HANDLE; }
|
||||||
|
if (ribbonAdditivePipeline_) { vkDestroyPipeline(device, ribbonAdditivePipeline_, nullptr); ribbonAdditivePipeline_ = VK_NULL_HANDLE; }
|
||||||
|
|
||||||
// --- Load shaders ---
|
// --- Load shaders ---
|
||||||
rendering::VkShaderModule m2Vert, m2Frag;
|
rendering::VkShaderModule m2Vert, m2Frag;
|
||||||
|
|
@ -4624,6 +4936,46 @@ void M2Renderer::recreatePipelines() {
|
||||||
.build(device);
|
.build(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Ribbon pipelines ---
|
||||||
|
{
|
||||||
|
rendering::VkShaderModule ribVert, ribFrag;
|
||||||
|
ribVert.loadFromFile(device, "assets/shaders/m2_ribbon.vert.spv");
|
||||||
|
ribFrag.loadFromFile(device, "assets/shaders/m2_ribbon.frag.spv");
|
||||||
|
if (ribVert.isValid() && ribFrag.isValid()) {
|
||||||
|
VkVertexInputBindingDescription rBind{};
|
||||||
|
rBind.binding = 0;
|
||||||
|
rBind.stride = 9 * sizeof(float);
|
||||||
|
rBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
|
|
||||||
|
std::vector<VkVertexInputAttributeDescription> rAttrs = {
|
||||||
|
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0},
|
||||||
|
{1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * sizeof(float)},
|
||||||
|
{2, 0, VK_FORMAT_R32_SFLOAT, 6 * sizeof(float)},
|
||||||
|
{3, 0, VK_FORMAT_R32G32_SFLOAT, 7 * sizeof(float)},
|
||||||
|
};
|
||||||
|
|
||||||
|
auto buildRibbonPipeline = [&](VkPipelineColorBlendAttachmentState blend) -> VkPipeline {
|
||||||
|
return PipelineBuilder()
|
||||||
|
.setShaders(ribVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||||
|
ribFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||||
|
.setVertexInput({rBind}, rAttrs)
|
||||||
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)
|
||||||
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||||
|
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
|
||||||
|
.setColorBlendAttachment(blend)
|
||||||
|
.setMultisample(vkCtx_->getMsaaSamples())
|
||||||
|
.setLayout(ribbonPipelineLayout_)
|
||||||
|
.setRenderPass(mainPass)
|
||||||
|
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||||
|
.build(device);
|
||||||
|
};
|
||||||
|
|
||||||
|
ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha());
|
||||||
|
ribbonAdditivePipeline_ = buildRibbonPipeline(PipelineBuilder::blendAdditive());
|
||||||
|
}
|
||||||
|
ribVert.destroy(); ribFrag.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
m2Vert.destroy(); m2Frag.destroy();
|
m2Vert.destroy(); m2Frag.destroy();
|
||||||
particleVert.destroy(); particleFrag.destroy();
|
particleVert.destroy(); particleFrag.destroy();
|
||||||
smokeVert.destroy(); smokeFrag.destroy();
|
smokeVert.destroy(); smokeFrag.destroy();
|
||||||
|
|
|
||||||
|
|
@ -5159,6 +5159,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
m2Renderer->render(cmd, perFrameSet, *camera);
|
m2Renderer->render(cmd, perFrameSet, *camera);
|
||||||
m2Renderer->renderSmokeParticles(cmd, perFrameSet);
|
m2Renderer->renderSmokeParticles(cmd, perFrameSet);
|
||||||
m2Renderer->renderM2Particles(cmd, perFrameSet);
|
m2Renderer->renderM2Particles(cmd, perFrameSet);
|
||||||
|
m2Renderer->renderM2Ribbons(cmd, perFrameSet);
|
||||||
vkEndCommandBuffer(cmd);
|
vkEndCommandBuffer(cmd);
|
||||||
return std::chrono::duration<double, std::milli>(
|
return std::chrono::duration<double, std::milli>(
|
||||||
std::chrono::steady_clock::now() - t0).count();
|
std::chrono::steady_clock::now() - t0).count();
|
||||||
|
|
@ -5344,6 +5345,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
m2Renderer->render(currentCmd, perFrameSet, *camera);
|
m2Renderer->render(currentCmd, perFrameSet, *camera);
|
||||||
m2Renderer->renderSmokeParticles(currentCmd, perFrameSet);
|
m2Renderer->renderSmokeParticles(currentCmd, perFrameSet);
|
||||||
m2Renderer->renderM2Particles(currentCmd, perFrameSet);
|
m2Renderer->renderM2Particles(currentCmd, perFrameSet);
|
||||||
|
m2Renderer->renderM2Ribbons(currentCmd, perFrameSet);
|
||||||
lastM2RenderMs = std::chrono::duration<double, std::milli>(
|
lastM2RenderMs = std::chrono::duration<double, std::milli>(
|
||||||
std::chrono::steady_clock::now() - m2Start).count();
|
std::chrono::steady_clock::now() - m2Start).count();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue