diff --git a/CMakeLists.txt b/CMakeLists.txt index cb1ce4b05..e6307e85d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,14 @@ FetchContent_Declare(glslang GIT_REPOSITORY https://github.com/BabylonJS/glslang.git GIT_TAG 284e4301e5a6b44b279635276588db7cdd942624 EXCLUDE_FROM_ALL) +FetchContent_Declare(draco + GIT_REPOSITORY https://github.com/google/draco.git + GIT_TAG 1.5.7 + EXCLUDE_FROM_ALL) +FetchContent_Declare(meshoptimizer + GIT_REPOSITORY https://github.com/zeux/meshoptimizer.git + GIT_TAG v0.22 + EXCLUDE_FROM_ALL) FetchContent_Declare(googletest URL "https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz" EXCLUDE_FROM_ALL) @@ -117,6 +125,7 @@ option(BABYLON_NATIVE_CHECK_THREAD_AFFINITY "Checks thread safety in the graphic option(BABYLON_NATIVE_PLUGIN_EXTERNALTEXTURE "Include Babylon Native Plugin ExternalTexture." ON) option(BABYLON_NATIVE_PLUGIN_NATIVECAMERA "Include Babylon Native Plugin NativeCamera." ON) option(BABYLON_NATIVE_PLUGIN_NATIVECAPTURE "Include Babylon Native Plugin NativeCapture." ON) +option(BABYLON_NATIVE_PLUGIN_NATIVEDRACO "Include Babylon Native Plugin NativeDraco." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEENCODING "Include Babylon Native Plugin NativeEncoding." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE "Include Babylon Native Plugin NativeEngine." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_WEBP "Enable WebP image parsing in NativeEngine (via bimg)." ON) @@ -127,6 +136,7 @@ if(NOT BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES) endif() option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_COMPILESHADERS "Include Babylon Native Plugin NativeEngine - Compile Shaders." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEINPUT "Include Babylon Native Plugin NativeInput." ON) +option(BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT "Include Babylon Native Plugin NativeMeshopt." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS "Include Babylon Native Plugin NativeOptimizations." ON) option(BABYLON_NATIVE_PLUGIN_NATIVETRACING "Include Babylon Native Plugin NativeTracing." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEXR "Include Babylon Native Plugin XR." ON) diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index 5a41c1853..a085cd610 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -167,6 +167,75 @@ if(NOT TARGET glslang) set_property(TARGET SPIRV PROPERTY FOLDER Dependencies/glslang) endif() +# Assigns an IDE folder to every target a dependency declares, including those in +# nested directories. draco alone contributes ~25 internal object libraries; listing +# them by name would silently go stale whenever the dependency reorganizes its build, +# leaving targets ungrouped at the solution root. +function(group_directory_targets DIR FOLDER_NAME) + get_property(_targets DIRECTORY "${DIR}" PROPERTY BUILDSYSTEM_TARGETS) + foreach(_target IN LISTS _targets) + # INTERFACE libraries have no build output and cannot carry a FOLDER. + get_target_property(_type ${_target} TYPE) + if(NOT _type STREQUAL "INTERFACE_LIBRARY") + set_property(TARGET ${_target} PROPERTY FOLDER "${FOLDER_NAME}") + endif() + endforeach() + + get_property(_subdirs DIRECTORY "${DIR}" PROPERTY SUBDIRECTORIES) + foreach(_subdir IN LISTS _subdirs) + group_directory_targets("${_subdir}" "${FOLDER_NAME}") + endforeach() +endfunction() + +# -------------------------------------------------- +# draco (mesh decompression, native replacement for the WASM decoder) +# -------------------------------------------------- +if(BABYLON_NATIVE_PLUGIN_NATIVEDRACO AND NOT TARGET draco AND NOT TARGET draco_static) + set(DRACO_TESTS OFF CACHE BOOL "" FORCE) + set(DRACO_INSTALL OFF CACHE BOOL "" FORCE) + set(DRACO_BUILD_EXECUTABLES OFF CACHE BOOL "" FORCE) + set(DRACO_JS_GLUE OFF CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable_With_Message(draco) + + # The combined draco library target is named `draco` for MSVC and + # `draco_static` for other toolchains (static build); `draco::draco` aliases + # whichever exists. + if(TARGET draco) + set(_draco_lib draco) + else() + set(_draco_lib draco_static) + endif() + + # draco adds its include paths as PRIVATE (see cmake/draco_targets.cmake + # draco_add_library), so linking draco::draco does not expose the draco/* + # headers to consumers. Re-export them as PUBLIC on the combined target. + # The generated draco/draco_features.h is emitted under CMAKE_BINARY_DIR + # (draco uses ${CMAKE_BINARY_DIR} as its generated-sources root). + target_include_directories(${_draco_lib} SYSTEM PUBLIC + "${draco_SOURCE_DIR}" + "${draco_SOURCE_DIR}/src" + "${draco_BINARY_DIR}" + "${CMAKE_BINARY_DIR}") + + # Assign IDE folders for every draco target, not just the combined library. + group_directory_targets("${draco_SOURCE_DIR}" Dependencies/draco) +endif() + +# -------------------------------------------------- +# meshoptimizer (mesh decompression, native replacement for the WASM decoder) +# -------------------------------------------------- +if(BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT AND NOT TARGET meshoptimizer) + set(MESHOPT_BUILD_DEMO OFF CACHE BOOL "" FORCE) + set(MESHOPT_BUILD_GLTFPACK OFF CACHE BOOL "" FORCE) + set(MESHOPT_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(MESHOPT_WERROR OFF CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable_With_Message(meshoptimizer) + + group_directory_targets("${meshoptimizer_SOURCE_DIR}" Dependencies/meshoptimizer) +endif() + # -------------------------------------------------- # googletest # -------------------------------------------------- diff --git a/Embedding/CMakeLists.txt b/Embedding/CMakeLists.txt index 20715f0bb..b2f704726 100644 --- a/Embedding/CMakeLists.txt +++ b/Embedding/CMakeLists.txt @@ -107,6 +107,16 @@ if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING) target_link_libraries(Embedding PRIVATE NativeEncoding) endif() +if(BABYLON_NATIVE_PLUGIN_NATIVEDRACO) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEDRACO=1) + target_link_libraries(Embedding PRIVATE NativeDraco) +endif() + +if(BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT=1) + target_link_libraries(Embedding PRIVATE NativeMeshopt) +endif() + if(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS) target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS=1) target_link_libraries(Embedding PRIVATE NativeOptimizations) diff --git a/Embedding/Source/Runtime.cpp b/Embedding/Source/Runtime.cpp index 852528288..c27945818 100644 --- a/Embedding/Source/Runtime.cpp +++ b/Embedding/Source/Runtime.cpp @@ -5,6 +5,12 @@ #if BABYLON_NATIVE_PLUGIN_NATIVEENGINE #include #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEDRACO +#include +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT +#include +#endif #if BABYLON_NATIVE_PLUGIN_NATIVECAMERA #include #endif @@ -270,6 +276,12 @@ namespace Babylon::Embedding #if BABYLON_NATIVE_PLUGIN_NATIVEENGINE Babylon::Plugins::NativeEngine::Initialize(env); #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEDRACO + Babylon::Plugins::NativeDraco::Initialize(env); +#endif +#if BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT + Babylon::Plugins::NativeMeshopt::Initialize(env); +#endif #if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS Babylon::Plugins::NativeOptimizations::Initialize(env); #endif diff --git a/Plugins/CMakeLists.txt b/Plugins/CMakeLists.txt index 38b7aaa18..a5c82ee5d 100644 --- a/Plugins/CMakeLists.txt +++ b/Plugins/CMakeLists.txt @@ -10,18 +10,26 @@ if(BABYLON_NATIVE_PLUGIN_NATIVECAPTURE) add_subdirectory(NativeCapture) endif() +if(BABYLON_NATIVE_PLUGIN_NATIVEDRACO) + add_subdirectory(NativeDraco) +endif() + if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING) add_subdirectory(NativeEncoding) endif() if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) add_subdirectory(NativeEngine) -endif() + endif() -if(BABYLON_NATIVE_PLUGIN_NATIVEINPUT) + if(BABYLON_NATIVE_PLUGIN_NATIVEINPUT) add_subdirectory(NativeInput) endif() +if(BABYLON_NATIVE_PLUGIN_NATIVEMESHOPT) + add_subdirectory(NativeMeshopt) +endif() + if(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS) add_subdirectory(NativeOptimizations) endif() diff --git a/Plugins/NativeDraco/CMakeLists.txt b/Plugins/NativeDraco/CMakeLists.txt new file mode 100644 index 000000000..2a596b74d --- /dev/null +++ b/Plugins/NativeDraco/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES + "Include/Babylon/Plugins/NativeDraco.h" + "Source/NativeDraco.cpp") + +add_library(NativeDraco ${SOURCES}) +warnings_as_errors(NativeDraco) + +target_include_directories(NativeDraco + PUBLIC "Include") + +target_link_libraries(NativeDraco + PUBLIC napi + PRIVATE JsRuntimeInternal + PRIVATE draco::draco) + +set_property(TARGET NativeDraco PROPERTY FOLDER Plugins) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Plugins/NativeDraco/Include/Babylon/Plugins/NativeDraco.h b/Plugins/NativeDraco/Include/Babylon/Plugins/NativeDraco.h new file mode 100644 index 000000000..6f56e1b94 --- /dev/null +++ b/Plugins/NativeDraco/Include/Babylon/Plugins/NativeDraco.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace Babylon::Plugins::NativeDraco +{ + // Exposes `_native.decodeDracoMesh(dataView, attributes?)`, a synchronous + // native replacement for Babylon's WebAssembly Draco decoder. Babylon.js + // routes its DracoDecoder to this function when it is present. + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Plugins/NativeDraco/Source/NativeDraco.cpp b/Plugins/NativeDraco/Source/NativeDraco.cpp new file mode 100644 index 000000000..c0e40fcec --- /dev/null +++ b/Plugins/NativeDraco/Source/NativeDraco.cpp @@ -0,0 +1,471 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Babylon::Plugins +{ + namespace + { + // De-interleaves and tightly packs one Draco attribute's per-point values into a + // freshly allocated JS typed array of type T. This mirrors emscripten's + // GetAttributeDataArrayForAllPoints, which Babylon's WASM decoder relies on. + template + Napi::Value CopyAttributeData(Napi::Env env, const draco::PointCloud& pointCloud, const draco::PointAttribute& attribute) + { + const int8_t numComponents = attribute.num_components(); + const uint32_t numPoints = pointCloud.num_points(); + const size_t numValues = static_cast(numPoints) * numComponents; + + auto array = Napi::TypedArrayOf::New(env, numValues); + T* out = array.Data(); + + for (draco::PointIndex i(0); i < numPoints; ++i) + { + const draco::AttributeValueIndex valueIndex = attribute.mapped_index(i); + attribute.ConvertValue(valueIndex, numComponents, out + static_cast(i.value()) * numComponents); + } + + return array; + } + + // Builds the { kind, data, size, byteOffset, byteStride, normalized } record that + // Babylon's DracoDecoder consumes for each decoded vertex attribute. + Napi::Value DecodeAttribute(Napi::Env env, const draco::PointCloud& pointCloud, const draco::PointAttribute& attribute, const std::string& kind) + { + const int8_t numComponents = attribute.num_components(); + + Napi::Value data; + uint32_t bytesPerComponent = 0; + switch (attribute.data_type()) + { + case draco::DT_FLOAT32: data = CopyAttributeData(env, pointCloud, attribute); bytesPerComponent = 4; break; + case draco::DT_INT8: data = CopyAttributeData(env, pointCloud, attribute); bytesPerComponent = 1; break; + case draco::DT_UINT8: data = CopyAttributeData(env, pointCloud, attribute); bytesPerComponent = 1; break; + case draco::DT_INT16: data = CopyAttributeData(env, pointCloud, attribute); bytesPerComponent = 2; break; + case draco::DT_UINT16: data = CopyAttributeData(env, pointCloud, attribute); bytesPerComponent = 2; break; + case draco::DT_INT32: data = CopyAttributeData(env, pointCloud, attribute); bytesPerComponent = 4; break; + case draco::DT_UINT32: data = CopyAttributeData(env, pointCloud, attribute); bytesPerComponent = 4; break; + default: + throw Napi::Error::New(env, "Draco: Cannot decode invalid attribute data type " + std::to_string(attribute.data_type())); + } + + auto result = Napi::Object::New(env); + result.Set("kind", Napi::String::New(env, kind)); + result.Set("data", data); + result.Set("size", Napi::Number::New(env, numComponents)); + // GetAttributeDataArrayForAllPoints returns a tightly packed array, so the + // consumable buffer has offset 0 and a stride of one full vertex. + result.Set("byteOffset", Napi::Number::New(env, 0)); + result.Set("byteStride", Napi::Number::New(env, static_cast(numComponents) * bytesPerComponent)); + result.Set("normalized", Napi::Boolean::New(env, attribute.normalized())); + return result; + } + + Napi::Value DecodeDracoMesh(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + + if (info.Length() < 1 || !info[0].IsTypedArray()) + { + throw Napi::TypeError::New(env, "decodeDracoMesh: expected a typed array of compressed Draco data"); + } + + const auto typedArray = info[0].As(); + const auto* data = static_cast(typedArray.ArrayBuffer().Data()) + typedArray.ByteOffset(); + const size_t size = typedArray.ByteLength(); + + draco::DecoderBuffer buffer; + buffer.Init(data, size); + + draco::Decoder decoder; + const auto geometryTypeStatus = draco::Decoder::GetEncodedGeometryType(&buffer); + if (!geometryTypeStatus.ok()) + { + throw Napi::Error::New(env, geometryTypeStatus.status().error_msg()); + } + + std::unique_ptr geometry; + Napi::Value indices = env.Null(); + + switch (geometryTypeStatus.value()) + { + case draco::TRIANGULAR_MESH: + { + auto meshStatus = decoder.DecodeMeshFromBuffer(&buffer); + if (!meshStatus.ok()) + { + throw Napi::Error::New(env, meshStatus.status().error_msg()); + } + + std::unique_ptr mesh = std::move(meshStatus).value(); + + const uint32_t numFaces = mesh->num_faces(); + auto indicesArray = Napi::Uint32Array::New(env, static_cast(numFaces) * 3); + uint32_t* out = indicesArray.Data(); + for (uint32_t f = 0; f < numFaces; ++f) + { + const draco::Mesh::Face& face = mesh->face(draco::FaceIndex(f)); + out[f * 3 + 0] = face[0].value(); + out[f * 3 + 1] = face[1].value(); + out[f * 3 + 2] = face[2].value(); + } + indices = indicesArray; + + geometry = std::move(mesh); + break; + } + case draco::POINT_CLOUD: + { + auto pointCloudStatus = decoder.DecodePointCloudFromBuffer(&buffer); + if (!pointCloudStatus.ok()) + { + throw Napi::Error::New(env, pointCloudStatus.status().error_msg()); + } + geometry = std::move(pointCloudStatus).value(); + break; + } + default: + throw Napi::Error::New(env, "Draco: Cannot decode invalid geometry type"); + } + + auto attributes = Napi::Array::New(env); + uint32_t attributeCount = 0; + + if (info.Length() > 1 && info[1].IsObject()) + { + // glTF path: caller provides a map of Babylon vertex-buffer kind -> Draco unique id. + const auto attributeIds = info[1].As(); + const auto keys = attributeIds.GetPropertyNames(); + for (uint32_t i = 0; i < keys.Length(); ++i) + { + const auto kind = keys.Get(i).As().Utf8Value(); + const uint32_t id = attributeIds.Get(kind).As().Uint32Value(); + const draco::PointAttribute* attribute = geometry->GetAttributeByUniqueId(id); + if (attribute != nullptr) + { + attributes.Set(attributeCount++, DecodeAttribute(env, *geometry, *attribute, kind)); + } + } + } + else + { + // Standalone path: probe the standard named attributes. + const struct + { + const char* kind; + draco::GeometryAttribute::Type type; + } namedAttributes[] = { + {"position", draco::GeometryAttribute::POSITION}, + {"normal", draco::GeometryAttribute::NORMAL}, + {"color", draco::GeometryAttribute::COLOR}, + {"uv", draco::GeometryAttribute::TEX_COORD}, + }; + + for (const auto& named : namedAttributes) + { + if (geometry->GetNamedAttributeId(named.type) != -1) + { + attributes.Set(attributeCount++, DecodeAttribute(env, *geometry, *geometry->GetNamedAttribute(named.type), named.kind)); + } + } + } + + auto result = Napi::Object::New(env); + result.Set("indices", indices); + result.Set("attributes", attributes); + result.Set("totalVertices", Napi::Number::New(env, static_cast(geometry->num_points()))); + return result; + } + + // ---------------------------------------------------------------------------- + // Encoder + // ---------------------------------------------------------------------------- + + draco::GeometryAttribute::Type DracoAttributeTypeFromName(const std::string& name) + { + if (name == "POSITION") return draco::GeometryAttribute::POSITION; + if (name == "NORMAL") return draco::GeometryAttribute::NORMAL; + if (name == "COLOR") return draco::GeometryAttribute::COLOR; + if (name == "TEX_COORD") return draco::GeometryAttribute::TEX_COORD; + return draco::GeometryAttribute::GENERIC; + } + + // Returns a typed pointer to the start of the typed array's data, honoring its byte offset. + template + const T* TypedArrayData(const Napi::TypedArray& array) + { + const auto* base = static_cast(array.ArrayBuffer().Data()) + array.ByteOffset(); + return reinterpret_cast(base); + } + + // Replicates draco's emscripten PointCloudBuilder::AddAttribute: creates a de-interleaved + // per-point attribute and returns its attribute id (which equals its unique id, see + // PointCloud::SetAttribute -> set_unique_id). + template + int AddAttributeToMesh(draco::Mesh& mesh, draco::GeometryAttribute::Type type, draco::DataType dataType, uint32_t numVertices, int8_t numComponents, const T* values) + { + std::unique_ptr att(new draco::PointAttribute()); + att->Init(type, numComponents, dataType, /* normalized */ false, numVertices); + const int attId = mesh.AddAttribute(std::move(att)); + draco::PointAttribute* attPtr = mesh.attribute(attId); + for (draco::PointIndex i(0); i < numVertices; ++i) + { + attPtr->SetAttributeValue(attPtr->mapped_index(i), &values[static_cast(i.value()) * numComponents]); + } + if (mesh.num_points() == 0) + { + mesh.set_num_points(numVertices); + } + else if (mesh.num_points() != numVertices) + { + return -1; + } + return attId; + } + + // Reads and validates an attribute's component count. draco stores the component count as + // an int8_t, and divides the element count by it, so a non-positive or oversized value would + // either divide by zero or silently truncate. + int8_t ReadAttributeSize(Napi::Env env, const Napi::Object& attr, const Napi::TypedArray& data) + { + const int32_t size = attr.Get("size").As().Int32Value(); + if (size <= 0 || size > std::numeric_limits::max()) + { + throw Napi::RangeError::New(env, "Draco: attribute 'size' must be in [1, 127], got " + std::to_string(size)); + } + if (data.ElementLength() % static_cast(size) != 0) + { + throw Napi::RangeError::New(env, "Draco: attribute data length (" + std::to_string(data.ElementLength()) + + ") is not a multiple of its component count (" + std::to_string(size) + ")."); + } + const size_t numVertices = data.ElementLength() / static_cast(size); + if (static_cast(numVertices) > 0xFFFFFFFFull) + { + throw Napi::RangeError::New(env, "Draco: attribute has too many vertices: " + std::to_string(numVertices)); + } + return static_cast(size); + } + + // Dispatches AddAttributeToMesh on the typed array's element type, mirroring the WASM + // encoder's addAttributeMap. + int AddTypedAttributeToMesh(Napi::Env env, draco::Mesh& mesh, draco::GeometryAttribute::Type type, const Napi::TypedArray& data, int8_t numComponents) + { + const uint32_t numVertices = static_cast(data.ElementLength() / static_cast(numComponents)); + switch (data.TypedArrayType()) + { + case napi_float32_array: return AddAttributeToMesh(mesh, type, draco::DT_FLOAT32, numVertices, numComponents, TypedArrayData(data)); + case napi_uint32_array: return AddAttributeToMesh(mesh, type, draco::DT_UINT32, numVertices, numComponents, TypedArrayData(data)); + case napi_uint16_array: return AddAttributeToMesh(mesh, type, draco::DT_UINT16, numVertices, numComponents, TypedArrayData(data)); + case napi_uint8_array: + case napi_uint8_clamped_array: return AddAttributeToMesh(mesh, type, draco::DT_UINT8, numVertices, numComponents, TypedArrayData(data)); + case napi_int32_array: return AddAttributeToMesh(mesh, type, draco::DT_INT32, numVertices, numComponents, TypedArrayData(data)); + case napi_int16_array: return AddAttributeToMesh(mesh, type, draco::DT_INT16, numVertices, numComponents, TypedArrayData(data)); + case napi_int8_array: return AddAttributeToMesh(mesh, type, draco::DT_INT8, numVertices, numComponents, TypedArrayData(data)); + default: + throw Napi::Error::New(env, "Draco: Unsupported attribute typed array for encoding"); + } + } + + // Reads an index typed array (Uint16Array or Uint32Array) into a flat int vector. + std::vector ReadIndices(Napi::Env env, const Napi::TypedArray& data) + { + const auto arrayType = data.TypedArrayType(); + if (arrayType != napi_uint32_array && arrayType != napi_uint16_array) + { + throw Napi::TypeError::New(env, "Draco: indices must be a Uint16Array or a Uint32Array."); + } + + const size_t count = data.ElementLength(); + if (count % 3 != 0) + { + throw Napi::RangeError::New(env, "Draco: index count (" + std::to_string(count) + ") is not a multiple of 3."); + } + if (static_cast(count / 3) > 0xFFFFFFFFull) + { + throw Napi::RangeError::New(env, "Draco: too many faces: " + std::to_string(count / 3)); + } + + std::vector out(count); + if (arrayType == napi_uint32_array) + { + const uint32_t* src = TypedArrayData(data); + for (size_t i = 0; i < count; ++i) { out[i] = static_cast(src[i]); } + } + else + { + const uint16_t* src = TypedArrayData(data); + for (size_t i = 0; i < count; ++i) { out[i] = static_cast(src[i]); } + } + return out; + } + + Napi::Value EncodeDracoMesh(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + + if (info.Length() < 1 || !info[0].IsArray()) + { + throw Napi::TypeError::New(env, "encodeDracoMesh: expected an array of attributes"); + } + + const auto attributesIn = info[0].As(); + const auto options = (info.Length() > 2 && info[2].IsObject()) ? info[2].As() : Napi::Object::New(env); + + // Locate the mandatory position attribute and its vertex count. + uint32_t positionVerticesCount = 0; + bool hasPosition = false; + for (uint32_t i = 0; i < attributesIn.Length(); ++i) + { + const auto attr = attributesIn.Get(i).As(); + if (attr.Get("dracoName").As().Utf8Value() == "POSITION") + { + const auto data = attr.Get("data").As(); + const int8_t size = ReadAttributeSize(env, attr, data); + positionVerticesCount = static_cast(data.ElementLength() / static_cast(size)); + hasPosition = true; + break; + } + } + if (!hasPosition) + { + throw Napi::Error::New(env, "Draco: Missing position attribute for encoding."); + } + + // Indices: use the provided buffer, or synthesize an identity list for unindexed meshes. + std::vector indices; + if (info.Length() > 1 && info[1].IsTypedArray()) + { + indices = ReadIndices(env, info[1].As()); + + // Every index addresses a point in the position attribute; draco would otherwise + // read out of bounds when building the corner table. + for (const int index : indices) + { + if (index < 0 || static_cast(index) >= positionVerticesCount) + { + throw Napi::RangeError::New(env, "Draco: index " + std::to_string(index) + + " is out of range for " + std::to_string(positionVerticesCount) + " vertices."); + } + } + } + else + { + if (positionVerticesCount % 3 != 0) + { + throw Napi::RangeError::New(env, "Draco: unindexed meshes need a vertex count that is a multiple of 3, got " + + std::to_string(positionVerticesCount) + "."); + } + indices.resize(positionVerticesCount); + for (uint32_t i = 0; i < positionVerticesCount; ++i) { indices[i] = static_cast(i); } + } + + draco::Mesh mesh; + const uint32_t numFaces = static_cast(indices.size() / 3); + mesh.SetNumFaces(numFaces); + for (draco::FaceIndex f(0); f < numFaces; ++f) + { + draco::Mesh::Face face; + face[0] = draco::PointIndex(indices[f.value() * 3 + 0]); + face[1] = draco::PointIndex(indices[f.value() * 3 + 1]); + face[2] = draco::PointIndex(indices[f.value() * 3 + 2]); + mesh.SetFace(f, face); + } + + draco::Encoder encoder; + + const bool hasQuantization = options.Has("quantizationBits") && options.Get("quantizationBits").IsObject(); + const auto quantizationBits = hasQuantization ? options.Get("quantizationBits").As() : Napi::Object::New(env); + + auto attributeIds = Napi::Object::New(env); + for (uint32_t i = 0; i < attributesIn.Length(); ++i) + { + const auto attr = attributesIn.Get(i).As(); + const std::string kind = attr.Get("kind").As().Utf8Value(); + const std::string dracoName = attr.Get("dracoName").As().Utf8Value(); + const auto data = attr.Get("data").As(); + const int8_t size = ReadAttributeSize(env, attr, data); + const draco::GeometryAttribute::Type type = DracoAttributeTypeFromName(dracoName); + + const int attId = AddTypedAttributeToMesh(env, mesh, type, data, size); + if (attId < 0) + { + throw Napi::Error::New(env, "Draco: Failed to add attribute '" + kind + "' (vertex count mismatch)."); + } + attributeIds.Set(kind, Napi::Number::New(env, attId)); + + if (hasQuantization && quantizationBits.Has(dracoName)) + { + const int32_t bits = quantizationBits.Get(dracoName).As().Int32Value(); + if (bits) // matches WASM path: only set for truthy (non-zero) values + { + encoder.SetAttributeQuantization(type, bits); + } + } + } + + if (options.Has("method") && options.Get("method").IsString()) + { + const std::string method = options.Get("method").As().Utf8Value(); + encoder.SetEncodingMethod(method == "MESH_SEQUENTIAL_ENCODING" ? draco::MESH_SEQUENTIAL_ENCODING : draco::MESH_EDGEBREAKER_ENCODING); + } + + if (options.Has("encodeSpeed") && options.Get("encodeSpeed").IsNumber() && options.Has("decodeSpeed") && options.Get("decodeSpeed").IsNumber()) + { + encoder.SetSpeedOptions(options.Get("encodeSpeed").As().Int32Value(), options.Get("decodeSpeed").As().Int32Value()); + } + + // Mirror Encoder::EncodeMeshToDracoBuffer. + if (mesh.GetNamedAttributeId(draco::GeometryAttribute::POSITION) == -1) + { + throw Napi::Error::New(env, "Draco: Missing position attribute for encoding."); + } + if (!mesh.DeduplicateAttributeValues()) + { + throw Napi::Error::New(env, "Draco: Failed to deduplicate attribute values."); + } + mesh.DeduplicatePointIds(); + + draco::EncoderBuffer buffer; + const draco::Status status = encoder.EncodeMeshToBuffer(mesh, &buffer); + if (!status.ok()) + { + throw Napi::Error::New(env, std::string("Draco: Failed to encode: ") + status.error_msg()); + } + + auto encodedData = Napi::Int8Array::New(env, buffer.size()); + std::memcpy(encodedData.Data(), buffer.data(), buffer.size()); + + auto result = Napi::Object::New(env); + result.Set("data", encodedData); + result.Set("attributeIds", attributeIds); + return result; + } + } +} + +namespace Babylon::Plugins::NativeDraco +{ + void BABYLON_API Initialize(Napi::Env env) + { + auto native{JsRuntime::NativeObject::GetFromJavaScript(env)}; + native.Set("decodeDracoMesh", Napi::Function::New(env, DecodeDracoMesh, "decodeDracoMesh")); + native.Set("encodeDracoMesh", Napi::Function::New(env, EncodeDracoMesh, "encodeDracoMesh")); + } +} diff --git a/Plugins/NativeMeshopt/CMakeLists.txt b/Plugins/NativeMeshopt/CMakeLists.txt new file mode 100644 index 000000000..a1014b648 --- /dev/null +++ b/Plugins/NativeMeshopt/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES + "Include/Babylon/Plugins/NativeMeshopt.h" + "Source/NativeMeshopt.cpp") + +add_library(NativeMeshopt ${SOURCES}) +warnings_as_errors(NativeMeshopt) + +target_include_directories(NativeMeshopt + PUBLIC "Include") + +target_link_libraries(NativeMeshopt + PUBLIC napi + PRIVATE JsRuntimeInternal + PRIVATE meshoptimizer) + +set_property(TARGET NativeMeshopt PROPERTY FOLDER Plugins) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Plugins/NativeMeshopt/Include/Babylon/Plugins/NativeMeshopt.h b/Plugins/NativeMeshopt/Include/Babylon/Plugins/NativeMeshopt.h new file mode 100644 index 000000000..12f004aed --- /dev/null +++ b/Plugins/NativeMeshopt/Include/Babylon/Plugins/NativeMeshopt.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace Babylon::Plugins::NativeMeshopt +{ + // Exposes `_native.decodeMeshopt(source, count, stride, mode, filter?)`, a + // synchronous native replacement for Babylon's WebAssembly meshopt decoder + // (zeux/meshoptimizer). Babylon.js routes its MeshoptCompression to this + // function when it is present. + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Plugins/NativeMeshopt/Source/NativeMeshopt.cpp b/Plugins/NativeMeshopt/Source/NativeMeshopt.cpp new file mode 100644 index 000000000..b485f5b02 --- /dev/null +++ b/Plugins/NativeMeshopt/Source/NativeMeshopt.cpp @@ -0,0 +1,146 @@ +#include +#include + +#include + +#include + +#include +#include +#include +#include + +namespace Babylon::Plugins +{ + namespace + { + // Native equivalent of MeshoptDecoder.decodeGltfBufferAsync: + // decodeMeshopt(source: Uint8Array, count, stride, mode, filter?) -> Uint8Array + // where mode is "ATTRIBUTES" | "TRIANGLES" | "INDICES" and filter (optional) + // is "NONE" | "OCTAHEDRAL" | "QUATERNION" | "EXPONENTIAL". Mirrors the + // reference meshopt_decoder.js decode() helper exactly: decode into a buffer + // sized for count rounded up to a multiple of 4, then apply the filter + // in-place over that rounded count, then return the first count*stride bytes. + Napi::Value DecodeMeshopt(const Napi::CallbackInfo& info) + { + const auto env = info.Env(); + + if (info.Length() < 4 || !info[0].IsTypedArray()) + { + throw Napi::TypeError::New(env, "Meshopt: decodeMeshopt(source, count, stride, mode, filter?) requires a source typed array."); + } + + const auto sourceArray = info[0].As(); + const auto* source = static_cast(sourceArray.ArrayBuffer().Data()) + sourceArray.ByteOffset(); + const size_t sourceSize = sourceArray.ByteLength(); + + const int64_t countIn = info[1].As().Int64Value(); + const int64_t strideIn = info[2].As().Int64Value(); + const std::string mode = info[3].As().Utf8Value(); + + // meshoptimizer validates these with assert(), which compiles out in release builds, + // so out-of-range values would be undefined behavior rather than a thrown error. + if (countIn < 0) + { + throw Napi::RangeError::New(env, "Meshopt: count must not be negative, got " + std::to_string(countIn)); + } + if (strideIn <= 0 || strideIn > 256) + { + throw Napi::RangeError::New(env, "Meshopt: stride must be in [1, 256], got " + std::to_string(strideIn)); + } + if (mode == "ATTRIBUTES") + { + if (strideIn % 4 != 0) + { + throw Napi::RangeError::New(env, "Meshopt: ATTRIBUTES stride must be a multiple of 4, got " + std::to_string(strideIn)); + } + } + else if (mode == "TRIANGLES" || mode == "INDICES") + { + if (strideIn != 2 && strideIn != 4) + { + throw Napi::RangeError::New(env, "Meshopt: " + mode + " stride must be 2 or 4, got " + std::to_string(strideIn)); + } + if (mode == "TRIANGLES" && countIn % 3 != 0) + { + throw Napi::RangeError::New(env, "Meshopt: TRIANGLES count must be a multiple of 3, got " + std::to_string(countIn)); + } + } + else + { + throw Napi::Error::New(env, "Meshopt: Unsupported decode mode: " + mode); + } + + const size_t count = static_cast(countIn); + const size_t stride = static_cast(strideIn); + + // Round count up to a multiple of 4 (the reference decoder over-allocates + // and runs the vertex filter over count4 elements). + const size_t count4 = (count + 3) & ~static_cast(3); + + // Guard the allocation size so a huge count cannot wrap size_t. + constexpr int64_t maxDecodedBytes = 1LL << 31; + if (static_cast(count4) * strideIn > maxDecodedBytes) + { + throw Napi::RangeError::New(env, "Meshopt: decoded size (" + std::to_string(count4) + " x " + + std::to_string(strideIn) + " bytes) exceeds the 2 GB limit."); + } + + std::vector temp(count4 * stride, 0); + + int result; + if (mode == "ATTRIBUTES") + { + result = meshopt_decodeVertexBuffer(temp.data(), count, stride, source, sourceSize); + } + else if (mode == "TRIANGLES") + { + result = meshopt_decodeIndexBuffer(temp.data(), count, stride, source, sourceSize); + } + else + { + result = meshopt_decodeIndexSequence(temp.data(), count, stride, source, sourceSize); + } + + if (result != 0) + { + throw Napi::Error::New(env, "Meshopt: Malformed buffer data: " + std::to_string(result)); + } + + if (info.Length() > 4 && info[4].IsString()) + { + const std::string filter = info[4].As().Utf8Value(); + if (filter == "OCTAHEDRAL") + { + meshopt_decodeFilterOct(temp.data(), count4, stride); + } + else if (filter == "QUATERNION") + { + meshopt_decodeFilterQuat(temp.data(), count4, stride); + } + else if (filter == "EXPONENTIAL") + { + meshopt_decodeFilterExp(temp.data(), count4, stride); + } + else if (filter != "NONE") + { + throw Napi::Error::New(env, std::string("Meshopt: Unsupported decode filter: ") + filter); + } + } + + const size_t outSize = count * stride; + auto output = Napi::Uint8Array::New(env, outSize); + std::memcpy(output.Data(), temp.data(), outSize); + return output; + } + } +} + +namespace Babylon::Plugins::NativeMeshopt +{ + void BABYLON_API Initialize(Napi::Env env) + { + auto native{JsRuntime::NativeObject::GetFromJavaScript(env)}; + native.Set("decodeMeshopt", Napi::Function::New(env, DecodeMeshopt, "decodeMeshopt")); + } +}