diff --git a/src/AcDream.App/Rendering/Shaders/wb_particle.frag b/src/AcDream.App/Rendering/Shaders/wb_particle.frag
deleted file mode 100644
index 1f88e28d..00000000
--- a/src/AcDream.App/Rendering/Shaders/wb_particle.frag
+++ /dev/null
@@ -1,22 +0,0 @@
-#version 330 core
-
-in vec2 TexCoord;
-in float Opacity;
-in float TextureIndex;
-
-uniform sampler2DArray uTextureArray;
-
-out vec4 FragColor;
-
-void main() {
- // Reverting to standard non-premultiplied sampling.
- vec4 color = texture(uTextureArray, vec3(TexCoord, TextureIndex));
-
- // Standard alpha blending: SrcAlpha, OneMinusSrcAlpha.
- color.a *= Opacity;
-
- // Alpha test to discard fully transparent pixels (standard AC behavior)
- if (color.a < 0.005) discard;
-
- FragColor = color;
-}
\ No newline at end of file
diff --git a/src/AcDream.App/Rendering/Shaders/wb_particle.vert b/src/AcDream.App/Rendering/Shaders/wb_particle.vert
deleted file mode 100644
index 6bdbc082..00000000
--- a/src/AcDream.App/Rendering/Shaders/wb_particle.vert
+++ /dev/null
@@ -1,52 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec3 aPosition; // Basic quad vertex (-0.5 to 0.5)
-layout (location = 1) in vec2 aTexCoord;
-
-// Instance attributes
-layout (location = 2) in vec3 iPosition;
-layout (location = 3) in vec3 iScaleOpacityActive; // x=Scale, y=Opacity, z=Active
-layout (location = 4) in float iTextureIndex;
-layout (location = 5) in vec4 iRotation; // Quaternion
-layout (location = 6) in vec2 iSize;
-layout (location = 7) in float iIsBillboard;
-
-uniform mat4 uViewProjection;
-uniform vec3 uCameraUp;
-uniform vec3 uCameraRight;
-
-out vec2 TexCoord;
-out float Opacity;
-out float TextureIndex;
-
-vec3 rotate_vector(vec3 v, vec4 q) {
- return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
-}
-
-void main() {
- TexCoord = aTexCoord;
- Opacity = iScaleOpacityActive.y;
- TextureIndex = iTextureIndex;
-
- float scale = iScaleOpacityActive.x;
- vec3 worldPos;
-
- if (iIsBillboard > 0.5) {
- // Spherical billboarding - always face camera
- vec3 billboardRight = uCameraRight;
- vec3 billboardUp = uCameraUp;
-
- worldPos = iPosition
- + billboardRight * aPosition.x * iSize.x * scale
- + billboardUp * aPosition.z * iSize.y * scale;
- } else {
- // Standard 3D rotation using quaternion
- vec3 localPos = vec3(aPosition.x * iSize.x * scale,
- 0.0,
- aPosition.z * iSize.y * scale);
-
- worldPos = iPosition + rotate_vector(localPos, iRotation);
- }
-
- gl_Position = uViewProjection * vec4(worldPos, 1.0);
-}
diff --git a/src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs b/src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs
deleted file mode 100644
index 4c564a9e..00000000
--- a/src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Numerics;
-
-namespace AcDream.App.Rendering.Wb {
- public class ActiveParticleEmitter {
- public ParticleEmitterRenderer Renderer { get; }
- public uint PartIndex { get; }
- public Matrix4x4 LocalOffset { get; }
-
- // Store reference info instead of struct copy.
- // ParentLandblock was ObjectLandblock (WB type) — typed as object? in Phase O-T7
- // to remove the WorldBuilder project reference; no consumer reads it.
- public object? ParentLandblock { get; set; }
- // ParentInstanceId was WorldBuilder.Shared.Models.ObjectId? — erased to ulong? in T7;
- // the field is stored but never read by any consumer in our codebase.
- public ulong? ParentInstanceId { get; set; }
-
- public ActiveParticleEmitter(ParticleEmitterRenderer renderer, uint partIndex, Matrix4x4 localOffset, object? parentLandblock = null, ulong? parentInstanceId = null) {
- Renderer = renderer;
- PartIndex = partIndex;
- LocalOffset = localOffset;
- ParentLandblock = parentLandblock;
- ParentInstanceId = parentInstanceId;
- }
-
- public void Update(float deltaTime, Matrix4x4 parentTransform) {
- Renderer.ParentTransform = parentTransform;
- Renderer.LocalOffset = LocalOffset;
- Renderer.Update(deltaTime);
- }
-
- public void Render(ParticleBatcher batcher) {
- Renderer.Render(batcher);
- }
-
- public void Dispose() {
- Renderer.Dispose();
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs b/src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs
deleted file mode 100644
index bc10f696..00000000
--- a/src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-using System.IO;
-
-namespace AcDream.App.Rendering.Wb {
-
- ///
- /// Resolves WB-style shader resource names (e.g. "Shaders.Particle.vert") to
- /// the corresponding file under the acdream binary's Rendering/Shaders directory.
- ///
- /// WB embeds shaders as assembly resources under the Chorizite.OpenGLSDLBackend
- /// namespace. acdream ships shaders as plain files copied to the output directory.
- /// This class adapts between the two conventions so ParticleBatcher and
- /// ParticleEmitterRenderer can call GetEmbeddedResource without modification.
- ///
- /// Mapping rule: "Shaders.Foo.vert" → Rendering/Shaders/wb_foo.vert
- /// (lower-case, wb_ prefix to distinguish WB-origin shaders from acdream's own)
- ///
- public static class EmbeddedResourceReader {
- public static string GetEmbeddedResource(string filename) {
- // Convert "Shaders.Particle.vert" → "wb_particle.vert"
- // Strip leading "Shaders." then lowercase and prefix with wb_
- string leafName;
- const string shadersPrefix = "Shaders.";
- if (filename.StartsWith(shadersPrefix, StringComparison.Ordinal))
- {
- var rest = filename.Substring(shadersPrefix.Length); // e.g. "Particle.vert"
- leafName = "wb_" + rest.ToLowerInvariant(); // e.g. "wb_particle.vert"
- }
- else
- {
- leafName = "wb_" + filename.ToLowerInvariant();
- }
-
- var shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
- var fullPath = Path.Combine(shadersDir, leafName);
-
- if (!File.Exists(fullPath))
- throw new InvalidOperationException(
- $"WB shader not found: '{fullPath}' (mapped from resource '{filename}'). " +
- $"Ensure {leafName} is in src/AcDream.App/Rendering/Shaders/ with CopyToOutputDirectory.");
-
- return File.ReadAllText(fullPath);
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
index 7b70774f..38bf7e1d 100644
--- a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
+++ b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
@@ -82,8 +82,6 @@ namespace AcDream.App.Rendering.Wb {
public uint SharedDebugVAO { get; private set; }
public uint SharedDebugInstanceVBO { get; private set; }
- public ParticleBatcher ParticleBatcher { get; internal set; } = null!;
-
/// OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).
public uint WrapSampler { get; private set; }
/// OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).
@@ -192,11 +190,6 @@ namespace AcDream.App.Rendering.Wb {
"OpenGLGraphicsDevice construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
-
- // ParticleBatcher is constructed post-ctor by WbMeshAdapter (WbMeshAdapter.cs:78)
- // after the adapter has wired up all dependencies. The null! here is overridden
- // immediately after construction; it is not observable as null at runtime.
- ParticleBatcher = null!;
}
///
@@ -760,7 +753,6 @@ namespace AcDream.App.Rendering.Wb {
ClampSampler = 0;
_sceneDataBuffer?.Dispose();
_sceneDataBuffer = null;
- ParticleBatcher?.Dispose();
}
public override IUniformBuffer CreateUniformBuffer(BufferUsage usage, int size) {
diff --git a/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs b/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
deleted file mode 100644
index 3b1a6c9a..00000000
--- a/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
+++ /dev/null
@@ -1,240 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-using System.Runtime.InteropServices;
-using Chorizite.Core.Render;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Wb {
- [StructLayout(LayoutKind.Sequential)]
- public struct ParticleInstance {
- public Vector3 Position;
- public Vector3 ScaleOpacityActive; // x=scale, y=opacity, z=active (1.0 or 0.0)
- public float TextureIndex;
- public Quaternion Rotation;
- public Vector2 Size;
- public float IsBillboard; // 1.0 for true, 0.0 for false
- }
-
- public struct ParticleRenderData {
- public ParticleInstance Instance;
- public float DistanceSq;
- public ManagedGLTextureArray? Atlas;
- public bool IsAdditive;
- }
-
- public unsafe class ParticleBatcher : IDisposable {
- private const int MAX_PARTICLES_TOTAL = 65536;
-
- private readonly OpenGLGraphicsDevice _graphicsDevice;
- private readonly uint _vao;
- private readonly uint _vbo;
- private readonly uint _ibo;
- private readonly uint _instanceVbo;
- private readonly IShader _shader;
- private readonly ResourceCleanupGroup _resources;
- private readonly ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL];
- private readonly List _allParticles = new();
- private int _currentInstanceCount = 0;
-
- private ManagedGLTextureArray? _currentAtlas;
- private bool _currentIsAdditive;
- private Matrix4x4 _viewProjection;
- private Vector3 _cameraUp;
- private Vector3 _cameraRight;
-
- public ParticleBatcher(OpenGLGraphicsDevice graphicsDevice) {
- _graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
- var gl = _graphicsDevice.GL;
- var resources = new ResourceCleanupGroup();
- IShader? shader = null;
- uint vao = 0;
- uint vbo = 0;
- uint ibo = 0;
- uint instanceVbo = 0;
- try {
- var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
- var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
- shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
- if (shader is IDisposable disposableShader)
- resources.Add("WB particle shader", disposableShader.Dispose);
-
- // Create quad vertices - centered to match ACViewer expansion logic
- float[] vertices = {
- -0.5f, 0.0f, -0.5f, 0.0f, 1.0f,
- 0.5f, 0.0f, -0.5f, 1.0f, 1.0f,
- 0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
- -0.5f, 0.0f, 0.5f, 0.0f, 0.0f
- };
- ushort[] indices = { 0, 1, 2, 2, 3, 0 };
-
- vao = CreateVertexArray(resources, gl, "WB particle VAO");
- vbo = CreateBuffer(resources, gl, "WB particle vertex buffer");
- ibo = CreateBuffer(resources, gl, "WB particle index buffer");
- instanceVbo = CreateBuffer(resources, gl, "WB particle instance buffer");
-
- GlResourceCommand.Execute(gl, "configure WB particle batcher", () => {
- gl.BindVertexArray(vao);
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
- fixed (float* p = vertices) {
- gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
- }
- gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ibo);
- fixed (ushort* p = indices) {
- gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
- }
-
- gl.EnableVertexAttribArray(0);
- gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
- gl.EnableVertexAttribArray(1);
- gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
-
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, instanceVbo);
- gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf()), (void*)0, BufferUsageARB.DynamicDraw);
- uint stride = (uint)Marshal.SizeOf();
- gl.EnableVertexAttribArray(2);
- gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
- gl.VertexAttribDivisor(2, 1);
- gl.EnableVertexAttribArray(3);
- gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
- gl.VertexAttribDivisor(3, 1);
- gl.EnableVertexAttribArray(4);
- gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
- gl.VertexAttribDivisor(4, 1);
- gl.EnableVertexAttribArray(5);
- gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
- gl.VertexAttribDivisor(5, 1);
- gl.EnableVertexAttribArray(6);
- gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
- gl.VertexAttribDivisor(6, 1);
- gl.EnableVertexAttribArray(7);
- gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
- gl.VertexAttribDivisor(7, 1);
- gl.BindVertexArray(0);
- });
-
- shader.Bind();
- shader.SetUniform("uTextureArray", 0);
- shader.Unbind();
- } catch (Exception constructionFailure) {
- resources.RollbackConstructionAndThrow(
- "ParticleBatcher construction failed and its GL prefix did not cleanly roll back.",
- constructionFailure);
- }
-
- _resources = resources;
- _shader = shader!;
- _vao = vao;
- _vbo = vbo;
- _ibo = ibo;
- _instanceVbo = instanceVbo;
- }
-
- private static uint CreateBuffer(
- ResourceCleanupGroup resources,
- GL gl,
- string name) {
- uint buffer = GlResourceCommand.CreateName(gl, name, gl.GenBuffer, gl.DeleteBuffer);
- resources.Add(name, () => GlResourceCommand.DeleteBuffer(gl, buffer, $"delete {name} {buffer}"));
- return buffer;
- }
-
- private static uint CreateVertexArray(
- ResourceCleanupGroup resources,
- GL gl,
- string name) {
- uint vao = GlResourceCommand.CreateName(gl, name, gl.GenVertexArray, gl.DeleteVertexArray);
- resources.Add(name, () => GlResourceCommand.DeleteVertexArray(gl, vao, $"delete {name} {vao}"));
- return vao;
- }
-
- public void Begin(Matrix4x4 viewProjection, Vector3 cameraUp, Vector3 cameraRight) {
- _viewProjection = viewProjection;
- _cameraUp = cameraUp;
- _cameraRight = cameraRight;
- _allParticles.Clear();
- }
-
- public void AddParticle(ManagedGLTextureArray? atlas, bool isAdditive, ParticleInstance instance, float distanceSq) {
- _allParticles.Add(new ParticleRenderData {
- Instance = instance,
- DistanceSq = distanceSq,
- Atlas = atlas,
- IsAdditive = isAdditive
- });
- }
-
- public void Flush() {
- if (_allParticles.Count == 0) return;
-
- // Sort back-to-front
- _allParticles.Sort((a, b) => b.DistanceSq.CompareTo(a.DistanceSq));
-
- var gl = _graphicsDevice.GL;
-
- gl.BindVertexArray(_vao);
- gl.DepthMask(false);
- gl.Enable(EnableCap.DepthTest);
- gl.Disable(EnableCap.StencilTest);
- gl.Disable(EnableCap.CullFace);
- gl.Disable(EnableCap.SampleAlphaToCoverage);
- gl.Disable(EnableCap.SampleAlphaToOne);
- gl.Enable(EnableCap.Blend);
-
- int i = 0;
- while (i < _allParticles.Count) {
- var p = _allParticles[i];
- _currentAtlas = p.Atlas;
- _currentIsAdditive = p.IsAdditive;
- _currentInstanceCount = 0;
-
- while (i < _allParticles.Count && _allParticles[i].Atlas == _currentAtlas && _allParticles[i].IsAdditive == _currentIsAdditive) {
- _instanceData[_currentInstanceCount++] = _allParticles[i].Instance;
- i++;
- if (_currentInstanceCount >= MAX_PARTICLES_TOTAL) break;
- }
-
- if (_currentInstanceCount > 0 && _currentAtlas != null) {
- if (_currentIsAdditive) {
- gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
- }
- else {
- gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
- }
-
- gl.ActiveTexture(TextureUnit.Texture0);
- gl.BindTexture(GLEnum.Texture2DArray, (uint)_currentAtlas.NativePtr);
- RenderStateCache.CurrentAtlas = (uint)_currentAtlas.Slot;
-
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
- unsafe {
- fixed (ParticleInstance* pData = _instanceData) {
- gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (uint)(_currentInstanceCount * Marshal.SizeOf()), pData);
- }
- }
-
- _shader.Bind();
- _shader.SetUniform("uViewProjection", _viewProjection);
- _shader.SetUniform("uCameraUp", _cameraUp);
- _shader.SetUniform("uCameraRight", _cameraRight);
-
- gl.DrawElementsInstanced(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedShort, (void*)0, (uint)_currentInstanceCount);
- }
- }
-
- gl.DepthMask(true);
- _allParticles.Clear();
-
- RenderStateCache.CurrentVAO = 0;
- RenderStateCache.CurrentIBO = 0;
- }
-
- public void End() {
- Flush();
- }
-
- public void Dispose() {
- _resources.RetryCleanup();
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ParticleEmitterRenderer.cs b/src/AcDream.App/Rendering/Wb/ParticleEmitterRenderer.cs
deleted file mode 100644
index eb18362b..00000000
--- a/src/AcDream.App/Rendering/Wb/ParticleEmitterRenderer.cs
+++ /dev/null
@@ -1,499 +0,0 @@
-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 _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);
- _meshManager.PrepareMeshDataAsync(emitter.HwGfxObjId.DataId, isSetup: false);
- }
- if (emitter.GfxObjId.DataId != 0 && emitter.GfxObjId.DataId != emitter.HwGfxObjId.DataId) {
- _meshManager.IncrementRefCount(emitter.GfxObjId.DataId);
- _meshManager.PrepareMeshDataAsync(emitter.GfxObjId.DataId, isSetup: false);
- }
- }
-
- 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) {
- _meshManager.PrepareMeshDataAsync(gfxId, isSetup: false);
- _gfxRenderData = _meshManager.TryGetRenderData(gfxId);
- }
- }
- if (_textureRenderData == null && _emitter.GfxObjId.DataId != 0) {
- _meshManager.PrepareMeshDataAsync(_emitter.GfxObjId.DataId, isSetup: false);
- _textureRenderData = _meshManager.TryGetRenderData(_emitter.GfxObjId.DataId);
- }
-
- _isPointSprite = _gfxRenderData == null;
- if (_gfxRenderData != null) {
- var degradeId = _gfxRenderData.DIDDegrade;
- if (degradeId != 0) {
- if (_meshManager.Dats.Portal.TryGet(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);
- }
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
index d2eb22b1..ee3ddf10 100644
--- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -145,7 +145,6 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
}
});
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
- graphicsDevice.ParticleBatcher = new ParticleBatcher(graphicsDevice);
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
meshManager = new ObjectMeshManager(