Merge pull request 'feat: implement ClusterLOD C# bindings in Ghost.Graphics.Meshlet' (#2) from Julian/GhostEngine:develop into develop
Reviewed-on: #2 Reviewed-by: Misaki <misaki_39@outlook.com>
This commit was merged in pull request #2.
This commit is contained in:
212
AGENT.md
Normal file
212
AGENT.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# GhostEngine — Agent Guidelines
|
||||||
|
|
||||||
|
## Repository Overview
|
||||||
|
|
||||||
|
GhostEngine is a C# game engine targeting .NET 10 / Windows, built around:
|
||||||
|
- **ECS runtime** (`Ghost.Entities`, `Ghost.Core`) — high-performance, AOT-compatible
|
||||||
|
- **Graphics** (`Ghost.Graphics`, `Ghost.Graphics.RHI`, `Ghost.Graphics.D3D12`) — D3D12 RHI
|
||||||
|
- **Editor** (`Ghost.Editor`, `Ghost.Editor.Core`, `Ghost.DSL`, `Ghost.Data`) — WinUI 3 (WindowsAppSDK)
|
||||||
|
- **Third-party bindings** (`Ghost.FMOD`, `Ghost.MeshOptimizer`, `Ghost.Nvtt`, `Ghost.Ufbx`)
|
||||||
|
- **Tools** (`Ghost.NativeWrapperGen`)
|
||||||
|
|
||||||
|
Solution file: `src/GhostEngine.slnx`
|
||||||
|
All commands below should be run from the `src/` directory unless noted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Build entire solution (x64, Debug)
|
||||||
|
dotnet build GhostEngine.slnx -c Debug -p:Platform=x64
|
||||||
|
|
||||||
|
# Build entire solution (Release)
|
||||||
|
dotnet build GhostEngine.slnx -c Release -p:Platform=x64
|
||||||
|
|
||||||
|
# Build a single project
|
||||||
|
dotnet build Runtime/Ghost.Entities/Ghost.Entities.csproj
|
||||||
|
|
||||||
|
# Clean
|
||||||
|
dotnet clean GhostEngine.slnx
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Editor projects (`Ghost.Editor`, `Ghost.Editor.Core`) require
|
||||||
|
> `net10.0-windows10.0.22621.0` and the Windows App SDK. They will only build
|
||||||
|
> on a Windows machine with the correct SDK installed. Platform-agnostic runtime
|
||||||
|
> and test projects target plain `net10.0`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Commands
|
||||||
|
|
||||||
|
There are two test frameworks in use:
|
||||||
|
|
||||||
|
### MSTest — `Ghost.UnitTest`
|
||||||
|
Standard `dotnet test` runner. Tests are parallelized at method level by default.
|
||||||
|
Currently the integration test file is `#if false`-guarded until the asset
|
||||||
|
service is fully wired.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Run all MSTest tests
|
||||||
|
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj -c Debug -p:Platform=x64
|
||||||
|
|
||||||
|
# Run a single test method by name
|
||||||
|
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj \
|
||||||
|
--filter "FullyQualifiedName~TestAutoMetaGeneration_WhenFileCreated"
|
||||||
|
|
||||||
|
# Run a single test class
|
||||||
|
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj \
|
||||||
|
--filter "ClassName~AssetDatabaseIntegrationTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom TestRunner — `Ghost.MicroTest` / `Ghost.Entities.Test`
|
||||||
|
These are console executables driven by `Ghost.Test.Core.TestRunner`. There is
|
||||||
|
no `dotnet test` integration; run them directly:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Micro tests (native binding smoke tests)
|
||||||
|
dotnet run --project Test/Ghost.MicroTest/Ghost.MicroTest.csproj
|
||||||
|
|
||||||
|
# ECS benchmarks / manual tests
|
||||||
|
dotnet run --project Test/Ghost.Entities.Test/Ghost.Entities.Test.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
To run a specific `ITest` implementation, edit `Program.cs` in the respective
|
||||||
|
project and call `TestRunner.Run<YourTestClass>()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### EditorConfig (enforced — `src/.editorconfig`)
|
||||||
|
- Max line length: **200**
|
||||||
|
- Opening braces always on a **new line** for all C# constructs
|
||||||
|
- Single-line statements and blocks are **preserved** (not force-expanded)
|
||||||
|
- **No** primary constructors (`csharp_style_prefer_primary_constructors = false`)
|
||||||
|
- `System.*` using directives are **not** sorted first
|
||||||
|
- Import directive groups are **not** separated by blank lines
|
||||||
|
- Collection expressions and collection initializer syntax are **disabled**
|
||||||
|
(`dotnet_style_prefer_collection_expression = false`)
|
||||||
|
|
||||||
|
### Language
|
||||||
|
- C# `latest` (runtime/test projects) or `preview` (editor projects, for `field`
|
||||||
|
keyword support in .NET 10)
|
||||||
|
- Nullable reference types: **enabled** everywhere (`<Nullable>enable</Nullable>`)
|
||||||
|
- Implicit usings: **enabled** (`<ImplicitUsings>enable</ImplicitUsings>`)
|
||||||
|
- Unsafe blocks: **enabled** where needed (ECS, graphics, native bindings)
|
||||||
|
|
||||||
|
### Namespaces & File Layout
|
||||||
|
- One type per file; file name matches type name exactly
|
||||||
|
- Namespace matches folder structure: `Ghost.<Module>[.<SubFolder>]`
|
||||||
|
- `partial` classes are split across files named `TypeName.Purpose.cs`
|
||||||
|
(e.g. `EntityManager.cs`, `EntityManager.Managed.cs`)
|
||||||
|
- `AssemblyInfo.cs` holds `[assembly: InternalsVisibleTo(...)]` and assembly
|
||||||
|
attributes; do not scatter these across regular source files
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
| Symbol | Convention | Example |
|
||||||
|
|--------|-----------|---------|
|
||||||
|
| Private fields | `_camelCase` | `_jobScheduler` |
|
||||||
|
| Private static fields | `s_camelCase` | `s_worlds`, `s_logger` |
|
||||||
|
| Constants (public/private) | `UPPER_SNAKE_CASE` | `ASSET_EXTENSION`, `ASSETS_FOLDER_NAME` |
|
||||||
|
| Properties & public members | `PascalCase` | `EntityManager`, `IsSuccess` |
|
||||||
|
| Local variables / params | `camelCase` | `entityCapacity`, `signatureHash` |
|
||||||
|
| Interfaces | `I` prefix | `IComponent`, `ISystem`, `ITest` |
|
||||||
|
| Generic type parameters | `T`, `TKey`, `TValue`, `E` | |
|
||||||
|
| Type-tagged structs (handles) | Generic param encodes context | `Handle<T>`, `Identifier<T>`, `Key64<T>` |
|
||||||
|
|
||||||
|
### Types & Structs
|
||||||
|
- Prefer `readonly struct` for value types that are logically immutable.
|
||||||
|
- Prefer `ref struct` / `readonly ref struct` for stack-only types
|
||||||
|
(`RefResult<T,E>`, `SystemAPI`, `ChunkView`).
|
||||||
|
- Use `partial class` to split large classes by concern.
|
||||||
|
- Avoid primary constructors (disabled by editorconfig).
|
||||||
|
- Use the `field` keyword (preview feature) for auto-property backing fields
|
||||||
|
where it simplifies code — only in editor projects that opt in via
|
||||||
|
`<langversion>preview</langversion>`.
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- `using` directives at the top of each file, before the `namespace` declaration.
|
||||||
|
- No blank line between `using` groups (enforced by editorconfig).
|
||||||
|
- `System.*` namespaces may appear in any order alongside project namespaces.
|
||||||
|
- Prefer specific `using` imports over global usings for clarity in low-level
|
||||||
|
performance-critical files.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- **Return `Result` / `Result<T>` instead of throwing** for expected failures
|
||||||
|
(file-not-found, invalid args, etc.).
|
||||||
|
`Result.Success()` / `Result.Failure(message)` or `Result.Failure(Error.XXX)`.
|
||||||
|
- Use the typed `Error` enum (`Ghost.Core.Error`) for structured error codes.
|
||||||
|
- Use `result.ThrowIfFailed()` / `result.GetValueOrThrow()` extension methods at
|
||||||
|
call sites that want throw-on-failure semantics.
|
||||||
|
- **Throw exceptions** only for programming errors / invariant violations
|
||||||
|
(corrupt state, null-ref on internal APIs).
|
||||||
|
- In performance-critical paths, guard validation behind `#if DEBUG || GHOST_EDITOR`
|
||||||
|
to eliminate overhead in release builds.
|
||||||
|
- `Logger.LogError(...)` / `Logger.LogWarning(...)` for non-fatal operational
|
||||||
|
issues; do not use `Console.WriteLine` in production library code.
|
||||||
|
|
||||||
|
### Performance Patterns
|
||||||
|
- Annotate hot paths with `[MethodImpl(MethodImplOptions.AggressiveInlining)]`.
|
||||||
|
- Annotate log/assert helpers with `[StackTraceHidden]`.
|
||||||
|
- Prefer `stackalloc` + `Span<T>` over heap allocation for small temporary arrays.
|
||||||
|
- Use the `Misaki.HighPerformance.*` allocation APIs (`AllocationManager`,
|
||||||
|
`UnsafeList<T>`, `UnsafeHashMap<T,V>`, etc.) for long-lived unmanaged buffers.
|
||||||
|
- All runtime/ECS types must be AOT-compatible and trimmable (set
|
||||||
|
`<IsAotCompatible>True</IsAotCompatible>` and `<IsTrimmable>True</IsTrimmable>`
|
||||||
|
in Release config).
|
||||||
|
- Avoid LINQ in hot paths; use `for` loops or `foreach` over `Span<T>`.
|
||||||
|
|
||||||
|
### Attributes & Extensibility
|
||||||
|
- Custom attributes for editor extension points inherit from
|
||||||
|
`DiscoverableAttributeBase` (discovered at startup via `TypeCache`).
|
||||||
|
- Use `[UpdateAfter(typeof(X))]` / `[UpdateBefore(typeof(X))]` to declare
|
||||||
|
`ISystem` ordering dependencies; `SystemGroup.SortSystems()` topologically sorts
|
||||||
|
them at startup.
|
||||||
|
- Use `[EditorInjection(ServiceLifetime.Singleton)]` to register editor services
|
||||||
|
via DI without manual wiring.
|
||||||
|
|
||||||
|
### XML Documentation
|
||||||
|
- All public API surface should have `<summary>` doc-comments.
|
||||||
|
- Use `<remarks>` for non-obvious behavior or threading constraints.
|
||||||
|
- Document thread-safety expectations explicitly (see `EntityCommandBuffer` /
|
||||||
|
`AssetRegistry` as reference).
|
||||||
|
|
||||||
|
### Preprocessor Defines
|
||||||
|
| Define | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| `DEBUG` | Standard debug build |
|
||||||
|
| `GHOST_EDITOR` | Editor build (extra validation, reflection helpers) |
|
||||||
|
| `PLATEFORME_WIN64` | Windows 64-bit platform target |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
GhostEngine.slnx # Solution
|
||||||
|
.editorconfig # Formatting rules
|
||||||
|
Runtime/
|
||||||
|
Ghost.Core/ # Core types: Result, Handle, Logger, math helpers
|
||||||
|
Ghost.Engine/ # Engine entry point & loop
|
||||||
|
Ghost.Entities/ # ECS: World, Entity, Component, System
|
||||||
|
Ghost.Generator/ # Source generators
|
||||||
|
Ghost.Graphics/ # High-level graphics API
|
||||||
|
Ghost.Graphics.RHI/ # Render hardware interface abstractions
|
||||||
|
Ghost.Graphics.D3D12/ # D3D12 backend
|
||||||
|
Editor/
|
||||||
|
Ghost.Editor/ # WinUI 3 shell
|
||||||
|
Ghost.Editor.Core/ # Editor services, asset registry, inspector
|
||||||
|
Ghost.DSL/ # Shader DSL compiler
|
||||||
|
Ghost.Data/ # Serialization / project data models
|
||||||
|
ThridParty/ # Native binding wrappers (FMOD, MeshOptimizer, Nvtt, Ufbx)
|
||||||
|
Test/
|
||||||
|
Ghost.Test.Core/ # Shared ITest / TestRunner infrastructure
|
||||||
|
Ghost.UnitTest/ # MSTest integration tests
|
||||||
|
Ghost.MicroTest/ # Native binding smoke tests (console app)
|
||||||
|
Ghost.Entities.Test/ # ECS benchmarks (BenchmarkDotNet, console app)
|
||||||
|
Ghost.Shader.Test/ # Shader DSL manual tests (console app)
|
||||||
|
Tools/
|
||||||
|
Ghost.NativeWrapperGen/ # Code-gen tool for native wrappers
|
||||||
|
```
|
||||||
19
README_julian.md
Normal file
19
README_julian.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Julian's Workspace: GhostEngine
|
||||||
|
|
||||||
|
Misaki has set up my environment to work from my own fork of "GhostEngine." Here's how I'll manage my workflow:
|
||||||
|
|
||||||
|
## What I'll Do
|
||||||
|
|
||||||
|
1. Write, commit, and push code changes directly to my local fork.
|
||||||
|
2. Use the "tea" CLI tool when I'm ready to create PRs back to the original repository.
|
||||||
|
|
||||||
|
## Tools & Resources
|
||||||
|
|
||||||
|
- **Gitea:** The repository is hosted here. I can authenticate directly.
|
||||||
|
- **Tea CLI:** Details for creating PRs can be found in the `gitea-pr` and `gitea` skill documentation.
|
||||||
|
|
||||||
|
## Collaboration Rules
|
||||||
|
|
||||||
|
We'll refine separately; I'll merge when...
|
||||||
|
...the functionality is complete and tested.
|
||||||
|
**PR Description Tip:** Include concise changelog markdown.
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
<ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" />
|
<ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" />
|
||||||
<ProjectReference Include="..\Ghost.Graphics.D3D12\Ghost.Graphics.D3D12.csproj" />
|
<ProjectReference Include="..\Ghost.Graphics.D3D12\Ghost.Graphics.D3D12.csproj" />
|
||||||
<ProjectReference Include="..\Ghost.Graphics.RHI\Ghost.Graphics.RHI.csproj" />
|
<ProjectReference Include="..\Ghost.Graphics.RHI\Ghost.Graphics.RHI.csproj" />
|
||||||
|
<ProjectReference Include="..\..\ThridParty\Ghost.MeshOptimizer\Ghost.MeshOptimizer.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
10
src/Runtime/Ghost.Graphics/Meshlet/ClodBounds.cs
Normal file
10
src/Runtime/Ghost.Graphics/Meshlet/ClodBounds.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
public struct ClodBounds
|
||||||
|
{
|
||||||
|
public Vector3 center;
|
||||||
|
public float radius;
|
||||||
|
public float error;
|
||||||
|
}
|
||||||
48
src/Runtime/Ghost.Graphics/Meshlet/ClodBoundsHelper.cs
Normal file
48
src/Runtime/Ghost.Graphics/Meshlet/ClodBoundsHelper.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
internal static class ClodBoundsHelper
|
||||||
|
{
|
||||||
|
public static unsafe ClodBounds ComputeBounds(ClodMesh mesh, UnsafeList<uint> indices, float error)
|
||||||
|
{
|
||||||
|
var bounds = MeshOptApi.ComputeClusterBounds((uint*)indices.GetUnsafePtr(), (nuint)indices.Count, mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
|
||||||
|
return new ClodBounds
|
||||||
|
{
|
||||||
|
center = new Vector3(bounds.center[0], bounds.center[1], bounds.center[2]),
|
||||||
|
radius = bounds.radius,
|
||||||
|
error = error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
|
||||||
|
{
|
||||||
|
var boundsList = new UnsafeList<ClodBounds>(group.Count, Allocator.Temp);
|
||||||
|
for (int j = 0; j < group.Count; j++)
|
||||||
|
boundsList.Add(clusters[group[j]].bounds);
|
||||||
|
|
||||||
|
var merged = MeshOptApi.ComputeSphereBounds(
|
||||||
|
(float*)boundsList.GetUnsafePtr(),
|
||||||
|
(nuint)group.Count,
|
||||||
|
(nuint)sizeof(ClodBounds),
|
||||||
|
(float*)boundsList.GetUnsafePtr() + 3,
|
||||||
|
(nuint)sizeof(ClodBounds)
|
||||||
|
);
|
||||||
|
|
||||||
|
float maxError = 0.0f;
|
||||||
|
for (int j = 0; j < group.Count; j++)
|
||||||
|
maxError = Math.Max(maxError, clusters[group[j]].bounds.error);
|
||||||
|
|
||||||
|
boundsList.Dispose();
|
||||||
|
return new ClodBounds
|
||||||
|
{
|
||||||
|
center = new Vector3(merged.center[0], merged.center[1], merged.center[2]),
|
||||||
|
radius = merged.radius,
|
||||||
|
error = maxError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
176
src/Runtime/Ghost.Graphics/Meshlet/ClodBuilder.cs
Normal file
176
src/Runtime/Ghost.Graphics/Meshlet/ClodBuilder.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
internal struct Cluster
|
||||||
|
{
|
||||||
|
public nuint vertices;
|
||||||
|
public UnsafeList<uint> indices;
|
||||||
|
public int group;
|
||||||
|
public int refined;
|
||||||
|
public ClodBounds bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe static class ClodBuilder
|
||||||
|
{
|
||||||
|
public static nuint Build(ClodConfig config, ClodMesh mesh, void* outputContext, ClodOutputDelegate outputCallback)
|
||||||
|
{
|
||||||
|
Debug.Assert(mesh.vertexAttributesStride % (nuint)sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)");
|
||||||
|
|
||||||
|
var locks = new UnsafeList<byte>((int)mesh.vertexCount, Allocator.Temp);
|
||||||
|
locks.AsSpan().Fill(0);
|
||||||
|
|
||||||
|
var remap = new UnsafeList<uint>((int)mesh.vertexCount, Allocator.Temp);
|
||||||
|
remap.Resize((int)mesh.vertexCount);
|
||||||
|
MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
|
||||||
|
|
||||||
|
if (mesh.attributeProtectMask != 0)
|
||||||
|
{
|
||||||
|
nuint maxAttributes = mesh.vertexAttributesStride / sizeof(float);
|
||||||
|
for (nuint i = 0; i < mesh.vertexCount; i++)
|
||||||
|
{
|
||||||
|
uint r = ((uint*)remap.GetUnsafePtr())[(int)i];
|
||||||
|
for (nuint j = 0; j < maxAttributes; j++)
|
||||||
|
{
|
||||||
|
if ((r != i) && ((mesh.attributeProtectMask & (1u << (int)j)) != 0))
|
||||||
|
{
|
||||||
|
if (mesh.vertexAttributes[i * maxAttributes + j] != mesh.vertexAttributes[r * maxAttributes + j])
|
||||||
|
{
|
||||||
|
((byte*)locks.GetUnsafePtr())[(int)i] |= (byte)(Api.meshopt_SimplifyVertex_Protect & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var clusters = ClodInternal.Clusterize(config, mesh, mesh.indices, mesh.indexCount, Allocator.Persistent);
|
||||||
|
|
||||||
|
for (int i = 0; i < clusters.Count; i++)
|
||||||
|
{
|
||||||
|
clusters[i].bounds = ClodBoundsHelper.ComputeBounds(mesh, clusters[i].indices, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pending = new UnsafeList<int>(clusters.Count, Allocator.Temp);
|
||||||
|
for (int i = 0; i < clusters.Count; i++)
|
||||||
|
pending.Add(i);
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
|
||||||
|
while (pending.Count > 1)
|
||||||
|
{
|
||||||
|
var groups = ClodPartition.Partition(config, mesh, clusters, pending, remap, Allocator.Temp);
|
||||||
|
pending.Clear();
|
||||||
|
|
||||||
|
ClodBoundary.LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
|
||||||
|
|
||||||
|
for (int i = 0; i < groups.Count; i++)
|
||||||
|
{
|
||||||
|
var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, Allocator.Temp);
|
||||||
|
for (int j = 0; j < groups[i].Count; j++)
|
||||||
|
{
|
||||||
|
var clusterIndices = clusters[groups[i][j]].indices;
|
||||||
|
for (int k = 0; k < clusterIndices.Count; k++)
|
||||||
|
merged.Add(clusterIndices[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
nuint targetSize = ((nuint)merged.Count / 3) * (nuint)config.simplifyRatio * 3;
|
||||||
|
var bounds = ClodBoundsHelper.MergeBounds(clusters, groups[i]);
|
||||||
|
|
||||||
|
float error = 0.0f;
|
||||||
|
var simplified = ClodSimplify.Simplify(config, mesh, merged, locks, targetSize, &error);
|
||||||
|
|
||||||
|
if ((nuint)simplified.Count > (nuint)(merged.Count * config.simplifyThreshold))
|
||||||
|
{
|
||||||
|
bounds.error = float.MaxValue;
|
||||||
|
OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
|
||||||
|
merged.Dispose();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive;
|
||||||
|
|
||||||
|
int refined = OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
|
||||||
|
|
||||||
|
for (int j = 0; j < groups[i].Count; j++)
|
||||||
|
clusters[groups[i][j]].indices.Dispose();
|
||||||
|
|
||||||
|
var split = ClodInternal.Clusterize(config, mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Count, Allocator.Persistent);
|
||||||
|
for (int j = 0; j < split.Count; j++)
|
||||||
|
{
|
||||||
|
split[j].refined = refined;
|
||||||
|
split[j].bounds = bounds;
|
||||||
|
clusters.Add(split[j]);
|
||||||
|
pending.Add(clusters.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
split.Dispose();
|
||||||
|
merged.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < groups.Count; i++)
|
||||||
|
groups[i].Dispose();
|
||||||
|
groups.Dispose();
|
||||||
|
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.Count > 0)
|
||||||
|
{
|
||||||
|
var bounds = clusters[pending[0]].bounds;
|
||||||
|
bounds.error = float.MaxValue;
|
||||||
|
OutputGroup(config, mesh, clusters, pending, bounds, depth, outputContext, outputCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
nuint finalClusterCount = (nuint)clusters.Count;
|
||||||
|
|
||||||
|
for (int i = 0; i < clusters.Count; i++)
|
||||||
|
clusters[i].indices.Dispose();
|
||||||
|
clusters.Dispose();
|
||||||
|
locks.Dispose();
|
||||||
|
remap.Dispose();
|
||||||
|
pending.Dispose();
|
||||||
|
|
||||||
|
return finalClusterCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int OutputGroup(
|
||||||
|
ClodConfig config,
|
||||||
|
ClodMesh mesh,
|
||||||
|
UnsafeList<Cluster> clusters,
|
||||||
|
UnsafeList<int> group,
|
||||||
|
ClodBounds simplified,
|
||||||
|
int depth,
|
||||||
|
void* outputContext,
|
||||||
|
ClodOutputDelegate outputCallback
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var groupClusters = new UnsafeList<ClodCluster>(group.Count, Allocator.Temp);
|
||||||
|
|
||||||
|
for (int i = 0; i < group.Count; i++)
|
||||||
|
{
|
||||||
|
ref var srcCluster = ref clusters[group[i]];
|
||||||
|
groupClusters.Add(new ClodCluster
|
||||||
|
{
|
||||||
|
refined = srcCluster.refined,
|
||||||
|
bounds = (config.optimizeBounds && srcCluster.refined != -1)
|
||||||
|
? ClodBoundsHelper.ComputeBounds(mesh, srcCluster.indices, srcCluster.bounds.error)
|
||||||
|
: srcCluster.bounds,
|
||||||
|
indices = (uint*)srcCluster.indices.GetUnsafePtr(),
|
||||||
|
indexCount = (nuint)srcCluster.indices.Count,
|
||||||
|
vertexCount = srcCluster.vertices
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
|
||||||
|
int result = outputCallback != null
|
||||||
|
? outputCallback(outputContext, clodGroup, (ClodCluster*)groupClusters.GetUnsafePtr(), (nuint)groupClusters.Count)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
groupClusters.Dispose();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Runtime/Ghost.Graphics/Meshlet/ClodConfig.cs
Normal file
37
src/Runtime/Ghost.Graphics/Meshlet/ClodConfig.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
public struct ClodConfig
|
||||||
|
{
|
||||||
|
public nuint maxVertices;
|
||||||
|
public nuint minTriangles;
|
||||||
|
public nuint maxTriangles;
|
||||||
|
|
||||||
|
public bool partitionSpatial;
|
||||||
|
public bool partitionSort;
|
||||||
|
public nuint partitionSize;
|
||||||
|
|
||||||
|
public bool clusterSpatial;
|
||||||
|
public float clusterFillWeight;
|
||||||
|
public float clusterSplitFactor;
|
||||||
|
|
||||||
|
public float simplifyRatio;
|
||||||
|
public float simplifyThreshold;
|
||||||
|
|
||||||
|
public float simplifyErrorMergePrevious;
|
||||||
|
public float simplifyErrorMergeAdditive;
|
||||||
|
|
||||||
|
public float simplifyErrorFactorSloppy;
|
||||||
|
|
||||||
|
public float simplifyErrorEdgeLimit;
|
||||||
|
|
||||||
|
public bool simplifyPermissive;
|
||||||
|
|
||||||
|
public bool simplifyFallbackPermissive;
|
||||||
|
public bool simplifyFallbackSloppy;
|
||||||
|
|
||||||
|
public bool simplifyRegularize;
|
||||||
|
|
||||||
|
public bool optimizeBounds;
|
||||||
|
|
||||||
|
public bool optimizeClusters;
|
||||||
|
}
|
||||||
83
src/Runtime/Ghost.Graphics/Meshlet/ClodInternal.cs
Normal file
83
src/Runtime/Ghost.Graphics/Meshlet/ClodInternal.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
internal static class ClodInternal
|
||||||
|
{
|
||||||
|
public static unsafe UnsafeList<Cluster> Clusterize(ClodConfig config, ClodMesh mesh, uint* indices, nuint indexCount, Allocator allocator)
|
||||||
|
{
|
||||||
|
nuint maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles);
|
||||||
|
|
||||||
|
var meshlets = new UnsafeList<meshopt_Meshlet>((int)maxMeshlets, Allocator.Temp);
|
||||||
|
meshlets.Resize((int)maxMeshlets);
|
||||||
|
var meshletVertices = new UnsafeList<uint>((int)indexCount, Allocator.Temp);
|
||||||
|
meshletVertices.Resize((int)indexCount);
|
||||||
|
var meshletTriangles = new UnsafeList<byte>((int)indexCount, Allocator.Temp);
|
||||||
|
meshletTriangles.Resize((int)indexCount);
|
||||||
|
|
||||||
|
meshopt_Meshlet* pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr();
|
||||||
|
uint* pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr();
|
||||||
|
byte* pMeshletTriangles = (byte*)meshletTriangles.GetUnsafePtr();
|
||||||
|
|
||||||
|
nuint meshletCount;
|
||||||
|
if (config.clusterSpatial)
|
||||||
|
{
|
||||||
|
meshletCount = pMeshlets[0].BuildsSpatial(
|
||||||
|
pMeshletVertices, pMeshletTriangles,
|
||||||
|
indices, indexCount,
|
||||||
|
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
|
||||||
|
config.maxVertices, config.minTriangles, config.maxTriangles,
|
||||||
|
config.clusterFillWeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
meshletCount = pMeshlets[0].BuildsFlex(
|
||||||
|
pMeshletVertices, pMeshletTriangles,
|
||||||
|
indices, indexCount,
|
||||||
|
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
|
||||||
|
config.maxVertices, config.minTriangles, config.maxTriangles,
|
||||||
|
0.0f, config.clusterSplitFactor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var clusters = new UnsafeList<Cluster>((int)meshletCount, allocator);
|
||||||
|
|
||||||
|
for (nuint i = 0; i < meshletCount; i++)
|
||||||
|
{
|
||||||
|
ref var meshlet = ref pMeshlets[i];
|
||||||
|
|
||||||
|
if (config.optimizeClusters)
|
||||||
|
{
|
||||||
|
MeshOptApi.OptimizeMeshlet(
|
||||||
|
pMeshletVertices + meshlet.vertex_offset,
|
||||||
|
pMeshletTriangles + meshlet.triangle_offset,
|
||||||
|
meshlet.triangle_count,
|
||||||
|
meshlet.vertex_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cluster = new Cluster
|
||||||
|
{
|
||||||
|
vertices = meshlet.vertex_count,
|
||||||
|
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), allocator),
|
||||||
|
group = -1,
|
||||||
|
refined = -1
|
||||||
|
};
|
||||||
|
|
||||||
|
for (nuint j = 0; j < meshlet.triangle_count * 3; j++)
|
||||||
|
cluster.indices.Add(pMeshletVertices[meshlet.vertex_offset + pMeshletTriangles[meshlet.triangle_offset + j]]);
|
||||||
|
|
||||||
|
clusters.Add(cluster);
|
||||||
|
}
|
||||||
|
|
||||||
|
meshlets.Dispose();
|
||||||
|
meshletVertices.Dispose();
|
||||||
|
meshletTriangles.Dispose();
|
||||||
|
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Runtime/Ghost.Graphics/Meshlet/ClodInternal_Boundary.cs
Normal file
48
src/Runtime/Ghost.Graphics/Meshlet/ClodInternal_Boundary.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
internal static class ClodBoundary
|
||||||
|
{
|
||||||
|
public static unsafe void LockBoundary(UnsafeList<byte> locks, UnsafeList<UnsafeList<int>> groups, UnsafeList<Cluster> clusters, UnsafeList<uint> remap, byte* vertexLock)
|
||||||
|
{
|
||||||
|
byte* pLocks = (byte*)locks.GetUnsafePtr();
|
||||||
|
uint* pRemap = (uint*)remap.GetUnsafePtr();
|
||||||
|
|
||||||
|
for (int i = 0; i < locks.Count; i++)
|
||||||
|
pLocks[i] = unchecked((byte)(pLocks[i] & ~((1 << 0) | (1 << 7))));
|
||||||
|
|
||||||
|
for (int i = 0; i < groups.Count; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < groups[i].Count; j++)
|
||||||
|
{
|
||||||
|
var cluster = clusters[groups[i][j]];
|
||||||
|
for (int k = 0; k < cluster.indices.Count; k++)
|
||||||
|
{
|
||||||
|
uint r = pRemap[(int)cluster.indices[k]];
|
||||||
|
pLocks[r] |= (byte)(pLocks[r] >> 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < groups[i].Count; j++)
|
||||||
|
{
|
||||||
|
var cluster = clusters[groups[i][j]];
|
||||||
|
for (int k = 0; k < cluster.indices.Count; k++)
|
||||||
|
{
|
||||||
|
uint r = pRemap[(int)cluster.indices[k]];
|
||||||
|
pLocks[r] |= (byte)(1 << 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < locks.Count; i++)
|
||||||
|
{
|
||||||
|
uint r = pRemap[i];
|
||||||
|
pLocks[i] = (byte)((pLocks[r] & 1) | (pLocks[i] & (byte)(Api.meshopt_SimplifyVertex_Protect & 0xFF)));
|
||||||
|
if (vertexLock != null)
|
||||||
|
pLocks[i] |= vertexLock[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/Runtime/Ghost.Graphics/Meshlet/ClodInternal_Partition.cs
Normal file
64
src/Runtime/Ghost.Graphics/Meshlet/ClodInternal_Partition.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
internal static class ClodPartition
|
||||||
|
{
|
||||||
|
public static unsafe UnsafeList<UnsafeList<int>> Partition(ClodConfig config, ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeList<uint> remap, Allocator allocator)
|
||||||
|
{
|
||||||
|
if (pending.Count <= (int)config.partitionSize)
|
||||||
|
{
|
||||||
|
var single = new UnsafeList<UnsafeList<int>>(1, allocator);
|
||||||
|
single.Add(pending);
|
||||||
|
return single;
|
||||||
|
}
|
||||||
|
|
||||||
|
nuint totalIndexCount = 0;
|
||||||
|
for (int i = 0; i < pending.Count; i++)
|
||||||
|
totalIndexCount += (nuint)clusters[pending[i]].indices.Count;
|
||||||
|
|
||||||
|
var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, Allocator.Temp);
|
||||||
|
var clusterCounts = new UnsafeList<uint>(pending.Count, Allocator.Temp);
|
||||||
|
|
||||||
|
nuint offset = 0;
|
||||||
|
for (int i = 0; i < pending.Count; i++)
|
||||||
|
{
|
||||||
|
var cluster = clusters[pending[i]];
|
||||||
|
clusterCounts.Add((uint)cluster.indices.Count);
|
||||||
|
for (int j = 0; j < cluster.indices.Count; j++)
|
||||||
|
clusterIndices.Add(((uint*)remap.GetUnsafePtr())[(int)cluster.indices[j]]);
|
||||||
|
offset += (nuint)cluster.indices.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clusterPart = new UnsafeList<uint>(pending.Count, Allocator.Temp);
|
||||||
|
clusterPart.Resize(pending.Count);
|
||||||
|
|
||||||
|
nuint partitionCount = MeshOptApi.PartitionClusters(
|
||||||
|
(uint*)clusterPart.GetUnsafePtr(),
|
||||||
|
(uint*)clusterIndices.GetUnsafePtr(),
|
||||||
|
totalIndexCount,
|
||||||
|
(uint*)clusterCounts.GetUnsafePtr(),
|
||||||
|
(nuint)pending.Count,
|
||||||
|
config.partitionSpatial ? mesh.vertexPositions : null,
|
||||||
|
(nuint)remap.Count,
|
||||||
|
mesh.vertexPositionsStride,
|
||||||
|
config.partitionSize
|
||||||
|
);
|
||||||
|
|
||||||
|
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, allocator);
|
||||||
|
for (nuint i = 0; i < partitionCount; i++)
|
||||||
|
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), allocator));
|
||||||
|
|
||||||
|
for (int i = 0; i < pending.Count; i++)
|
||||||
|
partitions[(int)((uint*)clusterPart.GetUnsafePtr())[i]].Add(pending[i]);
|
||||||
|
|
||||||
|
clusterIndices.Dispose();
|
||||||
|
clusterCounts.Dispose();
|
||||||
|
clusterPart.Dispose();
|
||||||
|
|
||||||
|
return partitions;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Runtime/Ghost.Graphics/Meshlet/ClodMesh.cs
Normal file
43
src/Runtime/Ghost.Graphics/Meshlet/ClodMesh.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using Misaki.HighPerformance.LowLevel;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
public unsafe struct ClodMesh
|
||||||
|
{
|
||||||
|
public float* vertexPositions;
|
||||||
|
public nuint vertexCount;
|
||||||
|
public nuint vertexPositionsStride;
|
||||||
|
|
||||||
|
public float* vertexAttributes;
|
||||||
|
public nuint vertexAttributesStride;
|
||||||
|
public float* attributeWeights;
|
||||||
|
public nuint attributeCount;
|
||||||
|
|
||||||
|
public uint* indices;
|
||||||
|
public nuint indexCount;
|
||||||
|
|
||||||
|
public byte* vertexLock;
|
||||||
|
public uint attributeProtectMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ClodGroup
|
||||||
|
{
|
||||||
|
public int depth;
|
||||||
|
public ClodBounds simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe struct ClodCluster
|
||||||
|
{
|
||||||
|
public int refined;
|
||||||
|
public ClodBounds bounds;
|
||||||
|
|
||||||
|
public uint* indices;
|
||||||
|
public nuint indexCount;
|
||||||
|
public nuint vertexCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ClodCluster* clusters, nuint clusterCount);
|
||||||
109
src/Runtime/Ghost.Graphics/Meshlet/ClodSimplify.cs
Normal file
109
src/Runtime/Ghost.Graphics/Meshlet/ClodSimplify.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Meshlet;
|
||||||
|
|
||||||
|
internal static class ClodSimplify
|
||||||
|
{
|
||||||
|
public static unsafe UnsafeList<uint> Simplify(
|
||||||
|
ClodConfig config,
|
||||||
|
ClodMesh mesh,
|
||||||
|
UnsafeList<uint> indices,
|
||||||
|
UnsafeList<byte> locks,
|
||||||
|
nuint targetCount,
|
||||||
|
float* error
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (targetCount >= (nuint)indices.Count)
|
||||||
|
return indices;
|
||||||
|
|
||||||
|
var lod = new UnsafeList<uint>(indices.Count, Allocator.Temp);
|
||||||
|
lod.Resize(indices.Count);
|
||||||
|
|
||||||
|
uint options = (uint)(Api.meshopt_SimplifySparse | Api.meshopt_SimplifyErrorAbsolute);
|
||||||
|
if (config.simplifyPermissive)
|
||||||
|
options |= (uint)Api.meshopt_SimplifyPermissive;
|
||||||
|
if (config.simplifyRegularize)
|
||||||
|
options |= (uint)Api.meshopt_SimplifyRegularize;
|
||||||
|
|
||||||
|
nuint resultSize = MeshOptApi.SimplifyWithAttributes(
|
||||||
|
(uint*)lod.GetUnsafePtr(),
|
||||||
|
(uint*)indices.GetUnsafePtr(),
|
||||||
|
(nuint)indices.Count,
|
||||||
|
mesh.vertexPositions,
|
||||||
|
mesh.vertexCount,
|
||||||
|
mesh.vertexPositionsStride,
|
||||||
|
mesh.vertexAttributes,
|
||||||
|
mesh.vertexAttributesStride,
|
||||||
|
mesh.attributeWeights,
|
||||||
|
mesh.attributeCount,
|
||||||
|
(byte*)locks.GetUnsafePtr(),
|
||||||
|
targetCount,
|
||||||
|
float.MaxValue,
|
||||||
|
options,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
lod.Resize((int)resultSize);
|
||||||
|
|
||||||
|
if ((nuint)lod.Count > targetCount && config.simplifyFallbackPermissive && !config.simplifyPermissive)
|
||||||
|
{
|
||||||
|
options |= (uint)Api.meshopt_SimplifyPermissive;
|
||||||
|
resultSize = MeshOptApi.SimplifyWithAttributes(
|
||||||
|
(uint*)lod.GetUnsafePtr(),
|
||||||
|
(uint*)indices.GetUnsafePtr(),
|
||||||
|
(nuint)indices.Count,
|
||||||
|
mesh.vertexPositions,
|
||||||
|
mesh.vertexCount,
|
||||||
|
mesh.vertexPositionsStride,
|
||||||
|
mesh.vertexAttributes,
|
||||||
|
mesh.vertexAttributesStride,
|
||||||
|
mesh.attributeWeights,
|
||||||
|
mesh.attributeCount,
|
||||||
|
(byte*)locks.GetUnsafePtr(),
|
||||||
|
targetCount,
|
||||||
|
float.MaxValue,
|
||||||
|
options,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
lod.Resize((int)resultSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((nuint)lod.Count > targetCount && config.simplifyFallbackSloppy)
|
||||||
|
{
|
||||||
|
*error *= config.simplifyErrorFactorSloppy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.simplifyErrorEdgeLimit > 0)
|
||||||
|
{
|
||||||
|
float maxEdgeSq = 0;
|
||||||
|
uint* pIdx = (uint*)indices.GetUnsafePtr();
|
||||||
|
int posStride = (int)(mesh.vertexPositionsStride / sizeof(float));
|
||||||
|
|
||||||
|
for (int i = 0; i < indices.Count; i += 3)
|
||||||
|
{
|
||||||
|
uint a = pIdx[i], b = pIdx[i + 1], c = pIdx[i + 2];
|
||||||
|
float* va = mesh.vertexPositions + (a * (uint)posStride);
|
||||||
|
float* vb = mesh.vertexPositions + (b * (uint)posStride);
|
||||||
|
float* vc = mesh.vertexPositions + (c * (uint)posStride);
|
||||||
|
|
||||||
|
float dx, dy, dz;
|
||||||
|
dx = va[0] - vb[0]; dy = va[1] - vb[1]; dz = va[2] - vb[2];
|
||||||
|
float eab = dx * dx + dy * dy + dz * dz;
|
||||||
|
dx = va[0] - vc[0]; dy = va[1] - vc[1]; dz = va[2] - vc[2];
|
||||||
|
float eac = dx * dx + dy * dy + dz * dz;
|
||||||
|
dx = vb[0] - vc[0]; dy = vb[1] - vc[1]; dz = vb[2] - vc[2];
|
||||||
|
float ebc = dx * dx + dy * dy + dz * dz;
|
||||||
|
|
||||||
|
float emax = Math.Max(Math.Max(eab, eac), ebc);
|
||||||
|
float emin = Math.Min(Math.Min(eab, eac), ebc);
|
||||||
|
maxEdgeSq = Math.Max(maxEdgeSq, Math.Max(emin, emax / 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
*error = Math.Min(*error, (float)Math.Sqrt(maxEdgeSq) * config.simplifyErrorEdgeLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lod;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
namespace Ghost.MeshOptimizer;
|
namespace Ghost.MeshOptimizer;
|
||||||
|
|
||||||
public unsafe partial struct NvttApi
|
public unsafe partial struct MeshOptApi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// From: <see cref="Api.meshopt_generateVertexRemap(uint*, uint*, nuint, void*, nuint, nuint)" />
|
/// From: <see cref="Api.meshopt_generateVertexRemap(uint*, uint*, nuint, void*, nuint, nuint)" />
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filter": "EXTERN_API",
|
"filter": "EXTERN_API",
|
||||||
"targetType": "NvttApi",
|
"targetType": "MeshOptApi",
|
||||||
"apply": {
|
"apply": {
|
||||||
"type": "STATIC_METHOD",
|
"type": "STATIC_METHOD",
|
||||||
"opts": {
|
"opts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user