feat(O-T4): extract ObjectMeshManager + mesh pipeline closure into AcDream.App.Rendering.Wb
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>
This commit is contained in:
parent
4cc38805b5
commit
d16d8cd4e5
14 changed files with 3535 additions and 19 deletions
22
src/AcDream.App/Rendering/Shaders/wb_particle.frag
Normal file
22
src/AcDream.App/Rendering/Shaders/wb_particle.frag
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#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;
|
||||
}
|
||||
52
src/AcDream.App/Rendering/Shaders/wb_particle.vert
Normal file
52
src/AcDream.App/Rendering/Shaders/wb_particle.vert
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#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);
|
||||
}
|
||||
37
src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs
Normal file
37
src/AcDream.App/Rendering/Wb/ActiveParticleEmitter.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Numerics;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
using WorldBuilder.Shared.Models;
|
||||
|
||||
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
|
||||
public ObjectLandblock? ParentLandblock { get; set; }
|
||||
public ObjectId? ParentInstanceId { get; set; }
|
||||
|
||||
public ActiveParticleEmitter(ParticleEmitterRenderer renderer, uint partIndex, Matrix4x4 localOffset, ObjectLandblock? parentLandblock = null, ObjectId? 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs
Normal file
167
src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using WorldBuilder.Shared.Services;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Adapts acdream's <see cref="DatCollection"/> to WB's <see cref="IDatReaderWriter"/> interface.
|
||||
///
|
||||
/// O-D7 fallback path: taken because ObjectMeshManager has 26 _dats.X call sites (threshold is 20),
|
||||
/// making a full refactor to DatCollection larger than spec permits in a single task.
|
||||
/// This adapter lets ObjectMeshManager stay byte-identical to the WB original while
|
||||
/// routing all DAT I/O through our single DatCollection. The adapter is dropped in T7
|
||||
/// when the WorldBuilder project reference is removed entirely.
|
||||
/// </summary>
|
||||
internal sealed class DatCollectionAdapter : IDatReaderWriter
|
||||
{
|
||||
private readonly DatCollection _dats;
|
||||
private readonly DatDatabaseWrapper _portal;
|
||||
private readonly DatDatabaseWrapper _cell;
|
||||
private readonly DatDatabaseWrapper _highRes;
|
||||
private readonly DatDatabaseWrapper _language;
|
||||
private readonly ReadOnlyDictionary<uint, IDatDatabase> _cellRegions;
|
||||
|
||||
public DatCollectionAdapter(DatCollection dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
_dats = dats;
|
||||
_portal = new DatDatabaseWrapper(dats.Portal);
|
||||
_cell = new DatDatabaseWrapper(dats.Cell);
|
||||
_highRes = new DatDatabaseWrapper(dats.HighRes);
|
||||
_language = new DatDatabaseWrapper(dats.Local);
|
||||
|
||||
// DatCollection has a single Cell, not multiple cell regions.
|
||||
// Expose it as region 0 to satisfy callers that iterate CellRegions.
|
||||
var regions = new Dictionary<uint, IDatDatabase> { [0u] = _cell };
|
||||
_cellRegions = new ReadOnlyDictionary<uint, IDatDatabase>(regions);
|
||||
}
|
||||
|
||||
/// <summary>Source directory of the underlying DatCollection.</summary>
|
||||
public string SourceDirectory => _dats.Options.DatDirectory ?? string.Empty;
|
||||
|
||||
public IDatDatabase Portal => _portal;
|
||||
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions => _cellRegions;
|
||||
public IDatDatabase HighRes => _highRes;
|
||||
public IDatDatabase Language => _language;
|
||||
|
||||
// RegionFileMap is used by some WB internals but not by ObjectMeshManager.
|
||||
public ReadOnlyDictionary<uint, uint> RegionFileMap =>
|
||||
new ReadOnlyDictionary<uint, uint>(new Dictionary<uint, uint>());
|
||||
|
||||
// Iteration properties — not used by ObjectMeshManager, so delegate to 0.
|
||||
public int PortalIteration => 0;
|
||||
public int CellIteration => 0;
|
||||
public int HighResIteration => 0;
|
||||
public int LanguageIteration => 0;
|
||||
|
||||
public bool TryGetFileBytes(uint regionId, uint fileId, ref byte[] bytes, out int bytesRead)
|
||||
{
|
||||
// Route to cell db (the only region we expose)
|
||||
return _dats.Cell.TryGetFileBytes(fileId, ref bytes, out bytesRead);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a DAT id to all databases that contain it, along with the DBObjType.
|
||||
/// Mirrors DefaultDatReaderWriter.ResolveId — checks each underlying DatDatabase
|
||||
/// via DatDatabase.TypeFromId (which reads the type range tables).
|
||||
/// </summary>
|
||||
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id)
|
||||
{
|
||||
var results = new List<IDatReaderWriter.IdResolution>();
|
||||
|
||||
void CheckDb(DatDatabaseWrapper wrapper)
|
||||
{
|
||||
var rawDb = wrapper.RawDatabase;
|
||||
if (rawDb.Tree.TryGetFile(id, out _))
|
||||
{
|
||||
var type = rawDb.TypeFromId(id);
|
||||
if (type != DBObjType.Unknown)
|
||||
results.Add(new IDatReaderWriter.IdResolution(wrapper, type));
|
||||
}
|
||||
}
|
||||
|
||||
// Match DefaultDatReaderWriter ordering: HighRes → Portal → Language → Cell
|
||||
CheckDb(_highRes);
|
||||
CheckDb(_portal);
|
||||
CheckDb(_language);
|
||||
CheckDb(_cell);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException("DatCollectionAdapter is read-only.");
|
||||
|
||||
public bool TrySave<T>(uint regionId, T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException("DatCollectionAdapter is read-only.");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// The underlying DatCollection is owned by the caller — do not dispose it here.
|
||||
// Individual wrapper objects hold no unmanaged resources.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="DatDatabase"/> as <see cref="IDatDatabase"/>.
|
||||
/// Mirrors WorldBuilder.Shared.Services.DefaultDatDatabase but lives in our namespace
|
||||
/// so the WorldBuilder project reference can be dropped in T7.
|
||||
/// </summary>
|
||||
internal sealed class DatDatabaseWrapper : IDatDatabase
|
||||
{
|
||||
private readonly DatDatabase _db;
|
||||
private readonly ConcurrentDictionary<(Type, uint), IDBObj> _cache = new();
|
||||
|
||||
public DatDatabaseWrapper(DatDatabase db)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(db);
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>Exposes the raw DatDatabase for ResolveId's Tree.TryGetFile + TypeFromId calls.</summary>
|
||||
internal DatDatabase RawDatabase => _db;
|
||||
|
||||
public DatDatabase Db => _db;
|
||||
public int Iteration => _db.Iteration?.CurrentIteration ?? 0;
|
||||
|
||||
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
||||
_db.GetAllIdsOfType<T>();
|
||||
|
||||
public bool TryGet<T>(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||
{
|
||||
if (_cache.TryGetValue((typeof(T), fileId), out var cached))
|
||||
{
|
||||
value = (T)cached;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_db.TryGet<T>(fileId, out value))
|
||||
{
|
||||
_cache.TryAdd((typeof(T), fileId), value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value) =>
|
||||
_db.TryGetFileBytes(fileId, out value);
|
||||
|
||||
public bool TryGetFileBytes(uint fileId, ref byte[] bytes, out int bytesRead) =>
|
||||
_db.TryGetFileBytes(fileId, ref bytes, out bytesRead);
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException("DatDatabaseWrapper is read-only.");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// The underlying DatDatabase is owned by DatCollection — do not dispose here.
|
||||
}
|
||||
}
|
||||
134
src/AcDream.App/Rendering/Wb/EdgeLineBuilder.cs
Normal file
134
src/AcDream.App/Rendering/Wb/EdgeLineBuilder.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
using System.Numerics;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public static class EdgeLineBuilder {
|
||||
public static List<Vector3> BuildEdgeLines(CellStruct cellStruct) {
|
||||
var edgeMap = new Dictionary<EdgeKey, List<Edge>>();
|
||||
|
||||
foreach (var kvp in cellStruct.Polygons) {
|
||||
var polyIdx = kvp.Key;
|
||||
var vertexIds = kvp.Value.VertexIds;
|
||||
|
||||
var v0 = cellStruct.VertexArray.Vertices[(ushort)vertexIds[0]].Origin;
|
||||
|
||||
// AC polys can either be triangles or triangle fans
|
||||
for (var i = 1; i < vertexIds.Count - 1; i++) {
|
||||
var v1 = cellStruct.VertexArray.Vertices[(ushort)vertexIds[i]].Origin;
|
||||
var v2 = cellStruct.VertexArray.Vertices[(ushort)vertexIds[i + 1]].Origin;
|
||||
|
||||
AddEdge(edgeMap, polyIdx, v0, v1);
|
||||
AddEdge(edgeMap, polyIdx, v1, v2);
|
||||
AddEdge(edgeMap, polyIdx, v2, v0);
|
||||
}
|
||||
}
|
||||
|
||||
var output = new List<Vector3>();
|
||||
var processedEdges = new HashSet<EdgeKey>();
|
||||
|
||||
foreach (var kvp in edgeMap) {
|
||||
var edgeKey = kvp.Key;
|
||||
var edgeList = kvp.Value;
|
||||
|
||||
if (processedEdges.Contains(edgeKey)) continue;
|
||||
|
||||
processedEdges.Add(edgeKey);
|
||||
|
||||
if (edgeList.Count == 2) {
|
||||
var poly1 = cellStruct.Polygons[edgeList[0].PolyIdx];
|
||||
var poly2 = cellStruct.Polygons[edgeList[1].PolyIdx];
|
||||
|
||||
if (HaveSameTexture(poly1, poly2) && IsCoplanar(poly1, poly2, cellStruct))
|
||||
continue;
|
||||
}
|
||||
|
||||
output.Add(edgeList[0].P0);
|
||||
output.Add(edgeList[0].P1);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static void AddEdge(Dictionary<EdgeKey, List<Edge>> edgeMap, ushort polyIdx, Vector3 p0, Vector3 p1) {
|
||||
var key = new EdgeKey(p0, p1);
|
||||
var edge = new Edge(polyIdx, p0, p1);
|
||||
|
||||
if (!edgeMap.ContainsKey(key))
|
||||
edgeMap[key] = new List<Edge>();
|
||||
|
||||
edgeMap[key].Add(edge);
|
||||
}
|
||||
|
||||
private static bool HaveSameTexture(Polygon a, Polygon b) {
|
||||
return a.PosSurface == b.PosSurface;
|
||||
}
|
||||
|
||||
private static Vector3 CalculateNormal(Polygon poly, CellStruct cellStruct) {
|
||||
var vertexIds = poly.VertexIds;
|
||||
var verts = cellStruct.VertexArray.Vertices;
|
||||
|
||||
var v0 = verts[(ushort)vertexIds[0]];
|
||||
var v1 = verts[(ushort)vertexIds[1]];
|
||||
var v2 = verts[(ushort)vertexIds[2]];
|
||||
|
||||
var edge1 = v1.Origin - v0.Origin;
|
||||
var edge2 = v2.Origin - v0.Origin;
|
||||
return Vector3.Normalize(Vector3.Cross(edge1, edge2));
|
||||
}
|
||||
|
||||
private static bool IsCoplanar(Polygon a, Polygon b, CellStruct cellStruct) {
|
||||
var normA = CalculateNormal(a, cellStruct);
|
||||
var normB = CalculateNormal(b, cellStruct);
|
||||
|
||||
var dp = Vector3.Dot(normA, normB);
|
||||
|
||||
// If dot product is 1 or -1, normals are parallel (coplanar)
|
||||
// Allow for both same and opposite facing normals
|
||||
const float tolerance = 0.01f;
|
||||
return Math.Abs(Math.Abs(dp) - 1) < tolerance;
|
||||
}
|
||||
|
||||
private class Edge {
|
||||
public ushort PolyIdx { get; }
|
||||
public Vector3 P0 { get; }
|
||||
public Vector3 P1 { get; }
|
||||
|
||||
public Edge(ushort polyIdx, Vector3 p0, Vector3 p1) {
|
||||
PolyIdx = polyIdx;
|
||||
P0 = p0;
|
||||
P1 = p1;
|
||||
}
|
||||
}
|
||||
|
||||
private class EdgeKey : IEquatable<EdgeKey> {
|
||||
private readonly Vector3 _p0;
|
||||
private readonly Vector3 _p1;
|
||||
|
||||
public EdgeKey(Vector3 p0, Vector3 p1) {
|
||||
if (CompareVector3(p0, p1) > 0) {
|
||||
_p0 = p1;
|
||||
_p1 = p0;
|
||||
}
|
||||
else {
|
||||
_p0 = p0;
|
||||
_p1 = p1;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(EdgeKey? e) {
|
||||
if (e == null) return false;
|
||||
return _p0 == e._p0 && _p1 == e._p1;
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return HashCode.Combine(_p0, _p1);
|
||||
}
|
||||
|
||||
private static int CompareVector3(Vector3 a, Vector3 b) {
|
||||
if (a.X != b.X) return a.X.CompareTo(b.X);
|
||||
if (a.Y != b.Y) return a.Y.CompareTo(b.Y);
|
||||
return a.Z.CompareTo(b.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,45 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
public static class EmbeddedResourceReader {
|
||||
internal static string GetEmbeddedResource(string filename) {
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = "Chorizite.OpenGLSDLBackend." + 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();
|
||||
}
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Could not find embedded resource '{resourceName}'");
|
||||
using var reader = new StreamReader(stream);
|
||||
var shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
|
||||
var fullPath = Path.Combine(shadersDir, leafName);
|
||||
|
||||
return reader.ReadToEnd();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
127
src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
Normal file
127
src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public class GlobalMeshBuffer : IDisposable {
|
||||
private readonly GL _gl;
|
||||
public uint VAO { get; private set; }
|
||||
public uint VBO { get; private set; }
|
||||
public uint IBO { get; private set; }
|
||||
|
||||
private int _vboCapacity = 1024 * 1024; // 1M vertices (~32MB)
|
||||
private int _iboCapacity = 3 * 1024 * 1024; // 3M indices (~6MB)
|
||||
private int _vboOffset = 0;
|
||||
private int _iboOffset = 0;
|
||||
|
||||
public GlobalMeshBuffer(GL gl) {
|
||||
_gl = gl;
|
||||
InitBuffers();
|
||||
}
|
||||
|
||||
private unsafe void InitBuffers() {
|
||||
_gl.GenVertexArrays(1, out uint vao);
|
||||
VAO = vao;
|
||||
_gl.BindVertexArray(VAO);
|
||||
|
||||
_gl.GenBuffers(1, out uint vbo);
|
||||
VBO = vbo;
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, VBO);
|
||||
_gl.BufferData(GLEnum.ArrayBuffer, (nuint)(_vboCapacity * VertexPositionNormalTexture.Size), null, GLEnum.StaticDraw);
|
||||
|
||||
int stride = VertexPositionNormalTexture.Size;
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
|
||||
_gl.GenBuffers(1, out uint ibo);
|
||||
IBO = ibo;
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, IBO);
|
||||
_gl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(_iboCapacity * sizeof(ushort)), null, GLEnum.StaticDraw);
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
public unsafe (int baseVertex, int firstIndex) Append(VertexPositionNormalTexture[] vertices, ushort[] indices) {
|
||||
if (vertices.Length == 0 || indices.Length == 0) return (0, 0);
|
||||
|
||||
// Check capacity
|
||||
if (_vboOffset + vertices.Length > _vboCapacity) {
|
||||
ResizeVBO(Math.Max(_vboCapacity * 2, _vboCapacity + vertices.Length));
|
||||
}
|
||||
if (_iboOffset + indices.Length > _iboCapacity) {
|
||||
ResizeIBO(Math.Max(_iboCapacity * 2, _iboCapacity + indices.Length));
|
||||
}
|
||||
|
||||
int baseVertex = _vboOffset;
|
||||
int firstIndex = _iboOffset;
|
||||
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, VBO);
|
||||
fixed (VertexPositionNormalTexture* ptr = vertices) {
|
||||
_gl.BufferSubData(GLEnum.ArrayBuffer, (nint)(baseVertex * VertexPositionNormalTexture.Size), (nuint)(vertices.Length * VertexPositionNormalTexture.Size), ptr);
|
||||
}
|
||||
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, IBO);
|
||||
fixed (ushort* ptr = indices) {
|
||||
_gl.BufferSubData(GLEnum.ElementArrayBuffer, (nint)(firstIndex * sizeof(ushort)), (nuint)(indices.Length * sizeof(ushort)), ptr);
|
||||
}
|
||||
|
||||
_vboOffset += vertices.Length;
|
||||
_iboOffset += indices.Length;
|
||||
|
||||
return (baseVertex, firstIndex);
|
||||
}
|
||||
|
||||
private unsafe void ResizeVBO(int newCapacity) {
|
||||
_gl.GenBuffers(1, out uint newVbo);
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, newVbo);
|
||||
_gl.BufferData(GLEnum.ArrayBuffer, (nuint)(newCapacity * VertexPositionNormalTexture.Size), null, GLEnum.StaticDraw);
|
||||
|
||||
_gl.BindBuffer(GLEnum.CopyReadBuffer, VBO);
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, newVbo);
|
||||
_gl.CopyBufferSubData(GLEnum.CopyReadBuffer, GLEnum.CopyWriteBuffer, 0, 0, (nuint)(_vboOffset * VertexPositionNormalTexture.Size));
|
||||
|
||||
_gl.DeleteBuffer(VBO);
|
||||
VBO = newVbo;
|
||||
_vboCapacity = newCapacity;
|
||||
|
||||
// Re-bind to VAO
|
||||
_gl.BindVertexArray(VAO);
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, VBO);
|
||||
int stride = VertexPositionNormalTexture.Size;
|
||||
_gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
|
||||
_gl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
|
||||
_gl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
private unsafe void ResizeIBO(int newCapacity) {
|
||||
_gl.GenBuffers(1, out uint newIbo);
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, newIbo);
|
||||
_gl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(newCapacity * sizeof(ushort)), null, GLEnum.StaticDraw);
|
||||
|
||||
_gl.BindBuffer(GLEnum.CopyReadBuffer, IBO);
|
||||
_gl.BindBuffer(GLEnum.CopyWriteBuffer, newIbo);
|
||||
_gl.CopyBufferSubData(GLEnum.CopyReadBuffer, GLEnum.CopyWriteBuffer, 0, 0, (nuint)(_iboOffset * sizeof(ushort)));
|
||||
|
||||
_gl.DeleteBuffer(IBO);
|
||||
IBO = newIbo;
|
||||
_iboCapacity = newCapacity;
|
||||
|
||||
// Re-bind to VAO
|
||||
_gl.BindVertexArray(VAO);
|
||||
_gl.BindBuffer(GLEnum.ElementArrayBuffer, IBO);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (VAO != 0) _gl.DeleteVertexArray(VAO);
|
||||
if (VBO != 0) _gl.DeleteBuffer(VBO);
|
||||
if (IBO != 0) _gl.DeleteBuffer(IBO);
|
||||
VAO = VBO = IBO = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/AcDream.App/Rendering/Wb/ModernRenderData.cs
Normal file
31
src/AcDream.App/Rendering/Wb/ModernRenderData.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using DatReaderWriter.Enums;
|
||||
using Chorizite.Core.Render;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Per-batch (draw call) data for modern rendering.
|
||||
/// Consists of a bindless texture handle to a texture array and the layer index.
|
||||
/// Indexed by gl_DrawIDARB in the vertex shader.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||
public struct ModernBatchData {
|
||||
public ulong TextureHandle; // 8 bytes
|
||||
public uint TextureIndex; // 4 bytes
|
||||
public uint Padding; // 4 bytes
|
||||
}
|
||||
|
||||
public struct LandblockMdiCommand {
|
||||
public ulong SortKey;
|
||||
public ulong ObjectId;
|
||||
public DrawElementsIndirectCommand Command;
|
||||
public ModernBatchData BatchData;
|
||||
public uint TextureIndex;
|
||||
public ManagedGLTextureArray Atlas;
|
||||
public uint VAO;
|
||||
public uint IBO;
|
||||
public bool IsTransparent;
|
||||
public bool IsAdditive;
|
||||
public bool HasWrappingUVs;
|
||||
}
|
||||
}
|
||||
2079
src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
Normal file
2079
src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -55,7 +55,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
public uint SharedDebugVAO { get; private set; }
|
||||
public uint SharedDebugInstanceVBO { get; private set; }
|
||||
|
||||
public Chorizite.OpenGLSDLBackend.Lib.ParticleBatcher ParticleBatcher { get; private set; } = null!;
|
||||
public ParticleBatcher ParticleBatcher { get; internal set; } = null!;
|
||||
|
||||
/// <summary>OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).</summary>
|
||||
public uint WrapSampler { get; private set; }
|
||||
|
|
|
|||
231
src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
Normal file
231
src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Chorizite.Core.Render;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
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 ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL];
|
||||
private readonly List<ParticleRenderData> _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;
|
||||
var gl = _graphicsDevice.GL;
|
||||
|
||||
var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
|
||||
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
|
||||
_shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
|
||||
|
||||
// Create quad vertices - centered to match ACViewer expansion logic
|
||||
float[] vertices = {
|
||||
// x, y, z, u, v
|
||||
-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 = gl.GenVertexArray();
|
||||
gl.BindVertexArray(_vao);
|
||||
|
||||
_vbo = gl.GenBuffer();
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
unsafe {
|
||||
fixed (float* p = vertices) {
|
||||
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
|
||||
}
|
||||
}
|
||||
|
||||
_ibo = gl.GenBuffer();
|
||||
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ibo);
|
||||
unsafe {
|
||||
fixed (ushort* p = indices) {
|
||||
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
|
||||
}
|
||||
}
|
||||
|
||||
// Quad attributes
|
||||
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)));
|
||||
|
||||
// Instance attributes
|
||||
_instanceVbo = gl.GenBuffer();
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
|
||||
|
||||
uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
|
||||
|
||||
// iPosition
|
||||
gl.EnableVertexAttribArray(2);
|
||||
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
gl.VertexAttribDivisor(2, 1);
|
||||
|
||||
// iScaleOpacityActive
|
||||
gl.EnableVertexAttribArray(3);
|
||||
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(3, 1);
|
||||
|
||||
// iTextureIndex
|
||||
gl.EnableVertexAttribArray(4);
|
||||
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(4, 1);
|
||||
|
||||
// iRotation (Quaternion)
|
||||
gl.EnableVertexAttribArray(5);
|
||||
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(5, 1);
|
||||
|
||||
// iSize
|
||||
gl.EnableVertexAttribArray(6);
|
||||
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(6, 1);
|
||||
|
||||
// iIsBillboard
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
// T4 interim: BaseObjectRenderManager state fields stay on the WB type until T7
|
||||
// when the WorldBuilder project reference is dropped entirely.
|
||||
BaseObjectRenderManager.CurrentAtlas = (uint)_currentAtlas.Slot;
|
||||
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
unsafe {
|
||||
fixed (ParticleInstance* pData = _instanceData) {
|
||||
gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (uint)(_currentInstanceCount * Marshal.SizeOf<ParticleInstance>()), 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();
|
||||
|
||||
// T4 interim: BaseObjectRenderManager state fields stay on the WB type until T7
|
||||
BaseObjectRenderManager.CurrentVAO = 0;
|
||||
BaseObjectRenderManager.CurrentIBO = 0;
|
||||
}
|
||||
|
||||
public void End() {
|
||||
Flush();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
var gl = _graphicsDevice.GL;
|
||||
gl.DeleteVertexArray(_vao);
|
||||
gl.DeleteBuffer(_vbo);
|
||||
gl.DeleteBuffer(_instanceVbo);
|
||||
gl.DeleteBuffer(_ibo);
|
||||
(_shader as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
495
src/AcDream.App/Rendering/Wb/ParticleEmitterRenderer.cs
Normal file
495
src/AcDream.App/Rendering/Wb/ParticleEmitterRenderer.cs
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs
Normal file
120
src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using DatReaderWriter.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Manages texture arrays grouped by (Width, Height, Format).
|
||||
/// Deduplicates textures by a TextureKey and supports reference counting.
|
||||
/// </summary>
|
||||
public class TextureAtlasManager : IDisposable {
|
||||
private static uint _nextSlot = 1;
|
||||
private readonly OpenGLGraphicsDevice _graphicsDevice;
|
||||
private readonly int _textureWidth;
|
||||
private readonly int _textureHeight;
|
||||
private readonly TextureFormat _format;
|
||||
private readonly Dictionary<TextureKey, int> _textureIndices = new();
|
||||
private readonly Dictionary<int, int> _refCounts = new();
|
||||
private readonly Stack<int> _freeSlots = new();
|
||||
private int _nextIndex = 0;
|
||||
private const int InitialCapacity = 32;
|
||||
|
||||
public uint Slot { get; }
|
||||
public ManagedGLTextureArray TextureArray { get; private set; } = null!;
|
||||
public int UsedSlots => _textureIndices.Count;
|
||||
public int TotalSlots => TextureArray?.Size ?? InitialCapacity;
|
||||
public int FreeSlots => TotalSlots - UsedSlots;
|
||||
|
||||
public TextureAtlasManager(OpenGLGraphicsDevice graphicsDevice, int width, int height, TextureFormat format = TextureFormat.RGBA8) {
|
||||
Slot = _nextSlot++;
|
||||
_graphicsDevice = graphicsDevice;
|
||||
_textureWidth = width;
|
||||
_textureHeight = height;
|
||||
_format = format;
|
||||
TextureArray = (ManagedGLTextureArray)graphicsDevice.CreateTextureArrayInternal(format, width, height, InitialCapacity, TextureParameters.ClampToEdge);
|
||||
}
|
||||
|
||||
public int AddTexture(TextureKey key, byte[] data, PixelFormat? uploadPixelFormat = null, PixelType? uploadPixelType = null) {
|
||||
if (_textureIndices.TryGetValue(key, out var existingIndex)) {
|
||||
_refCounts[existingIndex]++;
|
||||
return existingIndex;
|
||||
}
|
||||
|
||||
int index;
|
||||
if (_freeSlots.Count > 0) {
|
||||
index = _freeSlots.Pop();
|
||||
}
|
||||
else {
|
||||
index = _nextIndex++;
|
||||
if (index >= TextureArray.Size) {
|
||||
throw new Exception($"Texture atlas is full! {TextureArray.Size} / {_nextIndex} used.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
TextureArray.UpdateLayer(index, data, uploadPixelFormat, uploadPixelType);
|
||||
_textureIndices[key] = index;
|
||||
_refCounts[index] = 1;
|
||||
return index;
|
||||
}
|
||||
catch (Exception) {
|
||||
if (!_textureIndices.ContainsKey(key)) {
|
||||
_freeSlots.Push(index);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReleaseTexture(TextureKey key) {
|
||||
if (!_textureIndices.TryGetValue(key, out var index)) return;
|
||||
|
||||
if (!_refCounts.ContainsKey(index)) return;
|
||||
|
||||
_refCounts[index]--;
|
||||
if (_refCounts[index] <= 0) {
|
||||
_textureIndices.Remove(key);
|
||||
_refCounts.Remove(index);
|
||||
_freeSlots.Push(index);
|
||||
TextureArray?.RemoveLayer(index);
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasTexture(TextureKey key) => _textureIndices.ContainsKey(key);
|
||||
|
||||
public int GetTextureIndex(TextureKey key) =>
|
||||
_textureIndices.TryGetValue(key, out var index) ? index : -1;
|
||||
|
||||
public void Dispose() {
|
||||
TextureArray?.Dispose();
|
||||
_textureIndices.Clear();
|
||||
_refCounts.Clear();
|
||||
_freeSlots.Clear();
|
||||
}
|
||||
|
||||
public struct TextureKey : IEquatable<TextureKey> {
|
||||
public uint SurfaceId;
|
||||
public uint PaletteId;
|
||||
public StipplingType Stippling;
|
||||
public bool IsSolid;
|
||||
|
||||
public bool Equals(TextureKey other) {
|
||||
return SurfaceId == other.SurfaceId &&
|
||||
PaletteId == other.PaletteId &&
|
||||
Stippling == other.Stippling &&
|
||||
IsSolid == other.IsSolid;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) {
|
||||
return obj is TextureKey other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return HashCode.Combine(SurfaceId, PaletteId, Stippling, IsSolid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using DatReaderWriter.DBObjs;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Silk.NET.OpenGL;
|
||||
using WorldBuilder.Shared.Models;
|
||||
using WorldBuilder.Shared.Services;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
|
@ -28,9 +29,7 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// </summary>
|
||||
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||
{
|
||||
// T3 interim: ObjectMeshManager (T4-to-be-extracted) still expects the WB-original type.
|
||||
// Will become AcDream.App.Rendering.Wb.OpenGLGraphicsDevice when T4 is done.
|
||||
private readonly Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice? _graphicsDevice;
|
||||
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
||||
private readonly DefaultDatReaderWriter? _wbDats;
|
||||
private readonly ObjectMeshManager? _meshManager;
|
||||
private readonly DatCollection? _dats;
|
||||
|
|
@ -75,9 +74,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_dats = dats;
|
||||
// T3 interim: construct the WB-original device for ObjectMeshManager compatibility.
|
||||
// Will swap to AcDream.App.Rendering.Wb.OpenGLGraphicsDevice when T4 extracts ObjectMeshManager.
|
||||
_graphicsDevice = new Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice(gl, logger, new WorldBuilder.Shared.Models.DebugRenderSettings());
|
||||
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
||||
_graphicsDevice.ParticleBatcher = new ParticleBatcher(_graphicsDevice);
|
||||
_wbDats = new DefaultDatReaderWriter(datDir);
|
||||
// Phase 2 diagnostic — replace NullLogger with a Console-backed
|
||||
// logger so WB's internal catch block at ObjectMeshManager.cs:589
|
||||
|
|
@ -86,7 +84,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
// so successful operations stay quiet.
|
||||
_meshManager = new ObjectMeshManager(
|
||||
_graphicsDevice,
|
||||
_wbDats,
|
||||
new DatCollectionAdapter(dats),
|
||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue