mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
- Remove debug console.log statements from coroutine helpers - Split Tydra conversion into multiple phases with yields: - detecting: Format detection - parsing: USD parsing - setup: Converter environment setup - assets: Asset resolution setup - meshes: Tydra mesh conversion - complete: Done - Each phase yields to event loop, allowing browser repaints - Update progress-demo.js phase mapping with descriptive messages - The Tydra ConvertToRenderScene call is still blocking, but yields occur before and after it 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
4855 lines
156 KiB
C++
4855 lines
156 KiB
C++
// SPDX-License-Identifier: Apache 2.0
|
|
// Copyright 2024-Present Light Transport Entertainment, Inc.
|
|
//
|
|
#include <emscripten/bind.h>
|
|
#include <emscripten/console.h>
|
|
#include <emscripten/em_js.h>
|
|
#include <emscripten/fetch.h>
|
|
#include <emscripten/emscripten.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <set>
|
|
|
|
//#include "external/fast_float/include/fast_float/bigint.h"
|
|
#include "tinyusdz.hh"
|
|
#include "pprinter.hh"
|
|
#include "typed-array.hh"
|
|
#include "value-types.hh"
|
|
#include "tydra/render-data.hh"
|
|
#include "tydra/scene-access.hh"
|
|
#include "tydra/material-serializer.hh"
|
|
|
|
#include "tydra/mcp-context.hh"
|
|
#include "tydra/mcp-resources.hh"
|
|
#include "tydra/mcp-tools.hh"
|
|
#include "usd-to-json.hh"
|
|
#include "json-to-usd.hh"
|
|
#include "sha256.hh"
|
|
#include "logger.hh"
|
|
#include "image-loader.hh"
|
|
|
|
// TinyEXR for EXR decoding
|
|
#if defined(TINYUSDZ_WITH_EXR)
|
|
#include "external/tinyexr.h"
|
|
#endif
|
|
|
|
// stb_image for HDR (Radiance RGBE) decoding
|
|
// Only compile HDR support to minimize code size
|
|
#define STBI_ONLY_HDR
|
|
#define STBI_NO_STDIO
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
#include "external/stb_image.h"
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Weverything"
|
|
#endif
|
|
|
|
#include "external/jsonhpp/nlohmann/json.hpp"
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic pop
|
|
#endif
|
|
|
|
// Handling Asset
|
|
// Due to the limitatrion of C++(synchronous) initiated async file(fetch) read,
|
|
// We decided to fetch asset in JavaScript layer.
|
|
//
|
|
// 1. First list up assets(textures, USD scenes(for composition)
|
|
// 2. Load(fetch) assets to memory in JavaScript layer
|
|
// 3. Set binary data to EMAssetResolutionResolver.
|
|
// 4. Use EMAssetResolutionResolver to load asset(simply lookup binary data by asset name)
|
|
//
|
|
|
|
|
|
using namespace emscripten;
|
|
|
|
// ============================================================================
|
|
// EM_JS: Synchronous JavaScript callbacks for progress reporting
|
|
// These functions are called from C++ during Tydra conversion to report
|
|
// progress to JavaScript in real-time without ASYNCIFY.
|
|
// ============================================================================
|
|
|
|
// Report mesh conversion progress
|
|
// Called for each mesh during Tydra conversion
|
|
EM_JS(void, reportTydraProgress, (int current, int total, const char* stage, const char* meshName, float progress), {
|
|
if (typeof Module.onTydraProgress === 'function') {
|
|
Module.onTydraProgress({
|
|
meshCurrent: current,
|
|
meshTotal: total,
|
|
stage: UTF8ToString(stage),
|
|
meshName: UTF8ToString(meshName),
|
|
progress: progress
|
|
});
|
|
}
|
|
});
|
|
|
|
// Report conversion stage change
|
|
EM_JS(void, reportTydraStage, (const char* stage, const char* message), {
|
|
if (typeof Module.onTydraStage === 'function') {
|
|
Module.onTydraStage({
|
|
stage: UTF8ToString(stage),
|
|
message: UTF8ToString(message)
|
|
});
|
|
}
|
|
});
|
|
|
|
// Report conversion completion
|
|
EM_JS(void, reportTydraComplete, (int meshCount, int materialCount, int textureCount), {
|
|
if (typeof Module.onTydraComplete === 'function') {
|
|
Module.onTydraComplete({
|
|
meshCount: meshCount,
|
|
materialCount: materialCount,
|
|
textureCount: textureCount
|
|
});
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// C++20 Coroutine Support: Yield to JavaScript event loop
|
|
// ============================================================================
|
|
// This allows the browser to repaint between processing phases.
|
|
// Returns a Promise that resolves on the next animation frame.
|
|
//
|
|
// Enable with CMake option: -DTINYUSDZ_WASM_COROUTINE=ON (default)
|
|
// Disable with: -DTINYUSDZ_WASM_COROUTINE=OFF
|
|
|
|
#if defined(TINYUSDZ_USE_COROUTINE)
|
|
|
|
EM_JS(emscripten::EM_VAL, yieldToEventLoop_impl, (), {
|
|
// Return a Promise that resolves on next animation frame
|
|
// This gives the browser a chance to repaint
|
|
return Emval.toHandle(new Promise(resolve => {
|
|
if (typeof requestAnimationFrame === 'function') {
|
|
requestAnimationFrame(() => resolve());
|
|
} else {
|
|
// Fallback for non-browser environments (Node.js)
|
|
setTimeout(resolve, 0);
|
|
}
|
|
}));
|
|
});
|
|
|
|
// Wrapper for co_await usage
|
|
inline emscripten::val yieldToEventLoop() {
|
|
return emscripten::val::take_ownership(yieldToEventLoop_impl());
|
|
}
|
|
|
|
// Helper to yield with a custom delay (milliseconds)
|
|
EM_JS(emscripten::EM_VAL, yieldWithDelay_impl, (int delayMs), {
|
|
return Emval.toHandle(new Promise(resolve => {
|
|
setTimeout(resolve, delayMs);
|
|
}));
|
|
});
|
|
|
|
inline emscripten::val yieldWithDelay(int delayMs) {
|
|
return emscripten::val::take_ownership(yieldWithDelay_impl(delayMs));
|
|
}
|
|
|
|
// Report that async operation is starting (for JS progress UI)
|
|
EM_JS(void, reportAsyncPhaseStart, (const char* phase, float progress), {
|
|
if (typeof Module.onAsyncPhaseStart === 'function') {
|
|
Module.onAsyncPhaseStart({
|
|
phase: UTF8ToString(phase),
|
|
progress: progress
|
|
});
|
|
}
|
|
});
|
|
|
|
#endif // TINYUSDZ_USE_COROUTINE
|
|
|
|
namespace detail {
|
|
|
|
std::array<double, 9> toArray(const tinyusdz::value::matrix3d &m) {
|
|
std::array<double, 9> ret;
|
|
|
|
ret[0] = m.m[0][0];
|
|
ret[1] = m.m[0][1];
|
|
ret[2] = m.m[0][2];
|
|
|
|
ret[3] = m.m[1][0];
|
|
ret[4] = m.m[1][1];
|
|
ret[5] = m.m[1][2];
|
|
|
|
ret[6] = m.m[2][0];
|
|
ret[7] = m.m[2][1];
|
|
ret[8] = m.m[2][2];
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::array<double, 16> toArray(const tinyusdz::value::matrix4d &m) {
|
|
std::array<double, 16> ret;
|
|
|
|
ret[0] = m.m[0][0];
|
|
ret[1] = m.m[0][1];
|
|
ret[2] = m.m[0][2];
|
|
ret[3] = m.m[0][3];
|
|
|
|
ret[4] = m.m[1][0];
|
|
ret[5] = m.m[1][1];
|
|
ret[6] = m.m[1][2];
|
|
ret[7] = m.m[1][3];
|
|
|
|
ret[8] = m.m[2][0];
|
|
ret[9] = m.m[2][1];
|
|
ret[10] = m.m[2][2];
|
|
ret[11] = m.m[2][3];
|
|
|
|
ret[12] = m.m[3][0];
|
|
ret[13] = m.m[3][1];
|
|
ret[14] = m.m[3][2];
|
|
ret[15] = m.m[3][3];
|
|
|
|
return ret;
|
|
}
|
|
|
|
// To RGBA
|
|
bool ToRGBA(const std::vector<uint8_t> &src, int channels,
|
|
std::vector<uint8_t> &dst) {
|
|
uint32_t npixels = src.size() / channels;
|
|
dst.resize(npixels * 4);
|
|
|
|
if (channels == 1) { // grayscale
|
|
for (size_t i = 0; i < npixels; i++) {
|
|
dst[4 * i + 0] = src[i];
|
|
dst[4 * i + 1] = src[i];
|
|
dst[4 * i + 2] = src[i];
|
|
dst[4 * i + 3] = 1.0f;
|
|
}
|
|
} else if (channels == 2) { // assume luminance + alpha
|
|
for (size_t i = 0; i < npixels; i++) {
|
|
dst[4 * i + 0] = src[2 * i + 0];
|
|
dst[4 * i + 1] = src[2 * i + 0];
|
|
dst[4 * i + 2] = src[2 * i + 0];
|
|
dst[4 * i + 3] = src[2 * i + 1];
|
|
}
|
|
} else if (channels == 3) {
|
|
for (size_t i = 0; i < npixels; i++) {
|
|
dst[4 * i + 0] = src[3 * i + 0];
|
|
dst[4 * i + 1] = src[3 * i + 1];
|
|
dst[4 * i + 2] = src[3 * i + 2];
|
|
dst[4 * i + 3] = 1.0f;
|
|
}
|
|
} else if (channels == 4) {
|
|
dst = src;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool uint8arrayToBuffer(const emscripten::val& u8, tinyusdz::TypedArray<uint8_t> &buf) {
|
|
size_t n = u8["byteLength"].as<size_t>();
|
|
buf.resize(n);
|
|
|
|
// Copy JS typed array -> v (one memcpy under the hood)
|
|
emscripten::val view = emscripten::val::global("Uint8Array").new_(u8["buffer"], u8["byteOffset"], n);
|
|
emscripten::val heapView = emscripten::val(emscripten::typed_memory_view(n, buf.data()));
|
|
heapView.call<void>("set", view);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
} // namespace detail
|
|
|
|
// Simple UUID v4 generator
|
|
std::string generateUUID() {
|
|
static std::random_device rd;
|
|
static std::mt19937 gen(rd());
|
|
static std::uniform_int_distribution<> dis(0, 15);
|
|
static std::uniform_int_distribution<> dis2(8, 11);
|
|
|
|
std::stringstream ss;
|
|
ss << std::hex;
|
|
|
|
// Generate 32 hex characters with hyphens at positions 8, 12, 16, 20
|
|
for (int i = 0; i < 36; i++) {
|
|
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
|
ss << "-";
|
|
} else if (i == 14) {
|
|
ss << "4"; // Version 4 UUID
|
|
} else if (i == 19) {
|
|
ss << dis2(gen); // Variant bits
|
|
} else {
|
|
ss << dis(gen);
|
|
}
|
|
}
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
struct AssetCacheEntry {
|
|
std::string sha256_hash;
|
|
std::string binary;
|
|
std::string uuid;
|
|
|
|
AssetCacheEntry() : uuid(generateUUID()) {}
|
|
AssetCacheEntry(const std::string& data)
|
|
: sha256_hash(tinyusdz::sha256(data.c_str(), data.size())),
|
|
binary(data),
|
|
uuid(generateUUID()) {}
|
|
AssetCacheEntry(std::string&& data) noexcept
|
|
: sha256_hash(tinyusdz::sha256(data.c_str(), data.size())),
|
|
binary(std::move(data)),
|
|
uuid(generateUUID()) {}
|
|
};
|
|
|
|
// Progress callback function type for streaming
|
|
using ProgressCallback = std::function<void(const std::string&, size_t, size_t)>;
|
|
|
|
// Streaming asset entry that builds incrementally
|
|
struct StreamingAssetEntry {
|
|
std::string binary;
|
|
size_t expected_size;
|
|
size_t current_size;
|
|
std::string sha256_hash;
|
|
std::string uuid;
|
|
ProgressCallback progress_callback;
|
|
|
|
StreamingAssetEntry() : expected_size(0), current_size(0), uuid(generateUUID()) {}
|
|
|
|
bool appendChunk(const std::string& chunk) {
|
|
binary.append(chunk);
|
|
current_size = binary.size();
|
|
|
|
if (progress_callback && expected_size > 0) {
|
|
progress_callback(sha256_hash, current_size, expected_size);
|
|
}
|
|
|
|
return current_size <= expected_size;
|
|
}
|
|
|
|
bool isComplete() const {
|
|
return expected_size > 0 && current_size >= expected_size;
|
|
}
|
|
|
|
AssetCacheEntry finalize() {
|
|
if (isComplete()) {
|
|
sha256_hash = tinyusdz::sha256(binary.c_str(), binary.size());
|
|
AssetCacheEntry entry;
|
|
entry.sha256_hash = sha256_hash;
|
|
entry.binary = std::move(binary);
|
|
entry.uuid = uuid; // Preserve the UUID from streaming
|
|
return entry;
|
|
}
|
|
return AssetCacheEntry();
|
|
}
|
|
};
|
|
|
|
///
|
|
/// Zero-copy streaming buffer for memory-efficient JS->WASM transfer
|
|
///
|
|
/// This allows JS to write directly into pre-allocated WASM memory,
|
|
/// avoiding the need to hold the entire file in JS memory.
|
|
/// The workflow is:
|
|
/// 1. JS calls allocateStreamingBuffer() to pre-allocate WASM memory
|
|
/// 2. JS gets the buffer pointer via getStreamingBufferPtr()
|
|
/// 3. JS writes chunks directly to WASM heap using HEAPU8.set(chunk, ptr + offset)
|
|
/// 4. JS can immediately free each chunk after writing
|
|
/// 5. JS calls markChunkWritten() to update progress
|
|
/// 6. JS calls finalizeStreamingBuffer() when complete
|
|
///
|
|
struct ZeroCopyStreamingBuffer {
|
|
std::string buffer; // Pre-allocated buffer
|
|
size_t total_size{0}; // Total expected size
|
|
size_t bytes_written{0}; // Bytes written so far
|
|
std::string uuid; // Unique identifier (key for buffer map)
|
|
std::string asset_name; // Asset path/name (key for cache when finalized)
|
|
bool finalized{false};
|
|
|
|
ZeroCopyStreamingBuffer() : uuid(generateUUID()) {}
|
|
|
|
bool allocate(size_t size, const std::string &name = "") {
|
|
if (size == 0) return false;
|
|
try {
|
|
buffer.resize(size);
|
|
total_size = size;
|
|
bytes_written = 0;
|
|
finalized = false;
|
|
asset_name = name;
|
|
return true;
|
|
} catch (const std::bad_alloc&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Get raw pointer for direct memory access
|
|
uintptr_t getBufferPtr() const {
|
|
if (buffer.empty()) return 0;
|
|
return reinterpret_cast<uintptr_t>(buffer.data());
|
|
}
|
|
|
|
// Get pointer at specific offset
|
|
uintptr_t getBufferPtrAtOffset(size_t offset) const {
|
|
if (buffer.empty() || offset >= total_size) return 0;
|
|
return reinterpret_cast<uintptr_t>(buffer.data() + offset);
|
|
}
|
|
|
|
// Mark bytes as written (for progress tracking)
|
|
bool markBytesWritten(size_t count) {
|
|
if (bytes_written + count > total_size) {
|
|
bytes_written = total_size;
|
|
return false; // Overflow
|
|
}
|
|
bytes_written += count;
|
|
return true;
|
|
}
|
|
|
|
float getProgress() const {
|
|
if (total_size == 0) return 0.0f;
|
|
return static_cast<float>(bytes_written) / static_cast<float>(total_size);
|
|
}
|
|
|
|
bool isComplete() const {
|
|
return bytes_written >= total_size;
|
|
}
|
|
|
|
AssetCacheEntry finalize() {
|
|
if (!isComplete()) {
|
|
return AssetCacheEntry();
|
|
}
|
|
finalized = true;
|
|
std::string hash = tinyusdz::sha256(buffer.c_str(), buffer.size());
|
|
AssetCacheEntry entry;
|
|
entry.sha256_hash = hash;
|
|
entry.binary = std::move(buffer);
|
|
entry.uuid = uuid;
|
|
return entry;
|
|
}
|
|
|
|
emscripten::val toJS() const {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("uuid", uuid);
|
|
result.set("assetName", asset_name);
|
|
result.set("totalSize", double(total_size));
|
|
result.set("bytesWritten", double(bytes_written));
|
|
result.set("progress", getProgress());
|
|
result.set("isComplete", isComplete());
|
|
result.set("finalized", finalized);
|
|
result.set("bufferPtr", double(getBufferPtr()));
|
|
return result;
|
|
}
|
|
};
|
|
|
|
struct EMAssetResolutionResolver {
|
|
|
|
static int Resolve(const char *asset_name,
|
|
const std::vector<std::string> &search_paths,
|
|
std::string *resolved_asset_name, std::string *err,
|
|
void *userdata) {
|
|
(void)err;
|
|
(void)userdata;
|
|
(void)search_paths;
|
|
|
|
if (!asset_name) {
|
|
return -2; // err
|
|
}
|
|
|
|
if (!resolved_asset_name) {
|
|
return -2; // err
|
|
}
|
|
|
|
// TODO: searchpath
|
|
(*resolved_asset_name) = asset_name;
|
|
return 0; // OK
|
|
}
|
|
|
|
// AssetResoltion handlers
|
|
static int Size(const char *asset_name, uint64_t *nbytes, std::string *err,
|
|
void *userdata) {
|
|
(void)userdata;
|
|
|
|
if (!asset_name) {
|
|
if (err) {
|
|
(*err) += "asset_name arg is nullptr.\n";
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if (!nbytes) {
|
|
if (err) {
|
|
(*err) += "nbytes arg is nullptr.\n";
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
EMAssetResolutionResolver *p = reinterpret_cast<EMAssetResolutionResolver *>(userdata);
|
|
const AssetCacheEntry &entry = p->get(asset_name);
|
|
|
|
//std::cout << asset_name << ".size " << entry.binary.size() << "\n";
|
|
|
|
(*nbytes) = uint64_t(entry.binary.size());
|
|
return 0; // OK
|
|
}
|
|
|
|
static int Read(const char *asset_name, uint64_t req_nbytes, uint8_t *out_buf,
|
|
uint64_t *nbytes, std::string *err, void *userdata) {
|
|
if (!asset_name) {
|
|
if (err) {
|
|
(*err) += "asset_name arg is nullptr.\n";
|
|
}
|
|
return -3;
|
|
}
|
|
|
|
if (!nbytes) {
|
|
if (err) {
|
|
(*err) += "nbytes arg is nullptr.\n";
|
|
}
|
|
return -3;
|
|
}
|
|
|
|
if (req_nbytes < 9) { // at least 9 bytes(strlen("#usda 1.0")) or more
|
|
return -2;
|
|
}
|
|
|
|
EMAssetResolutionResolver *p = reinterpret_cast<EMAssetResolutionResolver *>(userdata);
|
|
|
|
if (p->has(asset_name)) {
|
|
const AssetCacheEntry &entry = p->get(asset_name);
|
|
if (entry.binary.size() > req_nbytes) {
|
|
return -2;
|
|
}
|
|
memcpy(out_buf, entry.binary.data(), entry.binary.size());
|
|
(*nbytes) = entry.binary.size();
|
|
return 0; // ok
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Assume content is loaded in JS layer.
|
|
bool add(const std::string &asset_name, const std::string &binary) {
|
|
bool overwritten = has(asset_name);
|
|
|
|
cache[asset_name] = AssetCacheEntry(binary);
|
|
|
|
return overwritten;
|
|
}
|
|
|
|
bool has(const std::string &asset_name) const {
|
|
return cache.count(asset_name);
|
|
}
|
|
|
|
const AssetCacheEntry &get(const std::string &asset_name) const {
|
|
if (!cache.count(asset_name)) {
|
|
return empty_entry_;
|
|
}
|
|
|
|
return cache.at(asset_name);
|
|
}
|
|
|
|
std::string getHash(const std::string &asset_name) const {
|
|
if (!cache.count(asset_name)) {
|
|
return std::string();
|
|
}
|
|
return cache.at(asset_name).sha256_hash;
|
|
}
|
|
|
|
bool verifyHash(const std::string &asset_name, const std::string &expected_hash) const {
|
|
if (!cache.count(asset_name)) {
|
|
return false;
|
|
}
|
|
return cache.at(asset_name).sha256_hash == expected_hash;
|
|
}
|
|
|
|
std::string getUUID(const std::string &asset_name) const {
|
|
if (!cache.count(asset_name)) {
|
|
return std::string();
|
|
}
|
|
return cache.at(asset_name).uuid;
|
|
}
|
|
|
|
std::string getStreamingUUID(const std::string &asset_name) const {
|
|
if (!streaming_cache.count(asset_name)) {
|
|
return std::string();
|
|
}
|
|
return streaming_cache.at(asset_name).uuid;
|
|
}
|
|
|
|
// Get all asset UUIDs
|
|
emscripten::val getAssetUUIDs() const {
|
|
emscripten::val uuids = emscripten::val::object();
|
|
for (const auto &pair : cache) {
|
|
uuids.set(pair.first, pair.second.uuid);
|
|
}
|
|
return uuids;
|
|
}
|
|
|
|
// Find asset by UUID
|
|
std::string findAssetByUUID(const std::string &uuid) const {
|
|
for (const auto &pair : cache) {
|
|
if (pair.second.uuid == uuid) {
|
|
return pair.first;
|
|
}
|
|
}
|
|
return std::string();
|
|
}
|
|
|
|
// Get asset by UUID
|
|
const AssetCacheEntry &getByUUID(const std::string &uuid) const {
|
|
for (const auto &pair : cache) {
|
|
if (pair.second.uuid == uuid) {
|
|
return pair.second;
|
|
}
|
|
}
|
|
return empty_entry_;
|
|
}
|
|
|
|
// Check if asset exists by UUID
|
|
bool hasByUUID(const std::string &uuid) const {
|
|
for (const auto &pair : cache) {
|
|
if (pair.second.uuid == uuid) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Delete asset by name
|
|
bool deleteAsset(const std::string &asset_name) {
|
|
if (!cache.count(asset_name)) {
|
|
return false;
|
|
}
|
|
cache.erase(asset_name);
|
|
return true;
|
|
}
|
|
|
|
// Delete asset by UUID
|
|
bool deleteAssetByUUID(const std::string &uuid) {
|
|
for (auto it = cache.begin(); it != cache.end(); ++it) {
|
|
if (it->second.uuid == uuid) {
|
|
cache.erase(it);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Delete streaming asset if exists
|
|
bool deleteStreamingAsset(const std::string &asset_name) {
|
|
if (!streaming_cache.count(asset_name)) {
|
|
return false;
|
|
}
|
|
streaming_cache.erase(asset_name);
|
|
return true;
|
|
}
|
|
|
|
emscripten::val getCacheDataAsMemoryView(const std::string &asset_name) const {
|
|
if (!cache.count(asset_name)) {
|
|
return emscripten::val::undefined();
|
|
}
|
|
const AssetCacheEntry &entry = cache.at(asset_name);
|
|
return emscripten::val(emscripten::typed_memory_view(entry.binary.size(),
|
|
reinterpret_cast<const uint8_t*>(entry.binary.data())));
|
|
}
|
|
|
|
// Zero-copy method using raw pointers for direct Uint8Array access
|
|
bool addFromRawPointer(const std::string &asset_name, uintptr_t dataPtr, size_t size) {
|
|
if (size == 0) {
|
|
return false;
|
|
}
|
|
|
|
// Direct access to the data without copying during read
|
|
const uint8_t* data = reinterpret_cast<const uint8_t*>(dataPtr);
|
|
|
|
// Only copy once into our storage format
|
|
std::string binary;
|
|
binary.reserve(size);
|
|
binary.assign(reinterpret_cast<const char*>(data), size);
|
|
|
|
bool overwritten = has(asset_name);
|
|
cache[asset_name] = AssetCacheEntry(std::move(binary));
|
|
|
|
return overwritten;
|
|
}
|
|
|
|
void clear() {
|
|
cache.clear();
|
|
streaming_cache.clear();
|
|
}
|
|
|
|
// Streaming asset methods
|
|
bool startStreamingAsset(const std::string &asset_name, size_t expected_size) {
|
|
streaming_cache[asset_name] = StreamingAssetEntry();
|
|
streaming_cache[asset_name].expected_size = expected_size;
|
|
streaming_cache[asset_name].current_size = 0;
|
|
return true;
|
|
}
|
|
|
|
bool appendAssetChunk(const std::string &asset_name, const std::string &chunk) {
|
|
if (!streaming_cache.count(asset_name)) {
|
|
return false;
|
|
}
|
|
return streaming_cache[asset_name].appendChunk(chunk);
|
|
}
|
|
|
|
bool finalizeStreamingAsset(const std::string &asset_name) {
|
|
if (!streaming_cache.count(asset_name)) {
|
|
return false;
|
|
}
|
|
|
|
StreamingAssetEntry &entry = streaming_cache[asset_name];
|
|
if (!entry.isComplete()) {
|
|
return false;
|
|
}
|
|
|
|
cache[asset_name] = std::move(entry.finalize());
|
|
streaming_cache.erase(asset_name);
|
|
return true;
|
|
}
|
|
|
|
bool isStreamingAssetComplete(const std::string &asset_name) const {
|
|
if (!streaming_cache.count(asset_name)) {
|
|
return false;
|
|
}
|
|
return streaming_cache.at(asset_name).isComplete();
|
|
}
|
|
|
|
emscripten::val getStreamingProgress(const std::string &asset_name) const {
|
|
emscripten::val progress = emscripten::val::object();
|
|
|
|
if (!streaming_cache.count(asset_name)) {
|
|
progress.set("exists", false);
|
|
return progress;
|
|
}
|
|
|
|
const StreamingAssetEntry &entry = streaming_cache.at(asset_name);
|
|
progress.set("exists", true);
|
|
progress.set("current", double(entry.current_size));
|
|
progress.set("total", double(entry.expected_size));
|
|
progress.set("complete", entry.isComplete());
|
|
progress.set("uuid", entry.uuid);
|
|
if (entry.expected_size > 0) {
|
|
progress.set("percentage", (double(entry.current_size) / double(entry.expected_size)) * 100.0);
|
|
} else {
|
|
progress.set("percentage", 0.0);
|
|
}
|
|
|
|
return progress;
|
|
}
|
|
|
|
//
|
|
// Zero-copy streaming buffer methods
|
|
//
|
|
|
|
/// Allocate a zero-copy buffer for streaming transfer
|
|
/// @param asset_name The asset path/name (used as cache key when finalized)
|
|
/// @param size Buffer size in bytes
|
|
/// Returns buffer info including UUID and pointer for direct memory access
|
|
emscripten::val allocateZeroCopyBuffer(const std::string &asset_name, size_t size) {
|
|
emscripten::val result = emscripten::val::object();
|
|
|
|
if (size == 0) {
|
|
result.set("success", false);
|
|
result.set("error", "Size must be greater than 0");
|
|
return result;
|
|
}
|
|
|
|
// Create a new buffer with unique UUID
|
|
ZeroCopyStreamingBuffer buf;
|
|
if (!buf.allocate(size, asset_name)) {
|
|
result.set("success", false);
|
|
result.set("error", "Failed to allocate buffer");
|
|
return result;
|
|
}
|
|
|
|
std::string uuid = buf.uuid;
|
|
zerocopy_buffers[uuid] = std::move(buf);
|
|
|
|
result.set("success", true);
|
|
result.set("uuid", uuid);
|
|
result.set("assetName", asset_name);
|
|
result.set("bufferPtr", double(zerocopy_buffers[uuid].getBufferPtr()));
|
|
result.set("totalSize", double(size));
|
|
return result;
|
|
}
|
|
|
|
/// Get buffer pointer for direct memory access
|
|
/// @param uuid The buffer UUID returned from allocateZeroCopyBuffer
|
|
double getZeroCopyBufferPtr(const std::string &uuid) {
|
|
if (!zerocopy_buffers.count(uuid)) {
|
|
return 0;
|
|
}
|
|
return double(zerocopy_buffers.at(uuid).getBufferPtr());
|
|
}
|
|
|
|
/// Get buffer pointer at specific offset
|
|
/// @param uuid The buffer UUID
|
|
double getZeroCopyBufferPtrAtOffset(const std::string &uuid, size_t offset) {
|
|
if (!zerocopy_buffers.count(uuid)) {
|
|
return 0;
|
|
}
|
|
return double(zerocopy_buffers.at(uuid).getBufferPtrAtOffset(offset));
|
|
}
|
|
|
|
/// Mark bytes as written and update progress
|
|
/// @param uuid The buffer UUID
|
|
bool markZeroCopyBytesWritten(const std::string &uuid, size_t count) {
|
|
if (!zerocopy_buffers.count(uuid)) {
|
|
return false;
|
|
}
|
|
return zerocopy_buffers[uuid].markBytesWritten(count);
|
|
}
|
|
|
|
/// Get current zero-copy buffer progress
|
|
/// @param uuid The buffer UUID
|
|
emscripten::val getZeroCopyProgress(const std::string &uuid) const {
|
|
if (!zerocopy_buffers.count(uuid)) {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("exists", false);
|
|
return result;
|
|
}
|
|
|
|
emscripten::val result = zerocopy_buffers.at(uuid).toJS();
|
|
result.set("exists", true);
|
|
return result;
|
|
}
|
|
|
|
/// Finalize zero-copy buffer and move to asset cache
|
|
/// Uses the asset_name stored in the buffer as the cache key
|
|
/// @param uuid The buffer UUID
|
|
bool finalizeZeroCopyBuffer(const std::string &uuid) {
|
|
if (!zerocopy_buffers.count(uuid)) {
|
|
return false;
|
|
}
|
|
|
|
ZeroCopyStreamingBuffer& buf = zerocopy_buffers[uuid];
|
|
if (!buf.isComplete()) {
|
|
return false;
|
|
}
|
|
|
|
// Use the stored asset_name as the cache key
|
|
std::string cache_key = buf.asset_name.empty() ? uuid : buf.asset_name;
|
|
cache[cache_key] = buf.finalize();
|
|
zerocopy_buffers.erase(uuid);
|
|
return true;
|
|
}
|
|
|
|
/// Cancel and free zero-copy buffer
|
|
/// @param uuid The buffer UUID
|
|
bool cancelZeroCopyBuffer(const std::string &uuid) {
|
|
if (!zerocopy_buffers.count(uuid)) {
|
|
return false;
|
|
}
|
|
zerocopy_buffers.erase(uuid);
|
|
return true;
|
|
}
|
|
|
|
/// Get all active zero-copy buffers
|
|
emscripten::val getActiveZeroCopyBuffers() const {
|
|
emscripten::val result = emscripten::val::array();
|
|
for (const auto& pair : zerocopy_buffers) {
|
|
emscripten::val item = emscripten::val::object();
|
|
item.set("uuid", pair.first);
|
|
item.set("info", pair.second.toJS());
|
|
result.call<void>("push", item);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// TODO: Use IndexDB?
|
|
//
|
|
// <uri, AssetCacheEntry>
|
|
std::map<std::string, AssetCacheEntry> cache;
|
|
std::map<std::string, StreamingAssetEntry> streaming_cache;
|
|
std::map<std::string, ZeroCopyStreamingBuffer> zerocopy_buffers;
|
|
AssetCacheEntry empty_entry_;
|
|
};
|
|
|
|
///
|
|
/// Parsing progress state for JS/WASM polling-based progress reporting
|
|
///
|
|
/// Since we cannot call async JS functions from C++ without Asyncify,
|
|
/// we use a polling approach where:
|
|
/// 1. C++ updates progress state via a callback
|
|
/// 2. JS can poll the progress state at any time
|
|
/// 3. JS can request cancellation which C++ checks at progress points
|
|
///
|
|
struct ParsingProgress {
|
|
enum class Stage {
|
|
Idle,
|
|
Parsing,
|
|
Converting,
|
|
Complete,
|
|
Error,
|
|
Cancelled
|
|
};
|
|
|
|
float progress{0.0f}; // 0.0 to 1.0
|
|
Stage stage{Stage::Idle};
|
|
std::string stage_name{"idle"};
|
|
std::string current_operation{""};
|
|
std::atomic<bool> cancel_requested{false};
|
|
std::string error_message{""};
|
|
uint64_t bytes_processed{0};
|
|
uint64_t total_bytes{0};
|
|
|
|
// Detailed mesh/material progress (from Tydra converter)
|
|
size_t meshes_processed{0};
|
|
size_t meshes_total{0};
|
|
std::string current_mesh_name{""};
|
|
size_t materials_processed{0};
|
|
size_t materials_total{0};
|
|
std::string tydra_stage{""}; // Stage name from DetailedProgressInfo
|
|
|
|
void reset() {
|
|
progress = 0.0f;
|
|
stage = Stage::Idle;
|
|
stage_name = "idle";
|
|
current_operation = "";
|
|
cancel_requested.store(false);
|
|
error_message = "";
|
|
bytes_processed = 0;
|
|
total_bytes = 0;
|
|
meshes_processed = 0;
|
|
meshes_total = 0;
|
|
current_mesh_name = "";
|
|
materials_processed = 0;
|
|
materials_total = 0;
|
|
tydra_stage = "";
|
|
}
|
|
|
|
void setStage(Stage s) {
|
|
stage = s;
|
|
switch (s) {
|
|
case Stage::Idle: stage_name = "idle"; break;
|
|
case Stage::Parsing: stage_name = "parsing"; break;
|
|
case Stage::Converting: stage_name = "converting"; break;
|
|
case Stage::Complete: stage_name = "complete"; break;
|
|
case Stage::Error: stage_name = "error"; break;
|
|
case Stage::Cancelled: stage_name = "cancelled"; break;
|
|
}
|
|
}
|
|
|
|
bool shouldCancel() const {
|
|
return cancel_requested.load();
|
|
}
|
|
|
|
emscripten::val toJS() const {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("progress", progress);
|
|
result.set("stage", stage_name);
|
|
result.set("currentOperation", current_operation);
|
|
result.set("cancelRequested", cancel_requested.load());
|
|
result.set("errorMessage", error_message);
|
|
result.set("bytesProcessed", double(bytes_processed));
|
|
result.set("totalBytes", double(total_bytes));
|
|
result.set("percentage", progress * 100.0f);
|
|
|
|
// Detailed mesh/material progress
|
|
result.set("meshesProcessed", double(meshes_processed));
|
|
result.set("meshesTotal", double(meshes_total));
|
|
result.set("currentMeshName", current_mesh_name);
|
|
result.set("materialsProcessed", double(materials_processed));
|
|
result.set("materialsTotal", double(materials_total));
|
|
result.set("tydraStage", tydra_stage);
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
bool SetupEMAssetResolution(
|
|
tinyusdz::AssetResolutionResolver &resolver,
|
|
/* must be the persistent pointer address until usd load finishes */
|
|
const EMAssetResolutionResolver *p) {
|
|
if (!p) {
|
|
return false;
|
|
}
|
|
|
|
tinyusdz::AssetResolutionHandler handler;
|
|
handler.resolve_fun = EMAssetResolutionResolver::Resolve;
|
|
handler.size_fun = EMAssetResolutionResolver::Size;
|
|
handler.read_fun = EMAssetResolutionResolver::Read;
|
|
handler.write_fun = nullptr;
|
|
handler.userdata =
|
|
reinterpret_cast<void *>(const_cast<EMAssetResolutionResolver *>(p));
|
|
|
|
resolver.register_wildcard_asset_resolution_handler(handler);
|
|
|
|
return true;
|
|
}
|
|
|
|
///
|
|
/// Simple C++ wrapper class for Emscripten
|
|
///
|
|
class TinyUSDZLoaderNative {
|
|
public:
|
|
struct CompositionFeatures {
|
|
bool subLayers{true};
|
|
bool inherits{true};
|
|
bool variantSets{true};
|
|
bool references{true};
|
|
bool payload{true}; // Not 'payloads'
|
|
bool specializes{true};
|
|
};
|
|
|
|
// Default constructor for async loading
|
|
TinyUSDZLoaderNative() : loaded_(false) {}
|
|
~TinyUSDZLoaderNative() {}
|
|
|
|
#if 0
|
|
///
|
|
/// `binary` is the buffer for TinyUSDZ binary(e.g. buffer read by
|
|
/// fs.readFileSync) std::string can be used as UInt8Array in JS layer.
|
|
///
|
|
TinyUSDZLoaderNative(const std::string &binary) {
|
|
loadFromBinary(binary);
|
|
}
|
|
#endif
|
|
|
|
bool stageToRenderScene(const tinyusdz::Stage &stage, bool is_usdz, const std::string &binary) {
|
|
|
|
tinyusdz::tydra::RenderSceneConverterEnv env(stage);
|
|
|
|
// load texture in C++ image loader? default = false(Use JS to decode texture image)
|
|
env.scene_config.load_texture_assets = loadTextureInNative_;
|
|
|
|
env.material_config.preserve_texel_bitdepth = true;
|
|
|
|
// Free GeomMesh data in stage after using it to save memory.
|
|
env.mesh_config.lowmem = true;
|
|
|
|
// Do not try to build indices(avoid temp memory consumption of vertex similarity search)
|
|
//env.mesh_config.prefer_non_indexed = true;
|
|
|
|
//env.mesh_config.build_index_method = 0; // simple
|
|
|
|
// Bone reduction configuration
|
|
env.mesh_config.enable_bone_reduction = enable_bone_reduction_;
|
|
env.mesh_config.target_bone_count = target_bone_count_;
|
|
|
|
if (is_usdz) {
|
|
// TODO: Support USDZ + Composition
|
|
// Setup AssetResolutionResolver to read a asset(file) from memory.
|
|
bool asset_on_memory =
|
|
false; // duplicate asset data from USDZ(binary) to UDSZAsset struct.
|
|
|
|
if (!tinyusdz::ReadUSDZAssetInfoFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
asset_on_memory, &usdz_asset_, &warn_, &error_)) {
|
|
std::cerr << "Failed to read USDZ assetInfo. \n";
|
|
loaded_ = false;
|
|
return false;
|
|
}
|
|
|
|
tinyusdz::AssetResolutionResolver arr;
|
|
|
|
// NOTE: Pointer address of usdz_asset must be valid until the call of
|
|
// RenderSceneConverter::ConvertToRenderScene.
|
|
if (!tinyusdz::SetupUSDZAssetResolution(arr, &usdz_asset_)) {
|
|
std::cerr << "Failed to setup AssetResolution for USDZ asset\n";
|
|
loaded_ = false;
|
|
return false;
|
|
}
|
|
|
|
env.asset_resolver = arr;
|
|
} else {
|
|
tinyusdz::AssetResolutionResolver arr;
|
|
if (!SetupEMAssetResolution(arr, &em_resolver_)) {
|
|
std::cerr << "Failed to setup FetchAssetResolution\n";
|
|
loaded_ = false;
|
|
return false;
|
|
}
|
|
|
|
env.asset_resolver = arr;
|
|
}
|
|
|
|
// RenderScene: Scene graph object which is suited for GL/Vulkan renderer
|
|
tinyusdz::tydra::RenderSceneConverter converter;
|
|
|
|
// Set up detailed progress callback to update parsing_progress_ and call JS
|
|
converter.SetDetailedProgressCallback(
|
|
[](const tinyusdz::tydra::DetailedProgressInfo &info, void *userptr) -> bool {
|
|
ParsingProgress *pp = static_cast<ParsingProgress *>(userptr);
|
|
if (pp) {
|
|
pp->meshes_processed = info.meshes_processed;
|
|
pp->meshes_total = info.meshes_total;
|
|
pp->current_mesh_name = info.current_mesh_name;
|
|
pp->materials_processed = info.materials_processed;
|
|
pp->materials_total = info.materials_total;
|
|
pp->tydra_stage = info.GetStageName();
|
|
pp->current_operation = info.message;
|
|
// Update progress: parsing is 0-80%, conversion is 80-100%
|
|
pp->progress = 0.8f + (info.progress * 0.2f);
|
|
}
|
|
|
|
// Call JavaScript synchronously via EM_JS
|
|
reportTydraProgress(
|
|
static_cast<int>(info.meshes_processed),
|
|
static_cast<int>(info.meshes_total),
|
|
info.GetStageName(), // Already returns const char*
|
|
info.current_mesh_name.c_str(),
|
|
info.progress
|
|
);
|
|
|
|
return true; // Continue conversion
|
|
},
|
|
&parsing_progress_);
|
|
|
|
// Set timecode to startTimeCode if authored, so xformOps with TimeSamples
|
|
// are evaluated at the start time (initial pose) for static viewers
|
|
if (stage.metas().startTimeCode.authored()) {
|
|
env.timecode = stage.metas().startTimeCode.get_value();
|
|
}
|
|
loaded_ = converter.ConvertToRenderScene(env, &render_scene_);
|
|
|
|
// Capture warnings from converter (available via warn() method)
|
|
if (!converter.GetWarning().empty()) {
|
|
if (!warn_.empty()) warn_ += "\n";
|
|
warn_ += converter.GetWarning();
|
|
// Note: Not printing to cerr to avoid console error spam
|
|
}
|
|
|
|
if (!loaded_) {
|
|
std::cerr << "Failed to convert USD Stage to RenderScene: \n"
|
|
<< converter.GetError() << "\n";
|
|
error_ = converter.GetError();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool loadAsLayerFromBinary(const std::string &binary, const std::string &filename) {
|
|
|
|
|
|
bool is_usdz = tinyusdz::IsUSDZ(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size());
|
|
|
|
tinyusdz::USDLoadOptions options;
|
|
options.max_memory_limit_in_mb = max_memory_limit_mb_;
|
|
|
|
loaded_ = tinyusdz::LoadLayerFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
filename, &layer_, &warn_, &error_, options);
|
|
|
|
if (!loaded_) {
|
|
return false;
|
|
}
|
|
|
|
loaded_as_layer_ = true;
|
|
filename_ = filename;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool loadFromBinary(const std::string &binary, const std::string &filename) {
|
|
|
|
//if (enableComposition_) {
|
|
// return loadAndCompositeFromBinary(binary, filename);
|
|
//}
|
|
|
|
bool is_usdz = tinyusdz::IsUSDZ(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size());
|
|
|
|
tinyusdz::USDLoadOptions options;
|
|
options.max_memory_limit_in_mb = max_memory_limit_mb_;
|
|
|
|
tinyusdz::Stage stage;
|
|
loaded_ = tinyusdz::LoadUSDFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
filename, &stage, &warn_, &error_, options);
|
|
|
|
if (!loaded_) {
|
|
return false;
|
|
}
|
|
|
|
loaded_as_layer_ = false;
|
|
filename_ = filename;
|
|
|
|
std::cout << "loaded\n";
|
|
#if 0
|
|
tinyusdz::tydra::RenderSceneConverterEnv env(stage);
|
|
|
|
//
|
|
// false = Load Texture in JS Layer
|
|
//
|
|
|
|
env.scene_config.load_texture_assets = loadTextureInNative_;
|
|
|
|
env.material_config.preserve_texel_bitdepth = true;
|
|
|
|
if (is_usdz) {
|
|
// TODO: Support USDZ + Composition
|
|
// Setup AssetResolutionResolver to read a asset(file) from memory.
|
|
bool asset_on_memory =
|
|
false; // duplicate asset data from USDZ(binary) to UDSZAsset struct.
|
|
|
|
if (!tinyusdz::ReadUSDZAssetInfoFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
asset_on_memory, &usdz_asset_, &warn_, &error_)) {
|
|
std::cerr << "Failed to read USDZ assetInfo. \n";
|
|
loaded_ = false;
|
|
return false;
|
|
}
|
|
|
|
tinyusdz::AssetResolutionResolver arr;
|
|
|
|
// NOTE: Pointer address of usdz_asset must be valid until the call of
|
|
// RenderSceneConverter::ConvertToRenderScene.
|
|
if (!tinyusdz::SetupUSDZAssetResolution(arr, &usdz_asset_)) {
|
|
std::cerr << "Failed to setup AssetResolution for USDZ asset\n";
|
|
loaded_ = false;
|
|
return false;
|
|
}
|
|
|
|
env.asset_resolver = arr;
|
|
} else {
|
|
tinyusdz::AssetResolutionResolver arr;
|
|
if (!SetupFetchAssetResolution(arr, &em_resolver_)) {
|
|
std::cerr << "Failed to setup FetchAssetResolution\n";
|
|
loaded_ = false;
|
|
return false;
|
|
}
|
|
|
|
env.asset_resolver = arr;
|
|
}
|
|
|
|
// RenderScene: Scene graph object which is suited for GL/Vulkan renderer
|
|
tinyusdz::tydra::RenderSceneConverter converter;
|
|
|
|
// Set up detailed progress callback to update parsing_progress_ and call JS
|
|
converter.SetDetailedProgressCallback(
|
|
[](const tinyusdz::tydra::DetailedProgressInfo &info, void *userptr) -> bool {
|
|
ParsingProgress *pp = static_cast<ParsingProgress *>(userptr);
|
|
if (pp) {
|
|
pp->meshes_processed = info.meshes_processed;
|
|
pp->meshes_total = info.meshes_total;
|
|
pp->current_mesh_name = info.current_mesh_name;
|
|
pp->materials_processed = info.materials_processed;
|
|
pp->materials_total = info.materials_total;
|
|
pp->tydra_stage = info.GetStageName();
|
|
pp->current_operation = info.message;
|
|
// Update progress: parsing is 0-80%, conversion is 80-100%
|
|
pp->progress = 0.8f + (info.progress * 0.2f);
|
|
}
|
|
|
|
// Call JavaScript synchronously via EM_JS
|
|
reportTydraProgress(
|
|
static_cast<int>(info.meshes_processed),
|
|
static_cast<int>(info.meshes_total),
|
|
info.GetStageName(), // Already returns const char*
|
|
info.current_mesh_name.c_str(),
|
|
info.progress
|
|
);
|
|
|
|
return true; // Continue conversion
|
|
},
|
|
&parsing_progress_);
|
|
|
|
// Set timecode to startTimeCode if authored, so xformOps with TimeSamples
|
|
// are evaluated at the start time (initial pose) for static viewers
|
|
if (stage.metas().startTimeCode.authored()) {
|
|
env.timecode = stage.metas().startTimeCode.get_value();
|
|
}
|
|
loaded_ = converter.ConvertToRenderScene(env, &render_scene_);
|
|
|
|
// Capture warnings from converter (available via warn() method)
|
|
if (!converter.GetWarning().empty()) {
|
|
if (!warn_.empty()) warn_ += "\n";
|
|
warn_ += converter.GetWarning();
|
|
// Note: Not printing to cerr to avoid console error spam
|
|
}
|
|
|
|
if (!loaded_) {
|
|
std::cerr << "Failed to convert USD Stage to RenderScene: \n"
|
|
<< converter.GetError() << "\n";
|
|
error_ = converter.GetError();
|
|
return false;
|
|
}
|
|
#else
|
|
return stageToRenderScene(stage, is_usdz, binary);
|
|
#endif
|
|
|
|
}
|
|
|
|
// ============================================================================
|
|
// C++20 Coroutine-based Async Loading
|
|
// ============================================================================
|
|
// This method uses C++20 coroutines to yield to the JavaScript event loop
|
|
// between processing phases, allowing the browser to repaint during loading.
|
|
//
|
|
// Enable with CMake option: -DTINYUSDZ_WASM_COROUTINE=ON (default)
|
|
// Disable with: -DTINYUSDZ_WASM_COROUTINE=OFF
|
|
//
|
|
// Returns a Promise that resolves to a JS object: { success: bool, error?: string }
|
|
//
|
|
#if defined(TINYUSDZ_USE_COROUTINE)
|
|
emscripten::val loadFromBinaryAsync(std::string binary, std::string filename) {
|
|
// IMPORTANT: Parameters are passed by VALUE (not by reference) to ensure
|
|
// data remains valid across co_await suspension points. References would
|
|
// become dangling after the coroutine yields to the event loop.
|
|
|
|
// Phase 1: Initial setup and format detection
|
|
reportAsyncPhaseStart("detecting", 0.0f);
|
|
|
|
bool is_usdz = tinyusdz::IsUSDZ(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size());
|
|
|
|
// Yield to allow UI to show "detecting" phase
|
|
co_await yieldToEventLoop();
|
|
|
|
// Phase 2: Parsing USD
|
|
reportAsyncPhaseStart("parsing", 0.1f);
|
|
|
|
tinyusdz::USDLoadOptions options;
|
|
options.max_memory_limit_in_mb = max_memory_limit_mb_;
|
|
|
|
tinyusdz::Stage stage;
|
|
loaded_ = tinyusdz::LoadUSDFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
filename, &stage, &warn_, &error_, options);
|
|
|
|
if (!loaded_) {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("success", false);
|
|
result.set("error", error_);
|
|
co_return result;
|
|
}
|
|
|
|
loaded_as_layer_ = false;
|
|
filename_ = filename;
|
|
|
|
// Yield after parsing to allow UI update
|
|
co_await yieldToEventLoop();
|
|
|
|
// Phase 3: Setup conversion environment
|
|
reportAsyncPhaseStart("setup", 0.3f);
|
|
|
|
tinyusdz::tydra::RenderSceneConverterEnv env(stage);
|
|
env.scene_config.load_texture_assets = loadTextureInNative_;
|
|
env.material_config.preserve_texel_bitdepth = true;
|
|
env.mesh_config.lowmem = true;
|
|
env.mesh_config.enable_bone_reduction = enable_bone_reduction_;
|
|
env.mesh_config.target_bone_count = target_bone_count_;
|
|
|
|
// Yield after setup
|
|
co_await yieldToEventLoop();
|
|
|
|
// Phase 4: Setup asset resolution
|
|
reportAsyncPhaseStart("assets", 0.4f);
|
|
|
|
if (is_usdz) {
|
|
bool asset_on_memory = false;
|
|
if (!tinyusdz::ReadUSDZAssetInfoFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
asset_on_memory, &usdz_asset_, &warn_, &error_)) {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("success", false);
|
|
result.set("error", "Failed to read USDZ assetInfo");
|
|
co_return result;
|
|
}
|
|
|
|
tinyusdz::AssetResolutionResolver arr;
|
|
if (!tinyusdz::SetupUSDZAssetResolution(arr, &usdz_asset_)) {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("success", false);
|
|
result.set("error", "Failed to setup AssetResolution for USDZ");
|
|
co_return result;
|
|
}
|
|
env.asset_resolver = arr;
|
|
} else {
|
|
tinyusdz::AssetResolutionResolver arr;
|
|
if (!SetupEMAssetResolution(arr, &em_resolver_)) {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("success", false);
|
|
result.set("error", "Failed to setup asset resolution");
|
|
co_return result;
|
|
}
|
|
env.asset_resolver = arr;
|
|
}
|
|
|
|
// Yield after asset resolution setup
|
|
co_await yieldToEventLoop();
|
|
|
|
// Phase 5: Converting meshes (Tydra)
|
|
reportAsyncPhaseStart("meshes", 0.5f);
|
|
|
|
tinyusdz::tydra::RenderSceneConverter converter;
|
|
|
|
// Set up progress callback that reports to JS
|
|
converter.SetDetailedProgressCallback(
|
|
[](const tinyusdz::tydra::DetailedProgressInfo &info, void *userptr) -> bool {
|
|
// Report progress to JS synchronously
|
|
reportTydraProgress(
|
|
static_cast<int>(info.meshes_processed),
|
|
static_cast<int>(info.meshes_total),
|
|
info.GetStageName(),
|
|
info.current_mesh_name.c_str(),
|
|
info.progress
|
|
);
|
|
return true;
|
|
},
|
|
nullptr);
|
|
|
|
if (stage.metas().startTimeCode.authored()) {
|
|
env.timecode = stage.metas().startTimeCode.get_value();
|
|
}
|
|
|
|
// Yield before heavy conversion
|
|
co_await yieldToEventLoop();
|
|
|
|
loaded_ = converter.ConvertToRenderScene(env, &render_scene_);
|
|
|
|
// Yield after conversion
|
|
co_await yieldToEventLoop();
|
|
|
|
if (!converter.GetWarning().empty()) {
|
|
if (!warn_.empty()) warn_ += "\n";
|
|
warn_ += converter.GetWarning();
|
|
}
|
|
|
|
if (!loaded_) {
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("success", false);
|
|
result.set("error", converter.GetError());
|
|
co_return result;
|
|
}
|
|
|
|
// Phase 6: Complete
|
|
reportAsyncPhaseStart("complete", 1.0f);
|
|
|
|
// Final yield to ensure UI updates
|
|
co_await yieldToEventLoop();
|
|
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("success", true);
|
|
result.set("meshCount", static_cast<int>(render_scene_.meshes.size()));
|
|
result.set("materialCount", static_cast<int>(render_scene_.materials.size()));
|
|
result.set("textureCount", static_cast<int>(render_scene_.textures.size()));
|
|
co_return result;
|
|
}
|
|
#endif // TINYUSDZ_USE_COROUTINE
|
|
|
|
// u8 : Uint8Array object.
|
|
bool loadTest(const std::string &filename, const emscripten::val &u8) {
|
|
|
|
tinyusdz::TypedArray<uint8_t> binary;
|
|
detail::uint8arrayToBuffer(u8, binary);
|
|
std::cout << "binary.size = " << binary.size() << "\n";
|
|
|
|
//bool is_usdz = tinyusdz::IsUSDZ(
|
|
// reinterpret_cast<const uint8_t *>(binary.data()), binary.size());
|
|
|
|
tinyusdz::USDLoadOptions options;
|
|
options.max_memory_limit_in_mb = max_memory_limit_mb_;
|
|
|
|
#if 0
|
|
tinyusdz::Stage stage;
|
|
loaded_ = tinyusdz::LoadUSDFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.data()), binary.size(),
|
|
filename, &stage, &warn_, &error_, options);
|
|
|
|
if (!loaded_) {
|
|
return false;
|
|
}
|
|
#else
|
|
std::cout << "layer\n";
|
|
tinyusdz::Layer layer;
|
|
loaded_ = tinyusdz::LoadLayerFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.data()), binary.size(),
|
|
filename, &layer, &warn_, &error_, options);
|
|
|
|
if (!loaded_) {
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
loaded_as_layer_ = false;
|
|
filename_ = filename;
|
|
|
|
//std::cout << "loaded\n";
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Load USD from a cached asset (previously streamed via zero-copy transfer)
|
|
/// @param asset_name The name/path used when the asset was cached
|
|
/// @returns true on success
|
|
bool loadFromCachedAsset(const std::string &asset_name) {
|
|
if (!em_resolver_.has(asset_name)) {
|
|
error_ = "Asset not found in cache: " + asset_name;
|
|
return false;
|
|
}
|
|
|
|
const AssetCacheEntry &entry = em_resolver_.get(asset_name);
|
|
if (entry.binary.empty()) {
|
|
error_ = "Cached asset is empty: " + asset_name;
|
|
return false;
|
|
}
|
|
|
|
// Delegate to loadFromBinary with the cached data
|
|
return loadFromBinary(entry.binary, asset_name);
|
|
}
|
|
|
|
/// Load USD as Layer from a cached asset
|
|
/// @param asset_name The name/path used when the asset was cached
|
|
/// @returns true on success
|
|
bool loadAsLayerFromCachedAsset(const std::string &asset_name) {
|
|
if (!em_resolver_.has(asset_name)) {
|
|
error_ = "Asset not found in cache: " + asset_name;
|
|
return false;
|
|
}
|
|
|
|
const AssetCacheEntry &entry = em_resolver_.get(asset_name);
|
|
if (entry.binary.empty()) {
|
|
error_ = "Cached asset is empty: " + asset_name;
|
|
return false;
|
|
}
|
|
|
|
// Delegate to loadAsLayerFromBinary with the cached data
|
|
return loadAsLayerFromBinary(entry.binary, asset_name);
|
|
}
|
|
|
|
// Test function for value::Value memory usage estimation
|
|
// arrayLength: optional parameter to specify the size of array tests (default: 10000)
|
|
emscripten::val testValueMemoryUsage(emscripten::val arrayLengthVal) {
|
|
emscripten::val result = emscripten::val::object();
|
|
emscripten::val tests = emscripten::val::array();
|
|
|
|
// Get array length from parameter or use default
|
|
int arrayLength = 10000;
|
|
if (!arrayLengthVal.isUndefined() && !arrayLengthVal.isNull()) {
|
|
arrayLength = arrayLengthVal.as<int>();
|
|
}
|
|
|
|
// Test 1: Empty value
|
|
{
|
|
tinyusdz::value::Value v;
|
|
size_t mem = v.estimate_memory_usage();
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "Empty value");
|
|
test.set("bytes", mem);
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 2: Simple types
|
|
{
|
|
tinyusdz::value::Value v1(42); // int32
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "int32(42)");
|
|
test.set("bytes", v1.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
{
|
|
tinyusdz::value::Value v2(3.14f); // float
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "float(3.14)");
|
|
test.set("bytes", v2.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
{
|
|
tinyusdz::value::Value v3(2.718); // double
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "double(2.718)");
|
|
test.set("bytes", v3.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 3: Vector types
|
|
{
|
|
tinyusdz::value::float3 f3{1.0f, 2.0f, 3.0f};
|
|
tinyusdz::value::Value v(f3);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "float3");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 4: Matrix types
|
|
{
|
|
tinyusdz::value::matrix4d m4d;
|
|
tinyusdz::value::Value v(m4d);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "matrix4d");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 5: String type
|
|
{
|
|
std::string str = "Hello, World! This is a test string.";
|
|
tinyusdz::value::Value v(str);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "string('" + str + "')");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 6: Token type
|
|
{
|
|
tinyusdz::value::token tok("myToken");
|
|
tinyusdz::value::Value v(tok);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "token('myToken')");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 7: Array of floats
|
|
{
|
|
std::vector<float> floats = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
|
|
tinyusdz::value::Value v(floats);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "float array (5 elements)");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 8: Array of float3
|
|
{
|
|
std::vector<tinyusdz::value::float3> vec3s = {
|
|
{1.0f, 0.0f, 0.0f},
|
|
{0.0f, 1.0f, 0.0f},
|
|
{0.0f, 0.0f, 1.0f}
|
|
};
|
|
tinyusdz::value::Value v(vec3s);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "float3 array (3 elements)");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 9: Array of strings
|
|
{
|
|
std::vector<std::string> strings = {"one", "two", "three", "four"};
|
|
tinyusdz::value::Value v(strings);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "string array (4 elements)");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 10: Color types (role types)
|
|
{
|
|
tinyusdz::value::color3f c3f{1.0f, 0.5f, 0.0f};
|
|
tinyusdz::value::Value v(c3f);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "color3f");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 11: Normal types (role types)
|
|
{
|
|
tinyusdz::value::normal3f n3f{0.0f, 1.0f, 0.0f};
|
|
tinyusdz::value::Value v(n3f);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "normal3f");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 12: TimeSamples
|
|
{
|
|
tinyusdz::value::TimeSamples ts;
|
|
ts.add_sample(0.0, tinyusdz::value::Value(1.0f));
|
|
ts.add_sample(1.0, tinyusdz::value::Value(2.0f));
|
|
ts.add_sample(2.0, tinyusdz::value::Value(3.0f));
|
|
size_t mem = ts.estimate_memory_usage();
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "TimeSamples (3 samples)");
|
|
test.set("bytes", mem);
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 13: Large array test (using specified array length)
|
|
{
|
|
std::vector<float> large_array(arrayLength, 1.0f);
|
|
tinyusdz::value::Value v(large_array);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "float array (" + std::to_string(arrayLength) + " elements)");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 13b: Large float3 array test (using specified array length / 3)
|
|
{
|
|
int vec3Count = std::max(1, arrayLength / 3);
|
|
std::vector<tinyusdz::value::float3> large_vec3_array;
|
|
large_vec3_array.reserve(vec3Count);
|
|
for (int i = 0; i < vec3Count; ++i) {
|
|
large_vec3_array.push_back({static_cast<float>(i), static_cast<float>(i+1), static_cast<float>(i+2)});
|
|
}
|
|
tinyusdz::value::Value v(large_vec3_array);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "float3 array (" + std::to_string(vec3Count) + " elements)");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 13c: Large int array test (using specified array length)
|
|
{
|
|
std::vector<int32_t> large_int_array(arrayLength, 42);
|
|
tinyusdz::value::Value v(large_int_array);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "int32 array (" + std::to_string(arrayLength) + " elements)");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 14: Half precision types
|
|
{
|
|
tinyusdz::value::half h(tinyusdz::value::float_to_half_full(1.5f));
|
|
tinyusdz::value::Value v(h);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "half(1.5)");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Test 15: Quaternion types
|
|
{
|
|
tinyusdz::value::quatf q{0.0f, 0.0f, 0.0f, 1.0f};
|
|
tinyusdz::value::Value v(q);
|
|
emscripten::val test = emscripten::val::object();
|
|
test.set("name", "quatf");
|
|
test.set("bytes", v.estimate_memory_usage());
|
|
tests.call<void>("push", test);
|
|
}
|
|
|
|
// Calculate total memory
|
|
size_t totalMemory = 0;
|
|
int numTests = tests["length"].as<int>();
|
|
for (int i = 0; i < numTests; ++i) {
|
|
emscripten::val test = tests[i];
|
|
totalMemory += test["bytes"].as<size_t>();
|
|
}
|
|
|
|
result.set("tests", tests);
|
|
result.set("success", true);
|
|
result.set("totalTests", numTests);
|
|
result.set("totalMemory", totalMemory);
|
|
result.set("arrayLength", arrayLength);
|
|
|
|
return result;
|
|
}
|
|
|
|
emscripten::val testLayer(emscripten::val arrayLengthVal) {
|
|
|
|
// Get array length from parameter or use default
|
|
int arrayLength = 10000;
|
|
if (!arrayLengthVal.isUndefined() && !arrayLengthVal.isNull()) {
|
|
arrayLength = arrayLengthVal.as<int>();
|
|
}
|
|
|
|
std::cout << "arrayLen " << arrayLength << "\n";
|
|
#if 1
|
|
// create Attrib
|
|
std::vector<tinyusdz::value::point3f> points(arrayLength);
|
|
tinyusdz::Attribute attr;
|
|
attr.set_value(std::move(points));
|
|
|
|
std::cout << "Attr.memusage " << attr.estimate_memory_usage() << "\n";
|
|
size_t totalMemory = 0; //attr.estimate_memory_usage();
|
|
#else
|
|
tinyusdz::TypedArray<tinyusdz::value::point3f> points(arrayLength);
|
|
tinyusdz::Attribute attr;
|
|
//std::cout << "attr.set_value\n";
|
|
//attr.set_value(std::move(points));
|
|
|
|
tinyusdz::primvar::PrimVar var;
|
|
std::cout << "pvar";
|
|
var.set_value(std::move(points));
|
|
|
|
//std::vector<tinyusdz::value::point3f> points(arrayLength);
|
|
//tinyusdz::value::Value v(std::move(points));
|
|
size_t totalMemory = points.size() * sizeof(tinyusdz::value::point3f);
|
|
std::cout << "totalMemory " << totalMemory << "\n";
|
|
#endif
|
|
|
|
|
|
emscripten::val result = emscripten::val::object();
|
|
result.set("totalMemory", totalMemory);
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
#if 0 // TODO: Remove
|
|
//
|
|
// Current limitation: can't specify usdz for USD to be composited(e.g. subLayer'ed, reference'ed)
|
|
// Toplevel USD can be USDZ.
|
|
//
|
|
bool loadAndCompositeFromBinary(const std::string &binary, const std::string &filename) {
|
|
|
|
std::cout << "loadAndComposite " << std::endl;
|
|
|
|
bool is_usdz = tinyusdz::IsUSDZ(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size());
|
|
|
|
tinyusdz::Layer root_layer;
|
|
bool ret = tinyusdz::LoadLayerFromMemory(reinterpret_cast<const uint8_t*>(binary.data()), binary.size(), filename, &root_layer, &warn_, &error_);
|
|
|
|
if (!ret) {
|
|
return false;
|
|
}
|
|
|
|
tinyusdz::Stage stage;
|
|
stage.metas() = root_layer.metas();
|
|
|
|
std::string warn;
|
|
|
|
tinyusdz::AssetResolutionResolver resolver;
|
|
if (!SetupEMAssetResolution(resolver, &em_resolver_)) {
|
|
std::cerr << "Failed to setup FetchAssetResolution\n";
|
|
return false;
|
|
}
|
|
const std::string base_dir = "./"; // FIXME
|
|
resolver.set_current_working_path(base_dir);
|
|
resolver.set_search_paths({base_dir});
|
|
|
|
filename_ = filename;
|
|
|
|
// TODO: Control composition feature flag from JS layer.
|
|
CompositionFeatures comp_features;
|
|
constexpr int kMaxIteration = 32; // Reduce iterations for web
|
|
|
|
//
|
|
// LIVRPS strength ordering
|
|
// - [x] Local(subLayers)
|
|
// - [x] Inherits
|
|
// - [x] VariantSets
|
|
// - [x] References
|
|
// - [x] Payload
|
|
// - [ ] Specializes
|
|
//
|
|
|
|
tinyusdz::Layer src_layer = root_layer;
|
|
if (comp_features.subLayers) {
|
|
tinyusdz::Layer composited_layer;
|
|
if (!tinyusdz::CompositeSublayers(resolver, src_layer, &composited_layer, &warn_, &error_)) {
|
|
//std::cerr << "Failed to composite subLayers: " << err << "\n";
|
|
return false;
|
|
}
|
|
|
|
std::cout << "# `subLayers` composited\n";
|
|
//std::cout << composited_layer << "\n";
|
|
|
|
src_layer = std::move(composited_layer);
|
|
}
|
|
|
|
// TODO: Find more better way to Recursively resolve references/payload/variants
|
|
for (int i = 0; i < kMaxIteration; i++) {
|
|
|
|
bool has_unresolved = false;
|
|
|
|
if (comp_features.references) {
|
|
if (!src_layer.check_unresolved_references()) {
|
|
std::cout << "# iter " << i << ": no unresolved references.\n";
|
|
} else {
|
|
has_unresolved = true;
|
|
|
|
tinyusdz::Layer composited_layer;
|
|
if (!tinyusdz::CompositeReferences(resolver, src_layer, &composited_layer, &warn_, &error_)) {
|
|
return false;
|
|
}
|
|
|
|
|
|
src_layer = std::move(composited_layer);
|
|
}
|
|
}
|
|
|
|
|
|
if (comp_features.payload) {
|
|
if (!src_layer.check_unresolved_payload()) {
|
|
std::cout << "# iter " << i << ": no unresolved payload.\n";
|
|
} else {
|
|
has_unresolved = true;
|
|
|
|
tinyusdz::Layer composited_layer;
|
|
if (!tinyusdz::CompositePayload(resolver, src_layer, &composited_layer, &warn_, &error_)) {
|
|
return false;
|
|
}
|
|
|
|
src_layer = std::move(composited_layer);
|
|
}
|
|
}
|
|
|
|
if (comp_features.inherits) {
|
|
if (!src_layer.check_unresolved_inherits()) {
|
|
std::cout << "# iter " << i << ": no unresolved inherits.\n";
|
|
} else {
|
|
has_unresolved = true;
|
|
|
|
tinyusdz::Layer composited_layer;
|
|
if (!tinyusdz::CompositeInherits(src_layer, &composited_layer, &warn_, &error_)) {
|
|
return false;
|
|
}
|
|
|
|
src_layer = std::move(composited_layer);
|
|
}
|
|
}
|
|
|
|
if (comp_features.variantSets) {
|
|
if (!src_layer.check_unresolved_variant()) {
|
|
std::cout << "# iter " << i << ": no unresolved variant.\n";
|
|
} else {
|
|
has_unresolved = true;
|
|
|
|
tinyusdz::Layer composited_layer;
|
|
if (!tinyusdz::CompositeVariant(src_layer, &composited_layer, &warn_, &error_)) {
|
|
return false;
|
|
}
|
|
|
|
src_layer = std::move(composited_layer);
|
|
}
|
|
}
|
|
|
|
|
|
std::cout << "# has_unresolved_references: " << src_layer.check_unresolved_references() << "\n";
|
|
std::cout << "# all resolved? " << !has_unresolved << "\n";
|
|
|
|
if (!has_unresolved) {
|
|
std::cout << "# of composition iteration to resolve fully: " << (i + 1) << "\n";
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
tinyusdz::Stage comp_stage;
|
|
ret = LayerToStage(src_layer, &comp_stage, &warn_, &error_);
|
|
|
|
if (!ret) {
|
|
return false;
|
|
}
|
|
|
|
return stageToRenderScene(stage, is_usdz, binary);
|
|
}
|
|
#endif
|
|
|
|
|
|
int numMeshes() const { return render_scene_.meshes.size(); }
|
|
|
|
int numMaterials() const { return render_scene_.materials.size(); }
|
|
|
|
int numTextures() const { return render_scene_.textures.size(); }
|
|
|
|
int numImages() const { return render_scene_.images.size(); }
|
|
|
|
// Legacy method for backward compatibility
|
|
emscripten::val getMaterial(int mat_id) const {
|
|
// Default to JSON format for backward compatibility
|
|
return getMaterial(mat_id, "json");
|
|
}
|
|
|
|
// New method that supports format parameter (json or xml)
|
|
emscripten::val getMaterial(int mat_id, const std::string& format) const {
|
|
emscripten::val result = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
result.set("error", "Scene not loaded");
|
|
return result;
|
|
}
|
|
|
|
if (mat_id < 0 || mat_id >= render_scene_.materials.size()) {
|
|
result.set("error", "Invalid material ID");
|
|
return result;
|
|
}
|
|
|
|
const auto &material = render_scene_.materials[mat_id];
|
|
|
|
// Determine serialization format
|
|
tinyusdz::tydra::SerializationFormat serFormat;
|
|
if (format == "xml") {
|
|
serFormat = tinyusdz::tydra::SerializationFormat::XML;
|
|
} else if (format == "json") {
|
|
serFormat = tinyusdz::tydra::SerializationFormat::JSON;
|
|
} else {
|
|
// For backward compatibility, if format is not recognized,
|
|
// return the old format
|
|
if (format.empty() || format == "legacy") {
|
|
// Return legacy format for backward compatibility
|
|
emscripten::val mat = emscripten::val::object();
|
|
|
|
// Check if material has UsdPreviewSurface
|
|
if (!material.hasUsdPreviewSurface()) {
|
|
mat.set("error", "Material does not have UsdPreviewSurface shader");
|
|
return mat;
|
|
}
|
|
|
|
const auto &m = material;
|
|
const auto &shader = *m.surfaceShader;
|
|
|
|
mat.set("diffuseColor", shader.diffuseColor.value);
|
|
if (shader.diffuseColor.is_texture()) {
|
|
mat.set("diffuseColorTextureId", shader.diffuseColor.texture_id);
|
|
}
|
|
|
|
mat.set("emissiveColor", shader.emissiveColor.value);
|
|
if (shader.emissiveColor.is_texture()) {
|
|
mat.set("emissiveColorTextureId", shader.emissiveColor.texture_id);
|
|
}
|
|
|
|
mat.set("useSpecularWorkflow", shader.useSpecularWorkflow);
|
|
if (shader.useSpecularWorkflow) {
|
|
mat.set("specularColor", shader.specularColor.value);
|
|
if (shader.specularColor.is_texture()) {
|
|
mat.set("specularColorTextureId", shader.specularColor.texture_id);
|
|
}
|
|
} else {
|
|
mat.set("metallic", shader.metallic.value);
|
|
if (shader.metallic.is_texture()) {
|
|
mat.set("metallicTextureId", shader.metallic.texture_id);
|
|
}
|
|
}
|
|
|
|
mat.set("roughness", shader.roughness.value);
|
|
if (shader.roughness.is_texture()) {
|
|
mat.set("roughnessTextureId", shader.roughness.texture_id);
|
|
}
|
|
|
|
mat.set("clearcoat", shader.clearcoat.value);
|
|
if (shader.clearcoat.is_texture()) {
|
|
mat.set("clearcoatTextureId", shader.clearcoat.texture_id);
|
|
}
|
|
|
|
mat.set("clearcoatRoughness", shader.clearcoatRoughness.value);
|
|
if (shader.clearcoatRoughness.is_texture()) {
|
|
mat.set("clearcoatRoughnessTextureId", shader.clearcoatRoughness.texture_id);
|
|
}
|
|
|
|
mat.set("opacity", shader.opacity.value);
|
|
if (shader.opacity.is_texture()) {
|
|
mat.set("opacityTextureId", shader.opacity.texture_id);
|
|
}
|
|
|
|
mat.set("opacityThreshold", shader.opacityThreshold.value);
|
|
if (shader.opacityThreshold.is_texture()) {
|
|
mat.set("opacityThresholdTextureId", shader.opacityThreshold.texture_id);
|
|
}
|
|
|
|
mat.set("ior", shader.ior.value);
|
|
if (shader.ior.is_texture()) {
|
|
mat.set("iorTextureId", shader.ior.texture_id);
|
|
}
|
|
|
|
mat.set("normal", shader.normal.value);
|
|
if (shader.normal.is_texture()) {
|
|
mat.set("normalTextureId", shader.normal.texture_id);
|
|
}
|
|
|
|
mat.set("displacement", shader.displacement.value);
|
|
if (shader.displacement.is_texture()) {
|
|
mat.set("displacementTextureId", shader.displacement.texture_id);
|
|
}
|
|
|
|
mat.set("occlusion", shader.occlusion.value);
|
|
if (shader.occlusion.is_texture()) {
|
|
mat.set("occlusionTextureId", shader.occlusion.texture_id);
|
|
}
|
|
|
|
return mat;
|
|
}
|
|
|
|
result.set("error", "Unsupported format. Use 'json' or 'xml'");
|
|
return result;
|
|
}
|
|
|
|
// Use the new serialization function with RenderScene for texture info
|
|
auto serialized = tinyusdz::tydra::serializeMaterial(material, serFormat, &render_scene_);
|
|
|
|
if (serialized.has_value()) {
|
|
result.set("data", serialized.value());
|
|
result.set("format", format);
|
|
} else {
|
|
result.set("error", serialized.error());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int numLights() const { return static_cast<int>(render_scene_.lights.size()); }
|
|
|
|
// Get light as direct object with all properties
|
|
emscripten::val getLight(int light_id) const {
|
|
emscripten::val light = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
light.set("error", "Scene not loaded");
|
|
return light;
|
|
}
|
|
|
|
if (light_id < 0 || light_id >= static_cast<int>(render_scene_.lights.size())) {
|
|
light.set("error", "Invalid light ID");
|
|
return light;
|
|
}
|
|
|
|
const auto &l = render_scene_.lights[static_cast<size_t>(light_id)];
|
|
|
|
light.set("name", l.name);
|
|
light.set("absPath", l.abs_path);
|
|
light.set("displayName", l.display_name);
|
|
|
|
// Light type as string
|
|
std::string typeStr;
|
|
switch (l.type) {
|
|
case tinyusdz::tydra::RenderLight::Type::Point: typeStr = "point"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Sphere: typeStr = "sphere"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Disk: typeStr = "disk"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Rect: typeStr = "rect"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Cylinder: typeStr = "cylinder"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Distant: typeStr = "distant"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Dome: typeStr = "dome"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Geometry: typeStr = "geometry"; break;
|
|
case tinyusdz::tydra::RenderLight::Type::Portal: typeStr = "portal"; break;
|
|
}
|
|
light.set("type", typeStr);
|
|
|
|
// Common light properties
|
|
emscripten::val color = emscripten::val::array();
|
|
color.call<void>("push", l.color[0]);
|
|
color.call<void>("push", l.color[1]);
|
|
color.call<void>("push", l.color[2]);
|
|
light.set("color", color);
|
|
|
|
light.set("intensity", l.intensity);
|
|
light.set("exposure", l.exposure);
|
|
light.set("diffuse", l.diffuse);
|
|
light.set("specular", l.specular);
|
|
light.set("normalize", l.normalize);
|
|
|
|
// Color temperature
|
|
light.set("enableColorTemperature", l.enableColorTemperature);
|
|
light.set("colorTemperature", l.colorTemperature);
|
|
|
|
// Transform
|
|
emscripten::val transform = emscripten::val::array();
|
|
for (int i = 0; i < 4; i++) {
|
|
for (int j = 0; j < 4; j++) {
|
|
transform.call<void>("push", l.transform.m[i][j]);
|
|
}
|
|
}
|
|
light.set("transform", transform);
|
|
|
|
emscripten::val position = emscripten::val::array();
|
|
position.call<void>("push", l.position[0]);
|
|
position.call<void>("push", l.position[1]);
|
|
position.call<void>("push", l.position[2]);
|
|
light.set("position", position);
|
|
|
|
emscripten::val direction = emscripten::val::array();
|
|
direction.call<void>("push", l.direction[0]);
|
|
direction.call<void>("push", l.direction[1]);
|
|
direction.call<void>("push", l.direction[2]);
|
|
light.set("direction", direction);
|
|
|
|
// Type-specific parameters
|
|
light.set("radius", l.radius);
|
|
light.set("width", l.width);
|
|
light.set("height", l.height);
|
|
light.set("length", l.length);
|
|
light.set("angle", l.angle);
|
|
light.set("textureFile", l.textureFile);
|
|
|
|
// Shaping (spotlight/IES)
|
|
light.set("shapingConeAngle", l.shapingConeAngle);
|
|
light.set("shapingConeSoftness", l.shapingConeSoftness);
|
|
light.set("shapingFocus", l.shapingFocus);
|
|
emscripten::val shapingFocusTint = emscripten::val::array();
|
|
shapingFocusTint.call<void>("push", l.shapingFocusTint[0]);
|
|
shapingFocusTint.call<void>("push", l.shapingFocusTint[1]);
|
|
shapingFocusTint.call<void>("push", l.shapingFocusTint[2]);
|
|
light.set("shapingFocusTint", shapingFocusTint);
|
|
light.set("shapingIesFile", l.shapingIesFile);
|
|
light.set("shapingIesAngleScale", l.shapingIesAngleScale);
|
|
light.set("shapingIesNormalize", l.shapingIesNormalize);
|
|
|
|
// Shadow
|
|
light.set("shadowEnable", l.shadowEnable);
|
|
emscripten::val shadowColor = emscripten::val::array();
|
|
shadowColor.call<void>("push", l.shadowColor[0]);
|
|
shadowColor.call<void>("push", l.shadowColor[1]);
|
|
shadowColor.call<void>("push", l.shadowColor[2]);
|
|
light.set("shadowColor", shadowColor);
|
|
light.set("shadowDistance", l.shadowDistance);
|
|
light.set("shadowFalloff", l.shadowFalloff);
|
|
light.set("shadowFalloffGamma", l.shadowFalloffGamma);
|
|
|
|
// DomeLight specific
|
|
std::string domeTexFmtStr;
|
|
switch (l.domeTextureFormat) {
|
|
case tinyusdz::tydra::RenderLight::DomeTextureFormat::Automatic: domeTexFmtStr = "automatic"; break;
|
|
case tinyusdz::tydra::RenderLight::DomeTextureFormat::Latlong: domeTexFmtStr = "latlong"; break;
|
|
case tinyusdz::tydra::RenderLight::DomeTextureFormat::MirroredBall: domeTexFmtStr = "mirroredBall"; break;
|
|
case tinyusdz::tydra::RenderLight::DomeTextureFormat::Angular: domeTexFmtStr = "angular"; break;
|
|
}
|
|
light.set("domeTextureFormat", domeTexFmtStr);
|
|
light.set("guideRadius", l.guideRadius);
|
|
light.set("envmapTextureId", l.envmap_texture_id);
|
|
|
|
// GeometryLight specific
|
|
light.set("geometryMeshId", l.geometry_mesh_id);
|
|
light.set("materialSyncMode", l.material_sync_mode);
|
|
|
|
// LTE SpectralAPI: Spectral emission
|
|
if (l.hasSpectralEmission()) {
|
|
emscripten::val spd = emscripten::val::object();
|
|
const auto &emission = *l.spd_emission;
|
|
|
|
// Samples as array of [wavelength, value] pairs
|
|
emscripten::val samples = emscripten::val::array();
|
|
for (const auto &s : emission.samples) {
|
|
emscripten::val sample = emscripten::val::array();
|
|
sample.call<void>("push", s[0]);
|
|
sample.call<void>("push", s[1]);
|
|
samples.call<void>("push", sample);
|
|
}
|
|
spd.set("samples", samples);
|
|
|
|
// Interpolation method
|
|
std::string interpStr;
|
|
switch (emission.interpolation) {
|
|
case tinyusdz::tydra::SpectralInterpolation::Linear: interpStr = "linear"; break;
|
|
case tinyusdz::tydra::SpectralInterpolation::Held: interpStr = "held"; break;
|
|
case tinyusdz::tydra::SpectralInterpolation::Cubic: interpStr = "cubic"; break;
|
|
case tinyusdz::tydra::SpectralInterpolation::Sellmeier: interpStr = "sellmeier"; break;
|
|
}
|
|
spd.set("interpolation", interpStr);
|
|
|
|
// Wavelength unit
|
|
std::string unitStr = (emission.unit == tinyusdz::tydra::WavelengthUnit::Nanometers)
|
|
? "nanometers" : "micrometers";
|
|
spd.set("unit", unitStr);
|
|
|
|
// Illuminant preset
|
|
std::string presetStr;
|
|
switch (emission.preset) {
|
|
case tinyusdz::tydra::IlluminantPreset::None: presetStr = "none"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::A: presetStr = "a"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::D50: presetStr = "d50"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::D65: presetStr = "d65"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::E: presetStr = "e"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::F1: presetStr = "f1"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::F2: presetStr = "f2"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::F7: presetStr = "f7"; break;
|
|
case tinyusdz::tydra::IlluminantPreset::F11: presetStr = "f11"; break;
|
|
}
|
|
spd.set("preset", presetStr);
|
|
|
|
light.set("spectralEmission", spd);
|
|
}
|
|
|
|
return light;
|
|
}
|
|
|
|
// Get light with format parameter (json or xml) - serialized output
|
|
emscripten::val getLightWithFormat(int light_id, const std::string& format) const {
|
|
emscripten::val result = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
result.set("error", "Scene not loaded");
|
|
return result;
|
|
}
|
|
|
|
if (light_id < 0 || light_id >= static_cast<int>(render_scene_.lights.size())) {
|
|
result.set("error", "Invalid light ID");
|
|
return result;
|
|
}
|
|
|
|
const auto &light = render_scene_.lights[static_cast<size_t>(light_id)];
|
|
|
|
// Determine serialization format
|
|
tinyusdz::tydra::SerializationFormat serFormat;
|
|
if (format == "xml") {
|
|
serFormat = tinyusdz::tydra::SerializationFormat::XML;
|
|
} else if (format == "json") {
|
|
serFormat = tinyusdz::tydra::SerializationFormat::JSON;
|
|
} else {
|
|
result.set("error", "Unsupported format. Use 'json' or 'xml'");
|
|
return result;
|
|
}
|
|
|
|
// Use the serialization function with RenderScene for mesh info
|
|
auto serialized = tinyusdz::tydra::serializeLight(light, serFormat, &render_scene_);
|
|
|
|
if (serialized.has_value()) {
|
|
result.set("data", serialized.value());
|
|
result.set("format", format);
|
|
} else {
|
|
result.set("error", serialized.error());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
emscripten::val getAllLights() const {
|
|
emscripten::val lights = emscripten::val::array();
|
|
|
|
if (!loaded_) {
|
|
return lights;
|
|
}
|
|
|
|
for (int i = 0; i < static_cast<int>(render_scene_.lights.size()); i++) {
|
|
lights.call<void>("push", getLight(i));
|
|
}
|
|
|
|
return lights;
|
|
}
|
|
|
|
emscripten::val getTexture(int tex_id) const {
|
|
emscripten::val tex = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
return tex;
|
|
}
|
|
|
|
if (tex_id >= render_scene_.textures.size()) {
|
|
return tex;
|
|
}
|
|
|
|
const auto &t = render_scene_.textures[tex_id];
|
|
|
|
tex.set("textureImageId", int(t.texture_image_id));
|
|
tex.set("wrapS", to_string(t.wrapS));
|
|
tex.set("wrapT", to_string(t.wrapT));
|
|
// TOOD: bias, scale, rot/scale/trans, etc
|
|
|
|
return tex;
|
|
}
|
|
|
|
emscripten::val getImage(int img_id) const {
|
|
emscripten::val img = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
return img;
|
|
}
|
|
|
|
if (img_id >= render_scene_.images.size()) {
|
|
return img;
|
|
}
|
|
|
|
const auto &i = render_scene_.images[img_id];
|
|
|
|
img.set("width", int(i.width));
|
|
img.set("height", int(i.height));
|
|
img.set("channels", int(i.channels));
|
|
img.set("uri", i.asset_identifier);
|
|
img.set("decoded", bool(i.decoded));
|
|
img.set("colorSpace", to_string(i.colorSpace));
|
|
img.set("usdColorSpace", to_string(i.usdColorSpace));
|
|
img.set("bufferId", int(i.buffer_id));
|
|
|
|
if ((i.buffer_id >= 0) && (i.buffer_id < render_scene_.buffers.size())) {
|
|
const auto &b = render_scene_.buffers[i.buffer_id];
|
|
|
|
// TODO: Support HDR
|
|
|
|
img.set("data",
|
|
emscripten::typed_memory_view(b.data.size(), b.data.data()));
|
|
}
|
|
|
|
return img;
|
|
}
|
|
|
|
emscripten::val getMesh(int mesh_id) const {
|
|
emscripten::val mesh = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
return mesh;
|
|
}
|
|
|
|
if (mesh_id >= render_scene_.meshes.size()) {
|
|
return mesh;
|
|
}
|
|
|
|
const tinyusdz::tydra::RenderMesh &rmesh =
|
|
render_scene_.meshes[size_t(mesh_id)];
|
|
|
|
//if (rmesh.has_indices()) {
|
|
const uint32_t *indices_ptr = rmesh.faceVertexIndices().data();
|
|
mesh.set("faceVertexIndices",
|
|
emscripten::typed_memory_view(rmesh.faceVertexIndices().size(),
|
|
indices_ptr));
|
|
const uint32_t *counts_ptr = rmesh.faceVertexCounts().data();
|
|
mesh.set("faceVertexCounts",
|
|
emscripten::typed_memory_view(rmesh.faceVertexCounts().size(),
|
|
counts_ptr));
|
|
//} else {
|
|
// // Assume all triangles and facevarying attributes.
|
|
// if (!rmesh.is_triangulated()) {
|
|
// TUSDZ_LOG_E("Mesh must be triangulated when the mesh doesn't have indices\n");
|
|
// return mesh;
|
|
// }
|
|
//}
|
|
|
|
// TODO: Use three.js scene description format?
|
|
mesh.set("primName", rmesh.prim_name);
|
|
mesh.set("displayName", rmesh.display_name);
|
|
mesh.set("absPath", rmesh.abs_path);
|
|
//mesh.set("hasIndices", rmesh.has_indices());
|
|
|
|
|
|
const float *points_ptr =
|
|
reinterpret_cast<const float *>(rmesh.points.data());
|
|
// vec3
|
|
mesh.set("points", emscripten::typed_memory_view(rmesh.points.size() * 3,
|
|
points_ptr));
|
|
|
|
if (!rmesh.normals.empty()) {
|
|
const float *normals_ptr =
|
|
reinterpret_cast<const float *>(rmesh.normals.data.data());
|
|
|
|
mesh.set("normals", emscripten::typed_memory_view(
|
|
rmesh.normals.vertex_count() * 3, normals_ptr));
|
|
}
|
|
|
|
{
|
|
// Export all UV sets
|
|
emscripten::val uvSets = emscripten::val::object();
|
|
|
|
for (const auto& uv_pair : rmesh.texcoords) {
|
|
uint32_t uvSlotId = uv_pair.first;
|
|
const auto& uv_data = uv_pair.second;
|
|
|
|
const float *uvs_ptr = reinterpret_cast<const float *>(uv_data.data.data());
|
|
|
|
// Create UV set object with metadata
|
|
emscripten::val uvSet = emscripten::val::object();
|
|
uvSet.set("data", emscripten::typed_memory_view(
|
|
uv_data.vertex_count() * 2, uvs_ptr));
|
|
uvSet.set("vertexCount", uv_data.vertex_count());
|
|
uvSet.set("slotId", int(uvSlotId));
|
|
|
|
// Add to UV sets collection
|
|
std::string slotKey = "uv" + std::to_string(uvSlotId);
|
|
uvSets.set(slotKey.c_str(), uvSet);
|
|
}
|
|
|
|
mesh.set("uvSets", uvSets);
|
|
|
|
// Keep backward compatibility - slot 0 as "texcoords"
|
|
if (rmesh.texcoords.count(0)) {
|
|
const float *uvs_ptr = reinterpret_cast<const float *>(
|
|
rmesh.texcoords.at(0).data.data());
|
|
mesh.set("texcoords",
|
|
emscripten::typed_memory_view(
|
|
rmesh.texcoords.at(0).vertex_count() * 2, uvs_ptr));
|
|
}
|
|
}
|
|
|
|
if (!rmesh.tangents.empty()) {
|
|
const float *tangents_ptr =
|
|
reinterpret_cast<const float *>(rmesh.tangents.data.data());
|
|
|
|
mesh.set("tangents", emscripten::typed_memory_view(
|
|
rmesh.tangents.vertex_count() * 3, tangents_ptr));
|
|
}
|
|
|
|
mesh.set("materialId", rmesh.material_id);
|
|
mesh.set("doubleSided", rmesh.doubleSided);
|
|
|
|
// Export area light properties (MeshLightAPI)
|
|
mesh.set("isAreaLight", rmesh.is_area_light);
|
|
if (rmesh.is_area_light) {
|
|
const float *light_color_ptr = rmesh.light_color.data();
|
|
mesh.set("lightColor", emscripten::typed_memory_view(3, light_color_ptr));
|
|
mesh.set("lightIntensity", rmesh.light_intensity);
|
|
mesh.set("lightExposure", rmesh.light_exposure);
|
|
mesh.set("lightNormalize", rmesh.light_normalize);
|
|
mesh.set("lightMaterialSyncMode", emscripten::val(rmesh.light_material_sync_mode));
|
|
}
|
|
|
|
// Export skinning data (joint indices, joint weights)
|
|
if (!rmesh.joint_and_weights.jointIndices.empty()) {
|
|
const int *joint_indices_ptr = rmesh.joint_and_weights.jointIndices.data();
|
|
mesh.set("jointIndices",
|
|
emscripten::typed_memory_view(
|
|
rmesh.joint_and_weights.jointIndices.size(),
|
|
joint_indices_ptr));
|
|
}
|
|
|
|
if (!rmesh.joint_and_weights.jointWeights.empty()) {
|
|
const float *joint_weights_ptr = rmesh.joint_and_weights.jointWeights.data();
|
|
mesh.set("jointWeights",
|
|
emscripten::typed_memory_view(
|
|
rmesh.joint_and_weights.jointWeights.size(),
|
|
joint_weights_ptr));
|
|
}
|
|
|
|
// Export element size (influences per vertex)
|
|
mesh.set("elementSize", rmesh.joint_and_weights.elementSize);
|
|
|
|
// Export skeleton ID
|
|
if (rmesh.skel_id >= 0) {
|
|
mesh.set("skel_id", rmesh.skel_id);
|
|
}
|
|
|
|
// Export geomBindTransform matrix (4x4 matrix as 16 doubles)
|
|
const double *geom_bind_ptr =
|
|
reinterpret_cast<const double *>(
|
|
rmesh.joint_and_weights.geomBindTransform.m);
|
|
mesh.set("geomBindTransform",
|
|
emscripten::typed_memory_view(16, geom_bind_ptr));
|
|
|
|
// Export GeomSubsets (per-face materials) as optimized submeshes
|
|
// Reorder triangles by material so each material has exactly one contiguous group
|
|
if (!rmesh.material_subsetMap.empty()) {
|
|
// Step 1: Group face indices by material
|
|
std::map<int, std::vector<int>> materialToFaces;
|
|
size_t totalFaces = 0;
|
|
|
|
for (const auto& subset_pair : rmesh.material_subsetMap) {
|
|
const tinyusdz::tydra::MaterialSubset& subset = subset_pair.second;
|
|
const std::vector<int>& faceIndices = subset.indices();
|
|
|
|
int matId = subset.material_id;
|
|
if (materialToFaces.find(matId) == materialToFaces.end()) {
|
|
materialToFaces[matId] = std::vector<int>();
|
|
}
|
|
|
|
// Collect all face indices for this material
|
|
materialToFaces[matId].insert(materialToFaces[matId].end(),
|
|
faceIndices.begin(), faceIndices.end());
|
|
totalFaces += faceIndices.size();
|
|
}
|
|
|
|
// Step 2: Build reordering map - new triangle index -> old triangle index
|
|
// Group all triangles by material, creating contiguous ranges
|
|
std::vector<int> reorderMap;
|
|
reorderMap.reserve(totalFaces);
|
|
|
|
emscripten::val submeshes = emscripten::val::array();
|
|
int currentStart = 0;
|
|
|
|
for (auto& mat_pair : materialToFaces) {
|
|
int materialId = mat_pair.first;
|
|
std::vector<int>& faceIndices = mat_pair.second;
|
|
|
|
if (faceIndices.empty()) continue;
|
|
|
|
// Sort face indices within this material group (optional, helps cache coherence)
|
|
std::sort(faceIndices.begin(), faceIndices.end());
|
|
|
|
// Add all faces for this material to the reorder map
|
|
for (int faceIdx : faceIndices) {
|
|
reorderMap.push_back(faceIdx);
|
|
}
|
|
|
|
// Create one submesh group for this material
|
|
emscripten::val submesh = emscripten::val::object();
|
|
submesh.set("start", currentStart * 3); // Convert face index to vertex index
|
|
submesh.set("count", static_cast<int>(faceIndices.size()) * 3); // Number of vertices
|
|
submesh.set("materialId", materialId);
|
|
submeshes.call<void>("push", submesh);
|
|
|
|
currentStart += static_cast<int>(faceIndices.size());
|
|
}
|
|
|
|
mesh.set("submeshes", submeshes);
|
|
|
|
// Step 3: Reorder vertex attributes based on reorderMap
|
|
// For facevarying attributes, each triangle has 3 vertices
|
|
size_t numNewTriangles = reorderMap.size();
|
|
|
|
// Reorder points (vec3)
|
|
if (!rmesh.points.empty()) {
|
|
std::vector<float> reorderedPoints(numNewTriangles * 3 * 3); // numTris * 3 verts * 3 components
|
|
for (size_t newTriIdx = 0; newTriIdx < numNewTriangles; newTriIdx++) {
|
|
int oldTriIdx = reorderMap[newTriIdx];
|
|
for (int v = 0; v < 3; v++) { // 3 vertices per triangle
|
|
size_t oldVertIdx = static_cast<size_t>(oldTriIdx) * 3 + static_cast<size_t>(v);
|
|
size_t newVertIdx = newTriIdx * 3 + static_cast<size_t>(v);
|
|
if (oldVertIdx < rmesh.points.size()) {
|
|
reorderedPoints[newVertIdx * 3 + 0] = rmesh.points[oldVertIdx][0];
|
|
reorderedPoints[newVertIdx * 3 + 1] = rmesh.points[oldVertIdx][1];
|
|
reorderedPoints[newVertIdx * 3 + 2] = rmesh.points[oldVertIdx][2];
|
|
}
|
|
}
|
|
}
|
|
// Store in cache and update mesh pointer
|
|
auto& cache = reordered_mesh_cache_[mesh_id];
|
|
cache.points = std::move(reorderedPoints);
|
|
mesh.set("points", emscripten::typed_memory_view(cache.points.size(), cache.points.data()));
|
|
}
|
|
|
|
// Reorder normals (vec3) - data is raw uint8_t buffer, cast to float*
|
|
if (!rmesh.normals.empty()) {
|
|
const float* normalsData = reinterpret_cast<const float*>(rmesh.normals.data.data());
|
|
std::vector<float> reorderedNormals(numNewTriangles * 3 * 3);
|
|
for (size_t newTriIdx = 0; newTriIdx < numNewTriangles; newTriIdx++) {
|
|
int oldTriIdx = reorderMap[newTriIdx];
|
|
for (int v = 0; v < 3; v++) {
|
|
size_t oldVertIdx = static_cast<size_t>(oldTriIdx) * 3 + static_cast<size_t>(v);
|
|
size_t newVertIdx = newTriIdx * 3 + static_cast<size_t>(v);
|
|
if (oldVertIdx < rmesh.normals.vertex_count()) {
|
|
reorderedNormals[newVertIdx * 3 + 0] = normalsData[oldVertIdx * 3 + 0];
|
|
reorderedNormals[newVertIdx * 3 + 1] = normalsData[oldVertIdx * 3 + 1];
|
|
reorderedNormals[newVertIdx * 3 + 2] = normalsData[oldVertIdx * 3 + 2];
|
|
}
|
|
}
|
|
}
|
|
auto& cache = reordered_mesh_cache_[mesh_id];
|
|
cache.normals = std::move(reorderedNormals);
|
|
mesh.set("normals", emscripten::typed_memory_view(cache.normals.size(), cache.normals.data()));
|
|
}
|
|
|
|
// Reorder texcoords (vec2) - slot 0
|
|
if (rmesh.texcoords.count(0) && !rmesh.texcoords.at(0).data.empty()) {
|
|
const auto& uvData = rmesh.texcoords.at(0);
|
|
const float* uvDataPtr = reinterpret_cast<const float*>(uvData.data.data());
|
|
std::vector<float> reorderedTexcoords(numNewTriangles * 3 * 2);
|
|
for (size_t newTriIdx = 0; newTriIdx < numNewTriangles; newTriIdx++) {
|
|
int oldTriIdx = reorderMap[newTriIdx];
|
|
for (int v = 0; v < 3; v++) {
|
|
size_t oldVertIdx = static_cast<size_t>(oldTriIdx) * 3 + static_cast<size_t>(v);
|
|
size_t newVertIdx = newTriIdx * 3 + static_cast<size_t>(v);
|
|
if (oldVertIdx < uvData.vertex_count()) {
|
|
reorderedTexcoords[newVertIdx * 2 + 0] = uvDataPtr[oldVertIdx * 2 + 0];
|
|
reorderedTexcoords[newVertIdx * 2 + 1] = uvDataPtr[oldVertIdx * 2 + 1];
|
|
}
|
|
}
|
|
}
|
|
auto& cache = reordered_mesh_cache_[mesh_id];
|
|
cache.texcoords = std::move(reorderedTexcoords);
|
|
mesh.set("texcoords", emscripten::typed_memory_view(cache.texcoords.size(), cache.texcoords.data()));
|
|
}
|
|
|
|
// Reorder tangents (vec3)
|
|
if (!rmesh.tangents.empty()) {
|
|
const float* tangentsData = reinterpret_cast<const float*>(rmesh.tangents.data.data());
|
|
std::vector<float> reorderedTangents(numNewTriangles * 3 * 3);
|
|
for (size_t newTriIdx = 0; newTriIdx < numNewTriangles; newTriIdx++) {
|
|
int oldTriIdx = reorderMap[newTriIdx];
|
|
for (int v = 0; v < 3; v++) {
|
|
size_t oldVertIdx = static_cast<size_t>(oldTriIdx) * 3 + static_cast<size_t>(v);
|
|
size_t newVertIdx = newTriIdx * 3 + static_cast<size_t>(v);
|
|
if (oldVertIdx < rmesh.tangents.vertex_count()) {
|
|
reorderedTangents[newVertIdx * 3 + 0] = tangentsData[oldVertIdx * 3 + 0];
|
|
reorderedTangents[newVertIdx * 3 + 1] = tangentsData[oldVertIdx * 3 + 1];
|
|
reorderedTangents[newVertIdx * 3 + 2] = tangentsData[oldVertIdx * 3 + 2];
|
|
}
|
|
}
|
|
}
|
|
auto& cache = reordered_mesh_cache_[mesh_id];
|
|
cache.tangents = std::move(reorderedTangents);
|
|
mesh.set("tangents", emscripten::typed_memory_view(cache.tangents.size(), cache.tangents.data()));
|
|
}
|
|
|
|
// Generate new sequential indices (0, 1, 2, 3, 4, 5, ...)
|
|
// Since we reordered the vertex data, indices are now sequential
|
|
std::vector<uint32_t> newIndices(numNewTriangles * 3);
|
|
for (size_t i = 0; i < numNewTriangles * 3; i++) {
|
|
newIndices[i] = static_cast<uint32_t>(i);
|
|
}
|
|
auto& cache = reordered_mesh_cache_[mesh_id];
|
|
cache.faceVertexIndices = std::move(newIndices);
|
|
mesh.set("faceVertexIndices", emscripten::typed_memory_view(
|
|
cache.faceVertexIndices.size(), cache.faceVertexIndices.data()));
|
|
}
|
|
|
|
return mesh;
|
|
}
|
|
|
|
int getDefaultRootNodeId() { return render_scene_.default_root_node; }
|
|
|
|
emscripten::val getDefaultRootNode() {
|
|
return getRootNode(getDefaultRootNodeId());
|
|
}
|
|
|
|
emscripten::val getRootNode(int idx) {
|
|
emscripten::val val = emscripten::val::object();
|
|
|
|
if ((idx < 0) || (idx >= render_scene_.nodes.size())) {
|
|
return val;
|
|
}
|
|
|
|
val = buildNodeRec(render_scene_.nodes[size_t(idx)]);
|
|
return val;
|
|
}
|
|
|
|
int numRootNodes() { return render_scene_.nodes.size(); }
|
|
|
|
// Get the upAxis from the RenderScene metadata
|
|
std::string getUpAxis() const {
|
|
if (!loaded_) {
|
|
return "Y"; // Default
|
|
}
|
|
return render_scene_.meta.upAxis;
|
|
}
|
|
|
|
// Get the complete scene metadata as a JavaScript object
|
|
emscripten::val getSceneMetadata() const {
|
|
emscripten::val metadata = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
return metadata;
|
|
}
|
|
|
|
metadata.set("copyright", render_scene_.meta.copyright);
|
|
metadata.set("comment", render_scene_.meta.comment);
|
|
metadata.set("upAxis", render_scene_.meta.upAxis);
|
|
metadata.set("metersPerUnit", render_scene_.meta.metersPerUnit);
|
|
metadata.set("framesPerSecond", render_scene_.meta.framesPerSecond);
|
|
metadata.set("timeCodesPerSecond", render_scene_.meta.timeCodesPerSecond);
|
|
metadata.set("autoPlay", render_scene_.meta.autoPlay);
|
|
|
|
if (render_scene_.meta.startTimeCode) {
|
|
metadata.set("startTimeCode", render_scene_.meta.startTimeCode.value());
|
|
} else {
|
|
metadata.set("startTimeCode", emscripten::val::null());
|
|
}
|
|
|
|
if (render_scene_.meta.endTimeCode) {
|
|
metadata.set("endTimeCode", render_scene_.meta.endTimeCode.value());
|
|
} else {
|
|
metadata.set("endTimeCode", emscripten::val::null());
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
// Animation data access methods
|
|
int numAnimations() const { return render_scene_.animations.size(); }
|
|
|
|
// Get a single animation clip as Three.js friendly JSON
|
|
emscripten::val getAnimation(int anim_id) const {
|
|
emscripten::val anim = emscripten::val::object();
|
|
|
|
if (!loaded_) {
|
|
return anim;
|
|
}
|
|
|
|
if (anim_id >= render_scene_.animations.size()) {
|
|
return anim;
|
|
}
|
|
|
|
const auto &clip = render_scene_.animations[anim_id];
|
|
|
|
// Basic animation metadata
|
|
anim.set("name", clip.name.empty() ? "Animation" + std::to_string(anim_id) : clip.name);
|
|
anim.set("primName", clip.prim_name);
|
|
anim.set("absPath", clip.abs_path);
|
|
anim.set("displayName", clip.display_name);
|
|
anim.set("duration", clip.duration);
|
|
|
|
// Convert samplers to Three.js KeyframeTrack format
|
|
emscripten::val tracks = emscripten::val::array();
|
|
|
|
for (const auto &channel : clip.channels) {
|
|
if (!channel.is_valid() || channel.sampler >= clip.samplers.size()) {
|
|
continue;
|
|
}
|
|
|
|
const auto &sampler = clip.samplers[channel.sampler];
|
|
if (sampler.empty()) {
|
|
continue;
|
|
}
|
|
|
|
emscripten::val track = emscripten::val::object();
|
|
|
|
// Set track name based on target node and property
|
|
if (channel.target_node >= 0 && channel.target_node < render_scene_.nodes.size()) {
|
|
const auto &node = render_scene_.nodes[channel.target_node];
|
|
std::string trackName = node.abs_path.empty() ? node.prim_name : node.abs_path;
|
|
|
|
// Add property suffix for Three.js compatibility
|
|
switch (channel.path) {
|
|
case tinyusdz::tydra::AnimationPath::Translation:
|
|
trackName += ".position";
|
|
track.set("type", "vector3");
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Rotation:
|
|
trackName += ".quaternion";
|
|
track.set("type", "quaternion");
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Scale:
|
|
trackName += ".scale";
|
|
track.set("type", "vector3");
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Weights:
|
|
trackName += ".morphTargetInfluences";
|
|
track.set("type", "number");
|
|
break;
|
|
}
|
|
|
|
track.set("name", trackName);
|
|
track.set("nodeName", node.prim_name);
|
|
track.set("nodeIndex", channel.target_node);
|
|
}
|
|
|
|
// Set interpolation mode
|
|
std::string interpolation;
|
|
switch (sampler.interpolation) {
|
|
case tinyusdz::tydra::AnimationInterpolation::Step:
|
|
interpolation = "STEP";
|
|
break;
|
|
case tinyusdz::tydra::AnimationInterpolation::CubicSpline:
|
|
interpolation = "CUBICSPLINE";
|
|
break;
|
|
case tinyusdz::tydra::AnimationInterpolation::Linear:
|
|
default:
|
|
interpolation = "LINEAR";
|
|
break;
|
|
}
|
|
track.set("interpolation", interpolation);
|
|
|
|
// Convert times and values to typed arrays for efficiency
|
|
track.set("times", emscripten::typed_memory_view(sampler.times.size(), sampler.times.data()));
|
|
track.set("values", emscripten::typed_memory_view(sampler.values.size(), sampler.values.data()));
|
|
|
|
// Add property path for reference
|
|
std::string pathStr;
|
|
switch (channel.path) {
|
|
case tinyusdz::tydra::AnimationPath::Translation:
|
|
pathStr = "translation";
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Rotation:
|
|
pathStr = "rotation";
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Scale:
|
|
pathStr = "scale";
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Weights:
|
|
pathStr = "weights";
|
|
break;
|
|
}
|
|
track.set("path", pathStr);
|
|
|
|
tracks.call<void>("push", track);
|
|
}
|
|
|
|
anim.set("tracks", tracks);
|
|
|
|
// Also expose raw channels and samplers arrays for advanced use (skeletal animation, etc.)
|
|
emscripten::val channels = emscripten::val::array();
|
|
for (const auto &channel : clip.channels) {
|
|
emscripten::val ch = emscripten::val::object();
|
|
ch.set("sampler", channel.sampler);
|
|
ch.set("target_node", channel.target_node);
|
|
ch.set("skeleton_id", channel.skeleton_id);
|
|
ch.set("joint_id", channel.joint_id);
|
|
|
|
// Set target_type string
|
|
std::string targetTypeStr = (channel.target_type == tinyusdz::tydra::ChannelTargetType::SkeletonJoint)
|
|
? "SkeletonJoint" : "SceneNode";
|
|
ch.set("target_type", targetTypeStr);
|
|
|
|
// Set path string
|
|
std::string pathStr;
|
|
switch (channel.path) {
|
|
case tinyusdz::tydra::AnimationPath::Translation:
|
|
pathStr = "Translation";
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Rotation:
|
|
pathStr = "Rotation";
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Scale:
|
|
pathStr = "Scale";
|
|
break;
|
|
case tinyusdz::tydra::AnimationPath::Weights:
|
|
pathStr = "Weights";
|
|
break;
|
|
}
|
|
ch.set("path", pathStr);
|
|
|
|
channels.call<void>("push", ch);
|
|
}
|
|
anim.set("channels", channels);
|
|
|
|
// Expose samplers array
|
|
emscripten::val samplers = emscripten::val::array();
|
|
for (const auto &sampler : clip.samplers) {
|
|
emscripten::val samp = emscripten::val::object();
|
|
samp.set("times", emscripten::typed_memory_view(sampler.times.size(), sampler.times.data()));
|
|
samp.set("values", emscripten::typed_memory_view(sampler.values.size(), sampler.values.data()));
|
|
|
|
std::string interpolation;
|
|
switch (sampler.interpolation) {
|
|
case tinyusdz::tydra::AnimationInterpolation::Step:
|
|
interpolation = "STEP";
|
|
break;
|
|
case tinyusdz::tydra::AnimationInterpolation::CubicSpline:
|
|
interpolation = "CUBICSPLINE";
|
|
break;
|
|
case tinyusdz::tydra::AnimationInterpolation::Linear:
|
|
default:
|
|
interpolation = "LINEAR";
|
|
break;
|
|
}
|
|
samp.set("interpolation", interpolation);
|
|
|
|
samplers.call<void>("push", samp);
|
|
}
|
|
anim.set("samplers", samplers);
|
|
|
|
return anim;
|
|
}
|
|
|
|
// Get all animations as an array
|
|
emscripten::val getAllAnimations() const {
|
|
emscripten::val animations = emscripten::val::array();
|
|
|
|
if (!loaded_) {
|
|
return animations;
|
|
}
|
|
|
|
for (int i = 0; i < render_scene_.animations.size(); ++i) {
|
|
animations.call<void>("push", getAnimation(i));
|
|
}
|
|
|
|
return animations;
|
|
}
|
|
|
|
// Get animation summary info without full data (useful for listing)
|
|
emscripten::val getAnimationInfo(int anim_id) const {
|
|
emscripten::val info = emscripten::val::object();
|
|
|
|
if (!loaded_ || anim_id >= render_scene_.animations.size()) {
|
|
return info;
|
|
}
|
|
|
|
const auto &clip = render_scene_.animations[anim_id];
|
|
|
|
info.set("id", anim_id);
|
|
info.set("name", clip.name.empty() ? "Animation" + std::to_string(anim_id) : clip.name);
|
|
info.set("duration", clip.duration);
|
|
info.set("numTracks", int(clip.channels.size()));
|
|
info.set("numSamplers", int(clip.samplers.size()));
|
|
|
|
// Count unique target nodes
|
|
std::set<int32_t> targetNodes;
|
|
for (const auto &channel : clip.channels) {
|
|
if (channel.target_node >= 0) {
|
|
targetNodes.insert(channel.target_node);
|
|
}
|
|
}
|
|
info.set("numTargetNodes", int(targetNodes.size()));
|
|
|
|
return info;
|
|
}
|
|
|
|
// Get all animation summaries
|
|
emscripten::val getAllAnimationInfos() const {
|
|
emscripten::val infos = emscripten::val::array();
|
|
|
|
if (!loaded_) {
|
|
return infos;
|
|
}
|
|
|
|
for (int i = 0; i < render_scene_.animations.size(); ++i) {
|
|
infos.call<void>("push", getAnimationInfo(i));
|
|
}
|
|
|
|
return infos;
|
|
}
|
|
|
|
void setEnableComposition(bool enabled) { enableComposition_ = enabled; }
|
|
void setLoadTextureInNative(bool onoff) {
|
|
loadTextureInNative_ = onoff;
|
|
}
|
|
|
|
void setMaxMemoryLimitMB(int32_t limit_mb) {
|
|
max_memory_limit_mb_ = limit_mb;
|
|
}
|
|
|
|
int32_t getMaxMemoryLimitMB() const {
|
|
return max_memory_limit_mb_;
|
|
}
|
|
|
|
// Bone reduction configuration
|
|
void setEnableBoneReduction(bool enabled) {
|
|
enable_bone_reduction_ = enabled;
|
|
}
|
|
|
|
bool getEnableBoneReduction() const {
|
|
return enable_bone_reduction_;
|
|
}
|
|
|
|
void setTargetBoneCount(uint32_t count) {
|
|
if (count > 0 && count <= 64) { // Sanity check: 1-64 bones
|
|
target_bone_count_ = count;
|
|
}
|
|
}
|
|
|
|
uint32_t getTargetBoneCount() const {
|
|
return target_bone_count_;
|
|
}
|
|
|
|
emscripten::val getAssetSearchPaths() const {
|
|
emscripten::val arr = emscripten::val::array();
|
|
for (size_t i = 0; i < search_paths_.size(); i++) {
|
|
arr.call<void>("push", search_paths_[i]);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
void setBaseWorkingPath(const std::string &path) {
|
|
base_dir_ = path;
|
|
}
|
|
|
|
std::string getBaseWorkingPath() const {
|
|
return base_dir_;
|
|
}
|
|
|
|
void clearAssetSearchPaths() {
|
|
search_paths_.clear();
|
|
}
|
|
|
|
void addAssetSearchPath(const std::string &path) {
|
|
search_paths_.push_back(path);
|
|
}
|
|
|
|
// Return filename passed to loadFromBinary/loadAsLayerFromBinary.
|
|
std::string getURI() const {
|
|
return filename_;
|
|
}
|
|
|
|
emscripten::val extractSublayerAssetPaths() {
|
|
emscripten::val arr = emscripten::val::array();
|
|
|
|
|
|
std::vector<std::string> paths = tinyusdz::ExtractSublayerAssetPaths(layer_);
|
|
for (size_t i = 0; i < paths.size(); i++) {
|
|
arr.call<void>("push", paths[i]);
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
|
|
emscripten::val extractReferencesAssetPaths() {
|
|
emscripten::val arr = emscripten::val::array();
|
|
|
|
|
|
std::vector<std::string> paths = tinyusdz::ExtractReferencesAssetPaths(layer_);
|
|
for (size_t i = 0; i < paths.size(); i++) {
|
|
arr.call<void>("push", paths[i]);
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
|
|
emscripten::val extractPayloadAssetPaths() {
|
|
emscripten::val arr = emscripten::val::array();
|
|
|
|
|
|
std::vector<std::string> paths = tinyusdz::ExtractPayloadAssetPaths(layer_);
|
|
for (size_t i = 0; i < paths.size(); i++) {
|
|
arr.call<void>("push", paths[i]);
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
|
|
bool hasSublayers() {
|
|
const tinyusdz::Layer &curr = composited_ ? composed_layer_ : layer_;
|
|
return curr.metas().subLayers.size();
|
|
}
|
|
|
|
|
|
bool composeSublayers() {
|
|
|
|
tinyusdz::AssetResolutionResolver resolver;
|
|
if (!SetupEMAssetResolution(resolver, &em_resolver_)) {
|
|
std::cerr << "Failed to setup EMAssetResolution\n";
|
|
return false;
|
|
}
|
|
const std::string base_dir = "./"; // FIXME
|
|
resolver.set_current_working_path(base_dir);
|
|
resolver.set_search_paths({base_dir});
|
|
|
|
if (composited_) {
|
|
layer_ = std::move(composed_layer_);
|
|
}
|
|
|
|
if (!tinyusdz::CompositeSublayers(resolver, layer_, &composed_layer_, &warn_, &error_)) {
|
|
std::cerr << "Failed to composite subLayers: \n";
|
|
if (composited_) {
|
|
// make 'layer_' and 'composed_layer_' invalid
|
|
loaded_as_layer_ = false;
|
|
composited_ = false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
composited_ = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool hasReferences() {
|
|
return tinyusdz::HasReferences(composited_ ? composed_layer_ : layer_, /* force_check */true);
|
|
}
|
|
|
|
bool composeReferences() {
|
|
|
|
tinyusdz::AssetResolutionResolver resolver;
|
|
if (!SetupEMAssetResolution(resolver, &em_resolver_)) {
|
|
std::cerr << "Failed to setup EMAssetResolution\n";
|
|
return false;
|
|
}
|
|
const std::string base_dir = "./"; // FIXME
|
|
resolver.set_current_working_path(base_dir);
|
|
resolver.set_search_paths({base_dir});
|
|
|
|
|
|
if (composited_) {
|
|
layer_ = std::move(composed_layer_);
|
|
}
|
|
|
|
if (!tinyusdz::CompositeReferences(resolver, layer_, &composed_layer_, &warn_, &error_)) {
|
|
std::cerr << "Failed to composite references: \n";
|
|
if (composited_) {
|
|
// make 'layer_' and 'composed_layer_' invalid
|
|
loaded_as_layer_ = false;
|
|
composited_ = false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
composited_ = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool hasPayload() {
|
|
return tinyusdz::HasPayload(layer_, /* force_check */true);
|
|
}
|
|
|
|
bool composePayload() {
|
|
|
|
tinyusdz::AssetResolutionResolver resolver;
|
|
if (!SetupEMAssetResolution(resolver, &em_resolver_)) {
|
|
std::cerr << "Failed to setup EMAssetResolution\n";
|
|
return false;
|
|
}
|
|
const std::string base_dir = "./"; // FIXME
|
|
resolver.set_current_working_path(base_dir);
|
|
resolver.set_search_paths({base_dir});
|
|
|
|
if (composited_) {
|
|
layer_ = std::move(composed_layer_);
|
|
}
|
|
|
|
if (!tinyusdz::CompositePayload(resolver, layer_, &composed_layer_, &warn_, &error_)) {
|
|
std::cerr << "Failed to composite payload: \n";
|
|
if (composited_) {
|
|
// make 'layer_' and 'composed_layer_' invalid
|
|
loaded_as_layer_ = false;
|
|
composited_ = false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
composited_ = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool hasInherits() {
|
|
return tinyusdz::HasInherits(composited_ ? composed_layer_ : layer_ );
|
|
}
|
|
|
|
bool composeInherits() {
|
|
|
|
if (composited_) {
|
|
layer_ = std::move(composed_layer_);
|
|
}
|
|
|
|
if (!tinyusdz::CompositeInherits( layer_, &composed_layer_, &warn_, &error_)) {
|
|
std::cerr << "Failed to composite inherits: \n";
|
|
if (composited_) {
|
|
// make 'layer_' and 'composed_layer_' invalid
|
|
loaded_as_layer_ = false;
|
|
composited_ = false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
composited_ = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool hasVariants() {
|
|
return tinyusdz::HasVariants(composited_ ? composed_layer_ : layer_ );
|
|
}
|
|
|
|
bool composeVariants() {
|
|
|
|
if (composited_) {
|
|
layer_ = std::move(composed_layer_);
|
|
}
|
|
|
|
if (!tinyusdz::CompositeVariant( layer_, &composed_layer_, &warn_, &error_)) {
|
|
std::cerr << "Failed to composite variant: \n";
|
|
if (composited_) {
|
|
// make 'layer_' and 'composed_layer_' invalid
|
|
loaded_as_layer_ = false;
|
|
composited_ = false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
composited_ = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool layerToRenderScene() {
|
|
|
|
if (!loaded_as_layer_) {
|
|
std::cerr << "not loaded as layer\n";
|
|
return false;
|
|
}
|
|
|
|
tinyusdz::Stage stage;
|
|
|
|
const tinyusdz::Layer &curr = composited_ ? composed_layer_ : layer_;
|
|
|
|
// LayerToStage expects an rvalue reference, so make a copy
|
|
tinyusdz::Layer layer_copy = curr;
|
|
|
|
if (!tinyusdz::LayerToStage(std::move(layer_copy), &stage, &warn_, &error_)) {
|
|
std::cerr << "Failed to LayerToStage \n";
|
|
return false;
|
|
}
|
|
|
|
std::string empty;
|
|
return stageToRenderScene(stage, /* TODO: is_usdz*/false, empty);
|
|
|
|
}
|
|
|
|
std::string layerToString() const {
|
|
if (!loaded_) {
|
|
return std::string();
|
|
}
|
|
if (!loaded_as_layer_) {
|
|
return std::string();
|
|
}
|
|
|
|
const tinyusdz::Layer &curr = composited_ ? composed_layer_ : layer_;
|
|
|
|
return tinyusdz::to_string(curr);
|
|
}
|
|
|
|
void clearAssets() {
|
|
em_resolver_.clear();
|
|
}
|
|
|
|
/// Reset all state - clears render scene, assets, and all cached data
|
|
/// Call this before loading a new USD file to free memory
|
|
void reset() {
|
|
// Clear loaded flag
|
|
loaded_ = false;
|
|
loaded_as_layer_ = false;
|
|
composited_ = false;
|
|
|
|
// Clear strings
|
|
filename_.clear();
|
|
warn_.clear();
|
|
error_.clear();
|
|
|
|
// Clear render scene (meshes, materials, textures, buffers, etc.)
|
|
render_scene_ = tinyusdz::tydra::RenderScene();
|
|
|
|
// Clear layers
|
|
layer_ = tinyusdz::Layer();
|
|
composed_layer_ = tinyusdz::Layer();
|
|
|
|
// Clear USDZ asset
|
|
usdz_asset_ = tinyusdz::USDZAsset();
|
|
|
|
// Clear asset resolver cache
|
|
em_resolver_.clear();
|
|
|
|
// Clear reordered mesh cache
|
|
reordered_mesh_cache_.clear();
|
|
|
|
// Reset parsing progress
|
|
parsing_progress_.reset();
|
|
}
|
|
|
|
/// Get memory usage statistics
|
|
emscripten::val getMemoryStats() const {
|
|
emscripten::val stats = emscripten::val::object();
|
|
|
|
// Count meshes
|
|
stats.set("numMeshes", static_cast<int>(render_scene_.meshes.size()));
|
|
stats.set("numMaterials", static_cast<int>(render_scene_.materials.size()));
|
|
stats.set("numTextures", static_cast<int>(render_scene_.textures.size()));
|
|
stats.set("numImages", static_cast<int>(render_scene_.images.size()));
|
|
stats.set("numBuffers", static_cast<int>(render_scene_.buffers.size()));
|
|
stats.set("numNodes", static_cast<int>(render_scene_.nodes.size()));
|
|
stats.set("numLights", static_cast<int>(render_scene_.lights.size()));
|
|
|
|
// Estimate buffer memory
|
|
size_t bufferMemory = 0;
|
|
for (const auto &buf : render_scene_.buffers) {
|
|
bufferMemory += buf.data.size();
|
|
}
|
|
stats.set("bufferMemoryBytes", static_cast<double>(bufferMemory));
|
|
stats.set("bufferMemoryMB", static_cast<double>(bufferMemory) / (1024.0 * 1024.0));
|
|
|
|
// Asset cache count
|
|
stats.set("assetCacheCount", static_cast<int>(em_resolver_.cache.size()));
|
|
|
|
// Reordered mesh cache count
|
|
stats.set("reorderedMeshCacheCount", static_cast<int>(reordered_mesh_cache_.size()));
|
|
|
|
return stats;
|
|
}
|
|
|
|
void setAsset(const std::string &name, const std::string &binary) {
|
|
em_resolver_.add(name, binary);
|
|
}
|
|
|
|
// Streaming asset methods
|
|
bool startStreamingAsset(const std::string &name, size_t expected_size) {
|
|
return em_resolver_.startStreamingAsset(name, expected_size);
|
|
}
|
|
|
|
bool appendAssetChunk(const std::string &name, const std::string &chunk) {
|
|
return em_resolver_.appendAssetChunk(name, chunk);
|
|
}
|
|
|
|
bool finalizeStreamingAsset(const std::string &name) {
|
|
return em_resolver_.finalizeStreamingAsset(name);
|
|
}
|
|
|
|
bool isStreamingAssetComplete(const std::string &name) const {
|
|
return em_resolver_.isStreamingAssetComplete(name);
|
|
}
|
|
|
|
emscripten::val getStreamingProgress(const std::string &name) const {
|
|
return em_resolver_.getStreamingProgress(name);
|
|
}
|
|
|
|
//
|
|
// Zero-copy streaming buffer methods for memory-efficient transfer
|
|
//
|
|
|
|
/// Allocate a zero-copy buffer for streaming transfer from JS
|
|
/// Returns object with {success, uuid, bufferPtr, totalSize} or {success: false, error}
|
|
emscripten::val allocateZeroCopyBuffer(const std::string &name, size_t size) {
|
|
return em_resolver_.allocateZeroCopyBuffer(name, size);
|
|
}
|
|
|
|
/// Get the buffer pointer for direct memory writes
|
|
double getZeroCopyBufferPtr(const std::string &name) {
|
|
return em_resolver_.getZeroCopyBufferPtr(name);
|
|
}
|
|
|
|
/// Get buffer pointer at specific offset for chunked writes
|
|
double getZeroCopyBufferPtrAtOffset(const std::string &name, size_t offset) {
|
|
return em_resolver_.getZeroCopyBufferPtrAtOffset(name, offset);
|
|
}
|
|
|
|
/// Mark bytes as written (call after each chunk write)
|
|
bool markZeroCopyBytesWritten(const std::string &name, size_t count) {
|
|
return em_resolver_.markZeroCopyBytesWritten(name, count);
|
|
}
|
|
|
|
/// Get zero-copy buffer progress
|
|
emscripten::val getZeroCopyProgress(const std::string &name) const {
|
|
return em_resolver_.getZeroCopyProgress(name);
|
|
}
|
|
|
|
/// Finalize the zero-copy buffer and move to asset cache
|
|
bool finalizeZeroCopyBuffer(const std::string &name) {
|
|
return em_resolver_.finalizeZeroCopyBuffer(name);
|
|
}
|
|
|
|
/// Cancel and free zero-copy buffer
|
|
bool cancelZeroCopyBuffer(const std::string &name) {
|
|
return em_resolver_.cancelZeroCopyBuffer(name);
|
|
}
|
|
|
|
/// Get all active zero-copy buffers
|
|
emscripten::val getActiveZeroCopyBuffers() const {
|
|
return em_resolver_.getActiveZeroCopyBuffers();
|
|
}
|
|
|
|
bool hasAsset(const std::string &name) const {
|
|
return em_resolver_.has(name);
|
|
}
|
|
|
|
std::string getAssetHash(const std::string &name) const {
|
|
return em_resolver_.getHash(name);
|
|
}
|
|
|
|
bool verifyAssetHash(const std::string &name, const std::string &expected_hash) const {
|
|
return em_resolver_.verifyHash(name, expected_hash);
|
|
}
|
|
|
|
emscripten::val getAsset(const std::string &name) const {
|
|
emscripten::val val = emscripten::val::object();
|
|
if (em_resolver_.has(name)) {
|
|
const AssetCacheEntry &entry = em_resolver_.get(name);
|
|
val.set("name", name);
|
|
val.set("data", emscripten::typed_memory_view(entry.binary.size(), entry.binary.data()));
|
|
val.set("sha256", entry.sha256_hash);
|
|
val.set("uuid", entry.uuid);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
std::string getAssetUUID(const std::string &name) const {
|
|
return em_resolver_.getUUID(name);
|
|
}
|
|
|
|
std::string getStreamingAssetUUID(const std::string &name) const {
|
|
return em_resolver_.getStreamingUUID(name);
|
|
}
|
|
|
|
emscripten::val getAllAssetUUIDs() const {
|
|
return em_resolver_.getAssetUUIDs();
|
|
}
|
|
|
|
std::string findAssetByUUID(const std::string &uuid) const {
|
|
return em_resolver_.findAssetByUUID(uuid);
|
|
}
|
|
|
|
// Get asset by UUID instead of name
|
|
emscripten::val getAssetByUUID(const std::string &uuid) const {
|
|
emscripten::val val = emscripten::val::object();
|
|
|
|
if (!em_resolver_.hasByUUID(uuid)) {
|
|
val.set("error", "Asset not found with UUID: " + uuid);
|
|
return val;
|
|
}
|
|
|
|
const AssetCacheEntry &entry = em_resolver_.getByUUID(uuid);
|
|
const std::string name = em_resolver_.findAssetByUUID(uuid);
|
|
|
|
val.set("name", name);
|
|
val.set("data", emscripten::typed_memory_view(entry.binary.size(),
|
|
reinterpret_cast<const uint8_t*>(entry.binary.data())));
|
|
val.set("sha256", entry.sha256_hash);
|
|
val.set("uuid", entry.uuid);
|
|
|
|
return val;
|
|
}
|
|
|
|
// Delete asset by name or UUID
|
|
bool deleteAsset(const std::string &nameOrUuid) {
|
|
// First try to delete by name
|
|
if (em_resolver_.deleteAsset(nameOrUuid)) {
|
|
return true;
|
|
}
|
|
|
|
// If not found by name, try to delete by UUID
|
|
return em_resolver_.deleteAssetByUUID(nameOrUuid);
|
|
}
|
|
|
|
// Delete asset specifically by UUID
|
|
bool deleteAssetByUUID(const std::string &uuid) {
|
|
return em_resolver_.deleteAssetByUUID(uuid);
|
|
}
|
|
|
|
// Delete asset specifically by name
|
|
bool deleteAssetByName(const std::string &name) {
|
|
return em_resolver_.deleteAsset(name);
|
|
}
|
|
|
|
// Get number of cached assets
|
|
size_t getAssetCount() const {
|
|
return em_resolver_.cache.size();
|
|
}
|
|
|
|
// Check if asset exists (by name or UUID)
|
|
bool assetExists(const std::string &nameOrUuid) const {
|
|
return em_resolver_.has(nameOrUuid) || em_resolver_.hasByUUID(nameOrUuid);
|
|
}
|
|
|
|
emscripten::val getAssetCacheDataAsMemoryView(const std::string &name) const {
|
|
return em_resolver_.getCacheDataAsMemoryView(name);
|
|
}
|
|
|
|
bool setAssetFromRawPointer(const std::string &name, uintptr_t dataPtr, size_t size) {
|
|
return em_resolver_.addFromRawPointer(name, dataPtr, size);
|
|
}
|
|
|
|
emscripten::val extractUnresolvedTexturePaths() const {
|
|
emscripten::val val;
|
|
|
|
// Assume USD is converted to RenderScene before calling this function.
|
|
|
|
for (const tinyusdz::tydra::TextureImage &texImg : render_scene_.images) {
|
|
//
|
|
if (texImg.buffer_id == -1) {
|
|
std::string path = texImg.asset_identifier;
|
|
val.call<void>("push", path);
|
|
}
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
bool mcpCreateContext(const std::string &session_id) {
|
|
|
|
if (mcp_ctx_.count(session_id)) {
|
|
// Context already exists
|
|
return false;
|
|
}
|
|
|
|
mcp_ctx_[session_id] = tinyusdz::tydra::mcp::Context();
|
|
mcp_session_id_ = session_id;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool mcpSelectContext(const std::string &session_id) {
|
|
|
|
if (!mcp_ctx_.count(session_id)) {
|
|
// Context does not exist
|
|
return false;
|
|
}
|
|
|
|
mcp_session_id_ = session_id;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// return JSON string
|
|
std::string mcpToolsList() {
|
|
|
|
if (!mcp_ctx_.count(mcp_session_id_)) {
|
|
// TODO: better error message
|
|
return "{ \"error\": \"invalid session_id\"}";
|
|
}
|
|
|
|
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
|
|
tinyusdz::tydra::mcp::Context &ctx = mcp_global_ctx_;
|
|
|
|
nlohmann::json result;
|
|
if (!tinyusdz::tydra::mcp::GetToolsList(ctx, result)) {
|
|
std::cerr << "[tydra:mcp:GetToolsList] failed." << "\n";
|
|
// TODO: Report error more nice way.
|
|
result = nlohmann::json::object();
|
|
result["isError"] = true;
|
|
result["content"] = nlohmann::json::array();
|
|
}
|
|
|
|
std::string s_result = result.dump();
|
|
|
|
return s_result;
|
|
}
|
|
|
|
// args: JSON string
|
|
// return JSON string
|
|
std::string mcpToolsCall(const std::string &tool_name, const std::string &args) {
|
|
|
|
if (!mcp_ctx_.count(mcp_session_id_)) {
|
|
// TODO: better error message
|
|
return "{ \"error\": \"invalid session_id\"}";
|
|
}
|
|
|
|
nlohmann::json j_args = nlohmann::json::parse(args);
|
|
|
|
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
|
|
auto &ctx = mcp_global_ctx_;
|
|
|
|
nlohmann::json result;
|
|
|
|
std::string err;
|
|
if (!tinyusdz::tydra::mcp::CallTool(ctx, tool_name, j_args, result, err)) {
|
|
// TODO: Report error more nice way.
|
|
std::cerr << "[tydra:mcp:CallTool]" << err << "\n";
|
|
result = nlohmann::json::object();
|
|
result["isError"] = true;
|
|
result["content"] = nlohmann::json::array();
|
|
|
|
nlohmann::json e;
|
|
e["type"] = "text";
|
|
|
|
nlohmann::json msg;
|
|
msg["error"] = err;
|
|
e["text"] = msg.dump();
|
|
|
|
result["content"].push_back(e);
|
|
}
|
|
|
|
std::string s_result = result.dump();
|
|
|
|
return s_result;
|
|
}
|
|
|
|
std::string mcpResourcesList() {
|
|
|
|
if (!mcp_ctx_.count(mcp_session_id_)) {
|
|
// TODO: better error message
|
|
return "{ \"error\": \"invalid session_id\"}";
|
|
}
|
|
|
|
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
|
|
auto &ctx = mcp_global_ctx_;
|
|
|
|
nlohmann::json result;
|
|
|
|
if (!tinyusdz::tydra::mcp::GetResourcesList(ctx, result)) {
|
|
// TODO: Report error more nice way.
|
|
std::cerr << "[tydra:mcp:ListResources] failed\n";
|
|
result = nlohmann::json::object();
|
|
result["isError"] = true;
|
|
//result["content"] = nlohmann::json::array();
|
|
}
|
|
|
|
std::string s_result = result.dump();
|
|
|
|
return s_result;
|
|
}
|
|
|
|
std::string mcpResourcesRead(const std::string &uri) {
|
|
|
|
if (!mcp_ctx_.count(mcp_session_id_)) {
|
|
// TODO: better error message
|
|
return "{ \"error\": \"invalid session_id\"}";
|
|
}
|
|
|
|
//Context &ctx = mcp_ctx_.at(mcp_session_id_);
|
|
auto &ctx = mcp_global_ctx_;
|
|
|
|
nlohmann::json content;
|
|
|
|
if (!tinyusdz::tydra::mcp::ReadResource(ctx, uri, content)) {
|
|
// TODO: Report error more nice way.
|
|
std::cerr << "[tydra:mcp:ReadResources] failed\n";
|
|
content = nlohmann::json::object();
|
|
content["isError"] = true;
|
|
//content["content"] = nlohmann::json::array();
|
|
}
|
|
|
|
std::string s_content = content.dump();
|
|
|
|
return s_content;
|
|
}
|
|
|
|
// JSON <-> USD Layer conversion methods
|
|
std::string layerToJSON() const {
|
|
if (!loaded_as_layer_) {
|
|
return "{\"error\": \"No layer loaded\"}";
|
|
}
|
|
|
|
const tinyusdz::Layer &curr = composited_ ? composed_layer_ : layer_;
|
|
|
|
nlohmann::json json_obj = tinyusdz::ToJSON(curr);
|
|
return json_obj.dump(2); // Pretty print with 2 spaces
|
|
}
|
|
|
|
std::string layerToJSONWithOptions(bool embedBuffers, const std::string& arrayMode) const {
|
|
if (!loaded_as_layer_) {
|
|
return "{\"error\": \"No layer loaded\"}";
|
|
}
|
|
|
|
const tinyusdz::Layer &curr = composited_ ? composed_layer_ : layer_;
|
|
|
|
tinyusdz::USDToJSONOptions options;
|
|
options.embedBuffers = embedBuffers;
|
|
|
|
if (arrayMode == "buffer") {
|
|
options.arrayMode = tinyusdz::ArraySerializationMode::Buffer;
|
|
} else {
|
|
options.arrayMode = tinyusdz::ArraySerializationMode::Base64;
|
|
}
|
|
|
|
std::string json_str, warn, err;
|
|
bool success = tinyusdz::to_json_string(curr, options, &json_str, &warn, &err);
|
|
|
|
if (!success) {
|
|
return "{\"error\": \"Failed to convert layer to JSON: " + err + "\"}";
|
|
}
|
|
|
|
return json_str;
|
|
}
|
|
|
|
bool loadLayerFromJSON(const std::string& json_string) {
|
|
std::string warn, err;
|
|
|
|
bool success = tinyusdz::JSONToLayer(json_string, &layer_, &warn, &err);
|
|
|
|
if (success) {
|
|
loaded_ = true;
|
|
loaded_as_layer_ = true;
|
|
composited_ = false;
|
|
warn_ = warn;
|
|
error_.clear();
|
|
filename_ = "from_json.usd";
|
|
} else {
|
|
loaded_ = false;
|
|
loaded_as_layer_ = false;
|
|
composited_ = false;
|
|
warn_ = warn;
|
|
error_ = err;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
//
|
|
// Progress reporting methods for polling-based async progress
|
|
//
|
|
|
|
/// Get current parsing progress as a JS object
|
|
emscripten::val getProgress() const {
|
|
return parsing_progress_.toJS();
|
|
}
|
|
|
|
/// Request cancellation of current parsing operation
|
|
void cancelParsing() {
|
|
parsing_progress_.cancel_requested.store(true);
|
|
}
|
|
|
|
/// Check if parsing was cancelled
|
|
bool wasCancelled() const {
|
|
return parsing_progress_.stage == ParsingProgress::Stage::Cancelled;
|
|
}
|
|
|
|
/// Check if parsing is currently in progress
|
|
bool isParsingInProgress() const {
|
|
return parsing_progress_.stage == ParsingProgress::Stage::Parsing ||
|
|
parsing_progress_.stage == ParsingProgress::Stage::Converting;
|
|
}
|
|
|
|
/// Reset progress state (call before starting a new parse)
|
|
void resetProgress() {
|
|
parsing_progress_.reset();
|
|
}
|
|
|
|
/// Load from binary with progress reporting
|
|
/// Returns immediately, progress can be polled via getProgress()
|
|
bool loadFromBinaryWithProgress(const std::string &binary, const std::string &filename) {
|
|
// Reset progress state
|
|
parsing_progress_.reset();
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Parsing);
|
|
parsing_progress_.total_bytes = binary.size();
|
|
parsing_progress_.current_operation = "Loading USD file";
|
|
|
|
bool is_usdz = tinyusdz::IsUSDZ(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size());
|
|
|
|
tinyusdz::USDLoadOptions options;
|
|
options.max_memory_limit_in_mb = max_memory_limit_mb_;
|
|
|
|
// Set up progress callback
|
|
options.progress_callback = [](float progress, void *userptr) -> bool {
|
|
ParsingProgress *pp = static_cast<ParsingProgress *>(userptr);
|
|
pp->progress = progress * 0.8f; // Parsing is 80% of total work
|
|
pp->bytes_processed = static_cast<uint64_t>(progress * pp->total_bytes);
|
|
// Return false to cancel, true to continue
|
|
return !pp->shouldCancel();
|
|
};
|
|
options.progress_userptr = &parsing_progress_;
|
|
|
|
tinyusdz::Stage stage;
|
|
loaded_ = tinyusdz::LoadUSDFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
filename, &stage, &warn_, &error_, options);
|
|
|
|
if (!loaded_) {
|
|
if (parsing_progress_.shouldCancel()) {
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Cancelled);
|
|
parsing_progress_.error_message = "Parsing cancelled by user";
|
|
} else {
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Error);
|
|
parsing_progress_.error_message = error_;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
loaded_as_layer_ = false;
|
|
filename_ = filename;
|
|
|
|
// Now convert to render scene
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Converting);
|
|
parsing_progress_.current_operation = "Converting to render scene";
|
|
parsing_progress_.progress = 0.8f;
|
|
|
|
bool render_ok = stageToRenderScene(stage, is_usdz, binary);
|
|
|
|
if (!render_ok) {
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Error);
|
|
parsing_progress_.error_message = error_;
|
|
return false;
|
|
}
|
|
|
|
parsing_progress_.progress = 1.0f;
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Complete);
|
|
parsing_progress_.current_operation = "Complete";
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Load as layer with progress reporting
|
|
bool loadAsLayerFromBinaryWithProgress(const std::string &binary, const std::string &filename) {
|
|
// Reset progress state
|
|
parsing_progress_.reset();
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Parsing);
|
|
parsing_progress_.total_bytes = binary.size();
|
|
parsing_progress_.current_operation = "Loading USD layer";
|
|
|
|
tinyusdz::USDLoadOptions options;
|
|
options.max_memory_limit_in_mb = max_memory_limit_mb_;
|
|
|
|
// Set up progress callback
|
|
options.progress_callback = [](float progress, void *userptr) -> bool {
|
|
ParsingProgress *pp = static_cast<ParsingProgress *>(userptr);
|
|
pp->progress = progress;
|
|
pp->bytes_processed = static_cast<uint64_t>(progress * pp->total_bytes);
|
|
return !pp->shouldCancel();
|
|
};
|
|
options.progress_userptr = &parsing_progress_;
|
|
|
|
loaded_ = tinyusdz::LoadLayerFromMemory(
|
|
reinterpret_cast<const uint8_t *>(binary.c_str()), binary.size(),
|
|
filename, &layer_, &warn_, &error_, options);
|
|
|
|
if (!loaded_) {
|
|
if (parsing_progress_.shouldCancel()) {
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Cancelled);
|
|
parsing_progress_.error_message = "Parsing cancelled by user";
|
|
} else {
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Error);
|
|
parsing_progress_.error_message = error_;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
loaded_as_layer_ = true;
|
|
filename_ = filename;
|
|
|
|
parsing_progress_.progress = 1.0f;
|
|
parsing_progress_.setStage(ParsingProgress::Stage::Complete);
|
|
parsing_progress_.current_operation = "Complete";
|
|
|
|
return true;
|
|
}
|
|
|
|
// TODO: Deprecate
|
|
bool ok() const { return loaded_; }
|
|
|
|
const std::string &error() const { return error_; }
|
|
const std::string &warn() const { return warn_; }
|
|
|
|
private:
|
|
|
|
|
|
// Simple glTF-like Node
|
|
emscripten::val buildNodeRec(const tinyusdz::tydra::Node &rnode) {
|
|
emscripten::val node = emscripten::val::object();
|
|
|
|
node.set("primName", rnode.prim_name);
|
|
node.set("displayName", rnode.display_name);
|
|
node.set("absPath", rnode.abs_path);
|
|
|
|
std::string nodeCategoryStr = to_string(rnode.category);
|
|
node.set("nodeCategory", nodeCategoryStr);
|
|
|
|
std::string nodeTypeStr = to_string(rnode.nodeType);
|
|
node.set("nodeType", nodeTypeStr);
|
|
|
|
node.set("contentId",
|
|
rnode.id); // e.g. index to Mesh if nodeType == 'mesh'
|
|
|
|
std::array<double, 16> localMatrix = detail::toArray(rnode.local_matrix);
|
|
std::array<double, 16> globalMatrix = detail::toArray(rnode.global_matrix);
|
|
|
|
node.set("localMatrix", localMatrix);
|
|
node.set("globalMatrix", globalMatrix);
|
|
node.set("hasResetXform", rnode.has_resetXform);
|
|
|
|
emscripten::val children = emscripten::val::array();
|
|
|
|
for (const tinyusdz::tydra::Node &child : rnode.children) {
|
|
emscripten::val child_val = buildNodeRec(child);
|
|
|
|
children.call<void>("push", child_val);
|
|
}
|
|
|
|
node.set("children", children);
|
|
|
|
return node;
|
|
}
|
|
|
|
|
|
bool loaded_{false};
|
|
bool loaded_as_layer_{false};
|
|
bool enableComposition_{false};
|
|
bool loadTextureInNative_{false}; // true: Let JavaScript to decode texture image.
|
|
|
|
// Set appropriate default memory limits based on WASM architecture
|
|
#ifdef TINYUSDZ_WASM_MEMORY64
|
|
int32_t max_memory_limit_mb_{8192}; // 8GB for MEMORY64
|
|
#else
|
|
int32_t max_memory_limit_mb_{2048}; // 2GB for 32-bit WASM
|
|
#endif
|
|
|
|
// Bone reduction configuration (disabled by default for backward compatibility)
|
|
bool enable_bone_reduction_{false};
|
|
uint32_t target_bone_count_{4}; // Default to 4 bones (standard for WebGL/Three.js)
|
|
|
|
std::string filename_;
|
|
std::string warn_;
|
|
std::string error_;
|
|
|
|
tinyusdz::Layer layer_;
|
|
tinyusdz::Layer composed_layer_;
|
|
bool composited_{false};
|
|
std::vector<std::string> search_paths_;
|
|
std::string base_dir_{"./"};
|
|
|
|
tinyusdz::tydra::RenderScene render_scene_;
|
|
tinyusdz::USDZAsset usdz_asset_;
|
|
EMAssetResolutionResolver em_resolver_;
|
|
|
|
// Cache for reordered mesh data (triangles sorted by material for optimal submesh grouping)
|
|
struct ReorderedMeshCache {
|
|
std::vector<float> points;
|
|
std::vector<float> normals;
|
|
std::vector<float> texcoords;
|
|
std::vector<float> tangents;
|
|
std::vector<uint32_t> faceVertexIndices;
|
|
};
|
|
mutable std::unordered_map<int, ReorderedMeshCache> reordered_mesh_cache_;
|
|
|
|
// key = session_id
|
|
std::unordered_map<std::string, tinyusdz::tydra::mcp::Context> mcp_ctx_;
|
|
std::string mcp_session_id_;
|
|
|
|
tinyusdz::tydra::mcp::Context mcp_global_ctx_;
|
|
|
|
// Progress tracking for polling-based progress reporting
|
|
ParsingProgress parsing_progress_;
|
|
};
|
|
|
|
///
|
|
/// USD composition
|
|
///
|
|
class TinyUSDZComposerNative {
|
|
public:
|
|
// Default constructor for async loading
|
|
TinyUSDZComposerNative() : loaded_(false) {}
|
|
|
|
bool loaded() const { return loaded_; }
|
|
const std::string &error() const { return error_; }
|
|
|
|
private:
|
|
bool loaded_{false};
|
|
std::string warn_;
|
|
std::string error_;
|
|
|
|
tinyusdz::Layer root_layer_;
|
|
};
|
|
|
|
#if 0
|
|
// Helper to register std::array
|
|
namespace emscripten {
|
|
namespace internal {
|
|
template<typename T, size_t N>
|
|
struct TypeID<std::array<T, N>> {
|
|
static constexpr TYPEID get() {
|
|
return TypeID<val>::get();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// Convert std::array<float, 3> to/from JavaScript array
|
|
namespace emscripten {
|
|
namespace internal {
|
|
template<>
|
|
struct BindingType<std::array<float, 3>> {
|
|
typedef std::array<float, 3> WireType;
|
|
static WireType toWireType(const std::array<float, 3>& arr) {
|
|
return arr;
|
|
}
|
|
static std::array<float, 3> fromWireType(const WireType& arr) {
|
|
return arr;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// TODO: quaternion type.
|
|
|
|
// =============================================================================
|
|
// HDR/EXR Image Decoding Functions with FP16 Support
|
|
// =============================================================================
|
|
|
|
namespace {
|
|
|
|
// IEEE 754 half-precision float conversion utilities
|
|
// Based on public domain code from OpenEXR/TinyEXR
|
|
|
|
union FP32 {
|
|
uint32_t u;
|
|
float f;
|
|
struct {
|
|
unsigned int Mantissa : 23;
|
|
unsigned int Exponent : 8;
|
|
unsigned int Sign : 1;
|
|
} s;
|
|
};
|
|
|
|
union FP16 {
|
|
uint16_t u;
|
|
struct {
|
|
unsigned int Mantissa : 10;
|
|
unsigned int Exponent : 5;
|
|
unsigned int Sign : 1;
|
|
} s;
|
|
};
|
|
|
|
/// Convert float32 to float16 (IEEE 754 half-precision)
|
|
inline uint16_t float32ToFloat16(float value) {
|
|
FP32 f;
|
|
f.f = value;
|
|
FP16 o = {0};
|
|
|
|
if (f.s.Exponent == 0) {
|
|
// Signed zero/denormal (will underflow)
|
|
o.s.Exponent = 0;
|
|
} else if (f.s.Exponent == 255) {
|
|
// Inf or NaN
|
|
o.s.Exponent = 31;
|
|
o.s.Mantissa = f.s.Mantissa ? 0x200 : 0; // NaN->qNaN, Inf->Inf
|
|
} else {
|
|
// Normalized number
|
|
int newexp = f.s.Exponent - 127 + 15;
|
|
if (newexp >= 31) {
|
|
// Overflow -> infinity
|
|
o.s.Exponent = 31;
|
|
} else if (newexp <= 0) {
|
|
// Underflow
|
|
if ((14 - newexp) <= 24) {
|
|
unsigned int mant = f.s.Mantissa | 0x800000; // Hidden 1 bit
|
|
o.s.Mantissa = mant >> (14 - newexp);
|
|
if ((mant >> (13 - newexp)) & 1)
|
|
o.u++; // Round
|
|
}
|
|
} else {
|
|
o.s.Exponent = static_cast<unsigned int>(newexp);
|
|
o.s.Mantissa = f.s.Mantissa >> 13;
|
|
if (f.s.Mantissa & 0x1000)
|
|
o.u++; // Round
|
|
}
|
|
}
|
|
o.s.Sign = f.s.Sign;
|
|
return o.u;
|
|
}
|
|
|
|
/// Convert float16 to float32
|
|
inline float float16ToFloat32(uint16_t h) {
|
|
static const FP32 magic = {113 << 23};
|
|
static const unsigned int shifted_exp = 0x7c00 << 13;
|
|
FP32 o;
|
|
FP16 hp;
|
|
hp.u = h;
|
|
|
|
o.u = (hp.u & 0x7fffU) << 13U;
|
|
unsigned int exp_ = shifted_exp & o.u;
|
|
o.u += (127 - 15) << 23;
|
|
|
|
if (exp_ == shifted_exp)
|
|
o.u += (128 - 16) << 23;
|
|
else if (exp_ == 0) {
|
|
o.u += 1 << 23;
|
|
o.f -= magic.f;
|
|
}
|
|
|
|
o.u |= (hp.u & 0x8000U) << 16U;
|
|
return o.f;
|
|
}
|
|
|
|
/// Convert int32 to float16 (with normalization)
|
|
inline uint16_t int32ToFloat16(int32_t value, float scale = 1.0f / 2147483647.0f) {
|
|
float normalized = static_cast<float>(value) * scale;
|
|
return float32ToFloat16(normalized);
|
|
}
|
|
|
|
/// Convert uint32 to float16 (with normalization)
|
|
inline uint16_t uint32ToFloat16(uint32_t value, float scale = 1.0f / 4294967295.0f) {
|
|
float normalized = static_cast<float>(value) * scale;
|
|
return float32ToFloat16(normalized);
|
|
}
|
|
|
|
/// Convert float32 array to float16 array
|
|
void convertFloat32ToFloat16(const float* src, uint16_t* dst, size_t count) {
|
|
for (size_t i = 0; i < count; ++i) {
|
|
dst[i] = float32ToFloat16(src[i]);
|
|
}
|
|
}
|
|
|
|
/// Copy buffer from JS Uint8Array
|
|
void copyFromJSBuffer(const emscripten::val& data, std::vector<uint8_t>& buffer) {
|
|
size_t size = data["byteLength"].as<size_t>();
|
|
buffer.resize(size);
|
|
emscripten::val view = emscripten::val::global("Uint8Array").new_(
|
|
data["buffer"], data["byteOffset"], size);
|
|
emscripten::val heapView = emscripten::val(
|
|
emscripten::typed_memory_view(size, buffer.data()));
|
|
heapView.call<void>("set", view);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
#if defined(TINYUSDZ_WITH_EXR)
|
|
///
|
|
/// Decode EXR image with output format options
|
|
///
|
|
/// @param data Uint8Array containing EXR file data
|
|
/// @param outputFormat Output format: "float32", "float16", or "auto" (default)
|
|
/// - "float32": Always output as Float32Array (default, preserves precision)
|
|
/// - "float16": Convert to Uint16Array (IEEE 754 half-float, saves 50% memory)
|
|
/// - "auto": Use native format if fp16, otherwise float32
|
|
///
|
|
emscripten::val decodeEXR(const emscripten::val& data,
|
|
const std::string& outputFormat = "float32") {
|
|
emscripten::val result = emscripten::val::object();
|
|
|
|
std::vector<uint8_t> buffer;
|
|
copyFromJSBuffer(data, buffer);
|
|
|
|
if (IsEXRFromMemory(buffer.data(), buffer.size()) != TINYEXR_SUCCESS) {
|
|
result.set("success", false);
|
|
result.set("error", std::string("Not a valid EXR file"));
|
|
return result;
|
|
}
|
|
|
|
float* rgba = nullptr;
|
|
int width = 0;
|
|
int height = 0;
|
|
const char* err = nullptr;
|
|
|
|
// LoadEXRFromMemory always returns float32 RGBA
|
|
int ret = LoadEXRFromMemory(&rgba, &width, &height,
|
|
buffer.data(), buffer.size(), &err);
|
|
|
|
if (ret != TINYEXR_SUCCESS) {
|
|
result.set("success", false);
|
|
if (err) {
|
|
result.set("error", std::string(err));
|
|
FreeEXRErrorMessage(err);
|
|
} else {
|
|
result.set("error", std::string("Failed to decode EXR"));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
size_t pixelCount = size_t(width) * size_t(height) * 4;
|
|
|
|
if (outputFormat == "float16") {
|
|
// Convert to float16 and return as Uint16Array
|
|
std::vector<uint16_t> fp16Data(pixelCount);
|
|
convertFloat32ToFloat16(rgba, fp16Data.data(), pixelCount);
|
|
free(rgba);
|
|
|
|
emscripten::val Uint16Array = emscripten::val::global("Uint16Array");
|
|
emscripten::val pixelData = Uint16Array.new_(pixelCount);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(pixelCount, fp16Data.data()));
|
|
pixelData.call<void>("set", jsHeap);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("float16"));
|
|
result.set("bitsPerChannel", 16);
|
|
} else {
|
|
// Return as Float32Array (default)
|
|
emscripten::val Float32Array = emscripten::val::global("Float32Array");
|
|
emscripten::val pixelData = Float32Array.new_(pixelCount);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(pixelCount, rgba));
|
|
pixelData.call<void>("set", jsHeap);
|
|
free(rgba);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("float32"));
|
|
result.set("bitsPerChannel", 32);
|
|
}
|
|
|
|
result.set("success", true);
|
|
result.set("width", width);
|
|
result.set("height", height);
|
|
result.set("channels", 4);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Check if data is a valid EXR file
|
|
bool isEXR(const emscripten::val& data) {
|
|
size_t size = data["byteLength"].as<size_t>();
|
|
if (size < 8) return false;
|
|
|
|
std::vector<uint8_t> buffer;
|
|
copyFromJSBuffer(data, buffer);
|
|
return IsEXRFromMemory(buffer.data(), buffer.size()) == TINYEXR_SUCCESS;
|
|
}
|
|
#endif
|
|
|
|
///
|
|
/// Decode HDR (Radiance RGBE) image with output format options
|
|
/// Uses stb_image's stbi_loadf_from_memory for HDR decoding
|
|
///
|
|
/// @param data Uint8Array containing HDR file data
|
|
/// @param outputFormat Output format: "float16" (default) or "float32"
|
|
/// - "float16": Returns Uint16Array with IEEE 754 half-float (default, saves memory)
|
|
/// - "float32": Returns Float32Array (full precision)
|
|
///
|
|
emscripten::val decodeHDR(const emscripten::val& data,
|
|
const std::string& outputFormat = "float16") {
|
|
emscripten::val result = emscripten::val::object();
|
|
|
|
std::vector<uint8_t> buffer;
|
|
copyFromJSBuffer(data, buffer);
|
|
|
|
int width = 0, height = 0, channels = 0;
|
|
|
|
// Use stbi_loadf_from_memory which returns float32 RGBA data
|
|
// Request 4 channels (RGBA) for consistency
|
|
float* floatData = stbi_loadf_from_memory(
|
|
buffer.data(), static_cast<int>(buffer.size()),
|
|
&width, &height, &channels, 4);
|
|
|
|
if (!floatData) {
|
|
result.set("success", false);
|
|
result.set("error", std::string("Failed to decode HDR: ") + stbi_failure_reason());
|
|
return result;
|
|
}
|
|
|
|
// Always output 4 channels (RGBA)
|
|
const int outputChannels = 4;
|
|
size_t pixelCount = size_t(width) * size_t(height) * size_t(outputChannels);
|
|
|
|
if (outputFormat == "float32") {
|
|
// Return as Float32Array
|
|
emscripten::val Float32Array = emscripten::val::global("Float32Array");
|
|
emscripten::val pixelData = Float32Array.new_(pixelCount);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(pixelCount, floatData));
|
|
pixelData.call<void>("set", jsHeap);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("float32"));
|
|
result.set("bitsPerChannel", 32);
|
|
} else {
|
|
// Convert float32 to float16 and return as Uint16Array (default)
|
|
std::vector<uint16_t> fp16Data(pixelCount);
|
|
convertFloat32ToFloat16(floatData, fp16Data.data(), pixelCount);
|
|
|
|
emscripten::val Uint16Array = emscripten::val::global("Uint16Array");
|
|
emscripten::val pixelData = Uint16Array.new_(pixelCount);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(pixelCount, fp16Data.data()));
|
|
pixelData.call<void>("set", jsHeap);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("float16"));
|
|
result.set("bitsPerChannel", 16);
|
|
}
|
|
|
|
stbi_image_free(floatData);
|
|
|
|
result.set("success", true);
|
|
result.set("width", width);
|
|
result.set("height", height);
|
|
result.set("channels", outputChannels);
|
|
|
|
return result;
|
|
}
|
|
|
|
///
|
|
/// Generic image decoder with output format options
|
|
///
|
|
/// @param data Uint8Array containing image file data
|
|
/// @param hint Filename hint for format detection (e.g., "image.exr")
|
|
/// @param outputFormat Output format: "auto", "float32", "float16", "uint16", "uint8"
|
|
/// - "auto": Use native format (default)
|
|
/// - "float32": Convert HDR/EXR to float32
|
|
/// - "float16": Convert HDR/EXR to float16 (Uint16Array with IEEE 754 half-float)
|
|
/// - "uint16": Keep 16-bit data as Uint16Array
|
|
/// - "uint8": Keep 8-bit data as Uint8Array
|
|
///
|
|
emscripten::val decodeImage(const emscripten::val& data,
|
|
const std::string& hint = "",
|
|
const std::string& outputFormat = "auto") {
|
|
emscripten::val result = emscripten::val::object();
|
|
|
|
std::vector<uint8_t> buffer;
|
|
copyFromJSBuffer(data, buffer);
|
|
|
|
#if defined(TINYUSDZ_WITH_EXR)
|
|
// Check for EXR first
|
|
if (IsEXRFromMemory(buffer.data(), buffer.size()) == TINYEXR_SUCCESS) {
|
|
std::string exrFormat = (outputFormat == "auto") ? "float32" : outputFormat;
|
|
return decodeEXR(data, exrFormat);
|
|
}
|
|
#endif
|
|
|
|
// Use generic image loader for other formats (HDR, PNG, JPEG, etc.)
|
|
auto loadResult = tinyusdz::image::LoadImageFromMemory(
|
|
buffer.data(), buffer.size(), hint);
|
|
|
|
if (!loadResult) {
|
|
result.set("success", false);
|
|
result.set("error", loadResult.error());
|
|
return result;
|
|
}
|
|
|
|
const auto& img = loadResult.value().image;
|
|
size_t pixelCount = size_t(img.width) * size_t(img.height) * size_t(img.channels);
|
|
size_t dataSize = img.data.size();
|
|
|
|
// Determine actual output format
|
|
std::string actualFormat = outputFormat;
|
|
if (actualFormat == "auto") {
|
|
if (img.format == tinyusdz::Image::PixelFormat::Float) {
|
|
actualFormat = "float32";
|
|
} else if (img.bpp == 16) {
|
|
actualFormat = "uint16";
|
|
} else {
|
|
actualFormat = "uint8";
|
|
}
|
|
}
|
|
|
|
// Handle float data
|
|
if (img.format == tinyusdz::Image::PixelFormat::Float) {
|
|
const float* srcData = reinterpret_cast<const float*>(img.data.data());
|
|
|
|
if (actualFormat == "float16") {
|
|
// Downcast float32 to float16
|
|
std::vector<uint16_t> fp16Data(pixelCount);
|
|
convertFloat32ToFloat16(srcData, fp16Data.data(), pixelCount);
|
|
|
|
emscripten::val Uint16Array = emscripten::val::global("Uint16Array");
|
|
emscripten::val pixelData = Uint16Array.new_(pixelCount);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(pixelCount, fp16Data.data()));
|
|
pixelData.call<void>("set", jsHeap);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("float16"));
|
|
result.set("bitsPerChannel", 16);
|
|
} else {
|
|
// Keep as float32
|
|
emscripten::val Float32Array = emscripten::val::global("Float32Array");
|
|
emscripten::val pixelData = Float32Array.new_(pixelCount);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(pixelCount, srcData));
|
|
pixelData.call<void>("set", jsHeap);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("float32"));
|
|
result.set("bitsPerChannel", 32);
|
|
}
|
|
}
|
|
// Handle 16-bit integer data (e.g., 16-bit PNG)
|
|
else if (img.bpp == 16) {
|
|
const uint16_t* srcData = reinterpret_cast<const uint16_t*>(img.data.data());
|
|
|
|
// Return as Uint16Array (native format)
|
|
emscripten::val Uint16Array = emscripten::val::global("Uint16Array");
|
|
emscripten::val pixelData = Uint16Array.new_(pixelCount);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(pixelCount, srcData));
|
|
pixelData.call<void>("set", jsHeap);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("uint16"));
|
|
result.set("bitsPerChannel", 16);
|
|
}
|
|
// Handle 8-bit data
|
|
else {
|
|
emscripten::val Uint8Array = emscripten::val::global("Uint8Array");
|
|
emscripten::val pixelData = Uint8Array.new_(dataSize);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(dataSize, img.data.data()));
|
|
pixelData.call<void>("set", jsHeap);
|
|
|
|
result.set("data", pixelData);
|
|
result.set("pixelFormat", std::string("uint8"));
|
|
result.set("bitsPerChannel", 8);
|
|
}
|
|
|
|
result.set("success", true);
|
|
result.set("width", img.width);
|
|
result.set("height", img.height);
|
|
result.set("channels", img.channels);
|
|
|
|
return result;
|
|
}
|
|
|
|
///
|
|
/// Convert Float32Array to Float16 Uint16Array
|
|
/// Utility function for post-processing or manual conversion
|
|
///
|
|
emscripten::val convertFloat32ToFloat16Array(const emscripten::val& float32Data) {
|
|
size_t count = float32Data["length"].as<size_t>();
|
|
|
|
// Copy float32 data from JS
|
|
std::vector<float> srcData(count);
|
|
emscripten::val srcView = emscripten::val(
|
|
emscripten::typed_memory_view(count, srcData.data()));
|
|
srcView.call<void>("set", float32Data);
|
|
|
|
// Convert to float16
|
|
std::vector<uint16_t> fp16Data(count);
|
|
convertFloat32ToFloat16(srcData.data(), fp16Data.data(), count);
|
|
|
|
// Return as Uint16Array
|
|
emscripten::val Uint16Array = emscripten::val::global("Uint16Array");
|
|
emscripten::val result = Uint16Array.new_(count);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(count, fp16Data.data()));
|
|
result.call<void>("set", jsHeap);
|
|
|
|
return result;
|
|
}
|
|
|
|
///
|
|
/// Convert Float16 Uint16Array to Float32Array
|
|
/// Utility function for reading back float16 data
|
|
///
|
|
emscripten::val convertFloat16ToFloat32Array(const emscripten::val& uint16Data) {
|
|
size_t count = uint16Data["length"].as<size_t>();
|
|
|
|
// Copy uint16 data from JS
|
|
std::vector<uint16_t> srcData(count);
|
|
emscripten::val srcView = emscripten::val(
|
|
emscripten::typed_memory_view(count, srcData.data()));
|
|
srcView.call<void>("set", uint16Data);
|
|
|
|
// Convert to float32
|
|
std::vector<float> fp32Data(count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
fp32Data[i] = float16ToFloat32(srcData[i]);
|
|
}
|
|
|
|
// Return as Float32Array
|
|
emscripten::val Float32Array = emscripten::val::global("Float32Array");
|
|
emscripten::val result = Float32Array.new_(count);
|
|
emscripten::val jsHeap = emscripten::val(
|
|
emscripten::typed_memory_view(count, fp32Data.data()));
|
|
result.call<void>("set", jsHeap);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Register STL
|
|
EMSCRIPTEN_BINDINGS(stl_wrappters) {
|
|
register_vector<float>("VectorFloat");
|
|
register_vector<int16_t>("VectorInt16");
|
|
register_vector<uint16_t>("VectorUInt16");
|
|
register_vector<uint32_t>("VectorUInt");
|
|
register_vector<int>("VectorInt");
|
|
register_vector<std::string>("VectorString");
|
|
}
|
|
|
|
// Register the array type
|
|
EMSCRIPTEN_BINDINGS(array_bindings) {
|
|
value_array<std::array<int16_t, 2>>("Short2Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>());
|
|
value_array<std::array<int16_t, 3>>("Short3Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>());
|
|
value_array<std::array<int16_t, 4>>("Short4Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>());
|
|
|
|
value_array<std::array<uint16_t, 2>>("UShort2Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>());
|
|
value_array<std::array<uint16_t, 3>>("UShort3Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>());
|
|
value_array<std::array<uint16_t, 4>>("UShort4Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>());
|
|
|
|
value_array<std::array<int, 2>>("Int2Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>());
|
|
value_array<std::array<int, 3>>("Int3Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>());
|
|
value_array<std::array<int, 4>>("Int4Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>());
|
|
|
|
value_array<std::array<uint32_t, 2>>("UInt2Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>());
|
|
value_array<std::array<uint32_t, 3>>("UInt3Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>());
|
|
value_array<std::array<uint32_t, 4>>("UInt4Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>());
|
|
|
|
value_array<std::array<float, 2>>("Float2Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>());
|
|
value_array<std::array<float, 3>>("Float3Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>());
|
|
value_array<std::array<float, 4>>("Float4Array")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>());
|
|
|
|
// for mat33
|
|
value_array<std::array<float, 9>>("Mat33")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>())
|
|
.element(emscripten::index<4>())
|
|
.element(emscripten::index<5>())
|
|
.element(emscripten::index<6>())
|
|
.element(emscripten::index<7>())
|
|
.element(emscripten::index<8>());
|
|
|
|
value_array<std::array<double, 9>>("DMat33")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>())
|
|
.element(emscripten::index<4>())
|
|
.element(emscripten::index<5>())
|
|
.element(emscripten::index<6>())
|
|
.element(emscripten::index<7>())
|
|
.element(emscripten::index<8>());
|
|
|
|
// for mat44
|
|
value_array<std::array<float, 16>>("Mat44")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>())
|
|
.element(emscripten::index<4>())
|
|
.element(emscripten::index<5>())
|
|
.element(emscripten::index<6>())
|
|
.element(emscripten::index<7>())
|
|
.element(emscripten::index<8>())
|
|
.element(emscripten::index<9>())
|
|
.element(emscripten::index<10>())
|
|
.element(emscripten::index<11>())
|
|
.element(emscripten::index<12>())
|
|
.element(emscripten::index<13>())
|
|
.element(emscripten::index<14>())
|
|
.element(emscripten::index<15>());
|
|
|
|
value_array<std::array<double, 16>>("DMat44")
|
|
.element(emscripten::index<0>())
|
|
.element(emscripten::index<1>())
|
|
.element(emscripten::index<2>())
|
|
.element(emscripten::index<3>())
|
|
.element(emscripten::index<4>())
|
|
.element(emscripten::index<5>())
|
|
.element(emscripten::index<6>())
|
|
.element(emscripten::index<7>())
|
|
.element(emscripten::index<8>())
|
|
.element(emscripten::index<9>())
|
|
.element(emscripten::index<10>())
|
|
.element(emscripten::index<11>())
|
|
.element(emscripten::index<12>())
|
|
.element(emscripten::index<13>())
|
|
.element(emscripten::index<14>())
|
|
.element(emscripten::index<15>());
|
|
}
|
|
|
|
EMSCRIPTEN_BINDINGS(tinyusdz_module) {
|
|
class_<TinyUSDZLoaderNative>("TinyUSDZLoaderNative")
|
|
.constructor<>() // Default constructor for async loading
|
|
//.constructor<const std::string &>() // Keep original for compatibility
|
|
#if defined(TINYUSDZ_WASM_ASYNCIFY)
|
|
.function("loadAsync", &TinyUSDZLoaderNative::loadAsync)
|
|
#endif
|
|
.function("loadAsLayerFromBinary", &TinyUSDZLoaderNative::loadAsLayerFromBinary)
|
|
.function("loadFromBinary", &TinyUSDZLoaderNative::loadFromBinary)
|
|
#if defined(TINYUSDZ_USE_COROUTINE)
|
|
.function("loadFromBinaryAsync", &TinyUSDZLoaderNative::loadFromBinaryAsync) // C++20 coroutine async version
|
|
#endif
|
|
.function("loadTest", &TinyUSDZLoaderNative::loadTest)
|
|
.function("loadFromCachedAsset", &TinyUSDZLoaderNative::loadFromCachedAsset)
|
|
.function("loadAsLayerFromCachedAsset", &TinyUSDZLoaderNative::loadAsLayerFromCachedAsset)
|
|
.function("testValueMemoryUsage", &TinyUSDZLoaderNative::testValueMemoryUsage)
|
|
.function("testLayer", &TinyUSDZLoaderNative::testLayer)
|
|
//.function("loadAndCompositeFromBinary", &TinyUSDZLoaderNative::loadFromBinary)
|
|
|
|
// For Stage
|
|
.function("extractUnresolvedTexturePaths", &TinyUSDZLoaderNative::extractUnresolvedTexturePaths)
|
|
.function("getURI", &TinyUSDZLoaderNative::getURI)
|
|
.function("getMesh", &TinyUSDZLoaderNative::getMesh)
|
|
.function("numMeshes", &TinyUSDZLoaderNative::numMeshes)
|
|
.function("getMaterial", select_overload<emscripten::val(int) const>(&TinyUSDZLoaderNative::getMaterial))
|
|
.function("getMaterialWithFormat", select_overload<emscripten::val(int, const std::string&) const>(&TinyUSDZLoaderNative::getMaterial))
|
|
.function("numMaterials", &TinyUSDZLoaderNative::numMaterials)
|
|
.function("getLight", &TinyUSDZLoaderNative::getLight)
|
|
.function("getLightWithFormat", &TinyUSDZLoaderNative::getLightWithFormat)
|
|
.function("getAllLights", &TinyUSDZLoaderNative::getAllLights)
|
|
.function("numLights", &TinyUSDZLoaderNative::numLights)
|
|
.function("getTexture", &TinyUSDZLoaderNative::getTexture)
|
|
.function("numTextures", &TinyUSDZLoaderNative::numTextures)
|
|
.function("getImage", &TinyUSDZLoaderNative::getImage)
|
|
.function("numImages", &TinyUSDZLoaderNative::numImages)
|
|
.function("getDefaultRootNodeId",
|
|
&TinyUSDZLoaderNative::getDefaultRootNodeId)
|
|
.function("getRootNode", &TinyUSDZLoaderNative::getRootNode)
|
|
.function("getDefaultRootNode", &TinyUSDZLoaderNative::getDefaultRootNode)
|
|
.function("numRootNodes", &TinyUSDZLoaderNative::numRootNodes)
|
|
|
|
// Metadata access
|
|
.function("getUpAxis", &TinyUSDZLoaderNative::getUpAxis)
|
|
.function("getSceneMetadata", &TinyUSDZLoaderNative::getSceneMetadata)
|
|
|
|
// Animation methods
|
|
.function("numAnimations", &TinyUSDZLoaderNative::numAnimations)
|
|
.function("getAnimation", &TinyUSDZLoaderNative::getAnimation)
|
|
.function("getAllAnimations", &TinyUSDZLoaderNative::getAllAnimations)
|
|
.function("getAnimationInfo", &TinyUSDZLoaderNative::getAnimationInfo)
|
|
.function("getAllAnimationInfos", &TinyUSDZLoaderNative::getAllAnimationInfos)
|
|
|
|
.function("setLoadTextureInNative",
|
|
&TinyUSDZLoaderNative::setLoadTextureInNative)
|
|
|
|
.function("setMaxMemoryLimitMB",
|
|
&TinyUSDZLoaderNative::setMaxMemoryLimitMB)
|
|
.function("getMaxMemoryLimitMB",
|
|
&TinyUSDZLoaderNative::getMaxMemoryLimitMB)
|
|
|
|
// Bone reduction configuration
|
|
.function("setEnableBoneReduction",
|
|
&TinyUSDZLoaderNative::setEnableBoneReduction)
|
|
.function("getEnableBoneReduction",
|
|
&TinyUSDZLoaderNative::getEnableBoneReduction)
|
|
.function("setTargetBoneCount",
|
|
&TinyUSDZLoaderNative::setTargetBoneCount)
|
|
.function("getTargetBoneCount",
|
|
&TinyUSDZLoaderNative::getTargetBoneCount)
|
|
|
|
.function("setEnableComposition",
|
|
&TinyUSDZLoaderNative::setEnableComposition)
|
|
.function("extractSublayerAssetPaths",
|
|
&TinyUSDZLoaderNative::extractSublayerAssetPaths)
|
|
.function("extractReferencesAssetPaths",
|
|
&TinyUSDZLoaderNative::extractReferencesAssetPaths)
|
|
.function("extractPayloadAssetPaths",
|
|
&TinyUSDZLoaderNative::extractPayloadAssetPaths)
|
|
|
|
.function("composeSublayers",
|
|
&TinyUSDZLoaderNative::composeSublayers)
|
|
|
|
.function("hasReferences",
|
|
&TinyUSDZLoaderNative::hasReferences)
|
|
|
|
.function("composeReferences",
|
|
&TinyUSDZLoaderNative::composeReferences)
|
|
|
|
.function("hasPayload",
|
|
&TinyUSDZLoaderNative::hasPayload)
|
|
|
|
.function("composePayload",
|
|
&TinyUSDZLoaderNative::composePayload)
|
|
|
|
.function("hasInherits",
|
|
&TinyUSDZLoaderNative::hasInherits)
|
|
|
|
.function("composeInherits",
|
|
&TinyUSDZLoaderNative::composeInherits)
|
|
|
|
// TODO: nested variants
|
|
.function("hasVariants",
|
|
&TinyUSDZLoaderNative::hasInherits)
|
|
|
|
.function("composeVariants",
|
|
&TinyUSDZLoaderNative::composeVariants)
|
|
|
|
.function("layerToRenderScene",
|
|
&TinyUSDZLoaderNative::layerToRenderScene)
|
|
|
|
|
|
.function("setAsset",
|
|
&TinyUSDZLoaderNative::setAsset)
|
|
.function("startStreamingAsset",
|
|
&TinyUSDZLoaderNative::startStreamingAsset)
|
|
.function("appendAssetChunk",
|
|
&TinyUSDZLoaderNative::appendAssetChunk)
|
|
.function("finalizeStreamingAsset",
|
|
&TinyUSDZLoaderNative::finalizeStreamingAsset)
|
|
.function("isStreamingAssetComplete",
|
|
&TinyUSDZLoaderNative::isStreamingAssetComplete)
|
|
.function("getStreamingProgress",
|
|
&TinyUSDZLoaderNative::getStreamingProgress)
|
|
|
|
// Zero-copy streaming buffer methods
|
|
.function("allocateZeroCopyBuffer",
|
|
&TinyUSDZLoaderNative::allocateZeroCopyBuffer)
|
|
.function("getZeroCopyBufferPtr",
|
|
&TinyUSDZLoaderNative::getZeroCopyBufferPtr)
|
|
.function("getZeroCopyBufferPtrAtOffset",
|
|
&TinyUSDZLoaderNative::getZeroCopyBufferPtrAtOffset)
|
|
.function("markZeroCopyBytesWritten",
|
|
&TinyUSDZLoaderNative::markZeroCopyBytesWritten)
|
|
.function("getZeroCopyProgress",
|
|
&TinyUSDZLoaderNative::getZeroCopyProgress)
|
|
.function("finalizeZeroCopyBuffer",
|
|
&TinyUSDZLoaderNative::finalizeZeroCopyBuffer)
|
|
.function("cancelZeroCopyBuffer",
|
|
&TinyUSDZLoaderNative::cancelZeroCopyBuffer)
|
|
.function("getActiveZeroCopyBuffers",
|
|
&TinyUSDZLoaderNative::getActiveZeroCopyBuffers)
|
|
|
|
.function("hasAsset",
|
|
&TinyUSDZLoaderNative::hasAsset)
|
|
.function("getAsset",
|
|
&TinyUSDZLoaderNative::getAsset)
|
|
.function("getAssetCacheDataAsMemoryView",
|
|
&TinyUSDZLoaderNative::getAssetCacheDataAsMemoryView)
|
|
.function("setAssetFromRawPointer",
|
|
&TinyUSDZLoaderNative::setAssetFromRawPointer, emscripten::allow_raw_pointers())
|
|
.function("getAssetHash",
|
|
&TinyUSDZLoaderNative::getAssetHash)
|
|
.function("verifyAssetHash",
|
|
&TinyUSDZLoaderNative::verifyAssetHash)
|
|
.function("getAssetUUID",
|
|
&TinyUSDZLoaderNative::getAssetUUID)
|
|
.function("getStreamingAssetUUID",
|
|
&TinyUSDZLoaderNative::getStreamingAssetUUID)
|
|
.function("getAllAssetUUIDs",
|
|
&TinyUSDZLoaderNative::getAllAssetUUIDs)
|
|
.function("findAssetByUUID",
|
|
&TinyUSDZLoaderNative::findAssetByUUID)
|
|
.function("getAssetByUUID",
|
|
&TinyUSDZLoaderNative::getAssetByUUID)
|
|
.function("deleteAsset",
|
|
&TinyUSDZLoaderNative::deleteAsset)
|
|
.function("deleteAssetByUUID",
|
|
&TinyUSDZLoaderNative::deleteAssetByUUID)
|
|
.function("deleteAssetByName",
|
|
&TinyUSDZLoaderNative::deleteAssetByName)
|
|
.function("getAssetCount",
|
|
&TinyUSDZLoaderNative::getAssetCount)
|
|
.function("assetExists",
|
|
&TinyUSDZLoaderNative::assetExists)
|
|
.function("clearAssets",
|
|
&TinyUSDZLoaderNative::clearAssets)
|
|
.function("reset",
|
|
&TinyUSDZLoaderNative::reset)
|
|
.function("getMemoryStats",
|
|
&TinyUSDZLoaderNative::getMemoryStats)
|
|
|
|
.function("layerToString",
|
|
&TinyUSDZLoaderNative::layerToString)
|
|
|
|
// JSON conversion methods
|
|
.function("layerToJSON",
|
|
&TinyUSDZLoaderNative::layerToJSON)
|
|
.function("layerToJSONWithOptions",
|
|
&TinyUSDZLoaderNative::layerToJSONWithOptions)
|
|
.function("loadLayerFromJSON",
|
|
&TinyUSDZLoaderNative::loadLayerFromJSON)
|
|
|
|
.function("setBaseWorkingPath", &TinyUSDZLoaderNative::setBaseWorkingPath)
|
|
.function("getBaseWorkingPath", &TinyUSDZLoaderNative::getBaseWorkingPath)
|
|
.function("clearAssetSearchPaths", &TinyUSDZLoaderNative::clearAssetSearchPaths)
|
|
.function("addAssetSearchPath", &TinyUSDZLoaderNative::addAssetSearchPath)
|
|
.function("getAssetSearchPaths", &TinyUSDZLoaderNative::getAssetSearchPaths)
|
|
|
|
|
|
// MCP
|
|
.function("mcpCreateContext", &TinyUSDZLoaderNative::mcpCreateContext)
|
|
.function("mcpSelectContext", &TinyUSDZLoaderNative::mcpSelectContext)
|
|
.function("mcpResourcesList", &TinyUSDZLoaderNative::mcpResourcesList)
|
|
.function("mcpResourcesRead", &TinyUSDZLoaderNative::mcpResourcesRead)
|
|
.function("mcpToolsList", &TinyUSDZLoaderNative::mcpToolsList)
|
|
.function("mcpToolsCall", &TinyUSDZLoaderNative::mcpToolsCall)
|
|
|
|
// Progress reporting for async parsing
|
|
.function("getProgress", &TinyUSDZLoaderNative::getProgress)
|
|
.function("cancelParsing", &TinyUSDZLoaderNative::cancelParsing)
|
|
.function("wasCancelled", &TinyUSDZLoaderNative::wasCancelled)
|
|
.function("isParsingInProgress", &TinyUSDZLoaderNative::isParsingInProgress)
|
|
.function("resetProgress", &TinyUSDZLoaderNative::resetProgress)
|
|
.function("loadFromBinaryWithProgress", &TinyUSDZLoaderNative::loadFromBinaryWithProgress)
|
|
.function("loadAsLayerFromBinaryWithProgress", &TinyUSDZLoaderNative::loadAsLayerFromBinaryWithProgress)
|
|
|
|
.function("ok", &TinyUSDZLoaderNative::ok)
|
|
.function("error", &TinyUSDZLoaderNative::error)
|
|
.function("warn", &TinyUSDZLoaderNative::warn);
|
|
|
|
class_<TinyUSDZComposerNative>("TinyUSDZComposerNative")
|
|
.constructor<>() // Default constructor for async loading
|
|
.function("ok", &TinyUSDZComposerNative::loaded)
|
|
.function("error", &TinyUSDZComposerNative::error);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Image Decoding Bindings (EXR, HDR, PNG, JPEG, etc.)
|
|
// =============================================================================
|
|
|
|
// Wrapper functions for default parameters
|
|
#if defined(TINYUSDZ_WITH_EXR)
|
|
static emscripten::val decodeEXR_default(const emscripten::val& data) {
|
|
return decodeEXR(data, "float32");
|
|
}
|
|
#endif
|
|
|
|
static emscripten::val decodeHDR_default(const emscripten::val& data) {
|
|
return decodeHDR(data, "float16");
|
|
}
|
|
|
|
static emscripten::val decodeImage_default(const emscripten::val& data) {
|
|
return decodeImage(data, "", "auto");
|
|
}
|
|
|
|
static emscripten::val decodeImage_hint(const emscripten::val& data, const std::string& hint) {
|
|
return decodeImage(data, hint, "auto");
|
|
}
|
|
|
|
EMSCRIPTEN_BINDINGS(image_module) {
|
|
#if defined(TINYUSDZ_WITH_EXR)
|
|
// EXR decoding
|
|
// decodeEXR(data) - returns float32 by default
|
|
// decodeEXR(data, "float16") - returns Uint16Array with IEEE 754 half-float
|
|
function("decodeEXR", &decodeEXR);
|
|
function("decodeEXRDefault", &decodeEXR_default);
|
|
function("isEXR", &isEXR);
|
|
#endif
|
|
|
|
// HDR (Radiance RGBE) decoding
|
|
// decodeHDR(data) - returns float32 by default
|
|
// decodeHDR(data, "float16") - returns Uint16Array with IEEE 754 half-float
|
|
function("decodeHDR", &decodeHDR);
|
|
function("decodeHDRDefault", &decodeHDR_default);
|
|
|
|
// Generic image decoder (auto-detects EXR, HDR, PNG, JPEG, etc.)
|
|
// decodeImage(data) - auto format
|
|
// decodeImage(data, hint) - with filename hint
|
|
// decodeImage(data, hint, "float16") - with format specification
|
|
function("decodeImage", &decodeImage);
|
|
function("decodeImageDefault", &decodeImage_default);
|
|
function("decodeImageHint", &decodeImage_hint);
|
|
|
|
// Float16 <-> Float32 conversion utilities
|
|
function("convertFloat32ToFloat16Array", &convertFloat32ToFloat16Array);
|
|
function("convertFloat16ToFloat32Array", &convertFloat16ToFloat32Array);
|
|
}
|
|
|