diff --git a/binaries/data/mods/mod/shaders/glsl/particle.h b/binaries/data/mods/mod/shaders/glsl/particle.h index 21521ad1d1..2d6b376d61 100644 --- a/binaries/data/mods/mod/shaders/glsl/particle.h +++ b/binaries/data/mods/mod/shaders/glsl/particle.h @@ -9,6 +9,7 @@ END_DRAW_TEXTURES BEGIN_DRAW_UNIFORMS UNIFORM(mat4, modelViewMatrix) + UNIFORM(mat4, spaceTransform) END_DRAW_UNIFORMS BEGIN_MATERIAL_UNIFORMS @@ -17,6 +18,7 @@ BEGIN_MATERIAL_UNIFORMS UNIFORM(vec3, fogColor) UNIFORM(vec2, fogParams) UNIFORM(vec2, losTransform) + UNIFORM(vec3, cameraPos) END_MATERIAL_UNIFORMS VERTEX_OUTPUT(0, vec2, v_tex); diff --git a/binaries/data/mods/mod/shaders/glsl/particle.vs b/binaries/data/mods/mod/shaders/glsl/particle.vs index 481d93b546..b8d2291f39 100644 --- a/binaries/data/mods/mod/shaders/glsl/particle.vs +++ b/binaries/data/mods/mod/shaders/glsl/particle.vs @@ -9,14 +9,40 @@ VERTEX_INPUT_ATTRIBUTE(0, vec3, a_vertex); VERTEX_INPUT_ATTRIBUTE(1, vec4, a_color); VERTEX_INPUT_ATTRIBUTE(2, vec2, a_uv0); VERTEX_INPUT_ATTRIBUTE(3, vec2, a_uv1); +VERTEX_INPUT_ATTRIBUTE(4, vec3, a_axisX); +VERTEX_INPUT_ATTRIBUTE(5, vec3, a_axisY); void main() { - vec3 axis1 = vec3(modelViewMatrix[0][0], modelViewMatrix[1][0], modelViewMatrix[2][0]); - vec3 axis2 = vec3(modelViewMatrix[0][1], modelViewMatrix[1][1], modelViewMatrix[2][1]); + vec3 viewAxisX = vec3(modelViewMatrix[0][0], modelViewMatrix[1][0], modelViewMatrix[2][0]); + vec3 viewAxisY = vec3(modelViewMatrix[0][1], modelViewMatrix[1][1], modelViewMatrix[2][1]); vec2 offset = a_uv1; - vec3 position = axis1*offset.x + axis1*offset.y + axis2*offset.x + axis2*-offset.y + a_vertex; + vec3 axisX = (spaceTransform * vec4(a_axisX, 0.0)).xyz; + vec3 axisY = (spaceTransform * vec4(a_axisY, 0.0)).xyz; + vec3 particlePosition = (spaceTransform * vec4(a_vertex, 1.0)).xyz; + + vec3 particleAxisX = viewAxisX; + vec3 particleAxisY = viewAxisY; + if (a_axisX != vec3(0.0)) + { + particleAxisX = axisX; + if (a_axisY != vec3(0.0)) + particleAxisY = axisY; + else + { + vec3 particleDirection = particlePosition - cameraPos; + float particleDirectionLength = length(particleDirection); + if (particleDirectionLength != 0.0) + { + particleDirection *= 1.0 / particleDirectionLength; + if (abs(dot(axisX, particleDirection)) < 1.0) + particleAxisY = normalize(cross(particleDirection, axisX)); + } + } + } + + vec3 position = particleAxisX*offset.x + particleAxisX*offset.y + particleAxisY*offset.x + particleAxisY*-offset.y + particlePosition; OUTPUT_VERTEX_POSITION(transform * vec4(position, 1.0)); diff --git a/binaries/data/mods/mod/shaders/glsl/particle.xml b/binaries/data/mods/mod/shaders/glsl/particle.xml index 523a2db746..7cd239c3b5 100644 --- a/binaries/data/mods/mod/shaders/glsl/particle.xml +++ b/binaries/data/mods/mod/shaders/glsl/particle.xml @@ -6,6 +6,8 @@ + + diff --git a/binaries/data/mods/public/art/particles/particle.rng b/binaries/data/mods/public/art/particles/particle.rng index 01e4790949..1bbcae6eef 100644 --- a/binaries/data/mods/public/art/particles/particle.rng +++ b/binaries/data/mods/public/art/particles/particle.rng @@ -29,18 +29,65 @@ + + + + + + + + + + absolute + relative + velocity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/binaries/data/mods/public/art/particles/rain.xml b/binaries/data/mods/public/art/particles/rain.xml index 7cdce5115b..c139ec5189 100644 --- a/binaries/data/mods/public/art/particles/rain.xml +++ b/binaries/data/mods/public/art/particles/rain.xml @@ -12,4 +12,8 @@ + + + + diff --git a/source/graphics/ParticleEmitter.cpp b/source/graphics/ParticleEmitter.cpp index 0117c542d3..1be6d5f0e2 100644 --- a/source/graphics/ParticleEmitter.cpp +++ b/source/graphics/ParticleEmitter.cpp @@ -107,6 +107,12 @@ CParticleEmitter::CParticleEmitter(const CParticleEmitterTypePtr& type) : m_AttributeColor.format = Renderer::Backend::Format::R8G8B8A8_UNORM; m_VertexArray.AddAttribute(&m_AttributeColor); + m_AttributeAxisX.format = Renderer::Backend::Format::R32G32B32_SFLOAT; + m_VertexArray.AddAttribute(&m_AttributeAxisX); + + m_AttributeAxisY.format = Renderer::Backend::Format::R32G32B32_SFLOAT; + m_VertexArray.AddAttribute(&m_AttributeAxisY); + m_VertexArray.SetNumberOfVertices(m_Type->m_MaxParticles * 4); m_VertexArray.Layout(); @@ -126,7 +132,7 @@ CParticleEmitter::CParticleEmitter(const CParticleEmitterTypePtr& type) : m_IndexArray.FreeBackingStore(); const uint32_t stride = m_VertexArray.GetStride(); - const std::array attributes{{ + const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, m_AttributePos.format, m_AttributePos.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, @@ -139,6 +145,12 @@ CParticleEmitter::CParticleEmitter(const CParticleEmitterTypePtr& type) : {Renderer::Backend::VertexAttributeStream::UV1, m_AttributeAxis.format, m_AttributeAxis.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, + {Renderer::Backend::VertexAttributeStream::UV2, + m_AttributeAxisX.format, m_AttributeAxisX.offset, stride, + Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, + {Renderer::Backend::VertexAttributeStream::UV3, + m_AttributeAxisY.format, m_AttributeAxisY.offset, stride, + Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, }}; m_VertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); } @@ -165,6 +177,8 @@ void CParticleEmitter::UpdateArrayData(int frameNumber) VertexArrayIterator attrAxis = m_AttributeAxis.GetIterator(); VertexArrayIterator attrUV = m_AttributeUV.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); + VertexArrayIterator attrAxisX = m_AttributeAxisX.GetIterator(); + VertexArrayIterator attrAxisY = m_AttributeAxisY.GetIterator(); ENSURE(m_Particles.size() <= m_Type->m_MaxParticles); @@ -255,6 +269,16 @@ void CParticleEmitter::UpdateArrayData(int frameNumber) *attrColor++ = color; *attrColor++ = color; *attrColor++ = color; + + *attrAxisX++ = particle.axisX; + *attrAxisX++ = particle.axisX; + *attrAxisX++ = particle.axisX; + *attrAxisX++ = particle.axisX; + + *attrAxisY++ = particle.axisY; + *attrAxisY++ = particle.axisY; + *attrAxisY++ = particle.axisY; + *attrAxisY++ = particle.axisY; } m_ParticleBounds = bounds; diff --git a/source/graphics/ParticleEmitter.h b/source/graphics/ParticleEmitter.h index 8bc07c4c6c..1ab1e089cd 100644 --- a/source/graphics/ParticleEmitter.h +++ b/source/graphics/ParticleEmitter.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -52,6 +52,7 @@ struct SParticle SColor4ub color; float age; float maxAge; + CVector3D axisX, axisY; }; typedef std::shared_ptr CParticleEmitterPtr; @@ -187,6 +188,7 @@ private: VertexArray::Attribute m_AttributeAxis; VertexArray::Attribute m_AttributeUV; VertexArray::Attribute m_AttributeColor; + VertexArray::Attribute m_AttributeAxisX, m_AttributeAxisY; Renderer::Backend::IVertexInputLayout* m_VertexInputLayout = nullptr; diff --git a/source/graphics/ParticleEmitterType.cpp b/source/graphics/ParticleEmitterType.cpp index 3936975c05..033e54e3e8 100644 --- a/source/graphics/ParticleEmitterType.cpp +++ b/source/graphics/ParticleEmitterType.cpp @@ -368,6 +368,8 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path) m_BlendMode = BlendMode::ADD; m_SortMode = SortMode::UNSPECIFIED; m_StartFull = false; + m_UseLocalSpace = false; + m_UseRelativePosition = false; m_UseRelativeVelocity = false; m_Texture = g_Renderer.GetTextureManager().GetErrorTexture(); @@ -379,23 +381,27 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path) // Define all the elements and attributes used in the XML file #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) - EL(texture); EL(blend); - EL(start_full); - EL(use_relative_velocity); EL(constant); - EL(sort); - EL(uniform); EL(copy); EL(expr); EL(force); + EL(fixed_orientation); + EL(particle); + EL(sort); + EL(start_full); + EL(texture); + EL(uniform); + EL(use_local_space); + EL(use_relative_position); + EL(use_relative_velocity); + AT(from); + AT(max); + AT(min); AT(mode); + AT(mul); AT(name); AT(value); - AT(min); - AT(max); - AT(mul); - AT(from); AT(x); AT(y); AT(z); @@ -439,6 +445,14 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path) { m_StartFull = true; } + else if (Child.GetNodeName() == el_use_local_space) + { + m_UseLocalSpace = true; + } + else if (Child.GetNodeName() == el_use_relative_position) + { + m_UseRelativePosition = true; + } else if (Child.GetNodeName() == el_use_relative_velocity) { m_UseRelativeVelocity = true; @@ -490,6 +504,36 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path) float z = Child.GetAttributes().GetNamedItem(at_z).ToFloat(); m_Effectors.push_back(IParticleEffectorPtr(new CParticleEffectorForce(x, y, z))); } + else if (Child.GetNodeName() == el_particle) + { + XERO_ITER_EL(Child, particleChild) + { + if (particleChild.GetNodeName() == el_fixed_orientation) + { + CVector3D axis{ + particleChild.GetAttributes().GetNamedItem(at_x).ToFloat(), + particleChild.GetAttributes().GetNamedItem(at_y).ToFloat(), + particleChild.GetAttributes().GetNamedItem(at_z).ToFloat()}; + // The axis must be a non-zero vector else it's not valid. + const float axisLength{axis.Length()}; + if (axisLength > 0.0f) + axis *= 1.0f / axisLength; + else + axis = CVector3D{0.0f, 0.0f, 0.0f}; + if (particleChild.GetAttributes().GetNamedItem(at_name) == "axisX") + { + m_AxisX = axis; + m_UseRelativeAxisX = particleChild.GetAttributes().GetNamedItem(at_mode) == "relative"; + m_UseVelocityAsAxisX = particleChild.GetAttributes().GetNamedItem(at_mode) == "velocity"; + } + else if (particleChild.GetAttributes().GetNamedItem(at_name) == "axisY") + { + m_AxisY = axis; + m_UseRelativeAxisY = particleChild.GetAttributes().GetNamedItem(at_mode) == "relative"; + } + } + } + } } return true; @@ -533,6 +577,10 @@ void CParticleEmitterType::UpdateEmitterStep(CParticleEmitter& emitter, float dt // we'll immediately overwrite, so clamp it newParticles = std::min(newParticles, (int)m_MaxParticles); + const CMatrix3D rotationMatrix{emitter.GetRotation().ToMatrix()}; + const CVector3D axisX{!m_UseLocalSpace && m_UseRelativeAxisX ? rotationMatrix.Transform(m_AxisX) : m_AxisX}; + const CVector3D axisY{!m_UseLocalSpace && m_UseRelativeAxisY ? rotationMatrix.Transform(m_AxisY) : m_AxisY}; + for (int i = 0; i < newParticles; ++i) { // Compute new particle state based on variables @@ -541,22 +589,20 @@ void CParticleEmitterType::UpdateEmitterStep(CParticleEmitter& emitter, float dt particle.pos.X = m_Variables[VAR_POSITION_X]->Evaluate(emitter); particle.pos.Y = m_Variables[VAR_POSITION_Y]->Evaluate(emitter); particle.pos.Z = m_Variables[VAR_POSITION_Z]->Evaluate(emitter); - particle.pos += emitter.m_Pos; - if (m_UseRelativeVelocity) + particle.velocity.X = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter); + particle.velocity.Y = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter); + particle.velocity.Z = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter); + + if (!m_UseLocalSpace) { - float xVel = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter); - float yVel = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter); - float zVel = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter); - CVector3D EmitterAngle = emitter.GetRotation().ToMatrix().Transform(CVector3D(xVel,yVel,zVel)); - particle.velocity.X = EmitterAngle.X; - particle.velocity.Y = EmitterAngle.Y; - particle.velocity.Z = EmitterAngle.Z; - } else { - particle.velocity.X = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter); - particle.velocity.Y = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter); - particle.velocity.Z = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter); + if (m_UseRelativeVelocity) + particle.velocity = rotationMatrix.Transform(particle.velocity); + if (m_UseRelativePosition) + particle.pos = rotationMatrix.Transform(particle.pos); + particle.pos += emitter.m_Pos; } + particle.angle = m_Variables[VAR_ANGLE]->Evaluate(emitter); particle.angleSpeed = m_Variables[VAR_VELOCITY_ANGLE]->Evaluate(emitter); @@ -572,6 +618,16 @@ void CParticleEmitterType::UpdateEmitterStep(CParticleEmitter& emitter, float dt particle.age = 0.f; particle.maxAge = m_Variables[VAR_LIFETIME]->Evaluate(emitter); + particle.axisX = axisX; + particle.axisY = axisY; + + if (m_UseVelocityAsAxisX) + { + const float velocityLength{particle.velocity.Length()}; + if (velocityLength > 1e-3f) + particle.axisX = particle.velocity * (1.0f / velocityLength); + } + emitter.AddParticle(particle); } } @@ -590,6 +646,13 @@ void CParticleEmitterType::UpdateEmitterStep(CParticleEmitter& emitter, float dt p.age += dt; p.size += p.sizeGrowthRate * dt; + if (m_UseVelocityAsAxisX) + { + const float velocityLength{p.velocity.Length()}; + if (velocityLength > 1e-3f) + p.axisX = p.velocity * (1.0f / velocityLength); + } + // Make alpha fade in/out nicely // TODO: this should probably be done as a variable or something, // instead of hardcoding diff --git a/source/graphics/ParticleEmitterType.h b/source/graphics/ParticleEmitterType.h index bd1e91e67b..668b4eb2ed 100644 --- a/source/graphics/ParticleEmitterType.h +++ b/source/graphics/ParticleEmitterType.h @@ -117,7 +117,14 @@ private: BlendMode m_BlendMode{BlendMode::ADD}; SortMode m_SortMode{SortMode::UNSPECIFIED}; bool m_StartFull; - bool m_UseRelativeVelocity; + bool m_UseLocalSpace{false}; + bool m_UseRelativePosition{false}, m_UseRelativeVelocity{false}; + + // A non-zero vector in case of a fixed axis for the corresponding direction. + CVector3D m_AxisX{}, m_AxisY{}; + bool m_UseRelativeAxisX{false}, m_UseRelativeAxisY{false}; + + bool m_UseVelocityAsAxisX{false}; float m_MaxLifetime; u16 m_MaxParticles; diff --git a/source/ps/CStrInternStatic.h b/source/ps/CStrInternStatic.h index f54ee0d986..8c9548e189 100644 --- a/source/ps/CStrInternStatic.h +++ b/source/ps/CStrInternStatic.h @@ -163,6 +163,7 @@ X(skyBoxRot) X(skyCube) X(sky_simple) X(solid) +X(spaceTransform) X(sunColor) X(sunDir) X(terrain_base) diff --git a/source/renderer/ParticleRenderer.cpp b/source/renderer/ParticleRenderer.cpp index bee033acd7..d10b78c8c7 100644 --- a/source/renderer/ParticleRenderer.cpp +++ b/source/renderer/ParticleRenderer.cpp @@ -181,15 +181,33 @@ void ParticleRenderer::RenderParticles( deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = lastTech->GetShader(); - const CMatrix3D transform = - g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); - const CMatrix3D modelViewMatrix = - g_Renderer.GetSceneRenderer().GetViewCamera().GetOrientation().GetInverse(); + const CCamera& viewCamera{g_Renderer.GetSceneRenderer().GetViewCamera()}; + const CMatrix3D transform{viewCamera.GetViewProjection()}; + const CMatrix3D modelViewMatrix{viewCamera.GetOrientation().GetInverse()}; + deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_modelViewMatrix), modelViewMatrix.AsFloatArray()); + deviceCommandContext->SetUniform( + shader->GetBindingSlot(str_cameraPos), + viewCamera.GetOrientation().GetTranslation().AsFloatArray()); } + + + const CMatrix3D rotationMatrix{emitter->GetRotation().ToMatrix()}; + CMatrix3D spaceTransform; + if (emitter->m_Type->m_UseLocalSpace) + { + spaceTransform = rotationMatrix; + spaceTransform.Translate(emitter->GetPosition()); + } + else + spaceTransform.SetIdentity(); + + Renderer::Backend::IShaderProgram* shader = lastTech->GetShader(); + deviceCommandContext->SetUniform( + shader->GetBindingSlot(str_spaceTransform), spaceTransform.AsFloatArray()); emitter->Bind(deviceCommandContext, lastTech->GetShader()); emitter->RenderArray(deviceCommandContext); }