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;
|
||||
};
|
||||
|
||||
// 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
|
||||
struct M2Model {
|
||||
// Model metadata
|
||||
|
|
@ -213,6 +236,9 @@ struct M2Model {
|
|||
// Particle emitters
|
||||
std::vector<M2ParticleEmitter> particleEmitters;
|
||||
|
||||
// Ribbon emitters
|
||||
std::vector<M2RibbonEmitter> ribbonEmitters;
|
||||
|
||||
// Collision mesh (simplified geometry for physics)
|
||||
std::vector<glm::vec3> collisionVertices;
|
||||
std::vector<uint16_t> collisionIndices; // 3 per triangle
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
|
|
@ -130,6 +131,11 @@ struct M2ModelGPU {
|
|||
std::vector<VkTexture*> particleTextures; // Resolved Vulkan textures per emitter
|
||||
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
|
||||
std::vector<pipeline::M2TextureTransform> textureTransforms;
|
||||
std::vector<uint16_t> textureTransformLookup;
|
||||
|
|
@ -180,6 +186,19 @@ struct M2Instance {
|
|||
std::vector<float> emitterAccumulators; // fractional particle counter per emitter
|
||||
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)
|
||||
bool cachedHasAnimation = false;
|
||||
bool cachedDisableAnimation = false;
|
||||
|
|
@ -295,6 +314,11 @@ public:
|
|||
*/
|
||||
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 setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
|
||||
void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen);
|
||||
|
|
@ -374,6 +398,11 @@ private:
|
|||
VkPipeline smokePipeline_ = VK_NULL_HANDLE; // Smoke particles
|
||||
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
|
||||
VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; // set 1
|
||||
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_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
|
||||
::VkBuffer smokeVB_ = VK_NULL_HANDLE;
|
||||
VmaAllocation smokeVBAlloc_ = VK_NULL_HANDLE;
|
||||
|
|
@ -535,6 +570,7 @@ private:
|
|||
glm::vec3 interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeRatio);
|
||||
void emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt);
|
||||
void updateParticles(M2Instance& inst, float dt);
|
||||
void updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt);
|
||||
|
||||
// Helper to allocate descriptor sets
|
||||
VkDescriptorSet allocateMaterialSet();
|
||||
|
|
|
|||
|
|
@ -1258,6 +1258,125 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
} // 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)
|
||||
if (header.nBoundingVertices > 0 && header.ofsBoundingVertices > 0) {
|
||||
struct Vec3Disk { float x, y, z; };
|
||||
|
|
|
|||
|
|
@ -540,6 +540,54 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
|||
.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
|
||||
m2Vert.destroy(); m2Frag.destroy();
|
||||
particleVert.destroy(); particleFrag.destroy();
|
||||
|
|
@ -570,6 +618,11 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
|||
bci.size = MAX_GLOW_SPRITES * 9 * sizeof(float);
|
||||
vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &glowVB_, &glowVBAlloc_, &allocInfo);
|
||||
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 ---
|
||||
|
|
@ -666,10 +719,11 @@ void M2Renderer::shutdown() {
|
|||
whiteTexture_.reset();
|
||||
glowTexture_.reset();
|
||||
|
||||
// Clean up particle buffers
|
||||
// Clean up particle/ribbon buffers
|
||||
if (smokeVB_) { vmaDestroyBuffer(alloc, smokeVB_, smokeVBAlloc_); smokeVB_ = VK_NULL_HANDLE; }
|
||||
if (m2ParticleVB_) { vmaDestroyBuffer(alloc, m2ParticleVB_, m2ParticleVBAlloc_); m2ParticleVB_ = 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();
|
||||
|
||||
// Destroy pipelines
|
||||
|
|
@ -681,10 +735,13 @@ void M2Renderer::shutdown() {
|
|||
destroyPipeline(particlePipeline_);
|
||||
destroyPipeline(particleAdditivePipeline_);
|
||||
destroyPipeline(smokePipeline_);
|
||||
destroyPipeline(ribbonPipeline_);
|
||||
destroyPipeline(ribbonAdditivePipeline_);
|
||||
|
||||
if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; }
|
||||
if (particlePipelineLayout_) { vkDestroyPipelineLayout(device, particlePipelineLayout_, nullptr); particlePipelineLayout_ = 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
|
||||
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; }
|
||||
}
|
||||
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) {
|
||||
|
|
@ -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
|
||||
gpuModel.textureTransforms = model.textureTransforms;
|
||||
gpuModel.textureTransformLookup = model.textureTransformLookup;
|
||||
|
|
@ -2241,6 +2340,9 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
|||
if (!instance.cachedModel) continue;
|
||||
emitParticles(instance, *instance.cachedModel, 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) {
|
||||
if (!particlePipeline_ || !m2ParticleVB_) return;
|
||||
|
||||
|
|
@ -4505,6 +4815,8 @@ void M2Renderer::recreatePipelines() {
|
|||
if (particlePipeline_) { vkDestroyPipeline(device, particlePipeline_, nullptr); particlePipeline_ = VK_NULL_HANDLE; }
|
||||
if (particleAdditivePipeline_) { vkDestroyPipeline(device, particleAdditivePipeline_, nullptr); particleAdditivePipeline_ = 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 ---
|
||||
rendering::VkShaderModule m2Vert, m2Frag;
|
||||
|
|
@ -4624,6 +4936,46 @@ void M2Renderer::recreatePipelines() {
|
|||
.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();
|
||||
particleVert.destroy(); particleFrag.destroy();
|
||||
smokeVert.destroy(); smokeFrag.destroy();
|
||||
|
|
|
|||
|
|
@ -5159,6 +5159,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
m2Renderer->render(cmd, perFrameSet, *camera);
|
||||
m2Renderer->renderSmokeParticles(cmd, perFrameSet);
|
||||
m2Renderer->renderM2Particles(cmd, perFrameSet);
|
||||
m2Renderer->renderM2Ribbons(cmd, perFrameSet);
|
||||
vkEndCommandBuffer(cmd);
|
||||
return std::chrono::duration<double, std::milli>(
|
||||
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->renderSmokeParticles(currentCmd, perFrameSet);
|
||||
m2Renderer->renderM2Particles(currentCmd, perFrameSet);
|
||||
m2Renderer->renderM2Ribbons(currentCmd, perFrameSet);
|
||||
lastM2RenderMs = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - m2Start).count();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue