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);
}