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(