Phase O Task 4: extract the WB mesh pipeline (ObjectMeshManager + 7 support files) from references/WorldBuilder into src/AcDream.App/Rendering/Wb/ and bridge dat I/O through our DatCollection via a thin DatCollectionAdapter. O-D7 adapter path taken: ObjectMeshManager has 26 _dats.X call sites (threshold 20), so a DatCollectionAdapter : IDatReaderWriter is introduced rather than refactoring ObjectMeshManager's internal dat access directly. Files added (verbatim copies, namespace-only changes): - ObjectMeshManager.cs — mesh pipeline hub; IDatReaderWriter field satisfied by adapter - GlobalMeshBuffer.cs — single global VAO/VBO/IBO manager - EdgeLineBuilder.cs — wireframe edge geometry from CellStruct polygons - ModernRenderData.cs — ModernBatchData + LandblockMdiCommand structs - TextureAtlasManager.cs — texture array grouping by (Width, Height, Format) - ParticleBatcher.cs — GPU particle batching; T4 interim uses BaseObjectRenderManager static fields from Chorizite.OpenGLSDLBackend.Lib (stays until T7) - ParticleEmitterRenderer.cs — per-emitter particle lifecycle + rendering - ActiveParticleEmitter.cs — wrapper holding renderer + part index + local offset - DatCollectionAdapter.cs — NEW: bridges DatCollection → IDatReaderWriter; implements ResolveId() via DatDatabase.TypeFromId + Tree.TryGetFile in HighRes→Portal→Language→Cell order matching DefaultDatReaderWriter; DatDatabaseWrapper wraps DatDatabase as IDatDatabase WbMeshAdapter.cs changes (T4 Step 6): - _graphicsDevice switched from Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice to extracted AcDream.App.Rendering.Wb.OpenGLGraphicsDevice - ParticleBatcher = new ParticleBatcher(_graphicsDevice) restored (T3 had null! placeholder) - ObjectMeshManager now constructed with new DatCollectionAdapter(dats) instead of _wbDats - _wbDats field + its construction + disposal + [indoor-upload] NULL_RESULT diagnostic block left intact — T7 cleanup removes these once WorldBuilder project ref is dropped EmbeddedResourceReader.cs: replaced assembly manifest lookup (wrong prefix for our assembly) with disk-based lookup mapping "Shaders.Particle.vert" → Rendering/Shaders/wb_particle.vert; consistent with all other acdream shaders. wb_particle.vert / wb_particle.frag: WB particle shaders copied verbatim with wb_ prefix to distinguish from acdream's own particle.vert. OpenGLGraphicsDevice.cs: ParticleBatcher property type updated to extracted ParticleBatcher; setter changed from private to internal so WbMeshAdapter (same assembly) can assign post-ctor. Build: green (0 errors, 0 warnings in AcDream.App). Tests: 1147+8 baseline maintained (8 pre-existing failures unchanged). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
495 lines
21 KiB
C#
495 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using Chorizite.Core.Lib;
|
|
using Chorizite.Core.Render;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Rendering.Wb {
|
|
public class ParticleEmitterRenderer : IDisposable {
|
|
private const float EPSILON = 0.0002f;
|
|
|
|
private readonly OpenGLGraphicsDevice _graphicsDevice;
|
|
private readonly ObjectMeshManager _meshManager;
|
|
private readonly ParticleEmitter _emitter;
|
|
private readonly List<Particle> _particles = new();
|
|
private readonly Random _random = new();
|
|
|
|
private ObjectRenderData? _gfxRenderData;
|
|
private ObjectRenderData? _textureRenderData;
|
|
private bool _isPointSprite;
|
|
private Quaternion _planeRotation = Quaternion.Identity;
|
|
private float _emissionTimer;
|
|
private int _totalEmitted;
|
|
private float _timeRunning;
|
|
private float _deadTimer;
|
|
|
|
public bool IsActive => true; // Previews always loop
|
|
|
|
public Matrix4x4 ParentTransform { get; set; } = Matrix4x4.Identity;
|
|
public Matrix4x4 LocalOffset { get; set; } = Matrix4x4.Identity;
|
|
|
|
struct Particle {
|
|
public Vector3 WorldOffset;
|
|
public Vector3 WorldA;
|
|
public Vector3 WorldB;
|
|
public Vector3 WorldC;
|
|
public float Lifetime;
|
|
public float MaxLifetime;
|
|
public float FinalStartScale;
|
|
public float FinalFinalScale;
|
|
public float FinalStartTrans;
|
|
public float FinalFinalTrans;
|
|
public bool IsActive;
|
|
public Vector3 EmissionOrigin;
|
|
public Quaternion Orientation;
|
|
|
|
public Vector3 CalculatedPosition;
|
|
public float DistanceToCameraSq;
|
|
}
|
|
|
|
public ParticleEmitterRenderer(OpenGLGraphicsDevice graphicsDevice, ObjectMeshManager meshManager, ParticleEmitter emitter) {
|
|
_graphicsDevice = graphicsDevice;
|
|
_meshManager = meshManager;
|
|
_emitter = emitter;
|
|
|
|
if (emitter.HwGfxObjId.DataId != 0) {
|
|
_meshManager.IncrementRefCount(emitter.HwGfxObjId.DataId);
|
|
}
|
|
if (emitter.GfxObjId.DataId != 0 && emitter.GfxObjId.DataId != emitter.HwGfxObjId.DataId) {
|
|
_meshManager.IncrementRefCount(emitter.GfxObjId.DataId);
|
|
}
|
|
}
|
|
|
|
public void Update(float deltaTime) {
|
|
// Make sure textures are loaded
|
|
if (_gfxRenderData == null) {
|
|
var gfxId = _emitter.HwGfxObjId.DataId != 0 ? _emitter.HwGfxObjId.DataId : _emitter.GfxObjId.DataId;
|
|
if (gfxId != 0) {
|
|
_gfxRenderData = _meshManager.TryGetRenderData(gfxId);
|
|
}
|
|
}
|
|
if (_textureRenderData == null && _emitter.GfxObjId.DataId != 0) {
|
|
_textureRenderData = _meshManager.TryGetRenderData(_emitter.GfxObjId.DataId);
|
|
}
|
|
|
|
_isPointSprite = _gfxRenderData == null;
|
|
if (_gfxRenderData != null) {
|
|
var degradeId = _gfxRenderData.DIDDegrade;
|
|
if (degradeId != 0) {
|
|
if (_meshManager.Dats.Portal.TryGet<GfxObjDegradeInfo>(degradeId, out var degrades) && degrades.Degrades.Count > 0) {
|
|
_isPointSprite = degrades.Degrades[0].DegradeMode == 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isPersistent = _emitter.TotalParticles == 0 && _emitter.TotalSeconds == 0;
|
|
bool isPersistentStill = isPersistent && _emitter.ParticleType == ParticleType.Still;
|
|
|
|
// 1. Update existing particles and kill immediately if expired
|
|
for (int i = _particles.Count - 1; i >= 0; i--) {
|
|
var p = _particles[i];
|
|
|
|
if (isPersistentStill) {
|
|
p.Lifetime = 0;
|
|
}
|
|
else {
|
|
p.Lifetime += deltaTime;
|
|
}
|
|
|
|
if (!isPersistentStill && p.Lifetime >= p.MaxLifetime) {
|
|
_particles.RemoveAt(i);
|
|
continue;
|
|
}
|
|
|
|
p.CalculatedPosition = CalculatePosition(ref p);
|
|
_particles[i] = p;
|
|
}
|
|
|
|
_timeRunning += deltaTime;
|
|
|
|
// 2. Emission check
|
|
bool canEmit = (isPersistent || _timeRunning < _emitter.TotalSeconds) &&
|
|
(_emitter.TotalParticles == 0 || _totalEmitted < _emitter.TotalParticles);
|
|
|
|
if (!canEmit && _particles.Count == 0) {
|
|
_deadTimer += deltaTime;
|
|
if (_deadTimer >= 1.0f) {
|
|
_timeRunning = 0;
|
|
_totalEmitted = 0;
|
|
_emissionTimer = 0;
|
|
_deadTimer = 0f;
|
|
canEmit = true;
|
|
}
|
|
} else {
|
|
_deadTimer = 0f;
|
|
}
|
|
|
|
if (canEmit) {
|
|
if (_totalEmitted == 0 && _emitter.InitialParticles > 0) {
|
|
for (int i = 0; i < _emitter.InitialParticles; i++) {
|
|
if (_particles.Count < _emitter.MaxParticles) {
|
|
Emit();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_emitter.EmitterType == EmitterType.BirthratePerSec || _emitter.EmitterType == EmitterType.Unknown) {
|
|
_emissionTimer += deltaTime;
|
|
float interval = (float)_emitter.Birthrate;
|
|
if (interval <= 0.001f) {
|
|
while (_particles.Count < Math.Max(1, _emitter.MaxParticles)) {
|
|
if (_emitter.TotalParticles > 0 && _totalEmitted >= _emitter.TotalParticles) break;
|
|
Emit();
|
|
}
|
|
} else {
|
|
while (_emissionTimer >= interval) {
|
|
if (_emitter.TotalParticles > 0 && _totalEmitted >= _emitter.TotalParticles) break;
|
|
|
|
if (_particles.Count < _emitter.MaxParticles) {
|
|
Emit();
|
|
_emissionTimer -= interval;
|
|
}
|
|
else {
|
|
// Cap timer debt if we're full
|
|
_emissionTimer = interval;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Emit() {
|
|
var p = new Particle();
|
|
p.Lifetime = 0;
|
|
p.MaxLifetime = GetRandomLifespan();
|
|
if (p.MaxLifetime < 0.001f) p.MaxLifetime = 0.001f;
|
|
|
|
var localRandomOffset = GetRandomOffset();
|
|
var localA = GetRandomA();
|
|
var localB = GetRandomB();
|
|
var localC = GetRandomC();
|
|
|
|
var startFrame = LocalOffset * ParentTransform;
|
|
p.EmissionOrigin = startFrame.Translation;
|
|
|
|
p.WorldOffset = Vector3.Transform(localRandomOffset, startFrame) - p.EmissionOrigin;
|
|
|
|
// AC Client Logic for vector spaces (Particle::Init):
|
|
p.WorldA = localA;
|
|
p.WorldB = localB;
|
|
p.WorldC = localC;
|
|
|
|
switch (_emitter.ParticleType) {
|
|
case ParticleType.LocalVelocity: // 2
|
|
case ParticleType.ParabolicLVGA: // 3
|
|
p.WorldA = Vector3.TransformNormal(localA, startFrame);
|
|
break;
|
|
|
|
case ParticleType.ParabolicLVLA: // 8
|
|
p.WorldA = Vector3.TransformNormal(localA, startFrame);
|
|
p.WorldB = Vector3.TransformNormal(localB, startFrame);
|
|
break;
|
|
|
|
case ParticleType.ParabolicLVGAGR: // 4
|
|
p.WorldA = Vector3.TransformNormal(localA, startFrame);
|
|
p.WorldC = localC;
|
|
break;
|
|
|
|
case ParticleType.Swarm: // 5
|
|
p.WorldA = Vector3.TransformNormal(localA, startFrame);
|
|
break;
|
|
|
|
case ParticleType.Explode: // 6
|
|
// Type 6 (Explode) A and B are global
|
|
p.WorldA = localA;
|
|
p.WorldB = localB;
|
|
|
|
// Special WorldC initialization for Explode
|
|
float randA = (float)(_random.NextDouble() * 2.0 * Math.PI - Math.PI);
|
|
float randB = (float)(_random.NextDouble() * 2.0 * Math.PI - Math.PI);
|
|
float cosB = (float)Math.Cos(randB);
|
|
|
|
p.WorldC = new Vector3(
|
|
(float)(Math.Cos(randA) * localC.X * cosB),
|
|
(float)(Math.Sin(randA) * localC.Y * cosB),
|
|
(float)(Math.Sin(randB) * localC.Z)
|
|
);
|
|
if (NormalizeCheckSmall(ref p.WorldC)) p.WorldC = Vector3.Zero;
|
|
break;
|
|
|
|
case ParticleType.Implode: // 7
|
|
p.WorldOffset *= localC.X;
|
|
p.WorldC = p.WorldOffset;
|
|
break;
|
|
|
|
case ParticleType.ParabolicLVLALR: // 9
|
|
p.WorldA = Vector3.TransformNormal(localA, startFrame);
|
|
p.WorldC = Vector3.TransformNormal(localC, startFrame);
|
|
break;
|
|
|
|
case ParticleType.ParabolicGVGAGR: // 11
|
|
p.WorldC = localC;
|
|
break;
|
|
}
|
|
|
|
p.FinalStartScale = Math.Clamp(_emitter.StartScale + (float)(_random.NextDouble() * 2.0 - 1.0) * _emitter.ScaleRand, 0.1f, 10.0f);
|
|
p.FinalFinalScale = Math.Clamp(_emitter.FinalScale + (float)(_random.NextDouble() * 2.0 - 1.0) * _emitter.ScaleRand, 0.1f, 10.0f);
|
|
p.FinalStartTrans = Math.Clamp(_emitter.StartTrans + (float)(_random.NextDouble() * 2.0 - 1.0) * _emitter.TransRand, 0.0f, 1.0f);
|
|
p.FinalFinalTrans = Math.Clamp(_emitter.FinalTrans + (float)(_random.NextDouble() * 2.0 - 1.0) * _emitter.TransRand, 0.0f, 1.0f);
|
|
|
|
p.IsActive = true;
|
|
p.Orientation = Quaternion.CreateFromRotationMatrix(startFrame);
|
|
|
|
p.CalculatedPosition = CalculatePosition(ref p);
|
|
|
|
_particles.Add(p);
|
|
_totalEmitted++;
|
|
}
|
|
|
|
private float GetRandomLifespan() {
|
|
var result = (_random.NextDouble() * 2.0 - 1.0) * _emitter.LifespanRand + _emitter.Lifespan;
|
|
return (float)Math.Max(0.0, result);
|
|
}
|
|
|
|
private Vector3 GetRandomOffset() {
|
|
var rng = new Vector3(
|
|
(float)(_random.NextDouble() * 2.0 - 1.0),
|
|
(float)(_random.NextDouble() * 2.0 - 1.0),
|
|
(float)(_random.NextDouble() * 2.0 - 1.0)
|
|
);
|
|
|
|
var offsetDir = _emitter.OffsetDir;
|
|
var dot = Vector3.Dot(offsetDir, rng);
|
|
var randomAngle = rng - offsetDir * dot;
|
|
|
|
if (NormalizeCheckSmall(ref randomAngle))
|
|
return Vector3.Zero;
|
|
|
|
var magnitude = (float)(_random.NextDouble() * (_emitter.MaxOffset - _emitter.MinOffset) + _emitter.MinOffset);
|
|
return randomAngle * magnitude;
|
|
}
|
|
|
|
private Vector3 GetRandomA() {
|
|
var magnitude = (_emitter.MaxA - _emitter.MinA) * _random.NextDouble() + _emitter.MinA;
|
|
return _emitter.A * (float)magnitude;
|
|
}
|
|
|
|
private Vector3 GetRandomB() {
|
|
var magnitude = (_emitter.MaxB - _emitter.MinB) * _random.NextDouble() + _emitter.MinB;
|
|
return _emitter.B * (float)magnitude;
|
|
}
|
|
|
|
private Vector3 GetRandomC() {
|
|
var magnitude = (_emitter.MaxC - _emitter.MinC) * _random.NextDouble() + _emitter.MinC;
|
|
return _emitter.C * (float)magnitude;
|
|
}
|
|
|
|
private bool NormalizeCheckSmall(ref Vector3 v) {
|
|
var dist = v.Length();
|
|
if (dist < EPSILON)
|
|
return true;
|
|
|
|
v *= 1.0f / dist;
|
|
return false;
|
|
}
|
|
|
|
private Vector3 CalculatePosition(ref Particle p) {
|
|
float t = p.Lifetime;
|
|
Vector3 parentOrigin = _emitter.IsParentLocal ? (LocalOffset * ParentTransform).Translation : p.EmissionOrigin;
|
|
|
|
switch (_emitter.ParticleType) {
|
|
case ParticleType.Still:
|
|
return parentOrigin + p.WorldOffset;
|
|
|
|
case ParticleType.LocalVelocity:
|
|
case ParticleType.GlobalVelocity:
|
|
return parentOrigin + p.WorldOffset + (t * p.WorldA);
|
|
|
|
case ParticleType.ParabolicLVGA:
|
|
case ParticleType.ParabolicLVLA:
|
|
case ParticleType.ParabolicGVGA:
|
|
return parentOrigin + p.WorldOffset + (t * p.WorldA) + (0.5f * t * t * p.WorldB);
|
|
|
|
case ParticleType.ParabolicLVGAGR:
|
|
case ParticleType.ParabolicLVLALR:
|
|
case ParticleType.ParabolicGVGAGR:
|
|
return parentOrigin + p.WorldOffset + (t * p.WorldA) + (0.5f * t * t * p.WorldB);
|
|
|
|
case ParticleType.Swarm:
|
|
var swarmOrigin = parentOrigin + p.WorldOffset + (t * p.WorldA);
|
|
return new Vector3(
|
|
(float)Math.Cos(t * p.WorldB.X) * p.WorldC.X + swarmOrigin.X,
|
|
(float)Math.Sin(t * p.WorldB.Y) * p.WorldC.Y + swarmOrigin.Y,
|
|
(float)Math.Cos(t * p.WorldB.Z) * p.WorldC.Z + swarmOrigin.Z
|
|
);
|
|
|
|
case ParticleType.Explode:
|
|
return new Vector3(
|
|
(t * p.WorldB.X + p.WorldC.X * p.WorldA.X) * t + p.WorldOffset.X + parentOrigin.X,
|
|
(t * p.WorldB.Y + p.WorldC.Y * p.WorldA.X) * t + p.WorldOffset.Y + parentOrigin.Y,
|
|
(t * p.WorldB.Z + p.WorldC.Z * p.WorldA.X + p.WorldA.Z) * t + p.WorldOffset.Z + parentOrigin.Z
|
|
);
|
|
|
|
case ParticleType.Implode:
|
|
return ((float)Math.Cos(p.WorldA.X * t) * p.WorldC) + (t * t * p.WorldB) + parentOrigin + p.WorldOffset;
|
|
|
|
default:
|
|
return parentOrigin + p.WorldOffset + (t * p.WorldA);
|
|
}
|
|
}
|
|
|
|
|
|
public unsafe void Render(ParticleBatcher batcher) {
|
|
if (_particles.Count == 0) return;
|
|
|
|
// Decide which data to use for texturing.
|
|
// ACViewer uses HwGfxObjId for both geometry and texture.
|
|
var textureData = _gfxRenderData ?? _textureRenderData;
|
|
|
|
var cameraPos = _graphicsDevice.CurrentSceneData.CameraPosition;
|
|
|
|
// ACViewer PointSprite logic:
|
|
// Effective scale is 0.9 * BoundingBox size (1.8 * 0.5 in ACViewer shader)
|
|
// For DrawGfxObj, it uses actual scale.
|
|
float baseScale = _isPointSprite ? 0.9f : 1.0f;
|
|
Vector2 particleSize = new Vector2(1.0f, 1.0f);
|
|
Vector3 localCenter = Vector3.Zero;
|
|
_planeRotation = Quaternion.Identity;
|
|
if (_gfxRenderData != null) {
|
|
var size = _gfxRenderData.BoundingBox.Max - _gfxRenderData.BoundingBox.Min;
|
|
localCenter = (_gfxRenderData.BoundingBox.Max + _gfxRenderData.BoundingBox.Min) / 2.0f;
|
|
|
|
if (!_isPointSprite) {
|
|
if (size.Y > size.X && size.Y > size.Z) {
|
|
// Primarily in XY plane (if X is also large) or YZ plane (if Z is also large)
|
|
if (size.X > size.Z) {
|
|
// XY plane: Map shader X->X, Z->Y
|
|
particleSize.X = size.X;
|
|
particleSize.Y = size.Y;
|
|
_planeRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitX, -MathF.PI / 2.0f);
|
|
} else {
|
|
// YZ plane: Map shader X->Y, Z->Z
|
|
particleSize.X = size.Y;
|
|
particleSize.Y = size.Z;
|
|
_planeRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, MathF.PI / 2.0f);
|
|
}
|
|
} else if (size.X > size.Y && size.X > size.Z) {
|
|
// Primarily in XZ plane (normal Y) or XY plane (normal Z)
|
|
if (size.Z > size.Y) {
|
|
// XZ plane: Already matches shader
|
|
particleSize.X = size.X;
|
|
particleSize.Y = size.Z;
|
|
_planeRotation = Quaternion.Identity;
|
|
} else {
|
|
// XY plane: Map shader X->X, Z->Y
|
|
particleSize.X = size.X;
|
|
particleSize.Y = size.Y;
|
|
_planeRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitX, -MathF.PI / 2.0f);
|
|
}
|
|
} else {
|
|
// Primarily in XZ or YZ
|
|
if (size.X > size.Y) {
|
|
// XZ plane
|
|
particleSize.X = size.X;
|
|
particleSize.Y = size.Z;
|
|
_planeRotation = Quaternion.Identity;
|
|
} else {
|
|
// YZ plane: Map shader X->Y, Z->Z
|
|
particleSize.X = size.Y;
|
|
particleSize.Y = size.Z;
|
|
_planeRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, MathF.PI / 2.0f);
|
|
}
|
|
}
|
|
} else {
|
|
// Point sprite always uses XZ size
|
|
particleSize.X = size.X;
|
|
particleSize.Y = size.Z;
|
|
_planeRotation = Quaternion.Identity;
|
|
}
|
|
|
|
// If it's a unit quad, dimensions will be 1.0
|
|
if (particleSize.X < 0.001f) particleSize.X = 1.0f;
|
|
if (particleSize.Y < 0.001f) particleSize.Y = 1.0f;
|
|
}
|
|
|
|
// Update particle distances
|
|
for (int i = 0; i < _particles.Count; i++) {
|
|
var p = _particles[i];
|
|
p.DistanceToCameraSq = Vector3.DistanceSquared(p.CalculatedPosition, cameraPos);
|
|
_particles[i] = p;
|
|
}
|
|
|
|
// Prepare instance data
|
|
ManagedGLTextureArray? atlas = null;
|
|
uint textureIndex = 0;
|
|
bool isAdditive = false;
|
|
|
|
if (textureData?.Batches.Count > 0) {
|
|
var batch = textureData.Batches[0];
|
|
isAdditive = batch.IsAdditive;
|
|
textureIndex = (uint)batch.TextureIndex;
|
|
if (batch.Atlas != null && batch.Atlas.TextureArray is ManagedGLTextureArray managedTexArray) {
|
|
atlas = managedTexArray;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < _particles.Count; i++) {
|
|
var p = _particles[i];
|
|
float lerp = Math.Clamp(p.Lifetime / p.MaxLifetime, 0f, 1f);
|
|
|
|
float currentScale = (p.FinalStartScale + (p.FinalFinalScale - p.FinalStartScale) * lerp) * baseScale;
|
|
float opacity = 1.0f - (p.FinalStartTrans + (p.FinalFinalTrans - p.FinalStartTrans) * lerp);
|
|
|
|
var pos = p.CalculatedPosition;
|
|
var orientation = p.Orientation;
|
|
|
|
if (_emitter.ParticleType == ParticleType.ParabolicLVGAGR ||
|
|
_emitter.ParticleType == ParticleType.ParabolicLVLALR ||
|
|
_emitter.ParticleType == ParticleType.ParabolicGVGAGR) {
|
|
var w = p.WorldC * (lerp * p.MaxLifetime);
|
|
var magSq = w.LengthSquared();
|
|
if (magSq > 0.00000001f) {
|
|
var mag = MathF.Sqrt(magSq);
|
|
orientation *= Quaternion.CreateFromAxisAngle(w / mag, mag);
|
|
}
|
|
}
|
|
|
|
var offset = localCenter * currentScale;
|
|
// Align particle to the BoundingBox center since we render a mathematically centered quad.
|
|
if (_isPointSprite) {
|
|
pos.Z += offset.Z; // For billboards we only shift vertically to stay upright
|
|
} else {
|
|
pos += Vector3.Transform(offset, orientation);
|
|
}
|
|
|
|
var instance = new ParticleInstance {
|
|
Position = pos,
|
|
ScaleOpacityActive = new Vector3(currentScale, opacity, 1.0f),
|
|
TextureIndex = (float)textureIndex,
|
|
Rotation = _isPointSprite ? orientation : orientation * _planeRotation,
|
|
Size = particleSize,
|
|
IsBillboard = _isPointSprite ? 1.0f : 0.0f
|
|
};
|
|
|
|
|
|
batcher.AddParticle(atlas, isAdditive, instance, p.DistanceToCameraSq);
|
|
}
|
|
}
|
|
|
|
public void Dispose() {
|
|
// Decrement reference counts that were incremented when the renderer was created/initialized
|
|
if (_emitter.HwGfxObjId.DataId != 0) {
|
|
_meshManager.ReleaseRenderData(_emitter.HwGfxObjId.DataId);
|
|
}
|
|
if (_emitter.GfxObjId.DataId != 0 && _emitter.GfxObjId.DataId != _emitter.HwGfxObjId.DataId) {
|
|
_meshManager.ReleaseRenderData(_emitter.GfxObjId.DataId);
|
|
}
|
|
}
|
|
}
|
|
}
|