mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add smoke particle emitters with ember sparks and enable 4x MSAA
Replace UV scroll workaround for chimney smoke with proper GL_POINTS particle system. Smoke particles rise, expand, drift, and fade over 4-7 seconds. One in eight particles spawns as a bright orange/red ember spark. Enable 4x multisample antialiasing for smoother edges on player models, fences, and foliage.
This commit is contained in:
parent
11a4958e84
commit
c9adcd3d96
4 changed files with 249 additions and 36 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
||||||
|
|
@ -93,6 +94,19 @@ struct M2Instance {
|
||||||
void updateModelMatrix();
|
void updateModelMatrix();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single smoke particle emitted from a chimney or similar M2 model
|
||||||
|
*/
|
||||||
|
struct SmokeParticle {
|
||||||
|
glm::vec3 position;
|
||||||
|
glm::vec3 velocity;
|
||||||
|
float life = 0.0f;
|
||||||
|
float maxLife = 3.0f;
|
||||||
|
float size = 1.0f;
|
||||||
|
float isSpark = 0.0f; // 0 = smoke, 1 = ember/spark
|
||||||
|
uint32_t instanceId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* M2 Model Renderer
|
* M2 Model Renderer
|
||||||
*
|
*
|
||||||
|
|
@ -144,6 +158,11 @@ public:
|
||||||
*/
|
*/
|
||||||
void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
|
void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render smoke particles (call after render())
|
||||||
|
*/
|
||||||
|
void renderSmokeParticles(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a specific instance by ID
|
* Remove a specific instance by ID
|
||||||
* @param instanceId Instance ID returned by createInstance()
|
* @param instanceId Instance ID returned by createInstance()
|
||||||
|
|
@ -258,6 +277,15 @@ private:
|
||||||
// Collision query profiling (per frame).
|
// Collision query profiling (per frame).
|
||||||
mutable double queryTimeMs = 0.0;
|
mutable double queryTimeMs = 0.0;
|
||||||
mutable uint32_t queryCallCount = 0;
|
mutable uint32_t queryCallCount = 0;
|
||||||
|
|
||||||
|
// Smoke particle system
|
||||||
|
std::vector<SmokeParticle> smokeParticles;
|
||||||
|
GLuint smokeVAO = 0;
|
||||||
|
GLuint smokeVBO = 0;
|
||||||
|
std::unique_ptr<Shader> smokeShader;
|
||||||
|
static constexpr int MAX_SMOKE_PARTICLES = 1000;
|
||||||
|
float smokeEmitAccum = 0.0f;
|
||||||
|
std::mt19937 smokeRng{42};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ bool Window::initialize() {
|
||||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4);
|
||||||
|
|
||||||
// Create window
|
// Create window
|
||||||
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
|
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
|
||||||
|
|
@ -82,6 +84,7 @@ bool Window::initialize() {
|
||||||
LOG_INFO("Vendor: ", glGetString(GL_VENDOR));
|
LOG_INFO("Vendor: ", glGetString(GL_VENDOR));
|
||||||
|
|
||||||
// Set up OpenGL defaults
|
// Set up OpenGL defaults
|
||||||
|
glEnable(GL_MULTISAMPLE);
|
||||||
glEnable(GL_DEPTH_TEST);
|
glEnable(GL_DEPTH_TEST);
|
||||||
glDepthFunc(GL_LESS);
|
glDepthFunc(GL_LESS);
|
||||||
glEnable(GL_CULL_FACE);
|
glEnable(GL_CULL_FACE);
|
||||||
|
|
|
||||||
|
|
@ -218,8 +218,6 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
uniform mat4 uProjection;
|
uniform mat4 uProjection;
|
||||||
uniform bool uUseBones;
|
uniform bool uUseBones;
|
||||||
uniform mat4 uBones[128];
|
uniform mat4 uBones[128];
|
||||||
uniform float uScrollSpeed; // >0 for smoke UV scroll, 0 for normal
|
|
||||||
|
|
||||||
out vec3 FragPos;
|
out vec3 FragPos;
|
||||||
out vec3 Normal;
|
out vec3 Normal;
|
||||||
out vec2 TexCoord;
|
out vec2 TexCoord;
|
||||||
|
|
@ -241,9 +239,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
vec4 worldPos = uModel * vec4(pos, 1.0);
|
vec4 worldPos = uModel * vec4(pos, 1.0);
|
||||||
FragPos = worldPos.xyz;
|
FragPos = worldPos.xyz;
|
||||||
Normal = mat3(uModel) * norm;
|
Normal = mat3(uModel) * norm;
|
||||||
|
TexCoord = aTexCoord;
|
||||||
// Scroll UV for rising smoke effect (scroll both axes for diagonal drift)
|
|
||||||
TexCoord = vec2(aTexCoord.x - uScrollSpeed, aTexCoord.y - uScrollSpeed * 0.3);
|
|
||||||
|
|
||||||
gl_Position = uProjection * uView * worldPos;
|
gl_Position = uProjection * uView * worldPos;
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +257,6 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
uniform bool uHasTexture;
|
uniform bool uHasTexture;
|
||||||
uniform bool uAlphaTest;
|
uniform bool uAlphaTest;
|
||||||
uniform float uFadeAlpha;
|
uniform float uFadeAlpha;
|
||||||
uniform float uScrollSpeed; // >0 for smoke
|
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
|
@ -273,19 +268,13 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
texColor = vec4(0.6, 0.5, 0.4, 1.0); // Fallback brownish
|
texColor = vec4(0.6, 0.5, 0.4, 1.0); // Fallback brownish
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSmoke = (uScrollSpeed > 0.0);
|
// Alpha test for leaves, fences, etc.
|
||||||
|
if (uAlphaTest && texColor.a < 0.5) {
|
||||||
// Alpha test for leaves, fences, etc. (skip for smoke)
|
|
||||||
if (uAlphaTest && !isSmoke && texColor.a < 0.5) {
|
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distance fade - discard nearly invisible fragments
|
// Distance fade - discard nearly invisible fragments
|
||||||
float finalAlpha = texColor.a * uFadeAlpha;
|
float finalAlpha = texColor.a * uFadeAlpha;
|
||||||
if (isSmoke) {
|
|
||||||
// Very soft alpha so the 4-sided box mesh blends into a smooth plume
|
|
||||||
finalAlpha *= 0.25;
|
|
||||||
}
|
|
||||||
if (finalAlpha < 0.02) {
|
if (finalAlpha < 0.02) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|
@ -300,10 +289,6 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
vec3 diffuse = diff * texColor.rgb;
|
vec3 diffuse = diff * texColor.rgb;
|
||||||
|
|
||||||
vec3 result = ambient + diffuse;
|
vec3 result = ambient + diffuse;
|
||||||
if (isSmoke) {
|
|
||||||
// Lighten smoke color to look like wispy gray smoke
|
|
||||||
result = mix(result, vec3(0.7, 0.7, 0.72), 0.5);
|
|
||||||
}
|
|
||||||
FragColor = vec4(result, finalAlpha);
|
FragColor = vec4(result, finalAlpha);
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
@ -314,6 +299,91 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create smoke particle shader
|
||||||
|
const char* smokeVertSrc = R"(
|
||||||
|
#version 330 core
|
||||||
|
layout (location = 0) in vec3 aPos;
|
||||||
|
layout (location = 1) in float aLifeRatio;
|
||||||
|
layout (location = 2) in float aSize;
|
||||||
|
layout (location = 3) in float aIsSpark;
|
||||||
|
|
||||||
|
uniform mat4 uView;
|
||||||
|
uniform mat4 uProjection;
|
||||||
|
uniform float uScreenHeight;
|
||||||
|
|
||||||
|
out float vLifeRatio;
|
||||||
|
out float vIsSpark;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 viewPos = uView * vec4(aPos, 1.0);
|
||||||
|
gl_Position = uProjection * viewPos;
|
||||||
|
float dist = -viewPos.z;
|
||||||
|
float scale = (aIsSpark > 0.5) ? 0.12 : 0.3;
|
||||||
|
gl_PointSize = clamp(aSize * (uScreenHeight * scale) / max(dist, 1.0), 2.0, 200.0);
|
||||||
|
vLifeRatio = aLifeRatio;
|
||||||
|
vIsSpark = aIsSpark;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* smokeFragSrc = R"(
|
||||||
|
#version 330 core
|
||||||
|
in float vLifeRatio;
|
||||||
|
in float vIsSpark;
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
||||||
|
float dist = length(coord) * 2.0;
|
||||||
|
|
||||||
|
if (vIsSpark > 0.5) {
|
||||||
|
// Ember/spark: bright hot dot, fades quickly
|
||||||
|
float circle = 1.0 - smoothstep(0.3, 0.8, dist);
|
||||||
|
float fade = 1.0 - smoothstep(0.0, 1.0, vLifeRatio);
|
||||||
|
float alpha = circle * fade;
|
||||||
|
vec3 color = mix(vec3(1.0, 0.6, 0.1), vec3(1.0, 0.2, 0.0), vLifeRatio);
|
||||||
|
FragColor = vec4(color, alpha);
|
||||||
|
} else {
|
||||||
|
// Smoke: soft gray circle
|
||||||
|
float circle = 1.0 - smoothstep(0.5, 1.0, dist);
|
||||||
|
float fadeIn = smoothstep(0.0, 0.1, vLifeRatio);
|
||||||
|
float fadeOut = 1.0 - smoothstep(0.4, 1.0, vLifeRatio);
|
||||||
|
float alpha = circle * fadeIn * fadeOut * 0.5;
|
||||||
|
vec3 color = mix(vec3(0.5, 0.5, 0.53), vec3(0.65, 0.65, 0.68), vLifeRatio);
|
||||||
|
FragColor = vec4(color, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
smokeShader = std::make_unique<Shader>();
|
||||||
|
if (!smokeShader->loadFromSource(smokeVertSrc, smokeFragSrc)) {
|
||||||
|
LOG_ERROR("Failed to create smoke particle shader (non-fatal)");
|
||||||
|
smokeShader.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create smoke particle VAO/VBO (only if shader compiled)
|
||||||
|
if (smokeShader) {
|
||||||
|
glGenVertexArrays(1, &smokeVAO);
|
||||||
|
glGenBuffers(1, &smokeVBO);
|
||||||
|
glBindVertexArray(smokeVAO);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, smokeVBO);
|
||||||
|
// 5 floats per particle: pos(3) + lifeRatio(1) + size(1)
|
||||||
|
// 6 floats per particle: pos(3) + lifeRatio(1) + size(1) + isSpark(1)
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, MAX_SMOKE_PARTICLES * 6 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
// Position
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
|
||||||
|
// Life ratio
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||||
|
// Size
|
||||||
|
glEnableVertexAttribArray(2);
|
||||||
|
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(4 * sizeof(float)));
|
||||||
|
// IsSpark
|
||||||
|
glEnableVertexAttribArray(3);
|
||||||
|
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(5 * sizeof(float)));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Create white fallback texture
|
// Create white fallback texture
|
||||||
uint8_t white[] = {255, 255, 255, 255};
|
uint8_t white[] = {255, 255, 255, 255};
|
||||||
glGenTextures(1, &whiteTexture);
|
glGenTextures(1, &whiteTexture);
|
||||||
|
|
@ -354,6 +424,12 @@ void M2Renderer::shutdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
shader.reset();
|
shader.reset();
|
||||||
|
|
||||||
|
// Clean up smoke particle resources
|
||||||
|
if (smokeVAO != 0) { glDeleteVertexArrays(1, &smokeVAO); smokeVAO = 0; }
|
||||||
|
if (smokeVBO != 0) { glDeleteBuffers(1, &smokeVBO); smokeVBO = 0; }
|
||||||
|
smokeShader.reset();
|
||||||
|
smokeParticles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
|
|
@ -850,6 +926,71 @@ static void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance) {
|
||||||
|
|
||||||
void M2Renderer::update(float deltaTime) {
|
void M2Renderer::update(float deltaTime) {
|
||||||
float dtMs = deltaTime * 1000.0f;
|
float dtMs = deltaTime * 1000.0f;
|
||||||
|
|
||||||
|
// --- Smoke particle spawning ---
|
||||||
|
std::uniform_real_distribution<float> distXY(-0.4f, 0.4f);
|
||||||
|
std::uniform_real_distribution<float> distVelXY(-0.3f, 0.3f);
|
||||||
|
std::uniform_real_distribution<float> distVelZ(3.0f, 5.0f);
|
||||||
|
std::uniform_real_distribution<float> distLife(4.0f, 7.0f);
|
||||||
|
std::uniform_real_distribution<float> distDrift(-0.2f, 0.2f);
|
||||||
|
|
||||||
|
smokeEmitAccum += deltaTime;
|
||||||
|
float emitInterval = 1.0f / 8.0f; // 8 particles per second per emitter
|
||||||
|
|
||||||
|
for (auto& instance : instances) {
|
||||||
|
auto it = models.find(instance.modelId);
|
||||||
|
if (it == models.end()) continue;
|
||||||
|
const M2ModelGPU& model = it->second;
|
||||||
|
|
||||||
|
if (model.isSmoke && smokeEmitAccum >= emitInterval &&
|
||||||
|
static_cast<int>(smokeParticles.size()) < MAX_SMOKE_PARTICLES) {
|
||||||
|
// Emission point: model origin in world space (model matrix already positions at chimney)
|
||||||
|
glm::vec3 emitWorld = glm::vec3(instance.modelMatrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
|
||||||
|
|
||||||
|
// Occasionally spawn a spark instead of smoke (~1 in 8)
|
||||||
|
bool spark = (smokeRng() % 8 == 0);
|
||||||
|
|
||||||
|
SmokeParticle p;
|
||||||
|
p.position = emitWorld + glm::vec3(distXY(smokeRng), distXY(smokeRng), 0.0f);
|
||||||
|
if (spark) {
|
||||||
|
p.velocity = glm::vec3(distVelXY(smokeRng) * 2.0f, distVelXY(smokeRng) * 2.0f, distVelZ(smokeRng) * 1.5f);
|
||||||
|
p.maxLife = 0.8f + static_cast<float>(smokeRng() % 100) / 100.0f * 1.2f; // 0.8-2.0s
|
||||||
|
p.size = 0.5f;
|
||||||
|
p.isSpark = 1.0f;
|
||||||
|
} else {
|
||||||
|
p.velocity = glm::vec3(distVelXY(smokeRng), distVelXY(smokeRng), distVelZ(smokeRng));
|
||||||
|
p.maxLife = distLife(smokeRng);
|
||||||
|
p.size = 1.0f;
|
||||||
|
p.isSpark = 0.0f;
|
||||||
|
}
|
||||||
|
p.life = 0.0f;
|
||||||
|
p.instanceId = instance.id;
|
||||||
|
smokeParticles.push_back(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smokeEmitAccum >= emitInterval) {
|
||||||
|
smokeEmitAccum = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update existing smoke particles ---
|
||||||
|
for (auto it = smokeParticles.begin(); it != smokeParticles.end(); ) {
|
||||||
|
it->life += deltaTime;
|
||||||
|
if (it->life >= it->maxLife) {
|
||||||
|
it = smokeParticles.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
it->position += it->velocity * deltaTime;
|
||||||
|
it->velocity.z *= 0.98f; // Slight deceleration
|
||||||
|
it->velocity.x += distDrift(smokeRng) * deltaTime;
|
||||||
|
it->velocity.y += distDrift(smokeRng) * deltaTime;
|
||||||
|
// Grow from 1.0 to 3.5 over lifetime
|
||||||
|
float t = it->life / it->maxLife;
|
||||||
|
it->size = 1.0f + t * 2.5f;
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Normal M2 animation update ---
|
||||||
for (auto& instance : instances) {
|
for (auto& instance : instances) {
|
||||||
auto it = models.find(instance.modelId);
|
auto it = models.find(instance.modelId);
|
||||||
if (it == models.end()) continue;
|
if (it == models.end()) continue;
|
||||||
|
|
@ -955,6 +1096,9 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
const M2ModelGPU& model = it->second;
|
const M2ModelGPU& model = it->second;
|
||||||
if (!model.isValid()) continue;
|
if (!model.isValid()) continue;
|
||||||
|
|
||||||
|
// Skip smoke models — replaced by particle emitters
|
||||||
|
if (model.isSmoke) continue;
|
||||||
|
|
||||||
// Distance culling for small objects (scaled by object size)
|
// Distance culling for small objects (scaled by object size)
|
||||||
glm::vec3 toCam = instance.position - camPos;
|
glm::vec3 toCam = instance.position - camPos;
|
||||||
float distSq = glm::dot(toCam, toCam);
|
float distSq = glm::dot(toCam, toCam);
|
||||||
|
|
@ -988,11 +1132,6 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
shader->setUniform("uModel", instance.modelMatrix);
|
shader->setUniform("uModel", instance.modelMatrix);
|
||||||
shader->setUniform("uFadeAlpha", fadeAlpha);
|
shader->setUniform("uFadeAlpha", fadeAlpha);
|
||||||
|
|
||||||
// UV scroll for smoke models: pass pre-computed scroll offset
|
|
||||||
bool isSmoke = model.isSmoke;
|
|
||||||
float scrollSpeed = isSmoke ? (instance.animTime / 1000.0f * 0.15f) : 0.0f;
|
|
||||||
shader->setUniform("uScrollSpeed", scrollSpeed);
|
|
||||||
|
|
||||||
// Upload bone matrices if model has skeletal animation
|
// Upload bone matrices if model has skeletal animation
|
||||||
bool useBones = model.hasAnimation && !instance.boneMatrices.empty();
|
bool useBones = model.hasAnimation && !instance.boneMatrices.empty();
|
||||||
shader->setUniform("uUseBones", useBones);
|
shader->setUniform("uUseBones", useBones);
|
||||||
|
|
@ -1001,16 +1140,11 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
shader->setUniformMatrixArray("uBones[0]", instance.boneMatrices.data(), numBones);
|
shader->setUniformMatrixArray("uBones[0]", instance.boneMatrices.data(), numBones);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable depth writes for fading objects and smoke to avoid z-fighting
|
// Disable depth writes for fading objects to avoid z-fighting
|
||||||
if (fadeAlpha < 1.0f || isSmoke) {
|
if (fadeAlpha < 1.0f) {
|
||||||
glDepthMask(GL_FALSE);
|
glDepthMask(GL_FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additive blending for smoke
|
|
||||||
if (isSmoke) {
|
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindVertexArray(model.vao);
|
glBindVertexArray(model.vao);
|
||||||
|
|
||||||
for (const auto& batch : model.batches) {
|
for (const auto& batch : model.batches) {
|
||||||
|
|
@ -1034,12 +1168,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
// Restore blending mode after smoke
|
if (fadeAlpha < 1.0f) {
|
||||||
if (isSmoke) {
|
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fadeAlpha < 1.0f || isSmoke) {
|
|
||||||
glDepthMask(GL_TRUE);
|
glDepthMask(GL_TRUE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1049,6 +1178,56 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
||||||
glEnable(GL_CULL_FACE);
|
glEnable(GL_CULL_FACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void M2Renderer::renderSmokeParticles(const Camera& /*camera*/, const glm::mat4& view, const glm::mat4& projection) {
|
||||||
|
if (smokeParticles.empty() || !smokeShader || smokeVAO == 0) return;
|
||||||
|
|
||||||
|
// Build vertex data: pos(3) + lifeRatio(1) + size(1) + isSpark(1) per particle
|
||||||
|
std::vector<float> data;
|
||||||
|
data.reserve(smokeParticles.size() * 6);
|
||||||
|
for (const auto& p : smokeParticles) {
|
||||||
|
data.push_back(p.position.x);
|
||||||
|
data.push_back(p.position.y);
|
||||||
|
data.push_back(p.position.z);
|
||||||
|
data.push_back(p.life / p.maxLife);
|
||||||
|
data.push_back(p.size);
|
||||||
|
data.push_back(p.isSpark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload to VBO
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, smokeVBO);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, data.size() * sizeof(float), data.data());
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
// Set GL state
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
glEnable(GL_DEPTH_TEST); // Occlude behind buildings
|
||||||
|
glDepthMask(GL_FALSE);
|
||||||
|
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
|
||||||
|
smokeShader->use();
|
||||||
|
smokeShader->setUniform("uView", view);
|
||||||
|
smokeShader->setUniform("uProjection", projection);
|
||||||
|
|
||||||
|
// Get viewport height for point size scaling
|
||||||
|
GLint viewport[4];
|
||||||
|
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||||
|
smokeShader->setUniform("uScreenHeight", static_cast<float>(viewport[3]));
|
||||||
|
|
||||||
|
glBindVertexArray(smokeVAO);
|
||||||
|
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(smokeParticles.size()));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
// Restore state
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glDisable(GL_PROGRAM_POINT_SIZE);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glEnable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
|
|
||||||
void M2Renderer::removeInstance(uint32_t instanceId) {
|
void M2Renderer::removeInstance(uint32_t instanceId) {
|
||||||
for (auto it = instances.begin(); it != instances.end(); ++it) {
|
for (auto it = instances.begin(); it != instances.end(); ++it) {
|
||||||
if (it->id == instanceId) {
|
if (it->id == instanceId) {
|
||||||
|
|
@ -1069,6 +1248,8 @@ void M2Renderer::clear() {
|
||||||
instances.clear();
|
instances.clear();
|
||||||
spatialGrid.clear();
|
spatialGrid.clear();
|
||||||
instanceIndexById.clear();
|
instanceIndexById.clear();
|
||||||
|
smokeParticles.clear();
|
||||||
|
smokeEmitAccum = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void M2Renderer::setCollisionFocus(const glm::vec3& worldPos, float radius) {
|
void M2Renderer::setCollisionFocus(const glm::vec3& worldPos, float radius) {
|
||||||
|
|
|
||||||
|
|
@ -995,6 +995,7 @@ void Renderer::renderWorld(game::World* world) {
|
||||||
if (m2Renderer && camera) {
|
if (m2Renderer && camera) {
|
||||||
auto m2Start = std::chrono::steady_clock::now();
|
auto m2Start = std::chrono::steady_clock::now();
|
||||||
m2Renderer->render(*camera, view, projection);
|
m2Renderer->render(*camera, view, projection);
|
||||||
|
m2Renderer->renderSmokeParticles(*camera, view, projection);
|
||||||
auto m2End = std::chrono::steady_clock::now();
|
auto m2End = std::chrono::steady_clock::now();
|
||||||
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
lastM2RenderMs = std::chrono::duration<double, std::milli>(m2End - m2Start).count();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue