diff --git a/.github/workflows/run-dart-tests.yml b/.github/workflows/run-dart-tests.yml index 60ba6cb14..e0603f846 100644 --- a/.github/workflows/run-dart-tests.yml +++ b/.github/workflows/run-dart-tests.yml @@ -389,7 +389,7 @@ jobs: github-token: ${{ github.token }} # To refresh every baseline, first push a capture-only commit, # inspect its dart-*-output-* artifacts, then update this run ID. - run-id: 32575493711 + run-id: 32812764648 name: ${{ matrix.artifact }} path: thermion_dart/test/golden-baseline diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d27432ef..c9e1cf594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,16 +40,30 @@ - Apply overlapping custom morph animations oldest-first so the most recently added animation has final priority for shared targets. Active animations continue to overwrite manual weights on their next update. +- Fix topology-preserving glTF assets so their vertex streams can be updated + through `VertexBuffer.setBufferAt`; editable buffers no longer use + the `BufferObject` backing reserved for unwelded smooth/flat shading swaps. + Buffer updates, flat shading, and stencil highlighting now throw actionable + errors when used with incompatible buffer storage or asset capabilities. + Stencil highlighting accepts both editable and unwelded glTF geometry, plus + procedural geometry, because its silhouette pass only requires preserved + vertex/index buffers; flat shading remains unwelded-only. +- Expose native `VertexBuffer.storageMode` metadata and first-class + `BufferObject` creation, upload, and attachment APIs. `supportsSetBufferAt` + is now derived from immutable metadata supplied by the native builder or + owning asset, without a global pointer registry or duplicated glTF load + state in Dart. Asset-owned vertex buffers are explicitly borrowed. ### Breaking changes - Replace the `rebuildVertices` in `ThermionViewer.loadGltf`, `ThermionViewer.loadGltfFromBuffer`, and `FilamentApp.loadGltfFromBuffer` with - `vertexBufferMode: VertexBufferMode.unwelded`. Use - `VertexBufferMode.editable` when mutable indexed topology is required. - `original` leaves gltfio geometry untouched, `unwelded` creates per-triangle - vertices with barycentric coordinates, and `editable` exposes mutable vertex - buffers while preserving source vertex order, indices, and morph-target - compatibility. + `requiredGeometryCapabilities`. An empty set leaves gltfio geometry untouched; + requesting `barycentrics` or `uniqueTriangleCorners` creates per-triangle vertices, while + requesting `writableVertices`, `preservedTopology`, or `accessibleGeometryBuffers` + preserves source vertex order, indices, and morph-target compatibility in + directly writable buffers. + Assets report the complete capability set actually provided through + `ThermionAsset.geometryCapabilities`. - remove the unused `FilamentApp.createColorGrading` — it returned a raw pointer nobody could destroy; use `View.createColorGradingBuilder().build()` instead. diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index 33e6665dc..05c7cd3b8 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -614,6 +614,7 @@ external ffi.Pointer NameComponentManager_getName( ffi.Pointer>, ffi.Int, ffi.UnsignedInt, + ffi.UnsignedInt, Aabb3, ) >(isLeaf: true) @@ -624,6 +625,7 @@ external ffi.Pointer SceneAsset_createFromBuffers( ffi.Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, ); @@ -633,7 +635,7 @@ external ffi.Pointer SceneAsset_createFromBuffers( ffi.Pointer, ffi.Pointer, ffi.Pointer, - ffi.UnsignedInt, + ffi.Uint32, ) >(isLeaf: true) external ffi.Pointer SceneAsset_createFromFilamentAsset( @@ -641,7 +643,7 @@ external ffi.Pointer SceneAsset_createFromFilamentAsset( ffi.Pointer tAssetLoader, ffi.Pointer tNameComponentManager, ffi.Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ); @ffi.Native Function(ffi.Pointer)>(isLeaf: true) @@ -698,12 +700,21 @@ external ffi.Pointer SceneAsset_createInstance( @ffi.Native)>(isLeaf: true) external Aabb3 SceneAsset_getBoundingBox(ffi.Pointer asset); +@ffi.Native)>(isLeaf: true) +external int SceneAsset_getGeometryCapabilities(ffi.Pointer asset); + +@ffi.Native)>(isLeaf: true) +external bool SceneAsset_supportsFlatShading(ffi.Pointer asset); + @ffi.Native Function(ffi.Pointer, ffi.Int)>(isLeaf: true) external ffi.Pointer SceneAsset_getVertexBuffer( ffi.Pointer tSceneAsset, int primitiveIndex, ); +@ffi.Native, ffi.Int)>(isLeaf: true) +external int SceneAsset_getVertexBufferStorageMode(ffi.Pointer tSceneAsset, int primitiveIndex); + @ffi.Native Function(ffi.Pointer, ffi.Int)>(isLeaf: true) external ffi.Pointer SceneAsset_getIndexBuffer(ffi.Pointer tSceneAsset, int primitiveIndex); @@ -1396,6 +1407,12 @@ external void VertexBufferBuilder_bufferCount(ffi.Pointer @ffi.Native, ffi.Uint32)>(isLeaf: true) external void VertexBufferBuilder_vertexCount(ffi.Pointer builder, int count); +@ffi.Native, ffi.Bool)>(isLeaf: true) +external void VertexBufferBuilder_enableBufferObjects(ffi.Pointer builder, bool enabled); + +@ffi.Native)>(isLeaf: true) +external int VertexBufferBuilder_getStorageMode(ffi.Pointer builder); + @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -1449,6 +1466,16 @@ external void VertexBuffer_setBufferAt( int byteOffset, ); +@ffi.Native, ffi.Pointer, ffi.Uint8, ffi.Pointer)>( + isLeaf: true, +) +external void VertexBuffer_setBufferObjectAt( + ffi.Pointer engine, + ffi.Pointer buffer, + int bufferIndex, + ffi.Pointer bufferObject, +); + @ffi.Native, ffi.Pointer)>(isLeaf: true) external void VertexBuffer_destroy(ffi.Pointer engine, ffi.Pointer buffer); @@ -1487,6 +1514,35 @@ external void IndexBuffer_setBuffer( @ffi.Native, ffi.Pointer)>(isLeaf: true) external void IndexBuffer_destroy(ffi.Pointer engine, ffi.Pointer buffer); +@ffi.Native Function()>(isLeaf: true) +external ffi.Pointer BufferObjectBuilder_create(); + +@ffi.Native, ffi.Uint32)>(isLeaf: true) +external void BufferObjectBuilder_size(ffi.Pointer builder, int sizeInBytes); + +@ffi.Native Function(ffi.Pointer, ffi.Pointer)>(isLeaf: true) +external ffi.Pointer BufferObjectBuilder_build( + ffi.Pointer builder, + ffi.Pointer engine, +); + +@ffi.Native)>(isLeaf: true) +external void BufferObjectBuilder_destroy(ffi.Pointer builder); + +@ffi.Native< + ffi.Void Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Size, ffi.Uint32) +>(isLeaf: true) +external void BufferObject_setBuffer( + ffi.Pointer engine, + ffi.Pointer buffer, + ffi.Pointer data, + int sizeInBytes, + int byteOffset, +); + +@ffi.Native, ffi.Pointer)>(isLeaf: true) +external void BufferObject_destroy(ffi.Pointer engine, ffi.Pointer buffer); + @ffi.Native, EntityId)>(isLeaf: true) external double4x4 TransformManager_getLocalTransform(ffi.Pointer tTransformManager, int entityId); @@ -2695,7 +2751,7 @@ external void SceneAsset_destroyRenderThread( ffi.Pointer, ffi.Pointer, ffi.Pointer, - ffi.UnsignedInt, + ffi.Uint32, ffi.Pointer)>>, ) >(isLeaf: true) @@ -2704,7 +2760,7 @@ external void SceneAsset_createFromFilamentAssetRenderThread( ffi.Pointer tAssetLoader, ffi.Pointer tNameComponentManager, ffi.Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ffi.Pointer)>> onComplete, ); @@ -2716,6 +2772,7 @@ external void SceneAsset_createFromFilamentAssetRenderThread( ffi.Pointer>, ffi.Int, ffi.UnsignedInt, + ffi.UnsignedInt, Aabb3, ffi.Pointer)>>, ) @@ -2727,6 +2784,7 @@ external void SceneAsset_createFromBuffersRenderThread( ffi.Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, ffi.Pointer)>> callback, ); @@ -3429,6 +3487,67 @@ external void VertexBuffer_setBufferAtRenderThread( VoidCallback onComplete, ); +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Uint8, + ffi.Pointer, + ffi.Uint32, + VoidCallback, + ) +>(isLeaf: true) +external void VertexBuffer_setBufferObjectAtRenderThread( + ffi.Pointer tEngine, + ffi.Pointer tBuffer, + int bufferIndex, + ffi.Pointer tBufferObject, + int requestId, + VoidCallback onComplete, +); + +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>, + ) +>(isLeaf: true) +external void BufferObjectBuilder_buildRenderThread( + ffi.Pointer tBuilder, + ffi.Pointer tEngine, + ffi.Pointer)>> onComplete, +); + +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Uint32, + ffi.Uint32, + VoidCallback, + ) +>(isLeaf: true) +external void BufferObject_setBufferRenderThread( + ffi.Pointer tEngine, + ffi.Pointer tBuffer, + ffi.Pointer data, + int sizeInBytes, + int byteOffset, + int requestId, + VoidCallback onComplete, +); + +@ffi.Native, ffi.Pointer, ffi.Uint32, VoidCallback)>(isLeaf: true) +external void BufferObject_destroyRenderThread( + ffi.Pointer tEngine, + ffi.Pointer tBuffer, + int requestId, + VoidCallback onComplete, +); + @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -4801,6 +4920,10 @@ final class TVertexBufferBuilder extends ffi.Opaque {} final class TIndexBufferBuilder extends ffi.Opaque {} +final class TBufferObject extends ffi.Opaque {} + +final class TBufferObjectBuilder extends ffi.Opaque {} + final class TSurfaceOrientation extends ffi.Opaque {} final class TSurfaceOrientationBuilder extends ffi.Opaque {} @@ -4901,10 +5024,19 @@ sealed class TSceneAssetType { static const SCENE_ASSET_TYPE_GIZMO = 6; } -sealed class TVertexBufferMode { - static const VERTEX_BUFFER_MODE_ORIGINAL = 0; - static const VERTEX_BUFFER_MODE_UNWELDED = 1; - static const VERTEX_BUFFER_MODE_EDITABLE = 2; +sealed class TVertexBufferStorageMode { + static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; + static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; + static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; +} + +sealed class TSceneAssetGeometryCapability { + static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 2; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS = 4; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 8; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 16; } sealed class TFeatureLevel { diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index a96ce9dcf..24494b9db 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -294,6 +294,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Pointer boundingBoxPtr, ); external Pointer _SceneAsset_createFromFilamentAsset( @@ -301,7 +302,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ); external Pointer _SceneAsset_getFilamentAsset(Pointer tSceneAsset); external int _SceneAsset_getType(Pointer tSceneAsset); @@ -323,7 +324,10 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { int materialInstanceCount, ); external void _SceneAsset_getBoundingBox(Pointer Aabb3_out, Pointer asset); + external int _SceneAsset_getGeometryCapabilities(Pointer asset); + external int _SceneAsset_supportsFlatShading(Pointer asset); external Pointer _SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex); + external int _SceneAsset_getVertexBufferStorageMode(Pointer tSceneAsset, int primitiveIndex); external Pointer _SceneAsset_getIndexBuffer(Pointer tSceneAsset, int primitiveIndex); external int _SceneAsset_getPrimitiveOffsetForEntity(Pointer tSceneAsset, EntityId entity); external void _SceneAsset_releaseSourceData(Pointer tSceneAsset); @@ -623,6 +627,8 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { external Pointer _VertexBufferBuilder_create(); external void _VertexBufferBuilder_bufferCount(Pointer builder, int count); external void _VertexBufferBuilder_vertexCount(Pointer builder, int count); + external void _VertexBufferBuilder_enableBufferObjects(Pointer builder, bool enabled); + external int _VertexBufferBuilder_getStorageMode(Pointer builder); external void _VertexBufferBuilder_attribute( Pointer builder, int attribute, @@ -646,6 +652,12 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { size_t sizeInBytes, int byteOffset, ); + external void _VertexBuffer_setBufferObjectAt( + Pointer engine, + Pointer buffer, + int bufferIndex, + Pointer bufferObject, + ); external void _VertexBuffer_destroy(Pointer engine, Pointer buffer); external Pointer _IndexBufferBuilder_create(); external void _IndexBufferBuilder_indexCount(Pointer builder, int count); @@ -664,6 +676,21 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { int byteOffset, ); external void _IndexBuffer_destroy(Pointer engine, Pointer buffer); + external Pointer _BufferObjectBuilder_create(); + external void _BufferObjectBuilder_size(Pointer builder, int sizeInBytes); + external Pointer _BufferObjectBuilder_build( + Pointer builder, + Pointer engine, + ); + external void _BufferObjectBuilder_destroy(Pointer builder); + external void _BufferObject_setBuffer( + Pointer engine, + Pointer buffer, + Pointer data, + size_t sizeInBytes, + int byteOffset, + ); + external void _BufferObject_destroy(Pointer engine, Pointer buffer); external void _TransformManager_getLocalTransform( Pointer double4x4_out, Pointer tTransformManager, @@ -1362,7 +1389,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, Pointer)>> onComplete, ); external void _SceneAsset_createFromBuffersRenderThread( @@ -1372,6 +1399,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Pointer boundingBoxPtr, Pointer)>> callback, ); @@ -1730,6 +1758,34 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { int requestId, VoidCallback onComplete, ); + external void _VertexBuffer_setBufferObjectAtRenderThread( + Pointer tEngine, + Pointer tBuffer, + int bufferIndex, + Pointer tBufferObject, + int requestId, + VoidCallback onComplete, + ); + external void _BufferObjectBuilder_buildRenderThread( + Pointer tBuilder, + Pointer tEngine, + Pointer)>> onComplete, + ); + external void _BufferObject_setBufferRenderThread( + Pointer tEngine, + Pointer tBuffer, + Pointer data, + size_t sizeInBytes, + int byteOffset, + int requestId, + VoidCallback onComplete, + ); + external void _BufferObject_destroyRenderThread( + Pointer tEngine, + Pointer tBuffer, + int requestId, + VoidCallback onComplete, + ); external void _IndexBufferBuilder_buildRenderThread( Pointer tBuilder, Pointer tEngine, @@ -3322,6 +3378,7 @@ Pointer SceneAsset_createFromBuffers( Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, ) { final boundingBoxPtr = boundingBox.address; @@ -3332,6 +3389,7 @@ Pointer SceneAsset_createFromBuffers( materialInstances.cast(), materialInstanceCount, tPrimitiveType, + vertexBufferStorageMode, boundingBoxPtr.cast(), ); return Pointer(result); @@ -3342,14 +3400,14 @@ Pointer SceneAsset_createFromFilamentAsset( Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ) { final result = GeneratedBindings.instance._SceneAsset_createFromFilamentAsset( tEngine.cast(), tAssetLoader.cast(), tNameComponentManager.cast(), tFilamentAsset.cast(), - vertexBufferMode, + requiredGeometryCapabilities, ); return Pointer(result); } @@ -3443,11 +3501,26 @@ Aabb3 SceneAsset_getBoundingBox(Pointer asset) { return Aabb3_out.toDart(); } +int SceneAsset_getGeometryCapabilities(Pointer asset) { + final result = GeneratedBindings.instance._SceneAsset_getGeometryCapabilities(asset.cast()); + return result; +} + +bool SceneAsset_supportsFlatShading(Pointer asset) { + final result = GeneratedBindings.instance._SceneAsset_supportsFlatShading(asset.cast()); + return result == 1; +} + Pointer SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex) { final result = GeneratedBindings.instance._SceneAsset_getVertexBuffer(tSceneAsset.cast(), primitiveIndex); return Pointer(result); } +int SceneAsset_getVertexBufferStorageMode(Pointer tSceneAsset, int primitiveIndex) { + final result = GeneratedBindings.instance._SceneAsset_getVertexBufferStorageMode(tSceneAsset.cast(), primitiveIndex); + return result; +} + Pointer SceneAsset_getIndexBuffer(Pointer tSceneAsset, int primitiveIndex) { final result = GeneratedBindings.instance._SceneAsset_getIndexBuffer(tSceneAsset.cast(), primitiveIndex); return Pointer(result); @@ -4346,6 +4419,16 @@ void VertexBufferBuilder_vertexCount(Pointer builder, int return result; } +void VertexBufferBuilder_enableBufferObjects(Pointer builder, bool enabled) { + final result = GeneratedBindings.instance._VertexBufferBuilder_enableBufferObjects(builder.cast(), enabled); + return result; +} + +int VertexBufferBuilder_getStorageMode(Pointer builder) { + final result = GeneratedBindings.instance._VertexBufferBuilder_getStorageMode(builder.cast()); + return result; +} + void VertexBufferBuilder_attribute( Pointer builder, int attribute, @@ -4404,6 +4487,21 @@ void VertexBuffer_setBufferAt( return result; } +void VertexBuffer_setBufferObjectAt( + Pointer engine, + Pointer buffer, + int bufferIndex, + Pointer bufferObject, +) { + final result = GeneratedBindings.instance._VertexBuffer_setBufferObjectAt( + engine.cast(), + buffer.cast(), + bufferIndex, + bufferObject.cast(), + ); + return result; +} + void VertexBuffer_destroy(Pointer engine, Pointer buffer) { final result = GeneratedBindings.instance._VertexBuffer_destroy(engine.cast(), buffer.cast()); return result; @@ -4461,6 +4559,48 @@ void IndexBuffer_destroy(Pointer engine, Pointer buffer) return result; } +Pointer BufferObjectBuilder_create() { + final result = GeneratedBindings.instance._BufferObjectBuilder_create(); + return Pointer(result); +} + +void BufferObjectBuilder_size(Pointer builder, int sizeInBytes) { + final result = GeneratedBindings.instance._BufferObjectBuilder_size(builder.cast(), sizeInBytes); + return result; +} + +Pointer BufferObjectBuilder_build(Pointer builder, Pointer engine) { + final result = GeneratedBindings.instance._BufferObjectBuilder_build(builder.cast(), engine.cast()); + return Pointer(result); +} + +void BufferObjectBuilder_destroy(Pointer builder) { + final result = GeneratedBindings.instance._BufferObjectBuilder_destroy(builder.cast()); + return result; +} + +void BufferObject_setBuffer( + Pointer engine, + Pointer buffer, + Pointer data, + Dartsize_t sizeInBytes, + int byteOffset, +) { + final result = GeneratedBindings.instance._BufferObject_setBuffer( + engine.cast(), + buffer.cast(), + data, + sizeInBytes, + byteOffset, + ); + return result; +} + +void BufferObject_destroy(Pointer engine, Pointer buffer) { + final result = GeneratedBindings.instance._BufferObject_destroy(engine.cast(), buffer.cast()); + return result; +} + double4x4 TransformManager_getLocalTransform(Pointer tTransformManager, DartEntityId entityId) { final double4x4_out = double4x4.stackAlloc(); final result = GeneratedBindings.instance._TransformManager_getLocalTransform( @@ -6176,7 +6316,7 @@ void SceneAsset_createFromFilamentAssetRenderThread( Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, Pointer)>> onComplete, ) { final result = GeneratedBindings.instance._SceneAsset_createFromFilamentAssetRenderThread( @@ -6184,7 +6324,7 @@ void SceneAsset_createFromFilamentAssetRenderThread( tAssetLoader.cast(), tNameComponentManager.cast(), tFilamentAsset.cast(), - vertexBufferMode, + requiredGeometryCapabilities, onComplete.cast(), ); return result; @@ -6197,6 +6337,7 @@ void SceneAsset_createFromBuffersRenderThread( Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, Pointer)>> callback, ) { @@ -6208,6 +6349,7 @@ void SceneAsset_createFromBuffersRenderThread( materialInstances.cast(), materialInstanceCount, tPrimitiveType, + vertexBufferStorageMode, boundingBoxPtr.cast(), callback.cast(), ); @@ -7056,6 +7198,74 @@ void VertexBuffer_setBufferAtRenderThread( return result; } +void VertexBuffer_setBufferObjectAtRenderThread( + Pointer tEngine, + Pointer tBuffer, + int bufferIndex, + Pointer tBufferObject, + int requestId, + DartVoidCallback onComplete, +) { + final result = GeneratedBindings.instance._VertexBuffer_setBufferObjectAtRenderThread( + tEngine.cast(), + tBuffer.cast(), + bufferIndex, + tBufferObject.cast(), + requestId, + onComplete as Pointer>, + ); + return result; +} + +void BufferObjectBuilder_buildRenderThread( + Pointer tBuilder, + Pointer tEngine, + Pointer)>> onComplete, +) { + final result = GeneratedBindings.instance._BufferObjectBuilder_buildRenderThread( + tBuilder.cast(), + tEngine.cast(), + onComplete.cast(), + ); + return result; +} + +void BufferObject_setBufferRenderThread( + Pointer tEngine, + Pointer tBuffer, + Pointer data, + Dartsize_t sizeInBytes, + int byteOffset, + int requestId, + DartVoidCallback onComplete, +) { + final result = GeneratedBindings.instance._BufferObject_setBufferRenderThread( + tEngine.cast(), + tBuffer.cast(), + data, + sizeInBytes, + byteOffset, + requestId, + onComplete as Pointer>, + ); + return result; +} + +void BufferObject_destroyRenderThread( + Pointer tEngine, + Pointer tBuffer, + int requestId, + DartVoidCallback onComplete, +) { + final result = GeneratedBindings.instance._BufferObject_destroyRenderThread( + tEngine.cast(), + tBuffer.cast(), + requestId, + onComplete as Pointer>, + ); + return result; +} + void IndexBufferBuilder_buildRenderThread( Pointer tBuilder, Pointer tEngine, @@ -10153,6 +10363,12 @@ sealed class TPrimitiveType { static const PRIMITIVETYPE_TRIANGLE_STRIP = 5; } +sealed class TVertexBufferStorageMode { + static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; + static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; + static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; +} + extension Aabb3Ext on Pointer { Aabb3 toDart() { return Aabb3(this); @@ -10243,12 +10459,6 @@ final class TFilamentAsset extends Struct { } } -sealed class TVertexBufferMode { - static const VERTEX_BUFFER_MODE_ORIGINAL = 0; - static const VERTEX_BUFFER_MODE_UNWELDED = 1; - static const VERTEX_BUFFER_MODE_EDITABLE = 2; -} - sealed class TSceneAssetType { static const SCENE_ASSET_TYPE_GLTF = 0; static const SCENE_ASSET_TYPE_GEOMETRY = 1; @@ -10917,6 +11127,21 @@ sealed class TVertexAttributeType { static const TVERTEXATTRIBUTE_TYPE_HALF4 = 25; } +extension TBufferObjectExt on Pointer { + TBufferObject toDart() { + return TBufferObject(this); + } +} + +final class TBufferObject extends Struct { + Pointer get address => super.address.cast(); + TBufferObject(super.address); + + static Pointer stackAlloc() { + return Pointer(NativeLibrary.instance.stackAlloc(0)); + } +} + extension TIndexBufferBuilderExt on Pointer { TIndexBufferBuilder toDart() { return TIndexBufferBuilder(this); @@ -10937,6 +11162,21 @@ sealed class TIndexType { static const TINDEX_TYPE_UINT = 1; } +extension TBufferObjectBuilderExt on Pointer { + TBufferObjectBuilder toDart() { + return TBufferObjectBuilder(this); + } +} + +final class TBufferObjectBuilder extends Struct { + Pointer get address => super.address.cast(); + TBufferObjectBuilder(super.address); + + static Pointer stackAlloc() { + return Pointer(NativeLibrary.instance.stackAlloc(0)); + } +} + sealed class TLightType { static const LIGHT_TYPE_SUN = 0; static const LIGHT_TYPE_DIRECTIONAL = 1; @@ -11480,9 +11720,15 @@ extension StructAllocator on Struct { case TVertexBufferBuilder: final ptr = TVertexBufferBuilder.stackAlloc(); return ptr.toDart() as T; + case TBufferObject: + final ptr = TBufferObject.stackAlloc(); + return ptr.toDart() as T; case TIndexBufferBuilder: final ptr = TIndexBufferBuilder.stackAlloc(); return ptr.toDart() as T; + case TBufferObjectBuilder: + final ptr = TBufferObjectBuilder.stackAlloc(); + return ptr.toDart() as T; case TShadowOptions: final ptr = TShadowOptions.stackAlloc(); return ptr.toDart() as T; diff --git a/thermion_dart/lib/src/filament/filament.dart b/thermion_dart/lib/src/filament/filament.dart index 01c1178f4..29e273e04 100644 --- a/thermion_dart/lib/src/filament/filament.dart +++ b/thermion_dart/lib/src/filament/filament.dart @@ -13,6 +13,7 @@ export 'src/interface/tone_mapper.dart'; export 'src/interface/gltf_mesh_data.dart'; export 'src/interface/vertex_buffer.dart'; export 'src/interface/index_buffer.dart'; +export 'src/interface/buffer_object.dart'; export 'src/interface/translation_axis_material.dart'; export 'src/interface/ubershader_material.dart'; export 'src/interface/wireframe_material.dart'; diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart index 25a023484..8f99fe361 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart @@ -37,6 +37,26 @@ class FFIAsset extends ThermionAsset> { entity = SceneAsset_getEntity(asset); } + @override + Set get geometryCapabilities { + final bits = SceneAsset_getGeometryCapabilities(asset); + return { + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS != 0) + SceneAssetGeometryCapability.barycentrics, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES != 0) + SceneAssetGeometryCapability.writableVertices, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS != 0) + SceneAssetGeometryCapability.accessibleGeometryBuffers, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY != 0) + SceneAssetGeometryCapability.preservedTopology, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS != 0) + SceneAssetGeometryCapability.uniqueTriangleCorners, + }; + } + + @override + bool get supportsFlatShading => SceneAsset_supportsFlatShading(asset); + @override SceneAssetType get type { final t = SceneAsset_getType(asset); @@ -279,12 +299,15 @@ class FFIAsset extends ThermionAsset> { @override Future setFlatShading(bool flatShading) async { - // Flat shading swaps TANGENTS on the preserved (rebuilt) vertex buffers; - // without them it would silently do nothing — throw instead. - if (getVertexBuffer() == null) { - throw Exception( - "setFlatShading: asset has no preserved geometry. " - "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", + // Flat shading swaps between the BufferObjects created specifically for + // unwelded geometry. Editable geometry also has preserved buffers, but it + // deliberately uses ordinary writable streams and cannot perform this + // swap. + if (!supportsFlatShading) { + throw StateError( + "setFlatShading requires unwelded geometry. " + "Load it with requiredGeometryCapabilities containing " + "uniqueTriangleCorners.", ); } await withVoidCallback((requestId, cb) => SceneAsset_setFlatShadingRenderThread(asset, flatShading, requestId, cb)); @@ -905,7 +928,8 @@ class FFIAsset extends ThermionAsset> { if (vbPtr == nullptr) { return null; } - return FFIVertexBuffer(vbPtr, _app.engine); + final storageMode = vertexBufferStorageModeFromNative(SceneAsset_getVertexBufferStorageMode(asset, primitiveIndex)); + return FFIVertexBuffer.assetOwned(vbPtr, _app.engine, storageMode: storageMode); } } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_buffer_object.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_buffer_object.dart new file mode 100644 index 000000000..7d74e33bc --- /dev/null +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_buffer_object.dart @@ -0,0 +1,76 @@ +import '../../../bindings/bindings.dart' as bindings; +import 'package:thermion_dart/thermion_dart.dart'; + +class FFIBufferObject extends BufferObject { + final bindings.Pointer _ptr; + final bindings.Pointer _engine; + + FFIBufferObject(this._ptr, this._engine); + + bindings.Pointer getNativeHandle() => _ptr; + + bool isOwnedBy(bindings.Pointer engine) => _engine == engine; + + @override + Future setBuffer(TypedData data, {int byteOffset = 0}) async { + final bytes = data.asUint8List(); + await withVoidCallback((requestId, cb) { + bindings.BufferObject_setBufferRenderThread( + _engine, + _ptr, + bytes.address.cast(), + bytes.lengthInBytes, + byteOffset, + requestId, + cb, + ); + }); + } + + @override + Future destroy() async { + await withVoidCallback((requestId, cb) { + bindings.BufferObject_destroyRenderThread(_engine, _ptr, requestId, cb); + }); + } +} + +class FFIBufferObjectBuilder implements BufferObjectBuilder { + bindings.Pointer? _builder; + final bindings.Pointer _engine; + bool _built = false; + + FFIBufferObjectBuilder(this._engine) { + _builder = bindings.BufferObjectBuilder_create(); + } + + void _checkNotBuilt() { + if (_built || _builder == null || _builder == bindings.nullptr) { + throw StateError('BufferObjectBuilder has already been built'); + } + } + + @override + void size(int sizeInBytes) { + _checkNotBuilt(); + if (sizeInBytes <= 0) { + throw ArgumentError.value(sizeInBytes, 'sizeInBytes', 'must be positive'); + } + bindings.BufferObjectBuilder_size(_builder!, sizeInBytes); + } + + @override + Future build() async { + _checkNotBuilt(); + final pointer = await withPointerCallback( + (cb) => bindings.BufferObjectBuilder_buildRenderThread(_builder!, _engine, cb), + ); + bindings.BufferObjectBuilder_destroy(_builder!); + _builder = null; + _built = true; + if (pointer == bindings.nullptr) { + throw StateError('Failed to build BufferObject'); + } + return FFIBufferObject(pointer, _engine); + } +} diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart index aaaf181dd..7a31cc966 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart @@ -32,6 +32,42 @@ import 'package:logging/logging.dart'; import 'ffi_gltf_mesh_data.dart'; import 'resource_loader.dart'; +int _geometryCapabilitiesToNative(Set capabilities) { + var bits = TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; + for (final capability in capabilities) { + bits |= switch (capability) { + SceneAssetGeometryCapability.barycentrics => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS, + SceneAssetGeometryCapability.writableVertices => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES, + SceneAssetGeometryCapability.accessibleGeometryBuffers => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS, + SceneAssetGeometryCapability.preservedTopology => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY, + SceneAssetGeometryCapability.uniqueTriangleCorners => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS, + }; + } + return bits; +} + +void _validateRequiredGeometryCapabilities(Set capabilities) { + final requiresUnwelded = + capabilities.contains(SceneAssetGeometryCapability.barycentrics) || + capabilities.contains(SceneAssetGeometryCapability.uniqueTriangleCorners); + final requiresPreservedTopology = + capabilities.contains(SceneAssetGeometryCapability.writableVertices) || + capabilities.contains(SceneAssetGeometryCapability.preservedTopology); + if (requiresUnwelded && requiresPreservedTopology) { + throw ArgumentError.value( + capabilities, + 'requiredGeometryCapabilities', + 'writableVertices or preservedTopology cannot be combined with ' + 'barycentrics or uniqueTriangleCorners', + ); + } +} + class FFIFilamentConfig extends FilamentConfig { FFIFilamentConfig({ super.loadResource = null, @@ -1123,12 +1159,14 @@ class FFIFilamentApp extends FilamentApp { int initialInstances = 1, bool releaseSourceData = false, bool loadResourcesAsync = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, String? resourceUri, }) async { + final geometryRequirements = Set.unmodifiable(requiredGeometryCapabilities); if (initialInstances <= 0) { throw Exception("initialInstances must be at least 1"); } + _validateRequiredGeometryCapabilities(geometryRequirements); _logger.info( "Loading glTF from buffer (${data.lengthInBytes} bytes)" " with resourceUri ${resourceUri}", @@ -1227,20 +1265,31 @@ class FFIFilamentApp extends FilamentApp { gltfAssetLoader, nameComponentManager, filamentAsset, - vertexBufferMode.index, + _geometryCapabilitiesToNative(geometryRequirements), cb, ), ); - if (asset == nullptr) { - throw Exception("Unknown error loading glTF asset. See logs for details."); - } - await withVoidCallback( (requestId, cb) => GltfResourceLoader_destroyRenderThread(engine, gltfResourceLoader, requestId, cb), ); + if (asset == nullptr) { + throw StateError( + 'Failed to load a glTF asset satisfying the required geometry ' + 'capabilities: $geometryRequirements. See native logs for details.', + ); + } + final ffiAsset = FFIAsset(asset, app: this); + if (!ffiAsset.geometryCapabilities.containsAll(geometryRequirements)) { + await withVoidCallback((requestId, cb) => SceneAsset_destroyRenderThread(asset, requestId, cb)); + throw StateError( + 'The loaded asset does not provide all required geometry ' + 'capabilities. Required: $geometryRequirements; provided: ' + '${ffiAsset.geometryCapabilities}.', + ); + } if (releaseSourceData) { await ffiAsset.releaseSourceData(); } @@ -1461,6 +1510,7 @@ class FFIFilamentApp extends FilamentApp { ptrList.address.cast(), ptrList.length, geometry.primitiveType.index, + vertexBufferStorageModeToNative(vertexBuffer.storageMode), cAabb, callback, ); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart index 2d0a075d3..42bc2b262 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart @@ -2,6 +2,7 @@ import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.d import '../../../bindings/bindings.dart' as bindings; import 'package:thermion_dart/src/filament/src/implementation/ffi_material.dart'; import 'package:thermion_dart/src/filament/src/implementation/ffi_vertex_buffer.dart'; +import 'package:thermion_dart/src/filament/src/implementation/ffi_buffer_object.dart'; import 'package:thermion_dart/src/filament/src/implementation/ffi_index_buffer.dart'; import 'package:thermion_dart/thermion_dart.dart'; @@ -415,6 +416,11 @@ class FFIRenderableManager extends RenderableManager return FFIVertexBufferBuilder(app.engine); } + @override + BufferObjectBuilder createBufferObjectBuilder() { + return FFIBufferObjectBuilder(app.engine); + } + @override IndexBufferBuilder createIndexBufferBuilder() { return FFIIndexBufferBuilder(app.engine); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart index 63584c427..0f38067f4 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart @@ -1,12 +1,34 @@ import '../../../bindings/bindings.dart' as bindings; import 'package:thermion_dart/thermion_dart.dart'; +import 'ffi_buffer_object.dart'; /// FFI implementation of VertexBuffer for native platforms. class FFIVertexBuffer extends VertexBuffer { final bindings.Pointer _ptr; final bindings.Pointer _engine; + final bool _ownedByCaller; - FFIVertexBuffer(this._ptr, this._engine); + @override + final VertexBufferStorageMode storageMode; + + FFIVertexBuffer._(this._ptr, this._engine, {required this.storageMode, required bool ownedByCaller}) + : _ownedByCaller = ownedByCaller; + + factory FFIVertexBuffer.callerOwned( + bindings.Pointer ptr, + bindings.Pointer engine, { + required VertexBufferStorageMode storageMode, + }) { + return FFIVertexBuffer._(ptr, engine, storageMode: storageMode, ownedByCaller: true); + } + + factory FFIVertexBuffer.assetOwned( + bindings.Pointer ptr, + bindings.Pointer engine, { + required VertexBufferStorageMode storageMode, + }) { + return FFIVertexBuffer._(ptr, engine, storageMode: storageMode, ownedByCaller: false); + } /// Returns the native handle for FFI calls. bindings.Pointer getNativeHandle() => _ptr; @@ -18,6 +40,13 @@ class FFIVertexBuffer extends VertexBuffer { @override Future setBufferAt(int bufferIndex, TypedData data, {int byteOffset = 0}) async { + if (storageMode != VertexBufferStorageMode.direct) { + throw StateError( + 'VertexBuffer.setBufferAt requires direct storage. Build the buffer ' + 'without enableBufferObjects(), or load glTF assets with ' + 'requiredGeometryCapabilities containing writableVertices.', + ); + } final byteData = data.asUint8List(); await withVoidCallback((requestId, cb) { bindings.VertexBuffer_setBufferAtRenderThread( @@ -33,8 +62,37 @@ class FFIVertexBuffer extends VertexBuffer { }); } + @override + Future setBufferObjectAt(int bufferIndex, BufferObject bufferObject) async { + if (storageMode != VertexBufferStorageMode.bufferObjects) { + throw StateError( + 'VertexBuffer.setBufferObjectAt requires BufferObject-backed storage. ' + 'Call VertexBufferBuilder.enableBufferObjects() before build().', + ); + } + if (bufferObject is! FFIBufferObject) { + throw ArgumentError.value(bufferObject, 'bufferObject', 'must be created by this Filament backend'); + } + if (!bufferObject.isOwnedBy(_engine)) { + throw ArgumentError.value(bufferObject, 'bufferObject', 'must belong to the same Filament engine'); + } + await withVoidCallback((requestId, cb) { + bindings.VertexBuffer_setBufferObjectAtRenderThread( + _engine, + _ptr, + bufferIndex, + bufferObject.getNativeHandle(), + requestId, + cb, + ); + }); + } + @override Future destroy() async { + if (!_ownedByCaller) { + throw StateError('Cannot destroy a VertexBuffer borrowed from a ThermionAsset'); + } await withVoidCallback((requestId, cb) { bindings.VertexBuffer_destroyRenderThread(_engine, _ptr, requestId, cb); }); @@ -72,6 +130,12 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { bindings.VertexBufferBuilder_vertexCount(_builderPtr!, count); } + @override + void enableBufferObjects({bool enabled = true}) { + _checkNotBuilt(); + bindings.VertexBufferBuilder_enableBufferObjects(_builderPtr!, enabled); + } + @override void attribute( VertexAttribute attribute, @@ -106,6 +170,8 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { Future build() async { _checkNotBuilt(); + final storageMode = vertexBufferStorageModeFromNative(bindings.VertexBufferBuilder_getStorageMode(_builderPtr!)); + final vertexBufferPtr = await withPointerCallback( (cb) => bindings.VertexBufferBuilder_buildRenderThread(_builderPtr!, _engine, cb), ); @@ -118,7 +184,7 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { throw Exception('Failed to build VertexBuffer'); } - return FFIVertexBuffer(vertexBufferPtr, _engine); + return FFIVertexBuffer.callerOwned(vertexBufferPtr, _engine, storageMode: storageMode); } int _vertexAttributeToInt(VertexAttribute attribute) { @@ -172,3 +238,21 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { }; } } + +VertexBufferStorageMode vertexBufferStorageModeFromNative(int storageMode) { + return switch (storageMode) { + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_DIRECT => VertexBufferStorageMode.direct, + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS => + VertexBufferStorageMode.bufferObjects, + _ => VertexBufferStorageMode.unknown, + }; +} + +int vertexBufferStorageModeToNative(VertexBufferStorageMode storageMode) { + return switch (storageMode) { + VertexBufferStorageMode.direct => bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_DIRECT, + VertexBufferStorageMode.bufferObjects => + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS, + VertexBufferStorageMode.unknown => bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_UNKNOWN, + }; +} diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart index 2961472d2..ec893497a 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart @@ -567,10 +567,6 @@ class FFIView extends View> { }) async { // primitiveIndex parameter is deprecated and ignored // The offset is now computed automatically from the entity - if (_highlightOverlayManager == null) { - await setHighlightOverlayEnabled(true); - } - entity ??= asset.entity; // Use geometrySource for vertex/index buffers when provided (e.g. for @@ -578,23 +574,28 @@ class FFIView extends View> { final geoAsset = geometrySource ?? asset; final ffiGeoAsset = geoAsset as FFIAsset; - // Misuse check: highlighting needs the preserved (rebuilt) vertex - // buffers, which only exist when the glTF was loaded with - // vertexBufferMode: VertexBufferMode.unwelded. Throw rather than silently doing nothing. - if (ffiGeoAsset.getVertexBuffer() == null) { - throw Exception( - "setStencilHighlight: asset has no preserved geometry. " - "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", + // The silhouette pass reuses the asset's vertex and index buffers but only + // consumes POSITION. It therefore needs accessible geometry buffers, not the + // barycentric coordinates used by wireframe and flat-shading features. + if (!ffiGeoAsset.geometryCapabilities.contains(SceneAssetGeometryCapability.accessibleGeometryBuffers)) { + throw StateError( + "setStencilHighlight requires accessible geometry buffers. " + "Load the asset with requiredGeometryCapabilities containing " + "accessibleGeometryBuffers.", ); } + if (_highlightOverlayManager == null) { + await setHighlightOverlayEnabled(true); + } + // Get the starting primitive offset for this entity final offset = await ffiGeoAsset.getPrimitiveOffsetForEntity(entity); if (offset < 0) { // The asset has preserved geometry, but this particular entity has no // rebuilt buffers (e.g. its primitives are all lines/points). _logger.warning( - "Stencil highlight: no preserved geometry for entity $entity " + "Stencil highlight: no accessible geometry buffers for entity $entity " "(its primitives are all non-triangles and have no rebuilt buffers).", ); return; diff --git a/thermion_dart/lib/src/filament/src/interface/asset.dart b/thermion_dart/lib/src/filament/src/interface/asset.dart index c0c9b3281..94198d66b 100644 --- a/thermion_dart/lib/src/filament/src/interface/asset.dart +++ b/thermion_dart/lib/src/filament/src/interface/asset.dart @@ -5,6 +5,29 @@ import 'package:thermion_dart/thermion_dart.dart'; export 'geometry.dart'; +/// A geometry property guaranteed to be available on a scene asset. +/// +/// The same values can be supplied to asset loaders as requirements. Loaders +/// may provide additional capabilities when they share the same geometry +/// representation. +enum SceneAssetGeometryCapability { + /// Triangle-corner barycentric coordinates are available to materials. + barycentrics, + + /// Vertex attributes can be updated through [VertexBuffer.setBufferAt]. + writableVertices, + + /// Thermion exposes reusable vertex and index buffers for operations such as + /// stencil highlighting. + accessibleGeometryBuffers, + + /// Source vertex order and triangle indices are preserved. + preservedTopology, + + /// Every triangle corner has its own vertex. + uniqueTriangleCorners, +} + enum SceneAssetType { gltf, geometry, light, skybox, ibl, image, gizmo } /// Describes one morph target on a renderable entity. @@ -69,6 +92,14 @@ abstract interface class MorphTargetSet { // entities. // abstract class ThermionAsset extends NativeHandle { + Set get geometryCapabilities { + return const {}; + } + + /// Whether [setFlatShading] can switch this asset between smooth and + /// per-face tangent frames at runtime. + bool get supportsFlatShading => false; + // The top-most entity in the hierarchy (if this is a glTF asset, this // entity will have a transform that sits at the top of the transform // hierarchy but is not itself renderable. @@ -122,8 +153,7 @@ abstract class ThermionAsset extends NativeHandle { } // Toggle between flat (per-face) and smooth (per-vertex) shading. - // Throws unless the asset was loaded with vertexBufferMode: VertexBufferMode.unwelded (flat - // shading swaps TANGENTS on the rebuilt vertex buffers). + // Throws unless [supportsFlatShading] is true. Future setFlatShading(bool flatShading) { throw UnimplementedError(); } @@ -423,12 +453,12 @@ abstract class ThermionAsset extends NativeHandle { throw UnimplementedError(); } - // Unweld all mesh primitives so each triangle has unique vertices, - // then assign barycentric coordinates to CUSTOM0 for wireframe rendering. // Returns the underlying [VertexBuffer] for this asset, if available. // // For geometry assets this exposes the backing Filament vertex buffer so you - // can update data via [VertexBuffer.setBufferAt]. + // can update data via [VertexBuffer.setBufferAt]. Assets with + // [SceneAssetGeometryCapability.writableVertices] expose directly writable + // buffers; barycentric/flat-shading geometry uses Filament BufferObjects. // // [primitiveIndex] is reserved for future use. Geometry assets currently // only support a single primitive, so it is ignored. diff --git a/thermion_dart/lib/src/filament/src/interface/buffer_object.dart b/thermion_dart/lib/src/filament/src/interface/buffer_object.dart new file mode 100644 index 000000000..f3663c000 --- /dev/null +++ b/thermion_dart/lib/src/filament/src/interface/buffer_object.dart @@ -0,0 +1,15 @@ +import 'dart:typed_data'; + +/// GPU storage that can be shared or swapped between compatible vertex +/// buffers. +abstract class BufferObject { + Future setBuffer(TypedData data, {int byteOffset = 0}); + + Future destroy(); +} + +abstract class BufferObjectBuilder { + void size(int sizeInBytes); + + Future build(); +} diff --git a/thermion_dart/lib/src/filament/src/interface/filament_app.dart b/thermion_dart/lib/src/filament/src/interface/filament_app.dart index b9344b6e0..949a3db0b 100644 --- a/thermion_dart/lib/src/filament/src/interface/filament_app.dart +++ b/thermion_dart/lib/src/filament/src/interface/filament_app.dart @@ -3,19 +3,6 @@ import 'package:thermion_dart/src/filament/src/interface/render_manager.dart'; import 'package:thermion_dart/src/filament/src/interface/scene.dart'; import 'package:thermion_dart/thermion_dart.dart'; -/// Controls whether glTF vertex buffers are left untouched or rebuilt for a -/// specific editing workflow. -enum VertexBufferMode { - /// Keep gltfio's original vertex and index buffers. - original, - - /// Duplicate vertices per triangle corner and add barycentric coordinates. - unwelded, - - /// Preserve source vertex order and indices in mutable vertex buffers. - editable, -} - class FilamentConfig { final Backend backend; Future Function(String)? loadResource; @@ -271,7 +258,7 @@ abstract class FilamentApp { Future createUnlitMaterialInstance(); /// Creates a wireframe material instance for use with assets loaded - /// with `vertexBufferMode: VertexBufferMode.unwelded`. Set parameters (edgeColor, faceColor, + /// with the required barycentric geometry capability. Set parameters (edgeColor, faceColor, /// edgeWidth) on the returned [WireframeMaterialInstance], then apply with /// [ThermionAsset.setMaterialInstanceForAll]. Future createWireframeMaterialInstance(); @@ -355,12 +342,17 @@ abstract class FilamentApp { bool clear = true, }); - // Loads a glTF asset from a raw memory buffer. + /// Loads a glTF asset from a raw memory buffer. + /// + /// [requiredGeometryCapabilities] describes the geometry properties that the loaded + /// asset must support. The loader may provide a compatible superset, which + /// is reported by [ThermionAsset.geometryCapabilities]. Incompatible + /// requirements throw [ArgumentError]. Future loadGltfFromBuffer( Uint8List data, { int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, bool loadResourcesAsync = false, String? resourceUri, }); diff --git a/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart b/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart index d62f57d72..275d68b2e 100644 --- a/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart +++ b/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart @@ -296,6 +296,8 @@ abstract class RenderableManager extends NativeHandle { /// Creates a builder for constructing vertex buffers. VertexBufferBuilder createVertexBufferBuilder(); + BufferObjectBuilder createBufferObjectBuilder(); + /// Creates a builder for constructing index buffers. IndexBufferBuilder createIndexBufferBuilder(); } diff --git a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart index 52ccee304..341ef1cc5 100644 --- a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart @@ -1,4 +1,3 @@ -import 'dart:typed_data'; import 'package:thermion_dart/thermion_dart.dart'; /// Vertex attribute types that can be stored in a VertexBuffer. @@ -136,6 +135,9 @@ enum VertexAttributeType { HALF4, } +/// Storage used by a [VertexBuffer]'s attribute streams. +enum VertexBufferStorageMode { unknown, direct, bufferObjects } + /// Holds a set of buffers that define the geometry of a Renderable. /// /// The geometry is defined by vertex attributes such as position, color, @@ -150,16 +152,30 @@ abstract class VertexBuffer { /// Returns the number of vertices in this buffer. int getVertexCount(); + VertexBufferStorageMode get storageMode; + + bool get supportsSetBufferAt => storageMode == VertexBufferStorageMode.direct; + /// Asynchronously copy-initializes the specified buffer from the given data. /// /// [bufferIndex] Index of the buffer to initialize (0 to bufferCount-1) /// [data] Raw vertex data to copy into the buffer /// [byteOffset] Offset in bytes into the buffer (default 0) + /// + /// Throws [StateError] when [supportsSetBufferAt] is false. Future setBufferAt(int bufferIndex, TypedData data, {int byteOffset = 0}); + /// Attaches a [BufferObject] to a stream. + /// + /// Throws [StateError] unless [storageMode] is + /// [VertexBufferStorageMode.bufferObjects]. + Future setBufferObjectAt(int bufferIndex, BufferObject bufferObject); + /// Destroys this vertex buffer and releases GPU resources. /// - /// The buffer must not be used after calling this method. + /// The buffer must not be used after calling this method. Throws + /// [StateError] when this is a borrowed buffer returned by a + /// [ThermionAsset]. Destroy the owning asset instead. Future destroy(); } @@ -191,6 +207,9 @@ abstract class VertexBufferBuilder { /// [count] Number of vertices in each buffer in this set void vertexCount(int count); + /// Enables BufferObject-backed streams. Direct storage is used by default. + void enableBufferObjects({bool enabled = true}); + /// Sets up an attribute for this vertex buffer. /// /// Attributes can be interleaved in the same buffer using byteOffset and byteStride. diff --git a/thermion_dart/lib/src/filament/src/interface/view.dart b/thermion_dart/lib/src/filament/src/interface/view.dart index 9911c21f9..6f12d0aa3 100644 --- a/thermion_dart/lib/src/filament/src/interface/view.dart +++ b/thermion_dart/lib/src/filament/src/interface/view.dart @@ -482,8 +482,8 @@ abstract class View extends NativeHandle { /// Uses a stencil-based two-pass rendering approach for clean, flicker-free /// outlines. /// - /// Throws if the asset (or [geometrySource]) has no preserved geometry — - /// glTF assets must be loaded with `vertexBufferMode: VertexBufferMode.unwelded` for outlining. + /// Throws if the asset (or [geometrySource]) does not provide + /// [SceneAssetGeometryCapability.accessibleGeometryBuffers]. /// /// The [scale] parameter is deprecated and ignored; use [outlineWidth] instead. Future setStencilHighlight( diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart index 323b4bf33..0eed2b4bb 100644 --- a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart +++ b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart @@ -516,10 +516,11 @@ class ThermionViewerFFI extends ThermionViewer { bool addToScene = true, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, String? resourceUri, bool loadAsync = false, }) async { + final geometryRequirements = Set.unmodifiable(requiredGeometryCapabilities); final data = await _app.loadResource(path); if (resourceUri == null) { var normalised = path.replaceAll("\\", "/"); @@ -536,7 +537,7 @@ class ThermionViewerFFI extends ThermionViewer { addToScene: addToScene, initialInstances: initialInstances, releaseSourceData: releaseSourceData, - vertexBufferMode: vertexBufferMode, + requiredGeometryCapabilities: geometryRequirements, resourceUri: resourceUri, loadResourcesAsync: loadAsync, ); @@ -549,7 +550,7 @@ class ThermionViewerFFI extends ThermionViewer { bool addToScene = true, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, bool loadResourcesAsync = false, String? resourceUri, }) async { @@ -557,7 +558,7 @@ class ThermionViewerFFI extends ThermionViewer { data, initialInstances: initialInstances, releaseSourceData: releaseSourceData, - vertexBufferMode: vertexBufferMode, + requiredGeometryCapabilities: requiredGeometryCapabilities, loadResourcesAsync: loadResourcesAsync, resourceUri: resourceUri, ); diff --git a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart index d53051d3a..80236f20b 100644 --- a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart +++ b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart @@ -166,8 +166,9 @@ abstract class ThermionViewer { // Creating instances by specifying [initialInstances] at asset load time is // generally more efficient than dynamically instantating at a later time. // - // If [vertexBufferMode] is [VertexBufferMode.unwelded], vertex buffers are - // rebuilt after loading with a superset of attributes (POSITION, TANGENTS, + // If [requiredGeometryCapabilities] contains [SceneAssetGeometryCapability.barycentrics] + // or [SceneAssetGeometryCapability.uniqueTriangleCorners], vertex buffers are rebuilt + // after loading with a superset of attributes (POSITION, TANGENTS, // UV0, CUSTOM0, and // optionally BONE_INDICES/BONE_WEIGHTS). Vertices are unwelded so each // triangle has unique vertices with barycentric coordinates in CUSTOM0. @@ -176,11 +177,17 @@ abstract class ThermionViewer { // Increases vertex memory usage (~3x vertex count) but preserves the full // glTF feature set (animations, skeleton, instancing). // - // If [vertexBufferMode] is [VertexBufferMode.editable], vertex buffers are + // If [requiredGeometryCapabilities] contains + // [SceneAssetGeometryCapability.writableVertices] or + // [SceneAssetGeometryCapability.preservedTopology], vertex buffers are // rebuilt without unwelding: source vertex order and triangle indices are - // preserved. This - // exposes mutable buffers while retaining compatibility with glTF morph - // targets. + // preserved in mutable buffers compatible with glTF morph targets. + // + // [SceneAssetGeometryCapability.writableVertices] and + // [SceneAssetGeometryCapability.preservedTopology] cannot be combined with + // [SceneAssetGeometryCapability.barycentrics] or + // [SceneAssetGeometryCapability.uniqueTriangleCorners]. The returned asset reports the + // complete set actually provided through [ThermionAsset.geometryCapabilities]. // // If [loadResourcesAsync] is true, resources (textures, materials, etc) will // be loaded asynchronously. Some material/texture pop-in is expected. @@ -190,7 +197,7 @@ abstract class ThermionViewer { bool addToScene = true, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, String? resourceUri, bool loadAsync = false, }); @@ -204,7 +211,7 @@ abstract class ThermionViewer { String? resourceUri, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, bool loadResourcesAsync = false, bool addToScene = true, }); diff --git a/thermion_dart/native/include/c_api/APIBoundaryTypes.h b/thermion_dart/native/include/c_api/APIBoundaryTypes.h index 6c68b4bb6..38c3b061a 100644 --- a/thermion_dart/native/include/c_api/APIBoundaryTypes.h +++ b/thermion_dart/native/include/c_api/APIBoundaryTypes.h @@ -52,6 +52,8 @@ extern "C" typedef struct TIndexBuffer TIndexBuffer; typedef struct TVertexBufferBuilder TVertexBufferBuilder; typedef struct TIndexBufferBuilder TIndexBufferBuilder; + typedef struct TBufferObject TBufferObject; + typedef struct TBufferObjectBuilder TBufferObjectBuilder; typedef struct TSurfaceOrientation TSurfaceOrientation; typedef struct TSurfaceOrientationBuilder TSurfaceOrientationBuilder; @@ -118,12 +120,22 @@ extern "C" }; typedef enum TSceneAssetType TSceneAssetType; - enum TVertexBufferMode { - VERTEX_BUFFER_MODE_ORIGINAL = 0, - VERTEX_BUFFER_MODE_UNWELDED = 1, - VERTEX_BUFFER_MODE_EDITABLE = 2 + enum TVertexBufferStorageMode { + VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0, + VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1, + VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2 }; - typedef enum TVertexBufferMode TVertexBufferMode; + typedef enum TVertexBufferStorageMode TVertexBufferStorageMode; + + enum TSceneAssetGeometryCapability { + SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0, + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1 << 0, + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 1 << 1, + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS = 1 << 2, + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 1 << 3, + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 1 << 4 + }; + typedef enum TSceneAssetGeometryCapability TSceneAssetGeometryCapability; enum TFeatureLevel { FEATURE_LEVEL_0 = 0, diff --git a/thermion_dart/native/include/c_api/TBufferObject.h b/thermion_dart/native/include/c_api/TBufferObject.h new file mode 100644 index 000000000..64ad9ecca --- /dev/null +++ b/thermion_dart/native/include/c_api/TBufferObject.h @@ -0,0 +1,26 @@ +#pragma once + +#include "APIExport.h" +#include "APIBoundaryTypes.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + EMSCRIPTEN_KEEPALIVE TBufferObjectBuilder* BufferObjectBuilder_create(); + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_size(TBufferObjectBuilder* builder, uint32_t sizeInBytes); + EMSCRIPTEN_KEEPALIVE TBufferObject* BufferObjectBuilder_build(TBufferObjectBuilder* builder, TEngine* engine); + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_destroy(TBufferObjectBuilder* builder); + + EMSCRIPTEN_KEEPALIVE void BufferObject_setBuffer( + TEngine* engine, + TBufferObject* buffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset); + EMSCRIPTEN_KEEPALIVE void BufferObject_destroy(TEngine* engine, TBufferObject* buffer); + +#ifdef __cplusplus +} +#endif diff --git a/thermion_dart/native/include/c_api/TSceneAsset.h b/thermion_dart/native/include/c_api/TSceneAsset.h index 920d84311..5e16d14d2 100644 --- a/thermion_dart/native/include/c_api/TSceneAsset.h +++ b/thermion_dart/native/include/c_api/TSceneAsset.h @@ -16,6 +16,7 @@ extern "C" TMaterialInstance **materialInstances, int materialInstanceCount, enum TPrimitiveType tPrimitiveType, + enum TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox ); EMSCRIPTEN_KEEPALIVE TSceneAsset * SceneAsset_createFromFilamentAsset( @@ -23,7 +24,7 @@ extern "C" TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - enum TVertexBufferMode vertexBufferMode + uint32_t requiredGeometryCapabilities ); EMSCRIPTEN_KEEPALIVE TFilamentAsset *SceneAsset_getFilamentAsset(TSceneAsset *tSceneAsset); EMSCRIPTEN_KEEPALIVE enum TSceneAssetType SceneAsset_getType(TSceneAsset *tSceneAsset); @@ -41,7 +42,10 @@ extern "C" EMSCRIPTEN_KEEPALIVE size_t SceneAsset_getInstanceCount(TSceneAsset *tSceneAsset); EMSCRIPTEN_KEEPALIVE TSceneAsset * SceneAsset_createInstance(TSceneAsset *asset, TMaterialInstance **materialInstances, int materialInstanceCount); EMSCRIPTEN_KEEPALIVE Aabb3 SceneAsset_getBoundingBox(TSceneAsset *asset); + EMSCRIPTEN_KEEPALIVE uint32_t SceneAsset_getGeometryCapabilities(TSceneAsset *asset); + EMSCRIPTEN_KEEPALIVE bool SceneAsset_supportsFlatShading(TSceneAsset *asset); EMSCRIPTEN_KEEPALIVE TVertexBuffer *SceneAsset_getVertexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode SceneAsset_getVertexBufferStorageMode(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE TIndexBuffer *SceneAsset_getIndexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE int SceneAsset_getPrimitiveOffsetForEntity(TSceneAsset *tSceneAsset, EntityId entity); EMSCRIPTEN_KEEPALIVE void SceneAsset_releaseSourceData(TSceneAsset *tSceneAsset); diff --git a/thermion_dart/native/include/c_api/TVertexBuffer.h b/thermion_dart/native/include/c_api/TVertexBuffer.h index 618c26d07..3d1f7a2a3 100644 --- a/thermion_dart/native/include/c_api/TVertexBuffer.h +++ b/thermion_dart/native/include/c_api/TVertexBuffer.h @@ -18,6 +18,8 @@ extern "C" // Configure the builder EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_bufferCount(TVertexBufferBuilder* builder, uint8_t count); EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_vertexCount(TVertexBufferBuilder* builder, uint32_t count); + EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_enableBufferObjects(TVertexBufferBuilder* builder, bool enabled); + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBufferBuilder_getStorageMode(TVertexBufferBuilder* builder); EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_attribute( TVertexBufferBuilder* builder, TVertexAttribute attribute, @@ -38,7 +40,6 @@ extern "C" // Get vertex count EMSCRIPTEN_KEEPALIVE size_t VertexBuffer_getVertexCount(TVertexBuffer* buffer); - // Set buffer data EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferAt( TEngine* engine, @@ -48,6 +49,12 @@ extern "C" size_t sizeInBytes, uint32_t byteOffset ); + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAt( + TEngine* engine, + TVertexBuffer* buffer, + uint8_t bufferIndex, + TBufferObject* bufferObject + ); // Destroy EMSCRIPTEN_KEEPALIVE void VertexBuffer_destroy(TEngine* engine, TVertexBuffer* buffer); diff --git a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h index 4c56df86f..9e479db3c 100644 --- a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h +++ b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h @@ -8,6 +8,7 @@ #include "TMaterialProvider.h" #include "TVertexBuffer.h" #include "TIndexBuffer.h" +#include "TBufferObject.h" #include "TTransformManager.h" #include "TLightManager.h" @@ -161,7 +162,7 @@ namespace thermion TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - enum TVertexBufferMode vertexBufferMode, + uint32_t requiredGeometryCapabilities, void (*onComplete)(TSceneAsset *) ); EMSCRIPTEN_KEEPALIVE void SceneAsset_createFromBuffersRenderThread( @@ -171,6 +172,7 @@ namespace thermion TMaterialInstance **materialInstances, int materialInstanceCount, TPrimitiveType tPrimitiveType, + TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox, void (*callback)(TSceneAsset *) ); @@ -408,6 +410,35 @@ namespace thermion uint32_t requestId, VoidCallback onComplete ); + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAtRenderThread( + TEngine* tEngine, + TVertexBuffer* tBuffer, + uint8_t bufferIndex, + TBufferObject* tBufferObject, + uint32_t requestId, + VoidCallback onComplete + ); + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_buildRenderThread( + TBufferObjectBuilder* tBuilder, + TEngine* tEngine, + void (*onComplete)(TBufferObject*) + ); + EMSCRIPTEN_KEEPALIVE void BufferObject_setBufferRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset, + uint32_t requestId, + VoidCallback onComplete + ); + EMSCRIPTEN_KEEPALIVE void BufferObject_destroyRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + uint32_t requestId, + VoidCallback onComplete + ); // IndexBuffer render thread methods EMSCRIPTEN_KEEPALIVE void IndexBufferBuilder_buildRenderThread( diff --git a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp index 43ba6c8f2..67fc18ded 100644 --- a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp +++ b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp @@ -23,6 +23,7 @@ namespace thermion size_t materialInstanceCount, RenderableManager::PrimitiveType primitiveType, Box boundingBox, + TVertexBufferStorageMode vertexBufferStorageMode, GeometrySceneAsset *instanceParent = std::nullptr_t()); ~GeometrySceneAsset(); @@ -61,6 +62,18 @@ namespace thermion VertexBuffer *getVertexBuffer() const { return _vertexBuffer; } IndexBuffer *getIndexBuffer() const { return _indexBuffer; } + uint32_t getGeometryCapabilities() const override { + uint32_t capabilities = SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS; + if (_vertexBufferStorageMode == VERTEX_BUFFER_STORAGE_MODE_DIRECT) { + capabilities |= SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES; + } + return capabilities; + } + TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { + return primitiveIndex == 0 + ? _vertexBufferStorageMode + : VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } void addAllEntities(Scene *scene) override { @@ -132,7 +145,8 @@ namespace thermion GeometrySceneAsset *_instanceOwner = std::nullptr_t(); utils::Entity _entity; RenderableManager::PrimitiveType _primitiveType; + TVertexBufferStorageMode _vertexBufferStorageMode = VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; std::vector> _instances; }; -} // namespace thermion \ No newline at end of file +} // namespace thermion diff --git a/thermion_dart/native/include/scene/GltfSceneAsset.hpp b/thermion_dart/native/include/scene/GltfSceneAsset.hpp index 32caa5596..2eb056898 100644 --- a/thermion_dart/native/include/scene/GltfSceneAsset.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAsset.hpp @@ -35,7 +35,7 @@ namespace thermion gltfio::AssetLoader *assetLoader, Engine *engine, utils::NameComponentManager* ncm, - TVertexBufferMode vertexBufferMode = VERTEX_BUFFER_MODE_ORIGINAL, + uint32_t requiredGeometryCapabilities = SCENE_ASSET_GEOMETRY_CAPABILITY_NONE, MaterialInstance **materialInstances = nullptr, size_t materialInstanceCount = 0); @@ -141,13 +141,27 @@ namespace thermion return _asset->getBoundingBox(); } + uint32_t getGeometryCapabilities() const override { return _geometryCapabilities; } + + bool supportsFlatShading() const override { return _supportsFlatShading; } + + TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { + if (primitiveIndex >= _preservedVertexBufferStorageModes.size()) { + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } + return _preservedVertexBufferStorageModes[primitiveIndex]; + } + /// Rebuild all mesh primitives with a superset vertex buffer layout /// (POSITION + TANGENTS + UV0 + CUSTOM0 + optional BONE_INDICES/WEIGHTS). - /// [VERTEX_BUFFER_MODE_UNWELDED] gives each triangle unique vertices - /// for barycentric wireframe rendering. [VERTEX_BUFFER_MODE_EDITABLE] - /// retains source vertex order and indices so glTF morph target buffers - /// remain compatible with the rebuilt geometry. - void rebuildVertexBuffers(TVertexBufferMode vertexBufferMode); + /// When [preserveTopology] is false, each triangle receives unique + /// vertices for barycentric wireframe rendering. When true, source + /// vertex order and indices remain compatible with glTF morph targets. + bool rebuildVertexBuffers(bool preserveTopology); + + /// Returns false when the requested capability combination cannot be + /// provided by a single rebuilt geometry representation. + static bool supportsRequiredGeometryCapabilities(uint32_t requiredGeometryCapabilities); /// Toggle between flat (per-face) and smooth (per-vertex) shading. /// Only valid after rebuildVertexBuffers() has been called. @@ -202,9 +216,12 @@ namespace thermion bool _sourceDataReleased = false; bool _geometryPreserved = false; bool _flatShading = false; + bool _supportsFlatShading = false; + uint32_t _geometryCapabilities = SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; // Buffers created by rebuildVertexBuffers, owned by this asset. std::vector _preservedVertexBuffers; + std::vector _preservedVertexBufferStorageModes; std::vector _preservedIndexBuffers; std::vector _preservedIndexCounts; std::vector _preservedBufferObjects; diff --git a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp index e8abdeac1..8793d6df3 100644 --- a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp @@ -140,6 +140,10 @@ namespace thermion return _instance->getBoundingBox(); } + uint32_t getGeometryCapabilities() const override; + bool supportsFlatShading() const override; + TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override; + size_t getBoneCount(size_t skinIndex) const override; const utils::Entity *getBones(size_t skinIndex) const override; const char *getBoneName(size_t skinIndex, size_t boneIndex) const override; @@ -155,4 +159,4 @@ namespace thermion GltfSceneAsset *_instanceOwner = std::nullptr_t(); }; -} // namespace thermion \ No newline at end of file +} // namespace thermion diff --git a/thermion_dart/native/include/scene/SceneAsset.hpp b/thermion_dart/native/include/scene/SceneAsset.hpp index 83dc97f5f..d70dc911a 100644 --- a/thermion_dart/native/include/scene/SceneAsset.hpp +++ b/thermion_dart/native/include/scene/SceneAsset.hpp @@ -9,6 +9,7 @@ #include "CustomGeometry.hpp" #include "Log.hpp" +#include "c_api/APIBoundaryTypes.h" namespace thermion { @@ -56,6 +57,18 @@ class SceneAsset { virtual const filament::Aabb getBoundingBox() const = 0; + virtual uint32_t getGeometryCapabilities() const { + return SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; + } + + virtual bool supportsFlatShading() const { + return false; + } + + virtual TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const { + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } + virtual size_t getBoneCount(size_t skinIndex) const { return 0; } @@ -69,4 +82,4 @@ class SceneAsset { } }; -} \ No newline at end of file +} diff --git a/thermion_dart/native/src/c_api/TBufferObject.cpp b/thermion_dart/native/src/c_api/TBufferObject.cpp new file mode 100644 index 000000000..61e1fd38a --- /dev/null +++ b/thermion_dart/native/src/c_api/TBufferObject.cpp @@ -0,0 +1,67 @@ +#include +#include + +#include +#include + +#include "c_api/TBufferObject.h" + +namespace thermion +{ + extern "C" + { + using namespace filament; + + EMSCRIPTEN_KEEPALIVE TBufferObjectBuilder* BufferObjectBuilder_create() + { + return reinterpret_cast(new BufferObject::Builder()); + } + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_size( + TBufferObjectBuilder* tBuilder, + uint32_t sizeInBytes) + { + reinterpret_cast(tBuilder)->size(sizeInBytes); + } + + EMSCRIPTEN_KEEPALIVE TBufferObject* BufferObjectBuilder_build( + TBufferObjectBuilder* tBuilder, + TEngine* tEngine) + { + auto* builder = reinterpret_cast(tBuilder); + auto* engine = reinterpret_cast(tEngine); + return reinterpret_cast(builder->build(*engine)); + } + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_destroy(TBufferObjectBuilder* tBuilder) + { + delete reinterpret_cast(tBuilder); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_setBuffer( + TEngine* tEngine, + TBufferObject* tBuffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset) + { + auto* copy = new uint8_t[sizeInBytes]; + std::memcpy(copy, data, sizeInBytes); + auto* engine = reinterpret_cast(tEngine); + auto* buffer = reinterpret_cast(tBuffer); + buffer->setBuffer( + *engine, + BufferObject::BufferDescriptor( + copy, + sizeInBytes, + [](void* data, size_t, void*) { delete[] static_cast(data); }), + byteOffset); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_destroy(TEngine* tEngine, TBufferObject* tBuffer) + { + reinterpret_cast(tEngine)->destroy( + reinterpret_cast(tBuffer)); + } + } +} diff --git a/thermion_dart/native/src/c_api/TGizmo.cpp b/thermion_dart/native/src/c_api/TGizmo.cpp index 0fcc82d05..56f8573cc 100644 --- a/thermion_dart/native/src/c_api/TGizmo.cpp +++ b/thermion_dart/native/src/c_api/TGizmo.cpp @@ -67,7 +67,7 @@ namespace thermion tAssetLoader, tNameComponentManager, tFilamentAsset, - VERTEX_BUFFER_MODE_ORIGINAL); + SCENE_ASSET_GEOMETRY_CAPABILITY_NONE); auto *gltfSceneAsset = reinterpret_cast(sceneAsset); diff --git a/thermion_dart/native/src/c_api/TSceneAsset.cpp b/thermion_dart/native/src/c_api/TSceneAsset.cpp index 278aadd9e..cb83d1c62 100644 --- a/thermion_dart/native/src/c_api/TSceneAsset.cpp +++ b/thermion_dart/native/src/c_api/TSceneAsset.cpp @@ -7,6 +7,7 @@ #include "c_api/TGltfAssetLoader.h" #include "c_api/TSceneAsset.h" +#include "Log.hpp" #include "scene/GeometrySceneAsset.hpp" #include "scene/GltfSceneAsset.hpp" @@ -27,6 +28,7 @@ extern "C" TMaterialInstance **materialInstances, int materialInstanceCount, TPrimitiveType tPrimitiveType, + TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox ) { auto *engine = reinterpret_cast(tEngine); @@ -59,6 +61,7 @@ extern "C" materialInstanceCount, primitiveType, box, + vertexBufferStorageMode, nullptr // instanceOwner - this is not an instance ); @@ -70,21 +73,34 @@ extern "C" TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - TVertexBufferMode vertexBufferMode + uint32_t requiredGeometryCapabilities ) { auto *engine = reinterpret_cast(tEngine); auto *nameComponentManager = reinterpret_cast(tNameComponentManager); auto *filamentAsset = reinterpret_cast(tFilamentAsset); auto *assetLoader = reinterpret_cast(tAssetLoader); + if (!GltfSceneAsset::supportsRequiredGeometryCapabilities(requiredGeometryCapabilities)) { + Log("Unsupported or incompatible required geometry capabilities: 0x%x", + requiredGeometryCapabilities); + return nullptr; + } auto *sceneAsset = new GltfSceneAsset( filamentAsset, assetLoader, engine, nameComponentManager, - vertexBufferMode + requiredGeometryCapabilities ); + if ((sceneAsset->getGeometryCapabilities() & requiredGeometryCapabilities) != + requiredGeometryCapabilities) { + Log("Failed to provide required geometry capabilities: requested 0x%x, provided 0x%x", + requiredGeometryCapabilities, sceneAsset->getGeometryCapabilities()); + delete sceneAsset; + return nullptr; + } + return reinterpret_cast(sceneAsset); } @@ -227,6 +243,14 @@ extern "C" return Aabb3{box.center().x, box.center().y, box.center().z, box.extent().x, box.extent().y, box.extent().z}; } + EMSCRIPTEN_KEEPALIVE uint32_t SceneAsset_getGeometryCapabilities(TSceneAsset *tSceneAsset) { + return reinterpret_cast(tSceneAsset)->getGeometryCapabilities(); + } + + EMSCRIPTEN_KEEPALIVE bool SceneAsset_supportsFlatShading(TSceneAsset *tSceneAsset) { + return reinterpret_cast(tSceneAsset)->supportsFlatShading(); + } + EMSCRIPTEN_KEEPALIVE TVertexBuffer *SceneAsset_getVertexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex) { auto *asset = reinterpret_cast(tSceneAsset); if (asset->getType() == SceneAsset::SceneAssetType::Geometry) { @@ -235,13 +259,24 @@ extern "C" return reinterpret_cast(vertexBuffer); } if (asset->getType() == SceneAsset::SceneAssetType::Gltf) { - auto gltfSceneAsset = reinterpret_cast(asset); + auto gltfSceneAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); auto *vertexBuffer = gltfSceneAsset->getPreservedVertexBuffer(primitiveIndex); return reinterpret_cast(vertexBuffer); } return nullptr; } + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode SceneAsset_getVertexBufferStorageMode( + TSceneAsset *tSceneAsset, + int primitiveIndex) { + if (primitiveIndex < 0) { + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } + return reinterpret_cast(tSceneAsset)->getVertexBufferStorageMode( + static_cast(primitiveIndex)); + } + EMSCRIPTEN_KEEPALIVE TIndexBuffer *SceneAsset_getIndexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex) { auto *asset = reinterpret_cast(tSceneAsset); if (asset->getType() == SceneAsset::SceneAssetType::Geometry) { @@ -250,7 +285,8 @@ extern "C" return reinterpret_cast(indexBuffer); } if (asset->getType() == SceneAsset::SceneAssetType::Gltf) { - auto gltfSceneAsset = reinterpret_cast(asset); + auto gltfSceneAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); auto *indexBuffer = gltfSceneAsset->getPreservedIndexBuffer(primitiveIndex); return reinterpret_cast(indexBuffer); } @@ -262,7 +298,8 @@ extern "C" if (asset->getType() != SceneAsset::SceneAssetType::Gltf) { return -1; } - auto gltfSceneAsset = reinterpret_cast(asset); + auto gltfSceneAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); // Convert EntityId to utils::Entity for the internal method return gltfSceneAsset->getPrimitiveOffsetForEntity(utils::Entity::import(entity)); } @@ -288,7 +325,8 @@ extern "C" Log("setFlatShading only supported on glTF assets"); return; } - auto *gltfAsset = reinterpret_cast(tSceneAsset); + auto *gltfAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); gltfAsset->setFlatShading(flatShading); } diff --git a/thermion_dart/native/src/c_api/TVertexBuffer.cpp b/thermion_dart/native/src/c_api/TVertexBuffer.cpp index cdda3dca9..f5cbf366d 100644 --- a/thermion_dart/native/src/c_api/TVertexBuffer.cpp +++ b/thermion_dart/native/src/c_api/TVertexBuffer.cpp @@ -1,9 +1,19 @@ #include +#include #include #include "Log.hpp" #include "c_api/TVertexBuffer.h" +namespace +{ + struct VertexBufferBuilderState + { + filament::VertexBuffer::Builder builder; + TVertexBufferStorageMode storageMode = VERTEX_BUFFER_STORAGE_MODE_DIRECT; + }; +} + namespace thermion { extern "C" @@ -15,18 +25,30 @@ namespace thermion // ============================================================================ EMSCRIPTEN_KEEPALIVE TVertexBufferBuilder* VertexBufferBuilder_create() { - auto* builder = new filament::VertexBuffer::Builder(); + auto* builder = new VertexBufferBuilderState(); return reinterpret_cast(builder); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_bufferCount(TVertexBufferBuilder* tBuilder, uint8_t count) { - auto* builder = reinterpret_cast(tBuilder); - builder->bufferCount(count); + auto* builder = reinterpret_cast(tBuilder); + builder->builder.bufferCount(count); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_vertexCount(TVertexBufferBuilder* tBuilder, uint32_t count) { - auto* builder = reinterpret_cast(tBuilder); - builder->vertexCount(count); + auto* builder = reinterpret_cast(tBuilder); + builder->builder.vertexCount(count); + } + + EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_enableBufferObjects(TVertexBufferBuilder* tBuilder, bool enabled) { + auto* builder = reinterpret_cast(tBuilder); + builder->builder.enableBufferObjects(enabled); + builder->storageMode = enabled + ? VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS + : VERTEX_BUFFER_STORAGE_MODE_DIRECT; + } + + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBufferBuilder_getStorageMode(TVertexBufferBuilder* tBuilder) { + return reinterpret_cast(tBuilder)->storageMode; } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_attribute( @@ -37,7 +59,7 @@ namespace thermion uint32_t byteOffset, uint8_t byteStride ) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); // Map TVertexAttribute to filament::VertexAttribute explicitly VertexAttribute vertexAttribute; @@ -97,11 +119,11 @@ namespace thermion return; } - builder->attribute(vertexAttribute, bufferIndex, elementType, byteOffset, byteStride); + builder->builder.attribute(vertexAttribute, bufferIndex, elementType, byteOffset, byteStride); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_normalized(TVertexBufferBuilder* tBuilder, TVertexAttribute attribute, bool normalize) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); // Map TVertexAttribute to filament::VertexAttribute explicitly VertexAttribute vertexAttribute; @@ -126,18 +148,18 @@ namespace thermion return; } - builder->normalized(vertexAttribute, normalize); + builder->builder.normalized(vertexAttribute, normalize); } EMSCRIPTEN_KEEPALIVE TVertexBuffer* VertexBufferBuilder_build(TVertexBufferBuilder* tBuilder, TEngine* tEngine) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); auto* engine = reinterpret_cast(tEngine); - auto* vertexBuffer = builder->build(*engine); + auto* vertexBuffer = builder->builder.build(*engine); return reinterpret_cast(vertexBuffer); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_destroy(TVertexBufferBuilder* tBuilder) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); delete builder; } @@ -181,6 +203,18 @@ namespace thermion vertexBuffer->setBufferAt(*engine, bufferIndex, std::move(bufferDescriptor), byteOffset); } + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAt( + TEngine* tEngine, + TVertexBuffer* tBuffer, + uint8_t bufferIndex, + TBufferObject* tBufferObject + ) { + auto* engine = reinterpret_cast(tEngine); + auto* vertexBuffer = reinterpret_cast(tBuffer); + auto* bufferObject = reinterpret_cast(tBufferObject); + vertexBuffer->setBufferObjectAt(*engine, bufferIndex, bufferObject); + } + EMSCRIPTEN_KEEPALIVE void VertexBuffer_destroy(TEngine* tEngine, TVertexBuffer* tBuffer) { auto* engine = reinterpret_cast(tEngine); auto* vertexBuffer = reinterpret_cast(tBuffer); diff --git a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp index 925e9dc9f..4d71a86d5 100644 --- a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp +++ b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp @@ -11,6 +11,7 @@ #endif #include +#include #include "c_api/APIBoundaryTypes.h" #include "c_api/TAnimationManager.h" @@ -30,6 +31,7 @@ #include "c_api/TView.h" #include "c_api/TVertexBuffer.h" #include "c_api/TIndexBuffer.h" +#include "c_api/TBufferObject.h" #include "c_api/ThermionDartRenderThreadApi.h" #include "rendering/RenderThread.hpp" @@ -985,7 +987,7 @@ extern "C" TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - TVertexBufferMode vertexBufferMode, + uint32_t requiredGeometryCapabilities, void (*onComplete)(TSceneAsset *)) { auto *rt = RT(tEngine); @@ -994,7 +996,7 @@ extern "C" { auto sceneAsset = SceneAsset_createFromFilamentAsset( tEngine, tAssetLoader, tNameComponentManager, tFilamentAsset, - vertexBufferMode); + requiredGeometryCapabilities); setOwner(sceneAsset, rt); PROXY(onComplete(sceneAsset)); }); @@ -1008,6 +1010,7 @@ extern "C" TMaterialInstance **materialInstances, int materialInstanceCount, TPrimitiveType tPrimitiveType, + TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox, void (*callback)(TSceneAsset *)) { @@ -1015,7 +1018,15 @@ extern "C" std::packaged_task lambda( [=] { - auto sceneAsset = SceneAsset_createFromBuffers(tEngine, tVertexBuffer, tIndexBuffer, materialInstances, materialInstanceCount, tPrimitiveType, boundingBox); + auto sceneAsset = SceneAsset_createFromBuffers( + tEngine, + tVertexBuffer, + tIndexBuffer, + materialInstances, + materialInstanceCount, + tPrimitiveType, + vertexBufferStorageMode, + boundingBox); setOwner(sceneAsset, rt); PROXY(callback(sceneAsset)); }); @@ -2532,6 +2543,80 @@ extern "C" auto fut = rt->addTask(lambda); } + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAtRenderThread( + TEngine* tEngine, + TVertexBuffer* tBuffer, + uint8_t bufferIndex, + TBufferObject* tBufferObject, + uint32_t requestId, + VoidCallback onComplete) + { + auto* rt = RT(tEngine); + std::packaged_task lambda([=]() mutable { + VertexBuffer_setBufferObjectAt(tEngine, tBuffer, bufferIndex, tBufferObject); + PROXY(onComplete(requestId)); + }); + auto fut = rt->addTask(lambda); + } + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_buildRenderThread( + TBufferObjectBuilder* tBuilder, + TEngine* tEngine, + void (*onComplete)(TBufferObject*)) + { + auto* rt = RT(tEngine); + std::packaged_task lambda([=]() mutable { + auto* buffer = BufferObjectBuilder_build(tBuilder, tEngine); + setOwner(buffer, rt); + PROXY(onComplete(buffer)); + }); + auto fut = rt->addTask(lambda); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_setBufferRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset, + uint32_t requestId, + VoidCallback onComplete) + { + auto* rt = RT(tEngine); + auto* copy = new std::vector(sizeInBytes); + std::copy(static_cast(data), static_cast(data) + sizeInBytes, copy->begin()); + std::packaged_task lambda([=]() mutable { + auto* engine = reinterpret_cast(tEngine); + auto* buffer = reinterpret_cast(tBuffer); + buffer->setBuffer( + *engine, + filament::BufferObject::BufferDescriptor( + copy->data(), + copy->size(), + [](void*, size_t, void* user) { + delete reinterpret_cast*>(user); + }, + copy), + byteOffset); + PROXY(onComplete(requestId)); + }); + auto fut = rt->addTask(lambda); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_destroyRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + uint32_t requestId, + VoidCallback onComplete) + { + auto* rt = RT(tEngine); + std::packaged_task lambda([=]() mutable { + BufferObject_destroy(tEngine, tBuffer); + PROXY(onComplete(requestId)); + }); + auto fut = rt->addTask(lambda); + } + EMSCRIPTEN_KEEPALIVE void IndexBufferBuilder_buildRenderThread( TIndexBufferBuilder *tBuilder, TEngine *tEngine, diff --git a/thermion_dart/native/src/scene/GeometrySceneAsset.cpp b/thermion_dart/native/src/scene/GeometrySceneAsset.cpp index ef40e561a..0b6b01a0c 100644 --- a/thermion_dart/native/src/scene/GeometrySceneAsset.cpp +++ b/thermion_dart/native/src/scene/GeometrySceneAsset.cpp @@ -25,12 +25,14 @@ namespace thermion size_t materialInstanceCount, RenderableManager::PrimitiveType primitiveType, Box boundingBox, + TVertexBufferStorageMode vertexBufferStorageMode, GeometrySceneAsset *instanceOwner) : _engine(engine), _vertexBuffer(vertexBuffer), _indexBuffer(indexBuffer), + _instanceOwner(instanceOwner), _primitiveType(primitiveType), - _instanceOwner(instanceOwner) + _vertexBufferStorageMode(vertexBufferStorageMode) { _materialInstances.insert(_materialInstances.begin(), materialInstances, materialInstances + materialInstanceCount); @@ -71,8 +73,9 @@ namespace thermion utils::EntityManager::get().destroy(_entity); - if (_vertexBuffer && !isInstance()) + if (_vertexBuffer && !isInstance()) { _engine->destroy(_vertexBuffer); + } if (_indexBuffer && !isInstance()) _engine->destroy(_indexBuffer); @@ -100,6 +103,7 @@ namespace thermion materialInstanceCount, _primitiveType, filament::Box().set(_boundingBox.min, _boundingBox.max), + _vertexBufferStorageMode, this); auto *raw = instance.get(); _instances.push_back(std::move(instance)); @@ -113,4 +117,4 @@ namespace thermion _instances.erase(it, _instances.end()); } -} // namespace thermion \ No newline at end of file +} // namespace thermion diff --git a/thermion_dart/native/src/scene/GltfSceneAsset.cpp b/thermion_dart/native/src/scene/GltfSceneAsset.cpp index f4d993a8c..21e3f3d85 100644 --- a/thermion_dart/native/src/scene/GltfSceneAsset.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAsset.cpp @@ -37,7 +37,7 @@ namespace thermion gltfio::AssetLoader *assetLoader, Engine *engine, utils::NameComponentManager *ncm, - TVertexBufferMode vertexBufferMode, + uint32_t requiredGeometryCapabilities, MaterialInstance **materialInstances, size_t materialInstanceCount) : _asset(asset), _assetLoader(assetLoader), @@ -46,9 +46,34 @@ namespace thermion _materialInstances(materialInstances), _materialInstanceCount(materialInstanceCount) { - if (vertexBufferMode != VERTEX_BUFFER_MODE_ORIGINAL) + const bool requiresUnwelded = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS)) != 0; + const bool requiresPreserved = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY | + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS)) != 0; + + if (requiresUnwelded) { - rebuildVertexBuffers(vertexBufferMode); + if (rebuildVertexBuffers(false)) { + _geometryCapabilities = + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS; + _supportsFlatShading = true; + } + } + else if (requiresPreserved) + { + if (rebuildVertexBuffers(true)) { + _geometryCapabilities = + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY; + } } for (int i = 0; i < asset->getAssetInstanceCount(); i++) { @@ -57,28 +82,52 @@ namespace thermion TRACE("Created GltfSceneAsset from FilamentAsset %d with %d reserved instances", asset, asset->getAssetInstanceCount()); } + bool GltfSceneAsset::supportsRequiredGeometryCapabilities(uint32_t requiredGeometryCapabilities) + { + constexpr uint32_t supported = + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS; + if ((requiredGeometryCapabilities & ~supported) != 0) + { + return false; + } + + const bool requiresUnwelded = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS)) != 0; + const bool requiresPreservedTopology = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY)) != 0; + return !(requiresUnwelded && requiresPreservedTopology); + } + GltfSceneAsset::~GltfSceneAsset() { _instances.clear(); for (auto *vb : _preservedVertexBuffers) { - _engine->destroy(vb); + if (vb) _engine->destroy(vb); } for (auto *ib : _preservedIndexBuffers) { - _engine->destroy(ib); + if (ib) _engine->destroy(ib); } for (auto *bo : _preservedBufferObjects) { - _engine->destroy(bo); + if (bo) _engine->destroy(bo); } for (auto *bo : _smoothTangentBOs) { - _engine->destroy(bo); + if (bo) _engine->destroy(bo); } for (auto *bo : _flatTangentBOs) { - _engine->destroy(bo); + if (bo) _engine->destroy(bo); } releaseSourceData(); _assetLoader->destroyAsset(_asset); @@ -268,20 +317,20 @@ namespace thermion return nullptr; } - void GltfSceneAsset::rebuildVertexBuffers(TVertexBufferMode vertexBufferMode) + bool GltfSceneAsset::rebuildVertexBuffers(bool preserveTopology) { - const bool editableTopology = vertexBufferMode == VERTEX_BUFFER_MODE_EDITABLE; + const bool preserveSourceTopology = preserveTopology; auto *sourceData = (const cgltf_data *)_asset->getSourceAsset(); if (!sourceData) { Log("rebuildVertexBuffers: source data already released"); - return; + return false; } if (_geometryPreserved) { Log("rebuildVertexBuffers: already called"); - return; + return false; } std::vector meshEntries; @@ -293,6 +342,18 @@ namespace thermion auto *allEntities = _asset->getEntities(); size_t allEntityCount = _asset->getEntityCount(); auto &rm = _engine->getRenderableManager(); + bool allPrimitivesRebuilt = true; + size_t rebuiltPrimitiveCount = 0; + + auto appendPlaceholder = [&]() { + _preservedVertexBuffers.push_back(nullptr); + _preservedVertexBufferStorageModes.push_back(VERTEX_BUFFER_STORAGE_MODE_UNKNOWN); + _preservedIndexBuffers.push_back(nullptr); + _preservedIndexCounts.push_back(0); + _smoothTangentBOs.push_back(nullptr); + _flatTangentBOs.push_back(nullptr); + allPrimitivesRebuilt = false; + }; TRACE("rebuildVertexBuffers: meshEntries=%zu entityCount=%zu nodes=%zu", meshEntries.size(), allEntityCount, sourceData->nodes_count); @@ -334,18 +395,26 @@ namespace thermion TRACE("rebuildVertexBuffers: no mesh (named or fallback) for entity %zu — padding %zu placeholder slots", ei, skippedPrimCount); for (size_t pi = 0; pi < skippedPrimCount; pi++) { - _preservedVertexBuffers.push_back(nullptr); - _preservedIndexBuffers.push_back(nullptr); - _preservedIndexCounts.push_back(0); - _smoothTangentBOs.push_back(nullptr); - _flatTangentBOs.push_back(nullptr); + appendPlaceholder(); } continue; } } - for (cgltf_size pi = 0; pi < mesh->primitives_count; pi++) + const size_t renderablePrimitiveCount = rm.getPrimitiveCount(ri); + if (mesh->primitives_count != renderablePrimitiveCount) + { + allPrimitivesRebuilt = false; + TRACE("rebuildVertexBuffers: mesh/renderable primitive count mismatch at entity %zu (%zu vs %zu)", + ei, static_cast(mesh->primitives_count), renderablePrimitiveCount); + } + for (size_t pi = 0; pi < renderablePrimitiveCount; pi++) { + if (pi >= mesh->primitives_count) + { + appendPlaceholder(); + continue; + } const cgltf_primitive &prim = mesh->primitives[pi]; if (prim.type != cgltf_primitive_type_triangles) @@ -357,11 +426,7 @@ namespace thermion // this primitive. Callers (e.g. setStencilHighlight) // null-check getPreservedVertexBuffer. TRACE("rebuildVertexBuffers: placeholder for non-triangle primitive at entity %zu prim %zu", ei, pi); - _preservedVertexBuffers.push_back(nullptr); - _preservedIndexBuffers.push_back(nullptr); - _preservedIndexCounts.push_back(0); - _smoothTangentBOs.push_back(nullptr); - _flatTangentBOs.push_back(nullptr); + appendPlaceholder(); continue; } @@ -370,6 +435,8 @@ namespace thermion const cgltf_accessor *nrmAccessor = nullptr; const cgltf_accessor *tanAccessor = nullptr; const cgltf_accessor *uvAccessor = nullptr; + const cgltf_accessor *uv1Accessor = nullptr; + const cgltf_accessor *colorAccessor = nullptr; const cgltf_accessor *jointsAccessor = nullptr; const cgltf_accessor *weightsAccessor = nullptr; @@ -387,8 +454,14 @@ namespace thermion tanAccessor = prim.attributes[ai].data; break; case cgltf_attribute_type_texcoord: - if (!uvAccessor) + if (prim.attributes[ai].index == 0) uvAccessor = prim.attributes[ai].data; + else if (prim.attributes[ai].index == 1) + uv1Accessor = prim.attributes[ai].data; + break; + case cgltf_attribute_type_color: + if (prim.attributes[ai].index == 0) + colorAccessor = prim.attributes[ai].data; break; case cgltf_attribute_type_joints: if (!jointsAccessor) @@ -402,8 +475,10 @@ namespace thermion break; } } - if (!posAccessor) + if (!posAccessor) { + appendPlaceholder(); continue; + } // --- Read indices --- std::vector indices; @@ -425,11 +500,24 @@ namespace thermion } } - if (indices.size() < 3 || indices.size() % 3 != 0) + if (indices.size() < 3 || indices.size() % 3 != 0) { + appendPlaceholder(); continue; + } + bool indicesInRange = true; + for (const auto index : indices) { + if (index >= posAccessor->count) { + indicesInRange = false; + break; + } + } + if (!indicesInRange) { + appendPlaceholder(); + continue; + } uint32_t triangleCount = (uint32_t)(indices.size() / 3); - uint32_t newVertexCount = editableTopology + uint32_t newVertexCount = preserveSourceTopology ? (uint32_t)posAccessor->count : triangleCount * 3; @@ -456,6 +544,24 @@ namespace thermion cgltf_accessor_unpack_floats(uvAccessor, srcUVs.data(), srcUVs.size()); } + std::vector srcUV1s; + size_t uv1Components = 0; + if (uv1Accessor) + { + uv1Components = cgltf_num_components(uv1Accessor->type); + srcUV1s.resize(uv1Accessor->count * uv1Components); + cgltf_accessor_unpack_floats(uv1Accessor, srcUV1s.data(), srcUV1s.size()); + } + + std::vector srcColors; + size_t colorComponents = 0; + if (colorAccessor) + { + colorComponents = cgltf_num_components(colorAccessor->type); + srcColors.resize(colorAccessor->count * colorComponents); + cgltf_accessor_unpack_floats(colorAccessor, srcColors.data(), srcColors.size()); + } + std::vector srcTangents; size_t tanComponents = 0; if (tanAccessor) @@ -489,6 +595,12 @@ namespace thermion newTangents.resize(newVertexCount * 4); } std::vector newUVs(newVertexCount * 2); + std::vector newUV1s; + if (uv1Accessor) + { + newUV1s.resize(newVertexCount * 2); + } + std::vector newColors(newVertexCount * 4, 1.0f); std::vector newBarycentrics(newVertexCount * 4); std::vector newJoints; std::vector newWeights; @@ -505,7 +617,7 @@ namespace thermion for (uint32_t dstIdx = 0; dstIdx < newVertexCount; dstIdx++) { - uint32_t srcIdx = editableTopology ? dstIdx : indices[dstIdx]; + uint32_t srcIdx = preserveSourceTopology ? dstIdx : indices[dstIdx]; // Position newPositions[dstIdx * 3 + 0] = srcPositions[srcIdx * posComponents + 0]; @@ -526,6 +638,24 @@ namespace thermion newNormals[dstIdx * 3 + 2] = 0.0f; } + // Secondary UV + if (uv1Accessor && srcIdx < uv1Accessor->count) + { + newUV1s[dstIdx * 2 + 0] = srcUV1s[srcIdx * uv1Components + 0]; + newUV1s[dstIdx * 2 + 1] = srcUV1s[srcIdx * uv1Components + 1]; + } + + // Vertex color + if (colorAccessor && srcIdx < colorAccessor->count) + { + const size_t sourceOffset = srcIdx * colorComponents; + newColors[dstIdx * 4 + 0] = srcColors[sourceOffset + 0]; + newColors[dstIdx * 4 + 1] = srcColors[sourceOffset + 1]; + newColors[dstIdx * 4 + 2] = srcColors[sourceOffset + 2]; + newColors[dstIdx * 4 + 3] = colorComponents >= 4 + ? srcColors[sourceOffset + 3] + : 1.0f; + } // UV if (uvAccessor && srcIdx < uvAccessor->count) { @@ -550,7 +680,7 @@ namespace thermion } // Barycentric - if (!editableTopology) + if (!preserveSourceTopology) { const int corner = dstIdx % 3; newBarycentrics[dstIdx * 4 + 0] = bary[corner][0]; @@ -594,7 +724,7 @@ namespace thermion tris.resize(triangleCount); for (uint32_t i = 0; i < triangleCount; i++) { - tris[i] = editableTopology + tris[i] = preserveSourceTopology ? filament::math::uint3{indices[i * 3], indices[i * 3 + 1], indices[i * 3 + 2]} : filament::math::uint3{i * 3, i * 3 + 1, i * 3 + 2}; } @@ -613,7 +743,7 @@ namespace thermion // preserves shared vertices, so its flat buffer intentionally // matches the smooth buffer. std::vector flatTangentQuats; - if (editableTopology) + if (preserveSourceTopology) { flatTangentQuats = smoothTangentQuats; } @@ -658,119 +788,143 @@ namespace thermion } // --- Build VertexBuffer --- - // Buffer layout: POSITION(0), TANGENTS(1), UV0(2), CUSTOM0(3), COLOR(4), [BONE_INDICES(5), BONE_WEIGHTS(6)] - uint8_t bufferCount = hasSkinning ? 7 : 5; + // Buffer layout: POSITION(0), TANGENTS(1), UV0(2), CUSTOM0(3), + // COLOR(4), [UV1], [BONE_INDICES, BONE_WEIGHTS]. + const uint8_t uv1BufferIndex = 5; + const uint8_t boneIndicesBufferIndex = uv1Accessor ? 6 : 5; + const uint8_t boneWeightsBufferIndex = boneIndicesBufferIndex + 1; + uint8_t bufferCount = 5 + (uv1Accessor ? 1 : 0) + (hasSkinning ? 2 : 0); auto vbBuilder = VertexBuffer::Builder() .vertexCount(newVertexCount) - .bufferCount(bufferCount) - .enableBufferObjects() - .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT3) - .attribute(VertexAttribute::TANGENTS, 1, VertexBuffer::AttributeType::SHORT4) - .normalized(VertexAttribute::TANGENTS) - .attribute(VertexAttribute::UV0, 2, VertexBuffer::AttributeType::FLOAT2) - .attribute(VertexAttribute::CUSTOM0, 3, VertexBuffer::AttributeType::FLOAT4) - .attribute(VertexAttribute::COLOR, 4, VertexBuffer::AttributeType::FLOAT4); + .bufferCount(bufferCount); + + // Unwelded geometry swaps BufferObjects at runtime to toggle + // between smooth and flat tangent frames. Editable geometry, + // on the other hand, must remain writable through the public + // VertexBuffer::setBufferAt API, which is incompatible with + // BufferObject-backed streams. + if (!preserveSourceTopology) + { + vbBuilder.enableBufferObjects(); + } + + vbBuilder + .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT3) + .attribute(VertexAttribute::TANGENTS, 1, VertexBuffer::AttributeType::SHORT4) + .normalized(VertexAttribute::TANGENTS) + .attribute(VertexAttribute::UV0, 2, VertexBuffer::AttributeType::FLOAT2) + .attribute(VertexAttribute::CUSTOM0, 3, VertexBuffer::AttributeType::FLOAT4) + .attribute(VertexAttribute::COLOR, 4, VertexBuffer::AttributeType::FLOAT4); + + if (uv1Accessor) + { + vbBuilder.attribute(VertexAttribute::UV1, uv1BufferIndex, + VertexBuffer::AttributeType::FLOAT2); + } if (hasSkinning) { vbBuilder - .attribute(VertexAttribute::BONE_INDICES, 5, VertexBuffer::AttributeType::UBYTE4) - .attribute(VertexAttribute::BONE_WEIGHTS, 6, VertexBuffer::AttributeType::FLOAT4); + .attribute(VertexAttribute::BONE_INDICES, boneIndicesBufferIndex, VertexBuffer::AttributeType::UBYTE4) + .attribute(VertexAttribute::BONE_WEIGHTS, boneWeightsBufferIndex, VertexBuffer::AttributeType::FLOAT4); } VertexBuffer *vb = vbBuilder.build(*_engine); + auto uploadDirect = [&](uint8_t bufferIndex, const void *source, size_t size) + { + auto *data = new uint8_t[size]; + memcpy(data, source, size); + vb->setBufferAt(*_engine, bufferIndex, + VertexBuffer::BufferDescriptor(data, size, FREE_CB)); + }; + + auto uploadStream = [&](uint8_t bufferIndex, const void *source, size_t size) + { + if (preserveSourceTopology) + { + uploadDirect(bufferIndex, source, size); + return; + } + + auto *data = new uint8_t[size]; + memcpy(data, source, size); + BufferObject *bo = BufferObject::Builder().size(size).build(*_engine); + bo->setBuffer(*_engine, BufferObject::BufferDescriptor(data, size, FREE_CB)); + vb->setBufferObjectAt(*_engine, bufferIndex, bo); + _preservedBufferObjects.push_back(bo); + }; // Buffer 0: POSITION size_t posDataSize = newVertexCount * 3 * sizeof(float); - auto *posData = new uint8_t[posDataSize]; - memcpy(posData, newPositions.data(), posDataSize); - BufferObject *posBO = BufferObject::Builder().size(posDataSize).build(*_engine); - posBO->setBuffer(*_engine, BufferObject::BufferDescriptor(posData, posDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 0, posBO); + uploadStream(0, newPositions.data(), posDataSize); // Buffer 1: TANGENTS (SHORT4 quantized quaternions, matching gltfio's format) // Create both smooth and flat tangent BOs for runtime toggling. size_t tangDataSize = newVertexCount * sizeof(filament::math::short4); - auto *smoothTangData = new uint8_t[tangDataSize]; - memcpy(smoothTangData, smoothTangentQuats.data(), tangDataSize); - BufferObject *smoothTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); - smoothTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(smoothTangData, tangDataSize, FREE_CB)); - - auto *flatTangData = new uint8_t[tangDataSize]; - memcpy(flatTangData, flatTangentQuats.data(), tangDataSize); - BufferObject *flatTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); - flatTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(flatTangData, tangDataSize, FREE_CB)); - - // Bind smooth by default - vb->setBufferObjectAt(*_engine, 1, smoothTangBO); - _smoothTangentBOs.push_back(smoothTangBO); - _flatTangentBOs.push_back(flatTangBO); + if (preserveSourceTopology) + { + uploadDirect(1, smoothTangentQuats.data(), tangDataSize); + _smoothTangentBOs.push_back(nullptr); + _flatTangentBOs.push_back(nullptr); + } + else + { + auto *smoothTangData = new uint8_t[tangDataSize]; + memcpy(smoothTangData, smoothTangentQuats.data(), tangDataSize); + BufferObject *smoothTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); + smoothTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(smoothTangData, tangDataSize, FREE_CB)); + + auto *flatTangData = new uint8_t[tangDataSize]; + memcpy(flatTangData, flatTangentQuats.data(), tangDataSize); + BufferObject *flatTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); + flatTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(flatTangData, tangDataSize, FREE_CB)); + + // Bind smooth by default. + vb->setBufferObjectAt(*_engine, 1, smoothTangBO); + _smoothTangentBOs.push_back(smoothTangBO); + _flatTangentBOs.push_back(flatTangBO); + } // Buffer 2: UV0 size_t uvDataSize = newVertexCount * 2 * sizeof(float); - auto *uvData = new uint8_t[uvDataSize]; - memcpy(uvData, newUVs.data(), uvDataSize); - BufferObject *uvBO = BufferObject::Builder().size(uvDataSize).build(*_engine); - uvBO->setBuffer(*_engine, BufferObject::BufferDescriptor(uvData, uvDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 2, uvBO); + uploadStream(2, newUVs.data(), uvDataSize); // Buffer 3: CUSTOM0 (barycentrics) size_t baryDataSize = newVertexCount * 4 * sizeof(float); - auto *baryData = new uint8_t[baryDataSize]; - memcpy(baryData, newBarycentrics.data(), baryDataSize); - BufferObject *baryBO = BufferObject::Builder().size(baryDataSize).build(*_engine); - baryBO->setBuffer(*_engine, BufferObject::BufferDescriptor(baryData, baryDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 3, baryBO); + uploadStream(3, newBarycentrics.data(), baryDataSize); - // Buffer 4: COLOR (dummy, all white = 1.0) + // Buffer 4: COLOR (source COLOR_0 or white when absent) size_t colorDataSize = newVertexCount * 4 * sizeof(float); - auto *colorData = new uint8_t[colorDataSize]; - auto *colorFloats = reinterpret_cast(colorData); - for (uint32_t i = 0; i < newVertexCount * 4; i++) { - colorFloats[i] = 1.0f; - } - BufferObject *colorBO = BufferObject::Builder().size(colorDataSize).build(*_engine); - colorBO->setBuffer(*_engine, BufferObject::BufferDescriptor(colorData, colorDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 4, colorBO); + uploadStream(4, newColors.data(), colorDataSize); - _preservedBufferObjects.push_back(posBO); - _preservedBufferObjects.push_back(uvBO); - _preservedBufferObjects.push_back(baryBO); - _preservedBufferObjects.push_back(colorBO); + if (uv1Accessor) + { + const size_t uv1DataSize = newVertexCount * 2 * sizeof(float); + uploadStream(uv1BufferIndex, newUV1s.data(), uv1DataSize); + } if (hasSkinning) { // Buffer 5: BONE_INDICES size_t jointDataSize = newVertexCount * 4 * sizeof(uint8_t); - auto *jointData = new uint8_t[jointDataSize]; - memcpy(jointData, newJoints.data(), jointDataSize); - BufferObject *jointBO = BufferObject::Builder().size(jointDataSize).build(*_engine); - jointBO->setBuffer(*_engine, BufferObject::BufferDescriptor(jointData, jointDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 5, jointBO); + uploadStream(boneIndicesBufferIndex, newJoints.data(), jointDataSize); // Buffer 6: BONE_WEIGHTS size_t weightDataSize = newVertexCount * 4 * sizeof(float); - auto *weightData = new uint8_t[weightDataSize]; - memcpy(weightData, newWeights.data(), weightDataSize); - BufferObject *weightBO = BufferObject::Builder().size(weightDataSize).build(*_engine); - weightBO->setBuffer(*_engine, BufferObject::BufferDescriptor(weightData, weightDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 6, weightBO); - - _preservedBufferObjects.push_back(jointBO); - _preservedBufferObjects.push_back(weightBO); + uploadStream(boneWeightsBufferIndex, newWeights.data(), weightDataSize); } // Editable geometry retains source indices. Unwelded geometry // uses a sequential index buffer. - const size_t newIndexCount = editableTopology ? indices.size() : newVertexCount; + const size_t newIndexCount = preserveSourceTopology ? indices.size() : newVertexCount; size_t indexDataSize = newIndexCount * sizeof(uint32_t); auto *newIndices = new uint8_t[indexDataSize]; auto *indexPtr = reinterpret_cast(newIndices); for (uint32_t i = 0; i < newIndexCount; i++) { - indexPtr[i] = editableTopology ? indices[i] : i; + indexPtr[i] = preserveSourceTopology ? indices[i] : i; } IndexBuffer *ib = IndexBuffer::Builder() @@ -786,11 +940,16 @@ namespace thermion vb, ib, 0, newIndexCount); _preservedVertexBuffers.push_back(vb); + _preservedVertexBufferStorageModes.push_back( + preserveSourceTopology + ? VERTEX_BUFFER_STORAGE_MODE_DIRECT + : VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS); _preservedIndexBuffers.push_back(ib); _preservedIndexCounts.push_back(newIndexCount); + rebuiltPrimitiveCount++; TRACE("rebuildVertexBuffers: primitive %zu %s with %u vertices and %zu indices (skinned=%d)", - pi, editableTopology ? "editable topology" : "unwelded", + pi, preserveSourceTopology ? "preserved topology" : "unwelded", newVertexCount, newIndexCount, hasSkinning); } } @@ -834,7 +993,8 @@ namespace thermion } } - _geometryPreserved = true; + _geometryPreserved = rebuiltPrimitiveCount > 0; + return allPrimitivesRebuilt && rebuiltPrimitiveCount > 0; } int GltfSceneAsset::getPrimitiveOffsetForEntity(utils::Entity entity) const @@ -849,6 +1009,11 @@ namespace thermion void GltfSceneAsset::setFlatShading(bool flatShading) { + if (!_supportsFlatShading) + { + Log("setFlatShading called on an asset without flat-shading support"); + return; + } if (flatShading == _flatShading) return; _flatShading = flatShading; @@ -863,6 +1028,8 @@ namespace thermion if (!_preservedVertexBuffers[i]) continue; auto *bo = flatShading ? _flatTangentBOs[i] : _smoothTangentBOs[i]; + if (!bo) + continue; _preservedVertexBuffers[i]->setBufferObjectAt(*_engine, 1, bo); } } diff --git a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp index c0fc84767..b9b443333 100644 --- a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp @@ -14,6 +14,21 @@ namespace thermion return static_cast(_instanceOwner); } + uint32_t GltfSceneAssetInstance::getGeometryCapabilities() const + { + return _instanceOwner->getGeometryCapabilities(); + } + + bool GltfSceneAssetInstance::supportsFlatShading() const + { + return _instanceOwner->supportsFlatShading(); + } + + TVertexBufferStorageMode GltfSceneAssetInstance::getVertexBufferStorageMode(size_t primitiveIndex) const + { + return _instanceOwner->getVertexBufferStorageMode(primitiveIndex); + } + size_t GltfSceneAssetInstance::getBoneCount(size_t skinIndex) const { return _instance->getJointCountAt(skinIndex); @@ -39,4 +54,4 @@ namespace thermion return _ncm->getName(ci); } -} \ No newline at end of file +} diff --git a/thermion_dart/test/all_materials_smoke_test.dart b/thermion_dart/test/all_materials_smoke_test.dart index 3641faef2..d5fa43eaf 100644 --- a/thermion_dart/test/all_materials_smoke_test.dart +++ b/thermion_dart/test/all_materials_smoke_test.dart @@ -30,7 +30,7 @@ void main() async { // stencil highlight path below needs. final cube = await viewer.loadGltf( "file://${testHelper.assetsDir}/cube.glb", - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.barycentrics}, addToScene: true, ); final wireframe = await app.createWireframeMaterialInstance(); diff --git a/thermion_dart/test/geometry_tests.dart b/thermion_dart/test/geometry_tests.dart index a9cf59126..34ff10c16 100644 --- a/thermion_dart/test/geometry_tests.dart +++ b/thermion_dart/test/geometry_tests.dart @@ -27,6 +27,15 @@ void main() async { await testHelper.capture(result.viewer.view, "update_vertex_buffer_1"); final vb = await asset.getVertexBuffer(); expect(vb, isNotNull); + expect(vb!.storageMode, VertexBufferStorageMode.direct); + expect( + asset.geometryCapabilities, + containsAll(const { + SceneAssetGeometryCapability.accessibleGeometryBuffers, + SceneAssetGeometryCapability.writableVertices, + }), + ); + await expectLater(vb.destroy(), throwsStateError); final vertices = Float32List.fromList([ // Front face -1, -1, 1, // 0 @@ -59,7 +68,7 @@ void main() async { -1, 1, 1, // 3 (22) -1, 1, -1, // 7 (23) ]); - await vb!.setBufferAt(0, vertices); + await vb.setBufferAt(0, vertices); await testHelper.capture(result.viewer.view, "update_vertex_buffer_2"); }); }); diff --git a/thermion_dart/test/morph_animation_tests.dart b/thermion_dart/test/morph_animation_tests.dart index 4230e8150..f5b902bf6 100644 --- a/thermion_dart/test/morph_animation_tests.dart +++ b/thermion_dart/test/morph_animation_tests.dart @@ -53,7 +53,7 @@ void main() async { ); }); - test('editable vertices preserve source topology and morph animation', () async { + test('editable vertices support mutation and morph animation', () async { await testHelper.withViewer( (viewer) async { final path = '${testHelper.assetsDir}/cube_with_morph_targets.glb'; @@ -72,8 +72,20 @@ void main() async { final originalPose = await capture(original); await viewer.destroyAsset(original); - final editable = await viewer.loadGltf(path, vertexBufferMode: VertexBufferMode.editable); - expect(editable.getVertexBuffer()!.getVertexCount(), source.vertices.length ~/ 3); + final editable = await viewer.loadGltf( + path, + requiredGeometryCapabilities: const { + SceneAssetGeometryCapability.writableVertices, + SceneAssetGeometryCapability.preservedTopology, + }, + ); + final editableVertexBuffer = editable.getVertexBuffer()!; + expect(editableVertexBuffer.supportsSetBufferAt, isTrue); + expect(editableVertexBuffer.storageMode, VertexBufferStorageMode.direct); + expect(editableVertexBuffer.getVertexCount(), source.vertices.length ~/ 3); + + final instanceVertexBuffer = (await editable.getInstance(0)).getVertexBuffer()!; + expect(instanceVertexBuffer.storageMode, VertexBufferStorageMode.direct); final editablePose = await capture(editable); expect( @@ -84,6 +96,31 @@ void main() async { expect(_meanAbsoluteDifference(originalPose.rest, editablePose.rest), lessThan(0.02)); expect(_meanAbsoluteDifference(originalPose.morphed, editablePose.morphed), lessThan(0.02)); + final targets = (await editable.getMorphTargetSets()).single; + await targets.setAllWeights([0.0]); + + final editedPositions = Float32List.fromList(source.vertices); + for (var i = 0; i < editedPositions.length; i += 3) { + editedPositions[i] += 0.5; + } + + await editableVertexBuffer.setBufferAt(0, editedPositions).timeout(const Duration(seconds: 5)); + final editedRest = (await testHelper.capture(viewer.view, null)).values.single; + + expect( + _meanAbsoluteDifference(editablePose.rest, editedRest), + greaterThan(0.005), + reason: 'updating the editable position stream must change the rendered geometry', + ); + + await targets.setAllWeights([1.0]); + final editedMorphed = (await testHelper.capture(viewer.view, null)).values.single; + expect( + _meanAbsoluteDifference(editedRest, editedMorphed), + greaterThan(0.005), + reason: 'morph weights must still deform geometry after editing its base positions', + ); + await viewer.destroyAsset(editable); }, bg: kRed, diff --git a/thermion_dart/test/overlay_tests.dart b/thermion_dart/test/overlay_tests.dart index ed6beeab6..de04cf393 100644 --- a/thermion_dart/test/overlay_tests.dart +++ b/thermion_dart/test/overlay_tests.dart @@ -1,6 +1,5 @@ import 'package:test/test.dart'; import 'package:thermion_dart/thermion_dart.dart'; -import 'package:vector_math/vector_math_64.dart'; import 'helpers.dart'; void main() async { @@ -222,34 +221,50 @@ void main() async { }, postProcessing: true); }); - test('setStencilHighlight and setFlatShading throw without unwelded vertex buffers', () async { + test('highlighting requires accessible buffers while flat shading requires unwelded geometry', () async { await testHelper.withViewer((viewer) async { - final cube = await viewer.loadGltf("file://${testHelper.assetsDir}/cube.glb", addToScene: true); - - // Outlining and flat shading both need the preserved (rebuilt) vertex - // buffers — without unwelded mode these used to silently do nothing; - // they must now throw with an actionable message. - await viewer.view.setHighlightOverlayEnabled(true); - expect( - () => viewer.view.setStencilHighlight(cube), - throwsA( - isA().having( - (e) => e.toString(), - 'message', - contains('vertexBufferMode: VertexBufferMode.unwelded'), - ), + final accessibleGeometryMatcher = throwsA( + isA().having((e) => e.toString(), 'message', contains('requires accessible geometry buffers')), + ); + final unweldedMatcher = throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('requiredGeometryCapabilities containing uniqueTriangleCorners'), ), ); - expect( - () => cube.setFlatShading(true), - throwsA( - isA().having( - (e) => e.toString(), - 'message', - contains('vertexBufferMode: VertexBufferMode.unwelded'), - ), + + final original = await viewer.loadGltf("file://${testHelper.assetsDir}/cube.glb", addToScene: true); + await expectLater(viewer.view.setStencilHighlight(original), accessibleGeometryMatcher); + await expectLater(original.setFlatShading(true), unweldedMatcher); + + await expectLater( + viewer.loadGltf( + "file://${testHelper.assetsDir}/cube.glb", + requiredGeometryCapabilities: const { + SceneAssetGeometryCapability.preservedTopology, + SceneAssetGeometryCapability.uniqueTriangleCorners, + }, ), + throwsArgumentError, + ); + + // Editable assets preserve reusable vertex/index buffers, so the + // POSITION-only silhouette pass works without barycentrics. They still + // cannot swap the tangent BufferObjects required by flat shading. + final editable = await viewer.loadGltf( + "file://${testHelper.assetsDir}/cube.glb", + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.accessibleGeometryBuffers}, + addToScene: true, ); + expect(editable.getVertexBuffer(), isNotNull); + expect(editable.getVertexBuffer()!.supportsSetBufferAt, isTrue); + expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.accessibleGeometryBuffers)); + expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.writableVertices)); + expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.preservedTopology)); + await viewer.view.setStencilHighlight(editable); + await viewer.view.removeStencilHighlight(editable); + await expectLater(editable.setFlatShading(true), unweldedMatcher); }); }); } diff --git a/thermion_dart/test/vertex_index_buffer_tests.dart b/thermion_dart/test/vertex_index_buffer_tests.dart index 181ce41b0..a2403d809 100644 --- a/thermion_dart/test/vertex_index_buffer_tests.dart +++ b/thermion_dart/test/vertex_index_buffer_tests.dart @@ -8,6 +8,40 @@ void main() async { final testHelper = TestHelper("vertex_index_buffer"); await testHelper.setup(); group("VertexBufferBuilder tests", () { + test('direct and BufferObject storage enforce their update APIs', () async { + await ViewerBuilder(testHelper).execute((result) async { + final manager = FilamentApp.instance!.renderableManager; + + final directBuilder = manager.createVertexBufferBuilder() + ..bufferCount(1) + ..vertexCount(3) + ..attribute(VertexAttribute.POSITION, 0, VertexAttributeType.FLOAT3); + final direct = await directBuilder.build(); + expect(direct.storageMode, VertexBufferStorageMode.direct); + + final bufferObjectBuilder = manager.createBufferObjectBuilder()..size(3 * 3 * Float32List.bytesPerElement); + final bufferObject = await bufferObjectBuilder.build(); + await bufferObject.setBuffer(Float32List.fromList([-1, -1, 0, 1, -1, 0, 0, 1, 0])); + + await expectLater(direct.setBufferObjectAt(0, bufferObject), throwsStateError); + + final bufferObjectVertexBuilder = manager.createVertexBufferBuilder() + ..bufferCount(1) + ..vertexCount(3) + ..enableBufferObjects() + ..attribute(VertexAttribute.POSITION, 0, VertexAttributeType.FLOAT3); + final bufferObjectVertexBuffer = await bufferObjectVertexBuilder.build(); + expect(bufferObjectVertexBuffer.storageMode, VertexBufferStorageMode.bufferObjects); + expect(bufferObjectVertexBuffer.supportsSetBufferAt, isFalse); + await expectLater(bufferObjectVertexBuffer.setBufferAt(0, Float32List(9)), throwsStateError); + await bufferObjectVertexBuffer.setBufferObjectAt(0, bufferObject); + + await bufferObjectVertexBuffer.destroy(); + await bufferObject.destroy(); + await direct.destroy(); + }); + }); + test('create and build simple vertex buffer', () async { await ViewerBuilder(testHelper).setBackgroundColor(kBlue).execute((result) async { final app = FilamentApp.instance!; diff --git a/thermion_dart/test/view_tests.dart b/thermion_dart/test/view_tests.dart index a47e868ad..d252b8464 100644 --- a/thermion_dart/test/view_tests.dart +++ b/thermion_dart/test/view_tests.dart @@ -652,7 +652,7 @@ void main() async { // Load FlightHelmet, a multi-mesh glTF asset final asset = await result.viewer.loadGltf( p.join(testHelper.assetsDir, "FlightHelmet", "FlightHelmet.gltf"), - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.accessibleGeometryBuffers}, ); expect(asset, isNotNull); diff --git a/thermion_dart/test/wireframe_renderable_test.dart b/thermion_dart/test/wireframe_renderable_test.dart index 161e46711..655c89cbd 100644 --- a/thermion_dart/test/wireframe_renderable_test.dart +++ b/thermion_dart/test/wireframe_renderable_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:thermion_dart/thermion_dart.dart'; import 'package:test/test.dart'; import 'helpers.dart'; @@ -6,23 +8,134 @@ void main() async { final testHelper = TestHelper("wireframe_renderable"); await testHelper.setup(); + test('required geometry capabilities reject unsupported primitives', () async { + await testHelper.withViewer((viewer) async { + final binary = ByteData(28) + ..setFloat32(12, 1.0, Endian.little) + ..setUint16(26, 1, Endian.little); + final jsonBytes = utf8.encode( + jsonEncode({ + 'asset': {'version': '2.0'}, + 'buffers': [ + {'byteLength': 28}, + ], + 'bufferViews': [ + {'buffer': 0, 'byteOffset': 0, 'byteLength': 24, 'target': 34962}, + {'buffer': 0, 'byteOffset': 24, 'byteLength': 4, 'target': 34963}, + ], + 'accessors': [ + { + 'bufferView': 0, + 'componentType': 5126, + 'count': 2, + 'type': 'VEC3', + 'min': [0, 0, 0], + 'max': [1, 0, 0], + }, + {'bufferView': 1, 'componentType': 5123, 'count': 2, 'type': 'SCALAR'}, + ], + 'meshes': [ + { + 'primitives': [ + { + 'attributes': {'POSITION': 0}, + 'indices': 1, + 'mode': 1, + }, + ], + }, + ], + 'nodes': [ + {'mesh': 0}, + ], + 'scenes': [ + { + 'nodes': [0], + }, + ], + 'scene': 0, + }), + ); + final paddedJsonLength = (jsonBytes.length + 3) & ~3; + final totalLength = 12 + 8 + paddedJsonLength + 8 + 28; + final glbData = ByteData(totalLength) + ..setUint32(0, 0x46546c67, Endian.little) + ..setUint32(4, 2, Endian.little) + ..setUint32(8, totalLength, Endian.little) + ..setUint32(12, paddedJsonLength, Endian.little) + ..setUint32(16, 0x4e4f534a, Endian.little); + final glb = glbData.buffer.asUint8List(); + glb.setRange(20, 20 + jsonBytes.length, jsonBytes); + glb.fillRange(20 + jsonBytes.length, 20 + paddedJsonLength, 0x20); + final binaryHeaderOffset = 20 + paddedJsonLength; + glbData + ..setUint32(binaryHeaderOffset, 28, Endian.little) + ..setUint32(binaryHeaderOffset + 4, 0x004e4942, Endian.little); + glb.setRange(binaryHeaderOffset + 8, totalLength, binary.buffer.asUint8List()); + + await expectLater( + viewer.loadGltfFromBuffer( + glb, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.writableVertices}, + ), + throwsA(isA()), + ); + }); + }); + + test('geometry requirements are snapshotted when loading begins', () async { + await testHelper.withViewer((viewer) async { + final requirements = {SceneAssetGeometryCapability.writableVertices}; + final load = viewer.loadGltf( + "file://${testHelper.assetsDir}/cube.glb", + requiredGeometryCapabilities: requirements, + ); + requirements + ..clear() + ..add(SceneAssetGeometryCapability.barycentrics); + + final asset = await load; + expect(asset.geometryCapabilities, contains(SceneAssetGeometryCapability.writableVertices)); + expect(asset.geometryCapabilities, isNot(contains(SceneAssetGeometryCapability.barycentrics))); + }); + }); + test('load glTF with unwelded vertex buffers and apply wireframe material', () async { await ViewerBuilder(testHelper).addSun().setCameraPosition(Vector3(0, 1, 1.5)).execute((result) async { final original = await result.viewer.loadGltf( "file://${testHelper.assetsDir}/FlightHelmet/FlightHelmet.gltf", - vertexBufferMode: VertexBufferMode.original, addToScene: true, ); - await testHelper.capture(result.viewer.view, "vertex_buffer_original"); + // Golden artifact names are stable IDs; keep the legacy names even when + // the public API terminology changes. + await testHelper.capture(result.viewer.view, "rebuildVertices_false"); + expect(original.geometryCapabilities, isEmpty); await result.viewer.removeFromScene(original); final rebuilt = await result.viewer.loadGltf( "file://${testHelper.assetsDir}/FlightHelmet/FlightHelmet.gltf", - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.barycentrics}, addToScene: true, ); + expect( + rebuilt.geometryCapabilities, + containsAll(const { + SceneAssetGeometryCapability.barycentrics, + SceneAssetGeometryCapability.accessibleGeometryBuffers, + }), + ); + expect(rebuilt.supportsFlatShading, isTrue); + expect(rebuilt.geometryCapabilities, isNot(contains(SceneAssetGeometryCapability.writableVertices))); + expect(rebuilt.geometryCapabilities, contains(SceneAssetGeometryCapability.uniqueTriangleCorners)); + + final unweldedVertexBuffer = rebuilt.getVertexBuffer()!; + expect(unweldedVertexBuffer.supportsSetBufferAt, isFalse); + await expectLater( + unweldedVertexBuffer.setBufferAt(0, Float32List(0)), + throwsA(isA().having((error) => error.toString(), 'message', contains('writableVertices'))), + ); - await testHelper.capture(result.viewer.view, "vertex_buffer_unwelded"); + await testHelper.capture(result.viewer.view, "rebuildVertices_true"); // Use typed wireframe wrapper final wireframe = await FilamentApp.instance!.createWireframeMaterialInstance(); @@ -32,7 +145,7 @@ void main() async { await wireframe.setDoubleSided(true); await rebuilt.setMaterialInstanceForAll(wireframe.materialInstance); - await testHelper.capture(result.viewer.view, "vertex_buffer_unwelded_wireframe"); + await testHelper.capture(result.viewer.view, "rebuildVertices_true_wireframe"); final ubershader = await FilamentApp.instance!.createUbershaderMaterial(doubleSided: true); @@ -41,13 +154,13 @@ void main() async { await ubershader.setRoughnessFactor(1.0); await rebuilt.setMaterialInstanceForAll(ubershader.materialInstance); - await testHelper.capture(result.viewer.view, "vertex_buffer_unwelded_ubershader"); + await testHelper.capture(result.viewer.view, "rebuildVertices_true_ubershader"); await result.viewer.removeFromScene(rebuilt); final flatAsset = await result.viewer.loadGltf( "file://${testHelper.assetsDir}/FlightHelmet/FlightHelmet.gltf", - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.uniqueTriangleCorners}, addToScene: true, ); @@ -70,10 +183,13 @@ void main() async { "file://${testHelper.assetsDir}/cube.glb", addToScene: true, initialInstances: 2, - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.barycentrics}, ); final instance2 = await asset.createInstance(); + expect(instance2.supportsFlatShading, isTrue); + await instance2.setFlatShading(true); + await instance2.setFlatShading(false); await instance2.setTransform(Matrix4.translation(Vector3(2, 0, 0))); await result.viewer.addToScene(instance2); await testHelper.capture(result.viewer.view, "instanced_preserved_before");