diff --git a/.claude/skills/chunk-guide/SKILL.md b/.claude/skills/chunk-guide/SKILL.md new file mode 100644 index 00000000..2728b325 --- /dev/null +++ b/.claude/skills/chunk-guide/SKILL.md @@ -0,0 +1,261 @@ +--- +name: chunk-guide +description: Guide for adding new Cryengine chunk types or versions. Use when implementing support for new chunk versions, understanding the chunk system architecture, or debugging chunk parsing issues. +allowed-tools: Read, Glob, Grep, Edit, Write +--- + +# Chunk System Implementation Guide + +This skill guides you through adding new chunk types or versions to the Cryengine Converter. + +## Architecture Overview + +The chunk system uses **versioned polymorphism via reflection**: + +``` +Chunk (abstract base) + └── ChunkMesh (abstract, type-specific properties) + ├── ChunkMesh_800 (sealed, version-specific Read()) + ├── ChunkMesh_801 + └── ChunkMesh_802 +``` + +### Key Files + +| File | Purpose | +|------|---------| +| `CgfConverter/CryEngineCore/Chunks/Chunk.cs` | Base class + factory methods | +| `CgfConverter/Enums/Enums.cs` | `ChunkType` enum (chunk type IDs) | +| `CgfConverter/CryEngineCore/Chunks/Chunk{Name}.cs` | Abstract base for each chunk type | +| `CgfConverter/CryEngineCore/Chunks/Chunk{Name}_{Version}.cs` | Version implementations | + +### Factory Pattern + +The factory in `Chunk.cs` uses two methods: + +1. **`New(ChunkType, uint version)`** - Switch statement mapping `ChunkType` enum to generic factory +2. **`New(uint version)`** - Reflection-based: finds class named `{BaseTypeName}_{version:X}` + +Example: `Chunk.New(0x800)` finds `ChunkMesh_800` via reflection. + +--- + +## Adding a New Version of an Existing Chunk Type + +**Scenario**: A game file has `ChunkMesh` version `0x803` that isn't supported. + +### Step 1: Create the version file + +Create `CgfConverter/CryEngineCore/Chunks/ChunkMesh_803.cs`: + +```csharp +using Extensions; +using System.IO; + +namespace CgfConverter.CryEngineCore; + +internal sealed class ChunkMesh_803 : ChunkMesh +{ + public override void Read(BinaryReader b) + { + base.Read(b); // Always call base first - handles header parsing + + // Parse version-specific fields + // Use b.ReadInt32(), b.ReadVector3(), etc. + } +} +``` + +### Step 2: Verify naming convention + +The class name MUST follow `{AbstractTypeName}_{VersionHex}`: +- Version `0x800` → `_800` +- Version `0x901` → `_901` +- Version `2048` (decimal) → `_800` (hex) + +### Step 3: No registration needed + +The reflection-based factory auto-discovers the class. No changes to `Chunk.cs` switch statement needed for existing chunk types. + +### Step 4: Write a test + +Add a test in `CgfConverterIntegrationTests` using an asset file that contains this version. + +--- + +## Adding a Completely New Chunk Type + +**Scenario**: Supporting a new chunk type like `ChunkFoliage`. + +### Step 1: Add to ChunkType enum + +In `CgfConverter/Enums/Enums.cs`, add the chunk type ID: + +```csharp +public enum ChunkType : uint +{ + // ... existing types ... + Foliage = 0xCCCC0020, // Use appropriate hex ID from file format +} +``` + +### Step 2: Create abstract base class + +Create `CgfConverter/CryEngineCore/Chunks/ChunkFoliage.cs`: + +```csharp +namespace CgfConverter.CryEngineCore; + +public abstract class ChunkFoliage : Chunk +{ + // Type-specific properties shared across all versions + public int BranchCount { get; set; } + public float WindStrength { get; set; } +} +``` + +### Step 3: Create version implementation + +Create `CgfConverter/CryEngineCore/Chunks/ChunkFoliage_800.cs`: + +```csharp +using System.IO; + +namespace CgfConverter.CryEngineCore; + +internal sealed class ChunkFoliage_800 : ChunkFoliage +{ + public override void Read(BinaryReader b) + { + base.Read(b); + + BranchCount = b.ReadInt32(); + WindStrength = b.ReadSingle(); + // ... parse remaining fields + } +} +``` + +### Step 4: Register in factory switch statement + +In `CgfConverter/CryEngineCore/Chunks/Chunk.cs`, add to `New(ChunkType, uint)`: + +```csharp +public static Chunk New(ChunkType chunkType, uint version) +{ + return chunkType switch + { + // ... existing cases ... + ChunkType.Foliage => Chunk.New(version), + _ => new ChunkUnknown(), + }; +} +``` + +### Step 5: Write tests + +Create both unit tests (if possible with mock data) and integration tests with real assets. + +--- + +## Reading Binary Data + +### Common patterns in Read() methods + +```csharp +public override void Read(BinaryReader b) +{ + base.Read(b); // ALWAYS call first + + // Primitives + int count = b.ReadInt32(); + uint flags = b.ReadUInt32(); + float value = b.ReadSingle(); + short shortVal = b.ReadInt16(); + + // Vectors (from Extensions namespace) + Vector3 position = b.ReadVector3(); + Vector4 color = b.ReadVector4(); + Matrix4x4 matrix = b.ReadMatrix4x4(); + Quaternion rotation = b.ReadQuaternion(); + + // Arrays + byte[] data = b.ReadBytes(count); + + // Strings (null-terminated) + string name = b.ReadCString(); + + // Skip padding/unknown bytes + SkipBytes(b, 16); +} +``` + +### Endianness + +The base `Read()` method handles endianness setup: +- `IsBigEndian` property indicates current endianness +- Wii U files use big-endian +- Most PC files are little-endian + +### DataSize vs Size + +- `Size` = total chunk size including header +- `DataSize` = data portion only (for Star Citizen files without embedded headers) + +--- + +## Debugging Tips + +### When chunks fail to parse + +1. Check version matches file: Log `Version` property after `base.Read(b)` +2. Verify stream position: `b.BaseStream.Position` should equal `Offset + Size` after reading +3. Use `SkipBytes(b)` to skip remaining unknown data without failing +4. Compare with working version implementations + +### When factory throws NotSupportedException + +``` +Version 803 of ChunkMesh is not supported +``` + +This means no `ChunkMesh_803.cs` class exists. Create it following the pattern above. + +### Inspecting chunk data + +Use hex editor or `CgfConverterTestingConsole` project: +1. Set breakpoint in chunk's `Read()` method +2. Examine `b.BaseStream.Position` and raw bytes +3. Compare against working versions + +--- + +## Star Citizen #ivo Specifics + +Star Citizen uses different chunk types with different IDs: + +- Traditional: `ChunkType.Mesh = 0xCCCC0000` +- Star Citizen: `ChunkType.MeshIvo = 0x9293B9D8` + +Both map to `ChunkMesh` in the factory, but may need different version implementations. + +Star Citizen files (0x900+ versions): +- No embedded chunk headers in data +- Use `DataSize` instead of `Size - 16` +- May use bounding-box compressed vertices + +--- + +## Checklist + +When adding chunk support: + +- [ ] Identify chunk type ID (hex) and version from file +- [ ] Check if abstract base class exists; create if new type +- [ ] Create version implementation with correct naming: `Chunk{Type}_{VersionHex}.cs` +- [ ] Register in `Chunk.New()` switch if new chunk type +- [ ] Add to `ChunkType` enum if new type +- [ ] Call `base.Read(b)` first in `Read()` method +- [ ] Handle all bytes in chunk (use `SkipBytes` for unknowns) +- [ ] Write integration test with real asset +- [ ] Test with `-throw` flag to surface parsing errors diff --git a/.gitignore b/.gitignore index e376253b..37f65683 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,10 @@ bld/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +# Claude Code - ignore local settings, commit skills +.claude/* +!.claude/skills/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..98969f79 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,262 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Cryengine Converter is a C# tool that converts Cryengine game assets (.cgf, .cga, .chr, .skin) into portable 3D formats (Collada .dae, glTF .gltf/.glb). It supports multiple Cryengine variants including traditional Cryengine games (MWO, Crysis) and Star Citizen's proprietary #ivo format. + +## Common Commands + +### Build +```bash +dotnet build +``` +Build the entire solution (default configuration: Debug). + +### Test +```bash +# Run unit tests only (fast) +dotnet test --filter TestCategory=unit + +# Run all tests including integration tests (slow, requires test data) +dotnet test +``` + +Integration tests are organized by game (StarCitizenTests, MWOIntegrationTests, CrysisIntegrationTests, etc.) and require actual game asset files in specific test data directories. + +### Publish +```bash +dotnet publish +``` +Creates a self-contained executable at `cgf-converter\bin\Release\net9.0\win-x64\publish\cgf-converter.exe`. The published binary is a single-file executable with all dependencies embedded. + +### Run Converter +```bash +# From the cgf-converter project directory +dotnet run -- [options] + +# Example: Convert to Collada +dotnet run -- "C:\GameAssets\ship.cga" -objectdir "C:\GameAssets\Objects" + +# Example: Convert to glTF +dotnet run -- ship.cgf -gltf -objectdir "C:\GameAssets\Objects" +``` + +## Solution Structure + +### Projects +- **CgfConverter** - Core library containing all conversion logic +- **cgf-converter** - CLI executable entry point +- **CgfConverterIntegrationTests** - Test suite (MSTest) with unit and integration tests +- **CgfConverterTestingConsole** - Testing/debugging console + +### Key Dependencies +- .NET 9.0 target framework +- ImageSharp (texture processing) +- BCnEncoder.Net (DDS texture compression) +- Newtonsoft.Json (glTF serialization) +- XmlSerializer.Generator (Collada performance optimization) + +## High-Level Architecture + +### Conversion Pipeline + +``` +Input File (.cgf/.cga/.chr/.skin) + ↓ +Model.FromStream() - Read file header, chunk table, parse all chunks + ↓ +CryEngine.ProcessCryengineFiles() - Build node hierarchy, load materials, create skinning info + ↓ +IRenderer.Render() - Serialize to output format + ↓ +Output File (.dae/.gltf/.glb/.usd) +``` + +### Core Components + +**CryEngine (Facade)**: High-level API for processing Cryengine files. Entry point: `CryEngine.ProcessCryengineFiles()`. + +**Model**: Represents a single Cryengine file with its chunks. Contains chunk table, node hierarchy, and file metadata. + +**Chunk System**: ~50+ chunk types implementing versioned polymorphism. Each chunk type has version-specific implementations (e.g., `ChunkMesh_800`, `ChunkMesh_801`). The factory pattern uses reflection to instantiate chunks based on type and version: `Chunk.New(version)`. + +**Renderers**: Implement `IRenderer` interface for pluggable output formats: +- `ColladaModelRenderer` - Default, fully featured (.dae) +- `GltfModelRenderer` - Modern format (.gltf/.glb) +- `UsdRenderer` - Experimental USD export (.usd/.usda/.usdc) +- `WavefrontModelRenderer` - Deprecated, not supported (.obj) + +**Material System**: Loads Cryengine .mtl files (text XML or binary CryXmlB format). Supports hierarchical submaterials, texture maps, and material layers. Resolution cascade: explicit paths → ChunkMtlName references → default materials. + +**PackFileSystem**: File system abstraction supporting direct filesystem access (`RealFileSystem`) and .pak archives (`WiiuStreamPackFileSystem`, `CascadedPackFileSystem`). + +### Key Data Structures + +**GeometryInfo**: Aggregated geometry data including vertices, normals, UVs, indices, material subsets, and vertex colors. Contains `Datastream` for typed data arrays. + +**SkinningInfo**: Consolidated bone hierarchy and vertex skinning weights from multiple chunks (`ChunkCompiledBones`, `ChunkCompiledIntSkinVertices`, `ChunkCompiledExtToIntMap`). + +**Material**: Hierarchical material system with submaterials, textures (diffuse, normal, specular), colors, and shader parameters. + +### File Format Variants + +**Traditional Cryengine**: Node hierarchy using `ChunkNode → ChunkMesh → ChunkDataStream`. May have companion geometry files (.cgam/.skinm) auto-detected and merged. + +**#ivo Format (Star Citizen 3.23+)**: Uses `ChunkNodeMeshCombo` + `ChunkIvoSkinMesh`. Consolidated mesh data in single chunk with `VertUV` structure. Vertices are bounding-box compressed and require decompression. + +### Version Handling + +Cryengine versions (0x744, 0x745, 0x746, 0x900) are handled via naming convention: `ChunkName_VERSION.cs`. The factory uses reflection to find and instantiate the correct version at runtime. When adding support for a new chunk version, create a new file following this pattern. + +## Important Implementation Details + +### Coordinate Systems +Cryengine uses Z-up coordinate system. Collada output preserves Z-up. Transform matrices are transposed when needed for format compatibility. + +### Binary XML (CryXmlB) +Star Citizen uses binary XML for material files. Parser in `CryXmlSerializer.cs` handles magic signature "CryXmlB\0" and deserializes node/reference/content tables. Legacy "pbxml\0" format is also supported. + +### Multithreading +Release builds support parallel file processing via `-maxthreads` argument. Debug builds are single-threaded for easier debugging. Use `#if DEBUG` guards around threading logic. + +### Test Categories +Integration tests use `[TestCategory("unit")]` or `[TestCategory("integration")]` attributes. Unit tests run fast without external dependencies. Integration tests require game asset files and validate XML schema compliance. + +### Manual Render Tests (Fast Iteration) +For quick iteration when developing renderers, use `ManualRenderTests.cs` in `CgfConverterIntegrationTests/ManualTests/`. These tests: +- Run directly from Visual Studio Test Explorer (no publish/command-line needed) +- Output files to the source asset's directory (e.g., `.usda` next to `.cga`) +- Are excluded from CI via `[TestCategory("manual")]` + +**To add a new test file:** +```csharp +[TestMethod] +public void MWO_YourAsset_USD() +{ + RenderToUsd($@"{mwoObjectDir}\path\to\asset.cga", mwoObjectDir); +} +``` + +Helper methods available: `RenderToUsd()`, `RenderToCollada()`, `RenderToGltf()` + +### Material File Resolution +The `-objectdir` argument is critical for correct material loading. Without it, materials may not be found and defaults will be generated. The resolver caches paths and tries multiple locations (as-provided, same directory, ObjectDir). + +## Namespace Organization + +- **CgfConverter.CryEngineCore**: Core chunk reading (`Chunks/`, `Model.cs`) +- **CgfConverter.Models**: Domain models (`Materials/`, `GeometryInfo.cs`, `SkinningInfo.cs`) +- **CgfConverter.Renderers**: Output renderers (`Collada/`, `Gltf/`, `Wavefront/`, `USD/`) +- **CgfConverter.PackFileSystem**: File system abstraction +- **CgfConverter.CryXmlB**: Binary XML parser +- **CgfConverter.Utilities**: Helper classes and utilities +- **CgfConverter.Services**: Service classes like `ArgsHandler` + +## Critical Files + +- `cgf-converter/cgf-converter.cs` - CLI entry point and argument parsing +- `CgfConverter/CryEngine/CryEngine.cs` - Main conversion orchestration +- `CgfConverter/CryEngineCore/Model.cs` - File reading and chunk parsing +- `CgfConverter/CryEngineCore/Chunks/Chunk.cs` - Chunk factory and base class +- `CgfConverter/Renderers/Collada/ColladaModelRenderer.cs` - Collada output +- `CgfConverter/Renderers/Gltf/GltfModelRenderer.cs` - glTF output +- `CgfConverter/Renderers/USD/UsdRenderer.cs` - USD output +- `CgfConverter/Utilities/MaterialUtilities.cs` - Material loading logic +- `CgfConverter/CryXmlB/CryXmlSerializer.cs` - Binary XML parser +- `CgfConverter/Models/GeometryInfo.cs` - Geometry aggregation +- `CgfConverter/Models/SkinningInfo.cs` - Bone/skinning data + +## Common Development Patterns + +### Adding a New Chunk Type +1. Create `Chunks/ChunkNewType_VERSION.cs` inheriting from `Chunk` +2. Override `Read(BinaryReader)` to parse chunk data +3. Add chunk type ID to `ChunkType` enum in `Enums.cs` +4. Factory will auto-discover via reflection + +### Adding a New Output Format +1. Create renderer class implementing `IRenderer` +2. Implement `Render(CryEngine, string)` method +3. Add command-line argument in `ArgsHandler.cs` +4. Add renderer instantiation in `cgf-converter.cs` + +### Debugging Chunk Reading Issues +1. Use `CgfConverterTestingConsole` project for focused debugging +2. Enable `-throw` argument to surface exceptions in debugger +3. Check `Model.ChunkMap` to see what chunks were parsed +4. Verify chunk version matches expected format + +### Working with Materials +Materials are loaded lazily during `CreateMaterials()`. Check `MaterialUtilities.LoadMaterial()` for resolution logic. Binary XML materials require `CryXmlSerializer` - add breakpoints there if materials aren't loading correctly. + +## Renderer Architecture Pattern + +### Partial Classes Pattern +Renderers use partial classes for organization, following the GltfRenderer precedent. + +**Rationale**: +- Maintains single logical type with shared private state +- Easy navigation and IntelliSense support +- Avoids state management complexity of composition +- Natural fit for tightly coupled rendering operations + +**Standard Organization** (apply to new/refactored renderers): +- `{Renderer}.cs` - Main class, constructor, public API, orchestration +- `{Renderer}.Materials.cs` - Material/shader creation, texture handling +- `{Renderer}.Geometry.cs` - Mesh creation, vertex/index processing +- `{Renderer}.Skeleton.cs` - Skeletal animation, skinning +- `{Renderer}.Animation.cs` - Animation export +- `{Renderer}.Utilities.cs` - Helper methods + +**Current implementations**: +- **ColladaModelRenderer**: `.cs`, `.Animation.cs`, `.Materials.cs`, `.Geometry.cs`, `.Skeleton.cs`, `.Nodes.cs`, `.Utilities.cs` +- **UsdRenderer**: `.cs`, `.Animation.cs`, `.Materials.cs`, `.Geometry.cs`, `.Skeleton.cs` +- **GltfModelRenderer**: Uses partial classes in `Renderers/Gltf/` with Models subdirectory for data types + +## Animation Support + +### Supported Animation Formats + +**DBA Files** (Animation Databases): +- Container format holding multiple animations +- Referenced via `.chrparams` file's `$TracksDatabase` entry (supports wildcards like `*.dba`) +- Parsed by `ChunkController_905` with two storage modes: + - **Standard mode**: Positive offsets relative to start of track data + - **In-place streaming mode**: Negative offsets relative to end of track data (detected by `keyTimeOffsets[0] < 0`) + +**CAF Files** (Individual Animation Clips): +- Single animation per file +- Referenced via `.chrparams` `` entries +- Support wildcard patterns (e.g., `animations/pilot/*.caf`) +- Controller versions: `ChunkController_829`, `ChunkController_830`, `ChunkController_831` + +**CAL Files** (Animation Lists): +- XML files defining animation sets (used by ArcheAge) +- Reference `.caf` files with additional metadata +- Loaded via `LoadCalAnimations()` in `CryEngine.cs` + +### Critical Animation Implementation Details + +**CryEngine Matrix Convention**: Translation stored in column 4 (M14, M24, M34), NOT row 4. **Never use `Matrix4x4.Translation` property** - it reads M41/M42/M43 which are zeros. + +**Rest Translation Fallback**: Bones without position animation must use rest translation from skeleton bind pose. Otherwise skeleton collapses. + +**Additive Animations**: Detected via `AssetFlags.Additive` (0x001). Must convert deltas to absolute transforms: `absolute = rest * additive` + +### Key Animation Files + +- `CgfConverter/CryEngine/CryEngine.cs`: `LoadCafAnimations()`, `LoadCalAnimations()`, `ExpandCafWildcard()`, `ExpandDbaWildcard()` +- `CgfConverter/Models/CafAnimation.cs`: Animation data model +- `CgfConverter/CryEngineCore/Chunks/ChunkController_905.cs`: DBA parsing (both storage modes) +- `CgfConverter/CryEngineCore/Chunks/ChunkController_829.cs`: CAF parsing +- `CgfConverter/Renderers/USD/UsdRenderer.Animation.cs`: USD export with additive conversion + +## Active Development Notes + +See `DEVNOTES.md` for: +- USD shader-based material system planning +- Known issues and debugging history +- In-progress feature work +- Star Citizen #ivo animation format (next target) diff --git a/CgfConverter/Cal/CalFile.cs b/CgfConverter/Cal/CalFile.cs new file mode 100644 index 00000000..83af4614 --- /dev/null +++ b/CgfConverter/Cal/CalFile.cs @@ -0,0 +1,152 @@ +using CgfConverter.PackFileSystem; +using System; +using System.Collections.Generic; +using System.IO; + +namespace CgfConverter.Cal; + +/// +/// Parser for ArcheAge .cal animation list files. +/// Format is simple key-value pairs: +/// - #filepath = path - base path for animations +/// - $Include = path.cal - includes another cal file +/// - animation_name = relative/path.caf - animation entries +/// - // or -- prefixes indicate comments +/// +public class CalFile +{ + /// Base path for animation files (from #filepath directive) + public string? FilePath { get; private set; } + + /// Animation entries (name -> relative path) + public Dictionary Animations { get; } = new(StringComparer.OrdinalIgnoreCase); + + /// Included .cal file paths + public List Includes { get; } = []; + + /// + /// Parses a .cal file from a stream. + /// + public static CalFile Parse(Stream stream) + { + var cal = new CalFile(); + + using var reader = new StreamReader(stream); + string? line; + + while ((line = reader.ReadLine()) is not null) + { + // Trim whitespace + line = line.Trim(); + + // Skip empty lines and comments + if (string.IsNullOrEmpty(line)) + continue; + if (line.StartsWith("//")) + continue; + if (line.StartsWith("--")) + continue; + if (line.StartsWith("---")) + continue; + + // Find the = separator + var equalsIndex = line.IndexOf('='); + if (equalsIndex < 0) + continue; + + var key = line[..equalsIndex].Trim(); + var value = line[(equalsIndex + 1)..].Trim(); + + // Strip inline comments from value (// style) + var commentIndex = value.IndexOf("//"); + if (commentIndex >= 0) + value = value[..commentIndex].Trim(); + + // Handle special directives + if (key.Equals("#filepath", StringComparison.OrdinalIgnoreCase)) + cal.FilePath = value; + else if (key.Equals("$Include", StringComparison.OrdinalIgnoreCase)) + cal.Includes.Add(value); + else if (key.StartsWith("$") || key.StartsWith("#")) + { + // Other special directives (ignored for now) + continue; + } + else if (key.StartsWith("_")) + { + // Locomotion group references (e.g., _NORMAL_WALK = lmg_files\normal_walk.lmg) + // These aren't individual animations, skip them + continue; + } + else + { + // Regular animation entry + cal.Animations[key] = value; + } + } + + return cal; + } + + /// + /// Parses a .cal file and recursively resolves all $Include directives. + /// + public static CalFile ParseWithIncludes(string calPath, IPackFileSystem packFileSystem) + { + var mainCal = Parse(packFileSystem.GetStream(calPath)); + var calDirectory = Path.GetDirectoryName(calPath) ?? ""; + + // Process includes recursively + foreach (var includePath in mainCal.Includes) + { + var includedCal = TryLoadInclude(includePath, calDirectory, packFileSystem); + if (includedCal is null) continue; + + // Merge included animations (don't override existing) + foreach (var (name, path) in includedCal.Animations) + { + mainCal.Animations.TryAdd(name, path); + } + + // If main cal doesn't have a filepath, inherit from included + if (string.IsNullOrEmpty(mainCal.FilePath) && !string.IsNullOrEmpty(includedCal.FilePath)) + mainCal.FilePath = includedCal.FilePath; + } + + return mainCal; + } + + /// + /// Attempts to load an included .cal file, trying multiple path resolutions. + /// + private static CalFile? TryLoadInclude(string includePath, string calDirectory, IPackFileSystem packFileSystem) + { + // Build list of paths to try + var pathsToTry = new List(); + + // 1. Try path as-is (relative to pack filesystem root) + pathsToTry.Add(includePath); + + // 2. Try with "game\" prefix (ArcheAge stores files under game/ subdirectory) + pathsToTry.Add(Path.Combine("game", includePath)); + + // 3. Try relative to the including cal file's directory + if (!string.IsNullOrEmpty(calDirectory)) + pathsToTry.Add(Path.Combine(calDirectory, includePath)); + + foreach (var path in pathsToTry) + { + try + { + return ParseWithIncludes(path, packFileSystem); + } + catch (FileNotFoundException) + { + // Try next path + } + } + + // Include not found at any path + return null; + } +} diff --git a/CgfConverter/CgfConverter.csproj b/CgfConverter/CgfConverter.csproj index 96d10f52..e2e1dd8c 100644 --- a/CgfConverter/CgfConverter.csproj +++ b/CgfConverter/CgfConverter.csproj @@ -13,7 +13,7 @@ - + diff --git a/CgfConverter/CryEngine/CryEngine.cs b/CgfConverter/CryEngine/CryEngine.cs index 2228ccd2..0a8c616d 100644 --- a/CgfConverter/CryEngine/CryEngine.cs +++ b/CgfConverter/CryEngine/CryEngine.cs @@ -1,4 +1,5 @@ -using CgfConverter.CryEngineCore; +using CgfConverter.Cal; +using CgfConverter.CryEngineCore; using CgfConverter.CryXmlB; using CgfConverter.Models; using CgfConverter.Models.Structs; @@ -33,7 +34,8 @@ public partial class CryEngine public string Name => Path.GetFileNameWithoutExtension(InputFile).ToLower(); public List Models { get; internal set; } = []; // All the model files associated with this game object - public List Animations { get; internal set; } = []; // Animation files for this object + public List Animations { get; internal set; } = []; // Animation files for this object (DBA format) + public List CafAnimations { get; internal set; } = []; // CAF animation files public List Nodes { get; internal set; } = []; // node hierarchy. public ChunkNode RootNode { get; internal set; } // can get node hierarchy from here public ChunkCompiledBones Bones { get; internal set; } // move to skinning info @@ -151,8 +153,8 @@ private void BuildNodeStructure() if (hasGeometry) { chunkMesh.ScalingVectors = geometryMeshDetails.ScalingBoundingBox; - chunkMesh.MaxBound = geometryMeshDetails.BoundingBox.Max; - chunkMesh.MinBound = geometryMeshDetails.BoundingBox.Min; + chunkMesh.MaxBound = node.BoundingBoxMax; + chunkMesh.MinBound = node.BoundingBoxMin; chunkMesh.NumVertices = (int)skinMesh.MeshDetails.NumberOfVertices; chunkMesh.NumIndices = (int)skinMesh.MeshDetails.NumberOfIndices; chunkMesh.NumVertSubsets = skinMesh.MeshDetails.NumberOfSubmeshes; @@ -166,7 +168,8 @@ private void BuildNodeStructure() ParentNodeIndex = node.ParentIndex, ParentNodeID = node.ParentIndex == 0xffff ? -1 : node.ParentIndex, NumChildren = node.NumberOfChildren, - MaterialID = node.GeometryType == IvoGeometryType.Geometry ? materialTable[index] : 0, + // Get material ID from mesh subsets (not materialTable which uses mesh subset indices, not node indices) + MaterialID = hasGeometry ? subsets[0].MatID : 0, Transform = node.BoneToWorld.ConvertToLocalTransformMatrix(), ChunkType = ChunkType.Node, ID = (int)node.Id, @@ -335,6 +338,53 @@ private ChunkMesh CreateMeshData() } } + // For Ivo format files, the bone indices in BoneMappings refer to NodeMeshCombo indices, + // not CompiledBone indices. We need to remap using ObjectNodeIndex. + // Build a map: ObjectNodeIndex (NodeMeshCombo index) → bone index (position in CompiledBones) + if (skin.BoneMappings is not null && skin.CompiledBones is not null) + { + var nodeMeshComboToBoneIndex = new Dictionary(); + for (int boneIndex = 0; boneIndex < skin.CompiledBones.Count; boneIndex++) + { + var bone = skin.CompiledBones[boneIndex]; + // ObjectNodeIndex maps bone to its corresponding NodeMeshCombo + // Only add if ObjectNodeIndex is valid (some bones may not have mesh associations) + if (bone.ObjectNodeIndex >= 0) + { + nodeMeshComboToBoneIndex[bone.ObjectNodeIndex] = boneIndex; + } + } + + // Only remap if we found any ObjectNodeIndex mappings (indicates Ivo format) + if (nodeMeshComboToBoneIndex.Count > 0) + { + for (int i = 0; i < skin.BoneMappings.Count; i++) + { + var mapping = skin.BoneMappings[i]; + var remappedBoneIndex = new ushort[4]; + for (int j = 0; j < 4; j++) + { + int nodeMeshComboIndex = mapping.BoneIndex[j]; + if (nodeMeshComboToBoneIndex.TryGetValue(nodeMeshComboIndex, out int actualBoneIndex)) + { + remappedBoneIndex[j] = (ushort)actualBoneIndex; + } + else + { + // Keep original index if no mapping found (fallback) + remappedBoneIndex[j] = (ushort)nodeMeshComboIndex; + } + } + skin.BoneMappings[i] = new MeshBoneMapping + { + BoneInfluenceCount = mapping.BoneInfluenceCount, + BoneIndex = remappedBoneIndex, + Weight = mapping.Weight + }; + } + } + } + return skin; } @@ -526,23 +576,621 @@ private Material CreateDefaultMaterialSet(uint maxNumberOfMaterials) } private void CreateAnimations() + { + var modelDir = Path.GetDirectoryName(InputFile); + + // Try chrparams first (XML format used by MWO, Star Citizen, etc.) + if (TryLoadChrParamsAnimations(modelDir)) + return; + + // Fall back to .cal files (ArcheAge format) + if (TryLoadCalAnimations(modelDir)) + return; + + Log.D("No animation configuration files found (.chrparams or .cal)"); + } + + /// + /// Attempts to load animations from a .chrparams file. + /// + private bool TryLoadChrParamsAnimations(string? modelDir) { try { + var chrparamsFileName = Path.ChangeExtension(Path.GetFileName(InputFile), ".chrparams"); + var chrparamsPath = string.IsNullOrEmpty(modelDir) + ? chrparamsFileName + : Path.Combine(modelDir, chrparamsFileName); + + Log.D("Looking for chrparams at: {0}", chrparamsPath); + var chrparams = CryXmlSerializer.Deserialize( - PackFileSystem.GetStream(Path.ChangeExtension(InputFile, ".chrparams"))); - var trackFilePath = chrparams.Animations?.FirstOrDefault(x => x.Name == "$TracksDatabase" || x.Name == "#filepath")?.Path; - if (trackFilePath is null) - throw new FileNotFoundException(); - if (Path.GetExtension(trackFilePath) != "dba") - trackFilePath = Path.ChangeExtension(trackFilePath, "dba"); - Log.D("Associated animation track database file found at {0}", trackFilePath); - Animations.Add(Model.FromStream(trackFilePath, PackFileSystem.GetStream(trackFilePath), true)); + PackFileSystem.GetStream(chrparamsPath)); + + Log.D("Successfully loaded chrparams, animations count: {0}", chrparams.Animations?.Length ?? 0); + + // Try to load DBA database first (preferred for batch animations) + var trackFilePath = chrparams.Animations?.FirstOrDefault(x => x.Name == "$TracksDatabase")?.Path; + if (trackFilePath is not null) + { + if (Path.GetExtension(trackFilePath) != ".dba") + trackFilePath = Path.ChangeExtension(trackFilePath, ".dba"); + + Log.D("Attempting to load animation database from: {0}", trackFilePath); + + // Check if this is a wildcard pattern + if (trackFilePath.Contains('*') || trackFilePath.Contains('?')) + { + var dbaFiles = ExpandDbaWildcard(trackFilePath); + Log.D("Wildcard pattern '{0}' matched {1} DBA files", trackFilePath, dbaFiles.Count); + + foreach (var dbaPath in dbaFiles) + { + try + { + Animations.Add(Model.FromStream(dbaPath, PackFileSystem.GetStream(dbaPath), true)); + Log.D("Successfully loaded animation database: {0}", dbaPath); + } + catch (Exception ex) + { + Log.D("Error loading DBA file {0}: {1}", dbaPath, ex.Message); + } + } + } + else + { + // Resolve relative paths against ObjectDir + var fullDbaPath = trackFilePath; + if (!Path.IsPathRooted(fullDbaPath) && !string.IsNullOrEmpty(ObjectDir)) + { + fullDbaPath = Path.Combine(ObjectDir, fullDbaPath); + } + + try + { + Animations.Add(Model.FromStream(fullDbaPath, PackFileSystem.GetStream(fullDbaPath), true)); + Log.D("Successfully loaded animation database: {0}", fullDbaPath); + } + catch (FileNotFoundException) + { + Log.D("DBA file not found: {0}", fullDbaPath); + } + } + } + + // Also load individual CAF files from animation entries + LoadCafAnimations(chrparams); + return true; + } + catch (FileNotFoundException) + { + Log.D("No chrparams file found"); + return false; + } + } + + /// + /// Attempts to load animations from a .cal file (ArcheAge format). + /// + private bool TryLoadCalAnimations(string? modelDir) + { + try + { + var calFileName = Path.ChangeExtension(Path.GetFileName(InputFile), ".cal"); + var calPath = string.IsNullOrEmpty(modelDir) + ? calFileName + : Path.Combine(modelDir, calFileName); + + Log.D("Looking for cal file at: {0}", calPath); + + var calFile = CalFile.ParseWithIncludes(calPath, PackFileSystem); + + Log.D("Successfully loaded cal file, filepath: {0}, animations count: {1}", + calFile.FilePath ?? "(none)", calFile.Animations.Count); + + if (calFile.Animations.Count == 0) + { + Log.D("No animations found in cal file"); + return false; + } + + // Load CAF files from cal entries + LoadCafAnimationsFromCal(calFile); + return true; + } + catch (FileNotFoundException) + { + Log.D("No cal file found"); + return false; + } + } + + /// + /// Loads CAF animation files from a parsed .cal file. + /// + private void LoadCafAnimationsFromCal(CalFile calFile) + { + var basePath = calFile.FilePath ?? ""; + Log.D("CAF base path from cal: {0}", basePath); + + foreach (var (name, relativePath) in calFile.Animations) + { + try + { + // Build full path + var cafPath = relativePath; + if (!Path.IsPathRooted(cafPath) && !string.IsNullOrEmpty(basePath)) + { + cafPath = Path.Combine(basePath, cafPath); + } + + // Ensure .caf extension + if (!cafPath.EndsWith(".caf", StringComparison.OrdinalIgnoreCase)) + cafPath = Path.ChangeExtension(cafPath, ".caf"); + + // Try loading with multiple path resolutions + if (!TryLoadCafWithPathVariants(cafPath, name)) + { + Log.D("CAF file not found: {0}", cafPath); + } + } + catch (Exception ex) + { + Log.D("Error loading CAF {0}: {1}", name, ex.Message); + } + } + + if (CafAnimations.Count > 0) + Log.I("Loaded {0} CAF animation(s) from cal file", CafAnimations.Count); + } + + /// + /// Tries to load a CAF file with multiple path variants (for ArcheAge's "game" subdirectory). + /// + private bool TryLoadCafWithPathVariants(string cafPath, string animationName) + { + // Try path as-is first + if (TryLoadSingleCafFile(cafPath, animationName)) + return true; + + // Try with "game\" prefix (ArcheAge stores files under game/ subdirectory) + var gamePathPrefixed = Path.Combine("game", cafPath); + if (TryLoadSingleCafFile(gamePathPrefixed, animationName)) + return true; + + return false; + } + + /// + /// Attempts to load a single CAF file. Returns true on success, false if file not found. + /// + private bool TryLoadSingleCafFile(string cafPath, string animationName) + { + // Resolve relative paths against ObjectDir + var fullPath = cafPath; + if (!Path.IsPathRooted(fullPath) && !string.IsNullOrEmpty(ObjectDir)) + { + fullPath = Path.Combine(ObjectDir, fullPath); + } + + // Check if file exists via PackFileSystem + try + { + var cafModel = Model.FromStream(fullPath, PackFileSystem.GetStream(fullPath), true); + var cafAnimation = ParseCafModel(cafModel, animationName, fullPath); + + if (cafAnimation is not null) + { + CafAnimations.Add(cafAnimation); + Log.D("Successfully loaded CAF animation: {0} from {1}", animationName, fullPath); + return true; + } + return false; + } + catch (FileNotFoundException) + { + return false; + } + catch (Exception ex) + { + Log.D("Error loading CAF {0}: {1}", fullPath, ex.Message); + return false; + } + } + + /// + /// Loads individual CAF animation files listed in chrparams. + /// + private void LoadCafAnimations(ChrParams.ChrParams chrparams) + { + if (chrparams.Animations is null) + return; + + // Get base path for CAF files (from #filepath entry) + var basePath = chrparams.Animations.FirstOrDefault(x => x.Name == "#filepath")?.Path ?? ""; + Log.D("CAF base path: {0}", basePath); + + // Find all animation entries that aren't special directives + var cafEntries = chrparams.Animations + .Where(a => !string.IsNullOrEmpty(a.Name) + && !a.Name.StartsWith("$") + && !a.Name.StartsWith("#") + && !string.IsNullOrEmpty(a.Path)) + .ToList(); + + Log.D("Found {0} potential CAF animation entries", cafEntries.Count); + + foreach (var entry in cafEntries) + { + try + { + // Build full path pattern + var cafPattern = entry.Path!; + if (!Path.IsPathRooted(cafPattern) && !string.IsNullOrEmpty(basePath)) + { + cafPattern = Path.Combine(basePath, cafPattern); + } + + // Check if this is a wildcard pattern + if (cafPattern.Contains('*') || cafPattern.Contains('?')) + { + // Expand wildcard pattern to find matching files + var cafFiles = ExpandCafWildcard(cafPattern); + Log.D("Wildcard pattern '{0}' matched {1} files", cafPattern, cafFiles.Count); + + foreach (var cafPath in cafFiles) + { + LoadSingleCafFile(cafPath); + } + } + else + { + // Single file path + if (Path.GetExtension(cafPattern).ToLowerInvariant() != ".caf") + cafPattern = Path.ChangeExtension(cafPattern, ".caf"); + + LoadSingleCafFile(cafPattern, entry.Name!); + } + } + catch (Exception ex) + { + Log.D("Error processing CAF entry {0}: {1}", entry.Path, ex.Message); + } + } + + if (CafAnimations.Count > 0) + Log.I("Loaded {0} CAF animation(s)", CafAnimations.Count); + } + + /// + /// Expands a wildcard pattern to find matching CAF files. + /// Handles wildcards in both directory path (e.g., "path/*/*.caf") and filename (e.g., "path/*.caf"). + /// + private List ExpandCafWildcard(string pattern) + { + var results = new List(); + + try + { + // Normalize path separators + pattern = pattern.Replace('\\', '/'); + + // Check if the directory portion contains wildcards + var lastSlash = pattern.LastIndexOf('/'); + var filePattern = lastSlash >= 0 ? pattern[(lastSlash + 1)..] : pattern; + var directoryPattern = lastSlash >= 0 ? pattern[..lastSlash] : ""; + + // Find the first wildcard in the directory path + var firstWildcardIndex = directoryPattern.IndexOfAny(['*', '?']); + + string baseDirectory; + bool searchRecursively = false; + + if (firstWildcardIndex >= 0) + { + // There's a wildcard in the directory path - need to search recursively + // Find the last slash before the wildcard to get the base directory + var lastSlashBeforeWildcard = directoryPattern.LastIndexOf('/', firstWildcardIndex); + baseDirectory = lastSlashBeforeWildcard >= 0 + ? directoryPattern[..lastSlashBeforeWildcard] + : ""; + searchRecursively = true; + } + else + { + baseDirectory = directoryPattern; + } + + // Resolve relative paths against ObjectDir + if (!Path.IsPathRooted(baseDirectory) && !string.IsNullOrEmpty(ObjectDir)) + { + baseDirectory = Path.Combine(ObjectDir, baseDirectory); + } + + // Normalize again after Path.Combine + baseDirectory = baseDirectory.Replace('\\', '/'); + + if (Directory.Exists(baseDirectory)) + { + var searchOption = searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + var matchingFiles = Directory.GetFiles(baseDirectory, filePattern, searchOption); + results.AddRange(matchingFiles.Where(f => + f.EndsWith(".caf", StringComparison.OrdinalIgnoreCase))); + + Log.D("Searched '{0}' with pattern '{1}', recursive={2}, found {3} files", + baseDirectory, filePattern, searchRecursively, results.Count); + } + else + { + Log.D("Directory not found for wildcard expansion: {0}", baseDirectory); + } + } + catch (Exception ex) + { + Log.D("Error expanding wildcard pattern {0}: {1}", pattern, ex.Message); + } + + return results; + } + + /// + /// Expands a wildcard pattern to find matching DBA files. + /// + private List ExpandDbaWildcard(string pattern) + { + var results = new List(); + + try + { + // Normalize path separators + pattern = pattern.Replace('\\', '/'); + + // Check if the directory portion contains wildcards + var lastSlash = pattern.LastIndexOf('/'); + var filePattern = lastSlash >= 0 ? pattern[(lastSlash + 1)..] : pattern; + var directoryPattern = lastSlash >= 0 ? pattern[..lastSlash] : ""; + + // Find the first wildcard in the directory path + var firstWildcardIndex = directoryPattern.IndexOfAny(['*', '?']); + + string baseDirectory; + bool searchRecursively = false; + + if (firstWildcardIndex >= 0) + { + // There's a wildcard in the directory path - need to search recursively + var lastSlashBeforeWildcard = directoryPattern.LastIndexOf('/', firstWildcardIndex); + baseDirectory = lastSlashBeforeWildcard >= 0 + ? directoryPattern[..lastSlashBeforeWildcard] + : ""; + searchRecursively = true; + } + else + { + baseDirectory = directoryPattern; + } + + // Resolve relative paths against ObjectDir + if (!Path.IsPathRooted(baseDirectory) && !string.IsNullOrEmpty(ObjectDir)) + { + baseDirectory = Path.Combine(ObjectDir, baseDirectory); + } + + // Normalize again after Path.Combine + baseDirectory = baseDirectory.Replace('\\', '/'); + + if (Directory.Exists(baseDirectory)) + { + var searchOption = searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + var matchingFiles = Directory.GetFiles(baseDirectory, filePattern, searchOption); + results.AddRange(matchingFiles.Where(f => + f.EndsWith(".dba", StringComparison.OrdinalIgnoreCase))); + + Log.D("Searched '{0}' with pattern '{1}', recursive={2}, found {3} DBA files", + baseDirectory, filePattern, searchRecursively, results.Count); + } + else + { + Log.D("Directory not found for DBA wildcard expansion: {0}", baseDirectory); + } + } + catch (Exception ex) + { + Log.D("Error expanding DBA wildcard pattern {0}: {1}", pattern, ex.Message); + } + + return results; + } + + /// + /// Loads a single CAF file. + /// + private void LoadSingleCafFile(string cafPath, string? animationName = null) + { + try + { + // Resolve relative paths against ObjectDir + var fullPath = cafPath; + if (!Path.IsPathRooted(fullPath) && !string.IsNullOrEmpty(ObjectDir)) + { + fullPath = Path.Combine(ObjectDir, fullPath); + } + + // Use filename without extension as animation name if not specified + animationName ??= Path.GetFileNameWithoutExtension(fullPath); + + Log.D("Loading CAF: {0} (name: {1})", fullPath, animationName); + + var cafModel = Model.FromStream(fullPath, PackFileSystem.GetStream(fullPath), true); + var cafAnimation = ParseCafModel(cafModel, animationName, fullPath); + + if (cafAnimation is not null) + { + CafAnimations.Add(cafAnimation); + Log.D("Successfully loaded CAF animation: {0}", animationName); + } } catch (FileNotFoundException) { - Log.I("Unable to find associated animation track database file."); + Log.D("CAF file not found: {0}", cafPath); + } + catch (Exception ex) + { + Log.D("Error loading CAF {0}: {1}", cafPath, ex.Message); + } + } + + /// + /// Parses a CAF Model into a CafAnimation structure. + /// + private CafAnimation? ParseCafModel(Model cafModel, string animationName, string filePath) + { + var animation = new CafAnimation + { + Name = animationName, + FilePath = filePath + }; + + // Get timing info from timing chunk + var timingChunk = cafModel.ChunkMap.Values.OfType().FirstOrDefault(); + if (timingChunk is not null) + { + animation.SecsPerTick = timingChunk.SecsPerTick; + animation.TicksPerFrame = timingChunk.TicksPerFrame; + animation.StartFrame = timingChunk.GlobalRange.Start; + animation.EndFrame = timingChunk.GlobalRange.End; + } + + // Check for additive animation flag from GlobalAnimationHeaderCAF chunk + var animHeaderChunk = cafModel.ChunkMap.Values.OfType().FirstOrDefault(); + if (animHeaderChunk is not null) + { + // AssetFlags.Additive = 0x001 + animation.IsAdditive = (animHeaderChunk.Flags & 0x001) != 0; + if (animation.IsAdditive) + Log.D($"CAF animation '{animationName}' is additive (flags=0x{animHeaderChunk.Flags:X})"); + } + + // Get bone name mapping from BoneNameList chunk (if present) + var boneNameList = cafModel.ChunkMap.Values.OfType().FirstOrDefault(); + if (boneNameList is not null) + { + // Build CRC32 -> bone name mapping + foreach (var boneName in boneNameList.BoneNames) + { + var crc = ComputeBoneNameCrc32(boneName); + animation.ControllerIdToBoneName[crc] = boneName; + } + } + + // Process controller chunks (829/831 = compressed, 830 = uncompressed CryKeyPQLog) + var controllers829 = cafModel.ChunkMap.Values.OfType().ToList(); + var controllers830 = cafModel.ChunkMap.Values.OfType().ToList(); + var controllers831 = cafModel.ChunkMap.Values.OfType().ToList(); + + // 830 uses unified key times for both rotation and position + foreach (var ctrl in controllers830) + { + var keyTimes = ctrl.KeyTimes.Select(t => (float)t).ToList(); + var track = new BoneTrack + { + ControllerId = ctrl.ControllerId, + RotationKeyTimes = keyTimes, + PositionKeyTimes = keyTimes, + Positions = ctrl.KeyPositions.ToList(), + Rotations = ctrl.KeyRotations.ToList() + }; + animation.BoneTracks[ctrl.ControllerId] = track; + } + + // 829 and 831 have separate rotation/position key times + // Key times are stored as actual frame numbers (byte/uint16/float format just determines storage size) + foreach (var ctrl in controllers829) + { + var track = new BoneTrack + { + ControllerId = ctrl.ControllerId, + RotationKeyTimes = ctrl.RotationKeyTimes.ToList(), + PositionKeyTimes = ctrl.PositionKeyTimes.ToList(), + Positions = ctrl.KeyPositions.ToList(), + Rotations = ctrl.KeyRotations.ToList() + }; + animation.BoneTracks[ctrl.ControllerId] = track; + } + + foreach (var ctrl in controllers831) + { + var track = new BoneTrack + { + ControllerId = ctrl.ControllerId, + RotationKeyTimes = ctrl.RotationKeyTimes.ToList(), + PositionKeyTimes = ctrl.PositionKeyTimes.ToList(), + Positions = ctrl.KeyPositions.ToList(), + Rotations = ctrl.KeyRotations.ToList() + }; + animation.BoneTracks[ctrl.ControllerId] = track; } + + // Process Star Citizen #ivo CAF chunks + var ivoCAFs = cafModel.ChunkMap.Values.OfType().ToList(); + foreach (var ivoCaf in ivoCAFs) + { + // Each bone hash maps to rotation/position data + foreach (var boneHash in ivoCaf.BoneHashes) + { + // Get rotation data + ivoCaf.Rotations.TryGetValue(boneHash, out var rotations); + ivoCaf.RotationTimes.TryGetValue(boneHash, out var rotTimes); + ivoCaf.Positions.TryGetValue(boneHash, out var positions); + ivoCaf.PositionTimes.TryGetValue(boneHash, out var posTimes); + + var track = new BoneTrack + { + ControllerId = boneHash, + Rotations = rotations ?? [], + Positions = positions ?? [], + RotationKeyTimes = rotTimes ?? [], + PositionKeyTimes = posTimes ?? [] + }; + + if (track.Rotations.Count > 0 || track.Positions.Count > 0) + { + animation.BoneTracks[boneHash] = track; + } + } + } + + if (animation.BoneTracks.Count == 0) + { + Log.D("No controller chunks found in CAF file: {0}", filePath); + return null; + } + + Log.D("Parsed CAF with {0} bone tracks, frames {1}-{2}", + animation.BoneTracks.Count, animation.StartFrame, animation.EndFrame); + + return animation; + } + + /// + /// Computes CRC32 of a bone name (lowercase) for controller ID matching. + /// + private static uint ComputeBoneNameCrc32(string boneName) + { + // CryEngine uses lowercase CRC32 for bone names + var bytes = System.Text.Encoding.ASCII.GetBytes(boneName.ToLowerInvariant()); + uint crc = 0xFFFFFFFF; + + foreach (byte b in bytes) + { + crc ^= b; + for (int i = 0; i < 8; i++) + { + crc = (crc >> 1) ^ (0xEDB88320 & ~((crc & 1) - 1)); + } + } + + return ~crc; } private GeometryInfo BuildNodeGeometryInfo(ChunkIvoSkinMesh skinMesh, IEnumerable subsets) diff --git a/CgfConverter/CryEngineCore/Chunks/Chunk.cs b/CgfConverter/CryEngineCore/Chunks/Chunk.cs index 7666e8a1..c83e26aa 100644 --- a/CgfConverter/CryEngineCore/Chunks/Chunk.cs +++ b/CgfConverter/CryEngineCore/Chunks/Chunk.cs @@ -45,6 +45,7 @@ public static Chunk New(ChunkType chunkType, uint version) ChunkType.SceneProps => Chunk.New(version), ChunkType.MeshPhysicsData => Chunk.New(version), ChunkType.BoneAnim => Chunk.New(version), + ChunkType.MotionParams => Chunk.New(version), // Compiled chunks ChunkType.CompiledBones => Chunk.New(version), ChunkType.CompiledPhysicalProxies => Chunk.New(version), @@ -75,6 +76,11 @@ public static Chunk New(ChunkType chunkType, uint version) ChunkType.BoneNameList => Chunk.New(version), ChunkType.MeshMorphTarget => Chunk.New(version), ChunkType.BinaryXmlDataSC => Chunk.New(version), + // Star Citizen #ivo animation chunks + ChunkType.IvoCAFData => Chunk.New(version), + ChunkType.IvoAnimInfo => Chunk.New(version), + ChunkType.IvoDBAData => Chunk.New(version), + ChunkType.IvoDBAMetadata => Chunk.New(version), _ => new ChunkUnknown(), }; } diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_801.cs b/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_801.cs index 674ddc36..a75b06e4 100644 --- a/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_801.cs +++ b/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_801.cs @@ -4,7 +4,8 @@ namespace CgfConverter.CryEngineCore; internal sealed class ChunkCompiledBones_801 : ChunkCompiledBones { - // Archeage drug_boy01 + // Archeage format - stores B2W (boneToWorld) matrix only, not both W2B and B2W like 0x800. + // ReadCompiledBone_801 handles computing W2B by inverting B2W. public override void Read(BinaryReader b) { base.Read(b); diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_900.cs b/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_900.cs index 18512d5e..ff40116d 100644 --- a/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_900.cs +++ b/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_900.cs @@ -4,6 +4,12 @@ namespace CgfConverter.CryEngineCore; +/// +/// WARNING: This chunk version has NOT been validated for animation export. +/// Only ChunkCompiledBones_800 has been vetted for animations (with MWO DBA files). +/// The BindPoseMatrix calculation here may not be correct - do not trust animation +/// data from this chunk until it has been properly validated. +/// internal sealed class ChunkCompiledBones_900 : ChunkCompiledBones { public override void Read(BinaryReader b) diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_901.cs b/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_901.cs index 6ed7f064..a6b8cc20 100644 --- a/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_901.cs +++ b/CgfConverter/CryEngineCore/Chunks/ChunkCompiledBones_901.cs @@ -5,6 +5,13 @@ using static CgfConverter.Utilities.HelperMethods; namespace CgfConverter.CryEngineCore.Chunks; + +/// +/// WARNING: This chunk version has NOT been validated for animation export. +/// Only ChunkCompiledBones_800 has been vetted for animations (with MWO DBA files). +/// The BindPoseMatrix calculation here may not be correct - do not trust animation +/// data from this chunk until it has been properly validated. +/// internal class ChunkCompiledBones_901 : ChunkCompiledBones { public override void Read(BinaryReader b) @@ -12,7 +19,7 @@ public override void Read(BinaryReader b) base.Read(b); NumBones = b.ReadInt32(); - var _ = b.ReadInt32(); // String table size. Won't use. + var stringTableSize = b.ReadInt32(); Flags1 = b.ReadInt32(); Flags2 = b.ReadInt32(); @@ -23,7 +30,7 @@ public override void Read(BinaryReader b) BoneList.Add(tempBone); } - var boneNames = GetNullSeparatedStrings(NumBones, b); + var boneNames = GetNullSeparatedStrings(NumBones, stringTableSize, b); // Post bone read setup. Parents, children, etc. // Add the ChildID to the parent bone. This will help with navigation. @@ -42,12 +49,12 @@ public override void Read(BinaryReader b) BoneList[i].BindPoseMatrix = bpm; BoneList[i].BoneName = boneNames[i]; - if (BoneList[i].OffsetParent != -1) + // ParentControllerIndex is read in ReadCompiledBone_901 as the parent bone index + // A value of -1 (or 0xFFFF as signed short) means no parent (root bone) + if (BoneList[i].ParentControllerIndex >= 0 && BoneList[i].ParentControllerIndex < BoneList.Count) { - BoneList[i].ParentBone = BoneList[BoneList[i].OffsetParent]; - BoneList[i].ParentControllerIndex = BoneList[i].OffsetParent; + BoneList[i].ParentBone = BoneList[BoneList[i].ParentControllerIndex]; BoneList[i].ParentBone.ChildIDs.Add(i); - BoneList[i].ParentBone.NumberOfChildren++; } BoneList[i].LocalTransformMatrix = Matrix3x4.CreateFromParts(relativeQuat, relativeTranslation); BoneList[i].WorldTransformMatrix = Matrix3x4.CreateFromParts(worldQuat, worldTranslation); diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkController_829.cs b/CgfConverter/CryEngineCore/Chunks/ChunkController_829.cs index 0c6fd485..5f877a29 100644 --- a/CgfConverter/CryEngineCore/Chunks/ChunkController_829.cs +++ b/CgfConverter/CryEngineCore/Chunks/ChunkController_829.cs @@ -1,25 +1,203 @@ -using CgfConverter.Services; +using System.Collections.Generic; using System.IO; +using System.Numerics; +using CgfConverter.Services; +using Extensions; namespace CgfConverter.CryEngineCore.Chunks; +/// +/// Controller chunk version 0x829 - Compressed format with separate rotation/position tracks. +/// Used in CAF animation files (Armored Warfare). Each chunk contains animation data for a single bone. +/// Similar to 0x831 but without the Flags field (16-byte header with 2-byte padding vs 18-byte). +/// +/// Vetted: Armored Warfare chicken walk/idle animations export correctly to USD/Blender. +/// internal sealed class ChunkController_829 : ChunkController { - public uint ControllerID { get; internal set; } + /// CRC32 of the bone name this controller animates. + public uint ControllerId { get; internal set; } + + /// Number of rotation keyframes. public ushort NumRotationKeys { get; internal set; } + + /// Number of position keyframes. public ushort NumPositionKeys { get; internal set; } + + /// Compression format for rotation data. public byte RotationFormat { get; internal set; } + + /// Time encoding format for rotation keys. public byte RotationTimeFormat { get; internal set; } + + /// Compression format for position data. public byte PositionFormat { get; internal set; } + + /// Position keys info byte. public byte PositionKeysInfo { get; internal set; } + + /// Time encoding format for position keys. public byte PositionTimeFormat { get; internal set; } + + /// Tracks alignment flag. public byte TracksAligned { get; internal set; } + /// Rotation keyframe times. + public List RotationKeyTimes { get; internal set; } = []; + + /// Position keyframe times. + public List PositionKeyTimes { get; internal set; } = []; + + /// Keyframe rotations. + public List KeyRotations { get; internal set; } = []; + + /// Keyframe positions. + public List KeyPositions { get; internal set; } = []; + public override void Read(BinaryReader b) { base.Read(b); - ((EndiannessChangeableBinaryReader) b).IsBigEndian = false; + ((EndiannessChangeableBinaryReader)b).IsBigEndian = false; + + // 829 format differs from 831 - no Flags field + ControllerId = b.ReadUInt32(); + NumRotationKeys = b.ReadUInt16(); + NumPositionKeys = b.ReadUInt16(); + RotationFormat = b.ReadByte(); + RotationTimeFormat = b.ReadByte(); + PositionFormat = b.ReadByte(); + PositionKeysInfo = b.ReadByte(); + PositionTimeFormat = b.ReadByte(); + TracksAligned = b.ReadByte(); + + // Per 010 template: 2 bytes of padding after header (unconditional) + b.ReadUInt16(); // padding + + // Data layout (v0829) - 16 byte header, then: + // [Rotation Values] -> [conditional padding] -> [Rotation Times] -> [conditional padding] -> + // [Position Values] -> [conditional padding] -> [Position Times (if PositionKeysInfo != 0)] + + // Read rotation values + if (NumRotationKeys > 0) + { + KeyRotations = ReadRotations(b, NumRotationKeys, RotationFormat); + AlignTo4Bytes(b); + + // Read rotation time keys + RotationKeyTimes = ReadKeyTimes(b, NumRotationKeys, RotationTimeFormat); + AlignTo4Bytes(b); + } + + // Read position values + if (NumPositionKeys > 0) + { + KeyPositions = ReadPositions(b, NumPositionKeys, PositionFormat); + AlignTo4Bytes(b); + + // Position time keys only if PositionKeysInfo != 0, otherwise shares rotation times + if (PositionKeysInfo != 0) + { + PositionKeyTimes = ReadKeyTimes(b, NumPositionKeys, PositionTimeFormat); + AlignTo4Bytes(b); + } + else + { + PositionKeyTimes = RotationKeyTimes; + } + } + } + + private void AlignTo4Bytes(BinaryReader b) + { + if (TracksAligned != 0) + { + var pos = b.BaseStream.Position; + var remainder = pos % 4; + if (remainder != 0) + b.ReadBytes((int)(4 - remainder)); + } + } + + private List ReadKeyTimes(BinaryReader b, int count, byte format) + { + var times = new List(count); + + // EKeyTimesFormat: 0 = eF32 (float), 1 = eUINT16, 2 = eByte + for (int i = 0; i < count; i++) + { + float time = format switch + { + 0 => b.ReadSingle(), // eF32 - 4 bytes + 1 => b.ReadUInt16(), // eUINT16 - 2 bytes + 2 => b.ReadByte(), // eByte - 1 byte + 3 => b.ReadSingle(), // eF32StartStop + 4 => b.ReadUInt16(), // eUINT16StartStop + 5 => b.ReadByte(), // eByteStartStop + 6 => b.ReadUInt16(), // eBitset + _ => b.ReadSingle() // Default to float + }; + times.Add(time); + } + + return times; + } + + private List ReadRotations(BinaryReader b, int count, byte format) + { + var rotations = new List(count); + + for (int i = 0; i < count; i++) + { + Quaternion rot = (ECompressionFormat)format switch + { + ECompressionFormat.eNoCompressQuat => b.ReadQuaternion(), + ECompressionFormat.eShotInt3Quat => b.ReadShortInt3Quat(), + ECompressionFormat.eSmallTreeDWORDQuat => b.ReadSmallTreeDWORDQuat(), + ECompressionFormat.eSmallTree48BitQuat => b.ReadSmallTree48BitQuat(), + ECompressionFormat.eSmallTree64BitQuat => b.ReadSmallTree64BitQuat(), + ECompressionFormat.eSmallTree64BitExtQuat => b.ReadSmallTree64BitExtQuat(), + _ => Quaternion.Identity + }; + rotations.Add(rot); + } + + return rotations; + } + + private List ReadPositions(BinaryReader b, int count, byte format) + { + var positions = new List(count); + + for (int i = 0; i < count; i++) + { + // Per 010 template: positions only use eNoCompress or eNoCompressVec3 (both read as Vector3) + Vector3 pos = (ECompressionFormat)format switch + { + ECompressionFormat.eNoCompress => b.ReadVector3(), + ECompressionFormat.eNoCompressVec3 => b.ReadVector3(), + _ => Vector3.Zero + }; + positions.Add(pos); + } + return positions; } + + private enum ECompressionFormat + { + eNoCompress = 0, + eNoCompressQuat = 1, + eNoCompressVec3 = 2, + eShotInt3Quat = 3, + eSmallTreeDWORDQuat = 4, + eSmallTree48BitQuat = 5, + eSmallTree64BitQuat = 6, + ePolarQuat = 7, + eSmallTree64BitExtQuat = 8, + eAutomaticQuat = 9 + } + + public override string ToString() => + $"ChunkController_829: ID={ID:X}, ControllerId={ControllerId:X}, RotKeys={NumRotationKeys}, PosKeys={NumPositionKeys}"; } diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkController_830.cs b/CgfConverter/CryEngineCore/Chunks/ChunkController_830.cs new file mode 100644 index 00000000..25f5a9da --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkController_830.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using CgfConverter.Services; +using Extensions; + +namespace CgfConverter.CryEngineCore; + +/// +/// Controller chunk version 0x830 - Uncompressed CryKeyPQLog format. +/// Used in CAF animation files. Each chunk contains animation data for a single bone. +/// +internal sealed class ChunkController_830 : ChunkController +{ + /// Number of keyframes in this controller. + public uint NumKeys { get; internal set; } + + /// CRC32 of the bone name this controller animates. + public uint ControllerId { get; internal set; } + + /// Controller flags. + public uint Flags { get; internal set; } + + /// Keyframe times in ticks. + public List KeyTimes { get; internal set; } = []; + + /// Keyframe positions. + public List KeyPositions { get; internal set; } = []; + + /// Keyframe rotations (converted from log quaternion). + public List KeyRotations { get; internal set; } = []; + + public override void Read(BinaryReader b) + { + base.Read(b); + + ((EndiannessChangeableBinaryReader)b).IsBigEndian = false; + + NumKeys = b.ReadUInt32(); + ControllerId = b.ReadUInt32(); + Flags = b.ReadUInt32(); + + // Read CryKeyPQLog data: 28 bytes per keyframe + // struct CryKeyPQLog { + // int nTime; // 4 bytes - Time in ticks + // Vec3 vPos; // 12 bytes - Position (x, y, z) + // Vec3 vRotLog; // 12 bytes - Logarithm of quaternion rotation + // }; + for (int i = 0; i < NumKeys; i++) + { + int time = b.ReadInt32(); + Vector3 position = b.ReadVector3(); + Vector3 rotLog = b.ReadVector3(); + + KeyTimes.Add(time); + KeyPositions.Add(position); + KeyRotations.Add(LogToQuaternion(rotLog)); + } + } + + /// + /// Converts a logarithmic quaternion representation to a standard quaternion. + /// The vRotLog is the axis of rotation scaled by the rotation angle. + /// + private static Quaternion LogToQuaternion(Vector3 rotLog) + { + float theta = rotLog.Length(); + + if (theta < 0.0001f) + { + // Very small rotation, return identity + return Quaternion.Identity; + } + + float halfTheta = theta * 0.5f; + float sinHalfTheta = MathF.Sin(halfTheta); + float cosHalfTheta = MathF.Cos(halfTheta); + + // Normalize the axis + Vector3 axis = rotLog / theta; + + // Build quaternion: w = cos(theta/2), xyz = axis * sin(theta/2) + return new Quaternion( + axis.X * sinHalfTheta, + axis.Y * sinHalfTheta, + axis.Z * sinHalfTheta, + cosHalfTheta + ); + } + + public override string ToString() => + $"ChunkController_830: ID={ID:X}, ControllerId={ControllerId:X}, NumKeys={NumKeys}, Flags={Flags:X}"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkController_831.cs b/CgfConverter/CryEngineCore/Chunks/ChunkController_831.cs new file mode 100644 index 00000000..bcf14ae8 --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkController_831.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using CgfConverter.Models.Structs; +using CgfConverter.Services; +using Extensions; + +namespace CgfConverter.CryEngineCore; + +/// +/// Controller chunk version 0x831 - Compressed format with separate rotation/position tracks. +/// Used in CAF animation files. Each chunk contains animation data for a single bone. +/// +internal sealed class ChunkController_831 : ChunkController +{ + /// CRC32 of the bone name this controller animates. + public uint ControllerId { get; internal set; } + + /// Controller flags. + public uint Flags { get; internal set; } + + /// Number of rotation keyframes. + public ushort NumRotationKeys { get; internal set; } + + /// Number of position keyframes. + public ushort NumPositionKeys { get; internal set; } + + /// Compression format for rotation data. + public byte RotationFormat { get; internal set; } + + /// Time encoding format for rotation keys. + public byte RotationTimeFormat { get; internal set; } + + /// Compression format for position data. + public byte PositionFormat { get; internal set; } + + /// Position keys info byte. + public byte PositionKeysInfo { get; internal set; } + + /// Time encoding format for position keys. + public byte PositionTimeFormat { get; internal set; } + + /// Tracks alignment flag. + public byte TracksAligned { get; internal set; } + + /// Rotation keyframe times. + public List RotationKeyTimes { get; internal set; } = []; + + /// Position keyframe times. + public List PositionKeyTimes { get; internal set; } = []; + + /// Keyframe rotations. + public List KeyRotations { get; internal set; } = []; + + /// Keyframe positions. + public List KeyPositions { get; internal set; } = []; + + public override void Read(BinaryReader b) + { + base.Read(b); + + ((EndiannessChangeableBinaryReader)b).IsBigEndian = false; + + ControllerId = b.ReadUInt32(); + Flags = b.ReadUInt32(); + NumRotationKeys = b.ReadUInt16(); + NumPositionKeys = b.ReadUInt16(); + RotationFormat = b.ReadByte(); + RotationTimeFormat = b.ReadByte(); + PositionFormat = b.ReadByte(); + PositionKeysInfo = b.ReadByte(); + PositionTimeFormat = b.ReadByte(); + TracksAligned = b.ReadByte(); + + // Data layout (v0831): + // [Rotation Values] -> [padding] -> [Rotation Times] -> [padding] -> + // [Position Values] -> [padding] -> [Position Times (if PositionKeysInfo != 0)] + + // Read rotation values first + KeyRotations = ReadRotations(b, NumRotationKeys, RotationFormat); + AlignTo4Bytes(b); + + // Read rotation time keys + RotationKeyTimes = ReadKeyTimes(b, NumRotationKeys, RotationTimeFormat); + AlignTo4Bytes(b); + + // Read position values + KeyPositions = ReadPositions(b, NumPositionKeys, PositionFormat); + AlignTo4Bytes(b); + + // Position time keys only if PositionKeysInfo != 0, otherwise shares rotation times + if (PositionKeysInfo != 0) + { + PositionKeyTimes = ReadKeyTimes(b, NumPositionKeys, PositionTimeFormat); + } + else + { + PositionKeyTimes = RotationKeyTimes; + } + } + + private void AlignTo4Bytes(BinaryReader b) + { + if (TracksAligned != 0) + { + var pos = b.BaseStream.Position; + var remainder = pos % 4; + if (remainder != 0) + b.ReadBytes((int)(4 - remainder)); + } + } + + private List ReadKeyTimes(BinaryReader b, int count, byte format) + { + var times = new List(count); + + // EKeyTimesFormat: 0 = eF32 (float), 1 = eUINT16, 2 = eByte + for (int i = 0; i < count; i++) + { + float time = format switch + { + 0 => b.ReadSingle(), // eF32 - 4 bytes + 1 => b.ReadUInt16(), // eUINT16 - 2 bytes + 2 => b.ReadByte(), // eByte - 1 byte + 3 => b.ReadSingle(), // eF32StartStop + 4 => b.ReadUInt16(), // eUINT16StartStop + 5 => b.ReadByte(), // eByteStartStop + 6 => b.ReadUInt16(), // eBitset + _ => b.ReadSingle() // Default to float + }; + times.Add(time); + } + + return times; + } + + private List ReadRotations(BinaryReader b, int count, byte format) + { + var rotations = new List(count); + + for (int i = 0; i < count; i++) + { + Quaternion rot = (ECompressionFormat)format switch + { + ECompressionFormat.eNoCompressQuat => b.ReadQuaternion(), + ECompressionFormat.eShotInt3Quat => b.ReadShortInt3Quat(), + ECompressionFormat.eSmallTreeDWORDQuat => b.ReadSmallTreeDWORDQuat(), + ECompressionFormat.eSmallTree48BitQuat => b.ReadSmallTree48BitQuat(), + ECompressionFormat.eSmallTree64BitQuat => b.ReadSmallTree64BitQuat(), + ECompressionFormat.eSmallTree64BitExtQuat => b.ReadSmallTree64BitExtQuat(), + _ => Quaternion.Identity + }; + rotations.Add(rot); + } + + return rotations; + } + + private List ReadPositions(BinaryReader b, int count, byte format) + { + var positions = new List(count); + + for (int i = 0; i < count; i++) + { + // Per 010 template: positions only use eNoCompress or eNoCompressVec3 (both read as Vector3) + // Other formats are not used for position data in 831 chunks + Vector3 pos = (ECompressionFormat)format switch + { + ECompressionFormat.eNoCompress => b.ReadVector3(), + ECompressionFormat.eNoCompressVec3 => b.ReadVector3(), + _ => Vector3.Zero + }; + positions.Add(pos); + } + + return positions; + } + + private enum ECompressionFormat + { + eNoCompress = 0, + eNoCompressQuat = 1, + eNoCompressVec3 = 2, + eShotInt3Quat = 3, + eSmallTreeDWORDQuat = 4, + eSmallTree48BitQuat = 5, + eSmallTree64BitQuat = 6, + ePolarQuat = 7, + eSmallTree64BitExtQuat = 8, + eAutomaticQuat = 9 + } + + public override string ToString() => + $"ChunkController_831: ID={ID:X}, ControllerId={ControllerId:X}, RotKeys={NumRotationKeys}, PosKeys={NumPositionKeys}"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkController_905.cs b/CgfConverter/CryEngineCore/Chunks/ChunkController_905.cs index 9bd09e9b..e689c490 100644 --- a/CgfConverter/CryEngineCore/Chunks/ChunkController_905.cs +++ b/CgfConverter/CryEngineCore/Chunks/ChunkController_905.cs @@ -1,7 +1,6 @@ -using System; +using System; using System.Buffers.Binary; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; @@ -13,6 +12,10 @@ namespace CgfConverter.CryEngineCore; +/// +/// Controller chunk version 0x905 - DBA animation database format. +/// Supports both standard mode and in-place streaming mode (detected by negative offsets). +/// internal sealed class ChunkController_905 : ChunkController { private readonly TaggedLogger Log = new TaggedLogger(nameof(ChunkController_905)); @@ -32,13 +35,15 @@ public override void Read(BinaryReader b) base.Read(b); // Assume little endian for now - ((EndiannessChangeableBinaryReader) b).IsBigEndian = false; + ((EndiannessChangeableBinaryReader)b).IsBigEndian = false; NumKeyPos = b.ReadUInt32(); NumKeyRot = b.ReadUInt32(); NumKeyTime = b.ReadUInt32(); NumAnims = b.ReadUInt32(); + Log.D($"Header: NumKeyPos={NumKeyPos}, NumKeyRot={NumKeyRot}, NumKeyTime={NumKeyTime}, NumAnims={NumAnims}"); + // Test: if there exists a number that exceeds 0x10000, it may not be in little endian. var endianCheck = (NumKeyPos >= 0x10000 ? 1 : 0) | @@ -56,7 +61,7 @@ public override void Read(BinaryReader b) NumKeyRot = BinaryPrimitives.ReverseEndianness(NumKeyRot); NumKeyTime = BinaryPrimitives.ReverseEndianness(NumKeyTime); NumAnims = BinaryPrimitives.ReverseEndianness(NumAnims); - ((EndiannessChangeableBinaryReader) b).IsBigEndian = true; + ((EndiannessChangeableBinaryReader)b).IsBigEndian = true; Log.I($"Assuming big endian: {this}"); break; @@ -65,212 +70,366 @@ public override void Read(BinaryReader b) break; } - var keyTimeLengths = Enumerable.Range(0, (int) NumKeyTime).Select(_ => b.ReadUInt16()).ToList(); - var keyTimeFormats = Enumerable.Range(0, (int) EKeyTimesFormat.eBitset + 1).Select(_ => b.ReadUInt32()).ToList(); - - var keyPosLengths = Enumerable.Range(0, (int) NumKeyPos).Select(_ => b.ReadUInt16()).ToList(); - var keyPosFormats = Enumerable.Range(0, (int) ECompressionFormat.eAutomaticQuat).Select(_ => b.ReadUInt32()).ToList(); - - var keyRotLengths = Enumerable.Range(0, (int) NumKeyRot).Select(_ => b.ReadUInt16()).ToList(); - var keyRotFormats = Enumerable.Range(0, (int) ECompressionFormat.eAutomaticQuat).Select(_ => b.ReadUInt32()).ToList(); - - var keyTimeOffsets = Enumerable.Range(0, (int) NumKeyTime).Select(_ => b.ReadUInt32()).ToList(); - var keyPosOffsets = Enumerable.Range(0, (int) NumKeyPos).Select(_ => b.ReadUInt32()).ToList(); - var keyRotOffsets = Enumerable.Range(0, (int) NumKeyRot).Select(_ => b.ReadUInt32()).ToList(); - var trackLength = b.ReadUInt32(); - - Debug.Assert(keyTimeOffsets.All(x => (x & 3) == 0)); - Debug.Assert(keyPosOffsets.All(x => (x & 3) == 0)); - Debug.Assert(keyRotOffsets.All(x => (x & 3) == 0)); - Debug.Assert((trackLength & 3) == 0); - - keyRotOffsets.Add(trackLength); - keyPosOffsets.Add(keyRotOffsets.First()); - keyTimeOffsets.Add(keyPosOffsets.First()); - - var trackOffset = b.BaseStream.Position; - if ((trackOffset & 3) != 0) - trackOffset = (trackOffset & ~3) + 4; + // Read size arrays (uint16 per track) + var keyTimeLengths = Enumerable.Range(0, (int)NumKeyTime).Select(_ => b.ReadUInt16()).ToList(); + var keyTimeFormats = ReadKeyTimeFormatCounts(b); - KeyTimes = new List>(); - foreach (var (length, from) in keyTimeLengths.Zip(keyTimeOffsets)) + var keyPosLengths = Enumerable.Range(0, (int)NumKeyPos).Select(_ => b.ReadUInt16()).ToList(); + var keyPosFormats = ReadCompressionFormatCounts(b); + + var keyRotLengths = Enumerable.Range(0, (int)NumKeyRot).Select(_ => b.ReadUInt16()).ToList(); + var keyRotFormats = ReadCompressionFormatCounts(b); + + // Read offset arrays (int32 - signed for in-place streaming detection) + var keyTimeOffsets = Enumerable.Range(0, (int)NumKeyTime).Select(_ => b.ReadInt32()).ToList(); + var keyPosOffsets = Enumerable.Range(0, (int)NumKeyPos).Select(_ => b.ReadInt32()).ToList(); + // Rotation offsets array includes terminator (+1) + var keyRotOffsets = Enumerable.Range(0, (int)NumKeyRot + 1).Select(_ => b.ReadInt32()).ToList(); + + // Detect in-place streaming mode (negative offsets) + bool isInPlaceStream = NumKeyTime > 0 && keyTimeOffsets[0] < 0; + Log.D($"In-place streaming mode: {isInPlaceStream}"); + + Log.D($"Position after reading offsets: 0x{b.BaseStream.Position:X}"); + + if (isInPlaceStream) { - b.BaseStream.Position = trackOffset + from; + // In-place streaming: read padding length and skip padding + var paddingLength = b.ReadUInt32(); + Log.D($"In-place padding length: {paddingLength}"); + if (paddingLength > 0) + b.ReadBytes((int)paddingLength); + } - List data; - if (keyTimeFormats[(int)EKeyTimesFormat.eF32] > 0) - { - --keyTimeFormats[(int)EKeyTimesFormat.eF32]; - data = Enumerable.Range(0, length).Select(_ => b.ReadSingle()).ToList(); - } - else if (keyTimeFormats[(int)EKeyTimesFormat.eUINT16] > 0) + // Align to 4 bytes + var pos = b.BaseStream.Position; + if ((pos & 3) != 0) + b.BaseStream.Position = (pos & ~3) + 4; + + Log.D($"Position after alignment: 0x{b.BaseStream.Position:X}"); + + // Get track data size from terminator offset + int trackDataSize = isInPlaceStream + ? -keyRotOffsets[(int)NumKeyRot] + : keyRotOffsets[(int)NumKeyRot]; + + Log.D($"Track data size: {trackDataSize}, Stream length: {b.BaseStream.Length}"); + + // Build complete offset chains for data reading + // Add terminators: keyTime ends where keyPos starts, keyPos ends where keyRot starts + // Note: In in-place mode, offsets are already negative - don't negate again! + if (NumKeyRot > 0) + keyPosOffsets.Add(keyRotOffsets[0]); + if (NumKeyPos > 0) + keyTimeOffsets.Add(keyPosOffsets[0]); + + long trackOffset; + + if (!isInPlaceStream) + { + // Standard mode: track data comes before animations + trackOffset = b.BaseStream.Position; + + // Read track data + ReadTrackData(b, trackOffset, trackDataSize, isInPlaceStream, + keyTimeLengths, keyTimeFormats, keyTimeOffsets, + keyPosLengths, keyPosFormats, keyPosOffsets, + keyRotLengths, keyRotFormats, keyRotOffsets); + + // Seek past track data to read animations + b.BaseStream.Position = trackOffset + trackDataSize; + + // Read animation entries + Animations = new List(); + for (int i = 0; i < NumAnims; i++) { - --keyTimeFormats[(int)EKeyTimesFormat.eUINT16]; - data = Enumerable.Range(0, length).Select(_ => (float)b.ReadUInt16()).ToList(); + Animations.Add(new Animation(b, isInPlaceStream: false)); } - else if (keyTimeFormats[(int)EKeyTimesFormat.eByte] > 0) + } + else + { + // In-place streaming: animations come first + Log.D($"Reading {NumAnims} animations in in-place streaming mode at position 0x{b.BaseStream.Position:X}"); + Animations = new List(); + for (int i = 0; i < NumAnims; i++) { - --keyTimeFormats[(int)EKeyTimesFormat.eByte]; - data = b.ReadBytes(length).Select(x => (float) x).ToList(); + Log.D($"Reading animation {i} at position 0x{b.BaseStream.Position:X}"); + Animations.Add(new Animation(b, isInPlaceStream: true)); } - else if (keyTimeFormats[(int)EKeyTimesFormat.eF32StartStop] > 0) - throw new NotImplementedException(); - else if (keyTimeFormats[(int)EKeyTimesFormat.eUINT16StartStop] > 0) - throw new NotImplementedException(); - else if (keyTimeFormats[(int)EKeyTimesFormat.eByteStartStop] > 0) - throw new NotImplementedException(); - else if (keyTimeFormats[(int)EKeyTimesFormat.eBitset] > 0) + + // Controller headers block (for all animations) + int totalControllers = Animations.Sum(a => a.ControllerCount); + Log.D($"Reading {totalControllers} controller headers at position 0x{b.BaseStream.Position:X}"); + foreach (var anim in Animations) { - --keyTimeFormats[(int)EKeyTimesFormat.eBitset]; - var start = b.ReadUInt16(); - var end = b.ReadUInt16(); - var size = b.ReadUInt16(); - - data = new List(size); - var keyValue = start; - for (var i = 3; i < length; i++) - { - var curr = b.ReadUInt16(); - for (var j = 0; j < 16; ++j) - { - if (((curr >> j) & 1) != 0) - data.Add(keyValue); - ++keyValue; - } - } - - if (data.Count != size) - HelperMethods.Log(LogLevelEnum.Warning, "eBitset: Expected {0} items, got {1} items", size, data.Count); - if (data.Any() && Math.Abs(data[^1] - end) > float.Epsilon) - HelperMethods.Log(LogLevelEnum.Warning, "eBitset: Expected last as {0}, got {1}", end, data[^1]); + anim.Controllers = new List(); + for (int j = 0; j < anim.ControllerCount; j++) + anim.Controllers.Add(new CControllerInfo(b)); } - else - throw new Exception("sum(count per format) != count of keytimes"); - - // Get rid of decreasing entries at end (zero-pads) - while (data.Count >= 2 && data[^2] > data[^1]) - data.RemoveAt(data.Count - 1); - - KeyTimes.Add(data); + + // Align to 4 bytes before track data (per 010 template) + var posBeforeAlign = b.BaseStream.Position; + if ((posBeforeAlign & 3) != 0) + b.BaseStream.Position = (posBeforeAlign & ~3) + 4; + + // Track data is at the end for in-place streaming + trackOffset = b.BaseStream.Position; + Log.D($"Reading track data at offset 0x{trackOffset:X} (aligned from 0x{posBeforeAlign:X})"); + + ReadTrackData(b, trackOffset, trackDataSize, isInPlaceStream, + keyTimeLengths, keyTimeFormats, keyTimeOffsets, + keyPosLengths, keyPosFormats, keyPosOffsets, + keyRotLengths, keyRotFormats, keyRotOffsets); } + } + private List ReadKeyTimeFormatCounts(BinaryReader b) + { + // 7 format types: eF32, eUINT16, eByte, eF32StartStop, eUINT16StartStop, eByteStartStop, eBitset + return Enumerable.Range(0, 7).Select(_ => b.ReadUInt32()).ToList(); + } + + private List ReadCompressionFormatCounts(BinaryReader b) + { + // 9 format types: eNoCompress through eSmallTree64BitExtQuat (not eAutomaticQuat) + return Enumerable.Range(0, 9).Select(_ => b.ReadUInt32()).ToList(); + } + + private void ReadTrackData(BinaryReader b, long trackOffset, int trackDataSize, bool isInPlaceStream, + List keyTimeLengths, List keyTimeFormats, List keyTimeOffsets, + List keyPosLengths, List keyPosFormats, List keyPosOffsets, + List keyRotLengths, List keyRotFormats, List keyRotOffsets) + { + // Clone format counts since we decrement them + var timeFormats = new List(keyTimeFormats); + var posFormats = new List(keyPosFormats); + var rotFormats = new List(keyRotFormats); + + // In-place streaming: offsets are negative and relative to END of track data + // Standard mode: offsets are positive and relative to START of track data + long trackDataEnd = trackOffset + trackDataSize; + + // Read key times + KeyTimes = new List>(); + for (int i = 0; i < keyTimeLengths.Count; i++) + { + long position = isInPlaceStream + ? trackDataEnd + keyTimeOffsets[i] // negative offset from end + : trackOffset + keyTimeOffsets[i]; // positive offset from start + b.BaseStream.Position = position; + KeyTimes.Add(ReadKeyTimeTrack(b, keyTimeLengths[i], timeFormats)); + } + + // Read positions KeyPositions = new List>(); - foreach (var (length, from) in keyPosLengths.Zip(keyPosOffsets)) - { - b.BaseStream.Position = trackOffset + from; - - List data; - if (keyPosFormats[(int) ECompressionFormat.eNoCompress] > 0) - throw new NotImplementedException(); - else if (keyPosFormats[(int) ECompressionFormat.eNoCompressQuat] > 0) - { - --keyPosFormats[(int) ECompressionFormat.eNoCompressQuat]; - data = Enumerable.Range(0, length).Select(_ => b.ReadQuaternion().DropW()).ToList(); - } - else if (keyPosFormats[(int) ECompressionFormat.eNoCompressVec3] > 0) - { - --keyPosFormats[(int) ECompressionFormat.eNoCompressVec3]; - data = Enumerable.Range(0, length).Select(_ => b.ReadVector3()).ToList(); - } - else if (keyPosFormats[(int) ECompressionFormat.eShotInt3Quat] > 0) - { - --keyPosFormats[(int) ECompressionFormat.eShotInt3Quat]; - data = Enumerable.Range(0, length).Select(_ => ((Quaternion)b.ReadShortInt3Quat()).DropW()).ToList(); - } - else if (keyPosFormats[(int) ECompressionFormat.eSmallTreeDWORDQuat] > 0) - { - --keyPosFormats[(int) ECompressionFormat.eSmallTreeDWORDQuat]; - data = Enumerable.Range(0, length).Select(_ => ((Quaternion)b.ReadSmallTreeDWORDQuat()).DropW()).ToList(); - } - else if (keyPosFormats[(int) ECompressionFormat.eSmallTree48BitQuat] > 0) - { - --keyPosFormats[(int) ECompressionFormat.eSmallTree48BitQuat]; - data = Enumerable.Range(0, length).Select(_ => ((Quaternion)b.ReadSmallTree48BitQuat()).DropW()).ToList(); - } - else if (keyPosFormats[(int) ECompressionFormat.eSmallTree64BitQuat] > 0) - { - --keyPosFormats[(int) ECompressionFormat.eSmallTree64BitQuat]; - data = Enumerable.Range(0, length).Select(_ => ((Quaternion)b.ReadSmallTree64BitQuat()).DropW()).ToList(); - } - else if (keyPosFormats[(int) ECompressionFormat.ePolarQuat] > 0) - throw new NotImplementedException(); - else if (keyPosFormats[(int) ECompressionFormat.eSmallTree64BitExtQuat] > 0) - { - --keyPosFormats[(int) ECompressionFormat.eSmallTree64BitExtQuat]; - data = Enumerable.Range(0, length).Select(_ => ((Quaternion)b.ReadSmallTree64BitExtQuat()).DropW()).ToList(); - } - else - throw new Exception("sum(count per format) != count of keypos"); - - KeyPositions.Add(data); + for (int i = 0; i < keyPosLengths.Count; i++) + { + long position = isInPlaceStream + ? trackDataEnd + keyPosOffsets[i] + : trackOffset + keyPosOffsets[i]; + b.BaseStream.Position = position; + KeyPositions.Add(ReadPositionTrack(b, keyPosLengths[i], posFormats)); } + // Read rotations KeyRotations = new List>(); - foreach (var (length, from) in keyRotLengths.Zip(keyRotOffsets)) - { - b.BaseStream.Position = trackOffset + from; - - List data; - if (keyRotFormats[(int) ECompressionFormat.eNoCompress] > 0) - throw new NotImplementedException(); - else if (keyRotFormats[(int) ECompressionFormat.eNoCompressQuat] > 0) - { - --keyRotFormats[(int) ECompressionFormat.eNoCompressQuat]; - data = Enumerable.Range(0, length).Select(_ => b.ReadQuaternion()).ToList(); - } - else if (keyRotFormats[(int) ECompressionFormat.eNoCompressVec3] > 0) - { - --keyRotFormats[(int) ECompressionFormat.eNoCompressVec3]; - data = Enumerable.Range(0, length).Select(_ => new Quaternion(b.ReadVector3(), float.NaN)).ToList(); - } - else if (keyRotFormats[(int) ECompressionFormat.eShotInt3Quat] > 0) - { - --keyRotFormats[(int) ECompressionFormat.eShotInt3Quat]; - data = Enumerable.Range(0, length).Select(_ => (Quaternion)b.ReadShortInt3Quat()).ToList(); - } - else if (keyRotFormats[(int) ECompressionFormat.eSmallTreeDWORDQuat] > 0) - { - --keyRotFormats[(int) ECompressionFormat.eSmallTreeDWORDQuat]; - data = Enumerable.Range(0, length).Select(_ => (Quaternion)b.ReadSmallTreeDWORDQuat()).ToList(); - } - else if (keyRotFormats[(int) ECompressionFormat.eSmallTree48BitQuat] > 0) - { - --keyRotFormats[(int) ECompressionFormat.eSmallTree48BitQuat]; - data = Enumerable.Range(0, length).Select(_ => (Quaternion)b.ReadSmallTree48BitQuat()).ToList(); - } - else if (keyRotFormats[(int) ECompressionFormat.eSmallTree64BitQuat] > 0) - { - --keyRotFormats[(int) ECompressionFormat.eSmallTree64BitQuat]; - data = Enumerable.Range(0, length).Select(_ => (Quaternion)b.ReadSmallTree64BitQuat()).ToList(); - } - else if (keyRotFormats[(int) ECompressionFormat.ePolarQuat] > 0) - throw new NotImplementedException(); - else if (keyRotFormats[(int) ECompressionFormat.eSmallTree64BitExtQuat] > 0) - { - --keyRotFormats[(int) ECompressionFormat.eSmallTree64BitExtQuat]; - data = Enumerable.Range(0, length).Select(_ => (Quaternion)b.ReadSmallTree64BitExtQuat()).ToList(); - } - else + for (int i = 0; i < keyRotLengths.Count; i++) + { + long position = isInPlaceStream + ? trackDataEnd + keyRotOffsets[i] + : trackOffset + keyRotOffsets[i]; + b.BaseStream.Position = position; + KeyRotations.Add(ReadRotationTrack(b, keyRotLengths[i], rotFormats)); + } + } + + private List ReadKeyTimeTrack(BinaryReader b, int elementCount, List formats) + { + List data; + + if (formats[(int)EKeyTimesFormat.eF32] > 0) + { + --formats[(int)EKeyTimesFormat.eF32]; + data = Enumerable.Range(0, elementCount).Select(_ => b.ReadSingle()).ToList(); + } + else if (formats[(int)EKeyTimesFormat.eUINT16] > 0) + { + --formats[(int)EKeyTimesFormat.eUINT16]; + data = Enumerable.Range(0, elementCount).Select(_ => (float)b.ReadUInt16()).ToList(); + } + else if (formats[(int)EKeyTimesFormat.eByte] > 0) + { + --formats[(int)EKeyTimesFormat.eByte]; + data = b.ReadBytes(elementCount).Select(x => (float)x).ToList(); + } + else if (formats[(int)EKeyTimesFormat.eF32StartStop] > 0) + { + throw new NotImplementedException("eF32StartStop key time format"); + } + else if (formats[(int)EKeyTimesFormat.eUINT16StartStop] > 0) + { + throw new NotImplementedException("eUINT16StartStop key time format"); + } + else if (formats[(int)EKeyTimesFormat.eByteStartStop] > 0) + { + throw new NotImplementedException("eByteStartStop key time format"); + } + else if (formats[(int)EKeyTimesFormat.eBitset] > 0) + { + --formats[(int)EKeyTimesFormat.eBitset]; + // eBitset: elementCount is the number of uint16s in the track + var start = b.ReadUInt16(); + var end = b.ReadUInt16(); + var size = b.ReadUInt16(); + + data = new List(size); + var keyValue = start; + for (var i = 3; i < elementCount; i++) { - throw new Exception("sum(count per format) != count of keyRot"); + var curr = b.ReadUInt16(); + for (var j = 0; j < 16; ++j) + { + if (((curr >> j) & 1) != 0) + data.Add(keyValue); + ++keyValue; + } } - - KeyRotations.Add(data); + + if (data.Count != size) + HelperMethods.Log(LogLevelEnum.Warning, "eBitset: Expected {0} items, got {1} items", size, data.Count); + if (data.Any() && Math.Abs(data[^1] - end) > float.Epsilon) + HelperMethods.Log(LogLevelEnum.Warning, "eBitset: Expected last as {0}, got {1}", end, data[^1]); + } + else + { + throw new Exception("sum(count per format) != count of keytimes"); + } + + // Get rid of decreasing entries at end (zero-pads) + while (data.Count >= 2 && data[^2] > data[^1]) + data.RemoveAt(data.Count - 1); + + return data; + } + + private List ReadPositionTrack(BinaryReader b, int elementCount, List formats) + { + List data; + + if (formats[(int)ECompressionFormat.eNoCompress] > 0) + { + throw new NotImplementedException("eNoCompress position format"); + } + else if (formats[(int)ECompressionFormat.eNoCompressQuat] > 0) + { + --formats[(int)ECompressionFormat.eNoCompressQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => b.ReadQuaternion().DropW()).ToList(); + } + else if (formats[(int)ECompressionFormat.eNoCompressVec3] > 0) + { + --formats[(int)ECompressionFormat.eNoCompressVec3]; + data = Enumerable.Range(0, elementCount).Select(_ => b.ReadVector3()).ToList(); + } + else if (formats[(int)ECompressionFormat.eShotInt3Quat] > 0) + { + --formats[(int)ECompressionFormat.eShotInt3Quat]; + data = Enumerable.Range(0, elementCount).Select(_ => ((Quaternion)b.ReadShortInt3Quat()).DropW()).ToList(); + } + else if (formats[(int)ECompressionFormat.eSmallTreeDWORDQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTreeDWORDQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => ((Quaternion)b.ReadSmallTreeDWORDQuat()).DropW()).ToList(); + } + else if (formats[(int)ECompressionFormat.eSmallTree48BitQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTree48BitQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => ((Quaternion)b.ReadSmallTree48BitQuat()).DropW()).ToList(); + } + else if (formats[(int)ECompressionFormat.eSmallTree64BitQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTree64BitQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => ((Quaternion)b.ReadSmallTree64BitQuat()).DropW()).ToList(); + } + else if (formats[(int)ECompressionFormat.ePolarQuat] > 0) + { + throw new NotImplementedException("ePolarQuat position format"); + } + else if (formats[(int)ECompressionFormat.eSmallTree64BitExtQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTree64BitExtQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => ((Quaternion)b.ReadSmallTree64BitExtQuat()).DropW()).ToList(); + } + else + { + throw new Exception("sum(count per format) != count of keypos"); } - - b.BaseStream.Position = trackOffset + trackLength; - Animations = Enumerable.Range(0, (int) NumAnims).Select(_ => new Animation(b)).ToList(); + return data; } - + + private List ReadRotationTrack(BinaryReader b, int elementCount, List formats) + { + List data; + + if (formats[(int)ECompressionFormat.eNoCompress] > 0) + { + throw new NotImplementedException("eNoCompress rotation format"); + } + else if (formats[(int)ECompressionFormat.eNoCompressQuat] > 0) + { + --formats[(int)ECompressionFormat.eNoCompressQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => b.ReadQuaternion()).ToList(); + } + else if (formats[(int)ECompressionFormat.eNoCompressVec3] > 0) + { + --formats[(int)ECompressionFormat.eNoCompressVec3]; + data = Enumerable.Range(0, elementCount).Select(_ => new Quaternion(b.ReadVector3(), float.NaN)).ToList(); + } + else if (formats[(int)ECompressionFormat.eShotInt3Quat] > 0) + { + --formats[(int)ECompressionFormat.eShotInt3Quat]; + data = Enumerable.Range(0, elementCount).Select(_ => (Quaternion)b.ReadShortInt3Quat()).ToList(); + } + else if (formats[(int)ECompressionFormat.eSmallTreeDWORDQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTreeDWORDQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => (Quaternion)b.ReadSmallTreeDWORDQuat()).ToList(); + } + else if (formats[(int)ECompressionFormat.eSmallTree48BitQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTree48BitQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => (Quaternion)b.ReadSmallTree48BitQuat()).ToList(); + } + else if (formats[(int)ECompressionFormat.eSmallTree64BitQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTree64BitQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => (Quaternion)b.ReadSmallTree64BitQuat()).ToList(); + } + else if (formats[(int)ECompressionFormat.ePolarQuat] > 0) + { + throw new NotImplementedException("ePolarQuat rotation format"); + } + else if (formats[(int)ECompressionFormat.eSmallTree64BitExtQuat] > 0) + { + --formats[(int)ECompressionFormat.eSmallTree64BitExtQuat]; + data = Enumerable.Range(0, elementCount).Select(_ => (Quaternion)b.ReadSmallTree64BitExtQuat()).ToList(); + } + else + { + throw new Exception("sum(count per format) != count of keyRot"); + } + + return data; + } + public enum EKeyTimesFormat { eF32, eUINT16, eByte, - eF32StartStop, // unused - eUINT16StartStop, // unused - eByteStartStop, // unused + eF32StartStop, + eUINT16StartStop, + eByteStartStop, eBitset, } @@ -290,14 +449,14 @@ public enum ECompressionFormat [Flags] public enum AssetFlags : uint - { + { Additive = 0x001, Cycle = 0x002, Loaded = 0x004, Lmg = 0x008, LmgValid = 0x020, Created = 0x800, - Requested = 0x1000, + Requested = 0x1000, Ondemand = 0x2000, Aimpose = 0x4000, AimposeUnloaded = 0x8000, @@ -420,28 +579,40 @@ public CControllerInfo(BinaryReader r) } } - public struct Animation + public class Animation { public string Name; public MotionParams905 MotionParams; public byte[] FootPlanBits; + public int ControllerCount; + public int OffsetToControllerHeaders; // Only used in in-place streaming mode public List Controllers; - public Animation(BinaryReader b) + public Animation(BinaryReader b, bool isInPlaceStream) { var nameLen = b.ReadUInt16(); Name = Encoding.UTF8.GetString(b.ReadBytes(nameLen)); - + MotionParams = new MotionParams905(b); var footPlantBitsCount = b.ReadUInt16(); FootPlanBits = b.ReadBytes(footPlantBitsCount); - var controllerCount = b.ReadUInt16(); + ControllerCount = b.ReadUInt16(); - Controllers = new List(); - for (var j = 0u; j < controllerCount; j++) - Controllers.Add(new CControllerInfo(b)); + if (isInPlaceStream) + { + // In-place streaming: controllers are in separate block, just store offset + OffsetToControllerHeaders = b.ReadInt32(); + Controllers = new List(); // Will be filled later + } + else + { + // Standard: controllers inline + Controllers = new List(); + for (var j = 0; j < ControllerCount; j++) + Controllers.Add(new CControllerInfo(b)); + } } } } diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoAnimInfo.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoAnimInfo.cs new file mode 100644 index 00000000..99212b0c --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoAnimInfo.cs @@ -0,0 +1,36 @@ +using System.Numerics; + +namespace CgfConverter.CryEngineCore; + +/// +/// Animation info chunk for Star Citizen #ivo CAF files. +/// Chunk ID: 0x4733C6ED (IvoAnimInfo) +/// Contains animation metadata: FPS, bone count, end frame, and reference pose. +/// Size: 48 bytes. +/// +public class ChunkIvoAnimInfo : Chunk +{ + /// Animation flags (0=normal, 2=pose?). + public uint Flags { get; set; } + + /// Frames per second - typically 30. + public ushort FramesPerSecond { get; set; } + + /// Number of bones in the animation. + public ushort NumBones { get; set; } + + /// Reserved/unknown value (usually 0). + public uint Reserved { get; set; } + + /// Last frame number (matches timeEnd in time keys). + public uint EndFrame { get; set; } + + /// Reference pose rotation. + public Quaternion StartRotation { get; set; } + + /// Reference pose position. + public Vector3 StartPosition { get; set; } + + /// Padding/reserved. + public uint Padding { get; set; } +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoAnimInfo_901.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoAnimInfo_901.cs new file mode 100644 index 00000000..a8ebd059 --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoAnimInfo_901.cs @@ -0,0 +1,28 @@ +using Extensions; +using System.IO; + +namespace CgfConverter.CryEngineCore.Chunks; + +/// +/// Animation info chunk version 0x901. +/// Size: 48 bytes (0x30). +/// +internal sealed class ChunkIvoAnimInfo_901 : ChunkIvoAnimInfo +{ + public override void Read(BinaryReader b) + { + base.Read(b); + + Flags = b.ReadUInt32(); + FramesPerSecond = b.ReadUInt16(); + NumBones = b.ReadUInt16(); + Reserved = b.ReadUInt32(); + EndFrame = b.ReadUInt32(); + StartRotation = b.ReadQuaternion(); + StartPosition = b.ReadVector3(); + Padding = b.ReadUInt32(); + } + + public override string ToString() => + $"ChunkIvoAnimInfo_901: Bones={NumBones}, FPS={FramesPerSecond}, EndFrame={EndFrame}"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF.cs new file mode 100644 index 00000000..3c861eb3 --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF.cs @@ -0,0 +1,40 @@ +using CgfConverter.Models.Structs; +using System.Collections.Generic; +using System.Numerics; + +namespace CgfConverter.CryEngineCore; + +/// +/// CAF animation data chunk for Star Citizen #ivo CAF files. +/// Chunk ID: 0xA9496CB5 (IvoCAFData) +/// Contains a single #caf animation block with bone controllers and keyframe data. +/// +public class ChunkIvoCAF : Chunk +{ + /// The animation block header. + public IvoAnimBlockHeader Header { get; set; } + + /// Bone hash array (CRC32 of bone names). + public uint[] BoneHashes { get; set; } = []; + + /// Controller entries for each bone. + public IvoAnimControllerEntry[] Controllers { get; set; } = []; + + /// File offsets where each controller starts (for relative offset calculations). + public long[] ControllerOffsets { get; set; } = []; + + /// Raw keyframe data. + public byte[] KeyframeData { get; set; } = []; + + /// Parsed rotation keyframes per bone (controller ID -> rotations). + public Dictionary> Rotations { get; set; } = []; + + /// Parsed position keyframes per bone (controller ID -> positions). + public Dictionary> Positions { get; set; } = []; + + /// Parsed rotation key times per bone (controller ID -> times). + public Dictionary> RotationTimes { get; set; } = []; + + /// Parsed position key times per bone (controller ID -> times). + public Dictionary> PositionTimes { get; set; } = []; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF_900.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF_900.cs new file mode 100644 index 00000000..8ab40f5a --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF_900.cs @@ -0,0 +1,317 @@ +using CgfConverter.Models.Structs; +using CgfConverter.Utilities; +using Extensions; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Text; + +namespace CgfConverter.CryEngineCore.Chunks; + +/// +/// CAF animation data chunk version 0x900. +/// Parses #caf animation blocks with bone controllers and keyframe data. +/// +/// Structure per 010 template: +/// - Block header (12 bytes): signature (4), boneCount (uint16), magic (uint16), dataSize (uint32) +/// - Bone hashes (4 bytes each): CRC32 identifiers +/// - Controller entries (24 bytes each): rotation track (12 bytes) + position track (12 bytes) +/// - Each track: numKeys (uint16), formatFlags (uint16), timeOffset (uint32), dataOffset (uint32) +/// - Offsets are relative to the start of each controller, not the controllers array +/// - Animation data: scattered throughout block, accessed via controller offsets +/// +internal sealed class ChunkIvoCAF_900 : ChunkIvoCAF +{ + private long _blockStartOffset; + + public override void Read(BinaryReader b) + { + base.Read(b); + + _blockStartOffset = b.BaseStream.Position; + + // Read block header (12 bytes) + Header = new IvoAnimBlockHeader + { + Signature = Encoding.ASCII.GetString(b.ReadBytes(4)), + BoneCount = b.ReadUInt16(), + Magic = b.ReadUInt16(), + DataSize = b.ReadUInt32() + }; + + if (Header.Signature != "#caf") + { + HelperMethods.Log(LogLevelEnum.Warning, $"ChunkIvoCAF_900: Expected #caf signature, got '{Header.Signature}'"); + return; + } + + // Magic value can be 0xAA55 (DBA) or 0xFFFF (CAF). NEEDS VERIFICATION + if (Header.Magic != 0xAA55 && Header.Magic != 0xFFFF) + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoCAF_900: Magic 0x{Header.Magic:X4}"); + + int numBones = Header.BoneCount; + + // Read bone hash array (4 bytes per bone) + BoneHashes = new uint[numBones]; + for (int i = 0; i < numBones; i++) + { + BoneHashes[i] = b.ReadUInt32(); + } + + // Track start of controllers array - offsets are relative to each controller's start + long controllersArrayStart = b.BaseStream.Position; + + // Read controller entries (24 bytes per bone) + // Per 010 template: rotation track (12 bytes) + position track (12 bytes) + // Offsets are relative to the start of each controller (not the array start) + Controllers = new IvoAnimControllerEntry[numBones]; + ControllerOffsets = new long[numBones]; + for (int i = 0; i < numBones; i++) + { + // Track the file position where this controller starts (needed for offset calculations) + ControllerOffsets[i] = b.BaseStream.Position; + + Controllers[i] = new IvoAnimControllerEntry + { + // Rotation track info (12 bytes) + NumRotKeys = b.ReadUInt16(), + RotFormatFlags = b.ReadUInt16(), + RotTimeOffset = b.ReadUInt32(), + RotDataOffset = b.ReadUInt32(), + + // Position track info (12 bytes) + NumPosKeys = b.ReadUInt16(), + PosFormatFlags = b.ReadUInt16(), + PosTimeOffset = b.ReadUInt32(), + PosDataOffset = b.ReadUInt32() + }; + + var ctrl = Controllers[i]; + + // Warn about unknown rotation format flags + // Known formats: 0x8040 = ubyte time array, 0x8042 = uint16 time with 8-byte header + if (ctrl.HasRotation && ctrl.RotFormatFlags != 0x8040 && ctrl.RotFormatFlags != 0x8042) + { + HelperMethods.Log(LogLevelEnum.Warning, + $"ChunkIvoCAF_900: Bone {i} (0x{BoneHashes[i]:X08}) has unknown rotation format flag 0x{ctrl.RotFormatFlags:X4} (expected 0x8040 or 0x8042)"); + } + + // Warn about unknown position format flags + // Known formats: 0xC0xx (float), 0xC1xx (SNORM full), 0xC2xx (SNORM packed) + if (ctrl.HasPosition) + { + var posFormat = IvoAnimationHelpers.GetPositionFormat(ctrl.PosFormatFlags); + if (posFormat == IvoPositionFormat.None) + { + HelperMethods.Log(LogLevelEnum.Warning, + $"ChunkIvoCAF_900: Bone {i} (0x{BoneHashes[i]:X08}) has unknown position format flag 0x{ctrl.PosFormatFlags:X4}"); + } + } + + // Debug: Log controller entry details + HelperMethods.Log(LogLevelEnum.Debug, + $" Bone {i} (0x{BoneHashes[i]:X08}): " + + $"Rot={ctrl.NumRotKeys}keys @time=0x{ctrl.RotTimeOffset:X} @data=0x{ctrl.RotDataOffset:X} (flags=0x{ctrl.RotFormatFlags:X4}), " + + $"Pos={ctrl.NumPosKeys}keys @time=0x{ctrl.PosTimeOffset:X} @data=0x{ctrl.PosDataOffset:X} (flags=0x{ctrl.PosFormatFlags:X4})"); + } + + // Parse animation data for each bone + // Offsets are relative to each controller's start position in the file + ParseAnimationData(b); + + // Seek to end of block + long blockEnd = _blockStartOffset + Header.DataSize; + b.BaseStream.Seek(blockEnd, SeekOrigin.Begin); + } + + private void ParseAnimationData(BinaryReader b) + { + for (int i = 0; i < Controllers.Length; i++) + { + var ctrl = Controllers[i]; + uint boneHash = BoneHashes[i]; + long controllerStart = ControllerOffsets[i]; + + // Parse rotation data (if present) + if (ctrl.HasRotation && ctrl.NumRotKeys > 0) + { + // Parse rotation time keys using shared helper + List times; + if (ctrl.RotTimeOffset > 0) + { + b.BaseStream.Seek(controllerStart + ctrl.RotTimeOffset, SeekOrigin.Begin); + times = IvoAnimationHelpers.ReadTimeKeys(b, ctrl.NumRotKeys, ctrl.RotFormatFlags); + } + else + { + // No time offset - use sequential frame numbers + times = new List(ctrl.NumRotKeys); + for (int t = 0; t < ctrl.NumRotKeys; t++) + times.Add(t); + } + + RotationTimes[boneHash] = times; + + // Rotation data is at controllerStart + rotDataOffset + b.BaseStream.Seek(controllerStart + ctrl.RotDataOffset, SeekOrigin.Begin); + var rotations = ReadRotationKeys(b, ctrl.NumRotKeys, ctrl.RotFormatFlags); + Rotations[boneHash] = rotations; + } + + // Parse position data (if present) + if (ctrl.HasPosition && ctrl.NumPosKeys > 0) + { + // Parse position time keys using shared helper + List posTimes; + if (ctrl.PosTimeOffset > 0) + { + b.BaseStream.Seek(controllerStart + ctrl.PosTimeOffset, SeekOrigin.Begin); + posTimes = IvoAnimationHelpers.ReadTimeKeys(b, ctrl.NumPosKeys, ctrl.PosFormatFlags); + } + else + { + // No time offset - use sequential frame numbers + posTimes = new List(ctrl.NumPosKeys); + for (int t = 0; t < ctrl.NumPosKeys; t++) + posTimes.Add(t); + } + + PositionTimes[boneHash] = posTimes; + + // Parse position data based on format (high byte) + b.BaseStream.Seek(controllerStart + ctrl.PosDataOffset, SeekOrigin.Begin); + var positions = ReadPositionKeys(b, ctrl.NumPosKeys, ctrl.PosFormatFlags, boneHash); + if (positions.Count > 0) + { + Positions[boneHash] = positions; + } + } + } + + HelperMethods.Log(LogLevelEnum.Debug, + $"ChunkIvoCAF_900: Parsed {Rotations.Count} rotation tracks, {Positions.Count} position tracks"); + } + + private List ReadRotationKeys(BinaryReader b, int count, ushort formatFlags) + { + var rotations = new List(count); + + // Per 010 template: #ivo CAF uses uncompressed quaternions (16 bytes each) + // Format flag 0x8042 indicates standard rotation track with uncompressed quats + byte compression = (byte)(formatFlags & 0xFF); + + for (int i = 0; i < count; i++) + { + Quaternion rot = compression switch + { + 0x42 => b.ReadQuaternion(), // Standard uncompressed (16 bytes) + 0x40 => b.ReadQuaternion(), // Uncompressed variant (16 bytes) + 0x00 => b.ReadQuaternion(), // NoCompressQuat (16 bytes) + _ => b.ReadQuaternion() // Default to uncompressed + }; + rotations.Add(rot); + } + + return rotations; + } + + /// + /// Reads position keyframes based on format flags. + /// + /// Binary reader positioned at position data. + /// Number of position keys. + /// Position format flags (high byte determines format). + /// Bone hash for logging. + private List ReadPositionKeys(BinaryReader b, int count, ushort formatFlags, uint boneHash) + { + var positions = new List(count); + var format = IvoAnimationHelpers.GetPositionFormat(formatFlags); + + switch (format) + { + case IvoPositionFormat.FloatVector3: + // 0xC0xx: Float Vector3 (12 bytes per key), no header + for (int i = 0; i < count; i++) + { + positions.Add(b.ReadVector3()); + } + HelperMethods.Log(LogLevelEnum.Debug, + $" Position (0xC0 float): {count} keys"); + break; + + case IvoPositionFormat.SNormFull: + // 0xC1xx: SNORM with 24-byte header, all channels (6 bytes per key) + { + // Read 24-byte header: channelMask (12 bytes) + scale (12 bytes) + Vector3 channelMask = b.ReadVector3(); + Vector3 scale = b.ReadVector3(); + + for (int i = 0; i < count; i++) + { + short sx = b.ReadInt16(); + short sy = b.ReadInt16(); + short sz = b.ReadInt16(); + + float x = IvoAnimationHelpers.DecompressSNorm(sx, scale.X); + float y = IvoAnimationHelpers.DecompressSNorm(sy, scale.Y); + float z = IvoAnimationHelpers.DecompressSNorm(sz, scale.Z); + + positions.Add(new Vector3(x, y, z)); + } + HelperMethods.Log(LogLevelEnum.Debug, + $" Position (0xC1 SNORM full): {count} keys, scale=({scale.X:F4}, {scale.Y:F4}, {scale.Z:F4})"); + } + break; + + case IvoPositionFormat.SNormPacked: + // 0xC2xx: SNORM with 24-byte header, packed active channels only + { + // Read 24-byte header: channelMask (12 bytes) + scale (12 bytes) + Vector3 channelMask = b.ReadVector3(); + Vector3 scale = b.ReadVector3(); + + bool xActive = IvoAnimationHelpers.IsChannelActive(channelMask.X); + bool yActive = IvoAnimationHelpers.IsChannelActive(channelMask.Y); + bool zActive = IvoAnimationHelpers.IsChannelActive(channelMask.Z); + + for (int i = 0; i < count; i++) + { + float x = 0, y = 0, z = 0; + + if (xActive) + { + short sx = b.ReadInt16(); + x = IvoAnimationHelpers.DecompressSNorm(sx, scale.X); + } + if (yActive) + { + short sy = b.ReadInt16(); + y = IvoAnimationHelpers.DecompressSNorm(sy, scale.Y); + } + if (zActive) + { + short sz = b.ReadInt16(); + z = IvoAnimationHelpers.DecompressSNorm(sz, scale.Z); + } + + positions.Add(new Vector3(x, y, z)); + } + + string activeChannels = $"{(xActive ? "X" : "")}{(yActive ? "Y" : "")}{(zActive ? "Z" : "")}"; + HelperMethods.Log(LogLevelEnum.Debug, + $" Position (0xC2 SNORM packed): {count} keys, active=[{activeChannels}], scale=({scale.X:F4}, {scale.Y:F4}, {scale.Z:F4})"); + } + break; + + default: + HelperMethods.Log(LogLevelEnum.Warning, + $"ChunkIvoCAF_900: Bone 0x{boneHash:X08} has unknown position format 0x{formatFlags:X4}"); + break; + } + + return positions; + } + + public override string ToString() => + $"ChunkIvoCAF_900: {Header.Signature} Bones={Header.BoneCount}, DataSize={Header.DataSize}"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAData.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAData.cs new file mode 100644 index 00000000..63a3aa98 --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAData.cs @@ -0,0 +1,18 @@ +using CgfConverter.Models.Structs; +using System.Collections.Generic; + +namespace CgfConverter.CryEngineCore; + +/// +/// DBA animation data chunk for Star Citizen #ivo DBA files. +/// Chunk ID: 0x194FBC50 (IvoDBAData) +/// Contains multiple #dba animation blocks. +/// +public class ChunkIvoDBAData : Chunk +{ + /// Total size of all animation blocks. + public uint TotalDataSize { get; set; } + + /// Parsed animation blocks. + public List AnimationBlocks { get; set; } = []; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAData_900.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAData_900.cs new file mode 100644 index 00000000..8fd92840 --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAData_900.cs @@ -0,0 +1,190 @@ +using CgfConverter.Models.Structs; +using CgfConverter.Utilities; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Text; + +namespace CgfConverter.CryEngineCore.Chunks; + +/// +/// DBA animation data chunk version 0x900. +/// Parses multiple #dba animation blocks with full keyframe data. +/// +internal sealed class ChunkIvoDBAData_900 : ChunkIvoDBAData +{ + public override void Read(BinaryReader b) + { + base.Read(b); + + TotalDataSize = b.ReadUInt32(); + + long dataEnd = b.BaseStream.Position + TotalDataSize - 4; + + // Parse #dba blocks until we reach the end + int blockIndex = 0; + while (b.BaseStream.Position < dataEnd) + { + // Check for #dba signature + long blockStart = b.BaseStream.Position; + string sig = Encoding.ASCII.GetString(b.ReadBytes(4)); + + if (sig != "#dba") + { + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAData_900: Expected #dba signature at offset {blockStart:X}, got '{sig}'"); + break; + } + + // Read block header + var header = new IvoAnimBlockHeader + { + Signature = sig, + BoneCount = b.ReadUInt16(), + Magic = b.ReadUInt16(), + DataSize = b.ReadUInt32() + }; + + if (header.Magic != 0xAA55) + { + HelperMethods.Log(LogLevelEnum.Warning, $"ChunkIvoDBAData_900: Expected magic 0xAA55, got 0x{header.Magic:X4}"); + } + + int numBones = header.BoneCount; + + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAData_900: Block {blockIndex} - {numBones} bones, size={header.DataSize}"); + + // Read bone hash array + var boneHashes = new uint[numBones]; + for (int i = 0; i < numBones; i++) + { + boneHashes[i] = b.ReadUInt32(); + } + + // Read controller entries (24 bytes per bone) + // Offsets are relative to the start of each controller entry (same as CAF) + var controllers = new IvoAnimControllerEntry[numBones]; + var controllerOffsets = new long[numBones]; + for (int i = 0; i < numBones; i++) + { + controllerOffsets[i] = b.BaseStream.Position; + + controllers[i] = new IvoAnimControllerEntry + { + // Rotation track info (12 bytes) + NumRotKeys = b.ReadUInt16(), + RotFormatFlags = b.ReadUInt16(), + RotTimeOffset = b.ReadUInt32(), + RotDataOffset = b.ReadUInt32(), + + // Position track info (12 bytes) + NumPosKeys = b.ReadUInt16(), + PosFormatFlags = b.ReadUInt16(), + PosTimeOffset = b.ReadUInt32(), + PosDataOffset = b.ReadUInt32() + }; + } + + // Save position after controller headers - this is where the next block starts! + // DBA format: headers are sequential, keyframe data is at the end accessed via offsets. + // The next #dba block starts right after the current block's controller headers, + // NOT at blockEnd (which is where this block's keyframe data extends to). + long positionAfterHeaders = b.BaseStream.Position; + + // Create animation block with parsed data + var animBlock = new IvoAnimationBlock + { + Header = header, + BoneHashes = boneHashes, + Controllers = controllers, + ControllerOffsets = controllerOffsets + }; + + // Parse keyframe data for each bone (this seeks around within the data area) + ParseAnimationData(b, animBlock); + + AnimationBlocks.Add(animBlock); + + // Restore position to after headers - next block starts here, not at blockEnd + b.BaseStream.Seek(positionAfterHeaders, SeekOrigin.Begin); + blockIndex++; + } + + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAData_900: Parsed {AnimationBlocks.Count} animation blocks"); + } + + private void ParseAnimationData(BinaryReader b, IvoAnimationBlock block) + { + int rotationCount = 0; + int positionCount = 0; + + for (int i = 0; i < block.Controllers.Length; i++) + { + var ctrl = block.Controllers[i]; + uint boneHash = block.BoneHashes[i]; + long controllerStart = block.ControllerOffsets[i]; + + // Parse rotation data (if present) + if (ctrl.HasRotation && ctrl.NumRotKeys > 0) + { + // Parse rotation time keys + List rotTimes; + if (ctrl.RotTimeOffset > 0) + { + b.BaseStream.Seek(controllerStart + ctrl.RotTimeOffset, SeekOrigin.Begin); + rotTimes = IvoAnimationHelpers.ReadTimeKeys(b, ctrl.NumRotKeys, ctrl.RotFormatFlags); + } + else + { + // No time offset - use sequential frame numbers + rotTimes = new List(ctrl.NumRotKeys); + for (int t = 0; t < ctrl.NumRotKeys; t++) + rotTimes.Add(t); + } + + block.RotationTimes[boneHash] = rotTimes; + + // Parse rotation data + b.BaseStream.Seek(controllerStart + ctrl.RotDataOffset, SeekOrigin.Begin); + var rotations = IvoAnimationHelpers.ReadRotationKeys(b, ctrl.NumRotKeys); + block.Rotations[boneHash] = rotations; + rotationCount++; + } + + // Parse position data (if present) + if (ctrl.HasPosition && ctrl.NumPosKeys > 0) + { + // Parse position time keys + List posTimes; + if (ctrl.PosTimeOffset > 0) + { + b.BaseStream.Seek(controllerStart + ctrl.PosTimeOffset, SeekOrigin.Begin); + posTimes = IvoAnimationHelpers.ReadTimeKeys(b, ctrl.NumPosKeys, ctrl.PosFormatFlags); + } + else + { + // No time offset - use sequential frame numbers + posTimes = new List(ctrl.NumPosKeys); + for (int t = 0; t < ctrl.NumPosKeys; t++) + posTimes.Add(t); + } + + block.PositionTimes[boneHash] = posTimes; + + // Parse position data + b.BaseStream.Seek(controllerStart + ctrl.PosDataOffset, SeekOrigin.Begin); + var positions = IvoAnimationHelpers.ReadPositionKeys(b, ctrl.NumPosKeys, ctrl.PosFormatFlags); + if (positions.Count > 0) + { + block.Positions[boneHash] = positions; + positionCount++; + } + } + } + + HelperMethods.Log(LogLevelEnum.Debug, + $" Parsed {rotationCount} rotation tracks, {positionCount} position tracks"); + } + + public override string ToString() => + $"ChunkIvoDBAData_900: TotalSize={TotalDataSize}, Animations={AnimationBlocks.Count}"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata.cs new file mode 100644 index 00000000..d1c9c0bc --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata.cs @@ -0,0 +1,21 @@ +using CgfConverter.Models.Structs; +using System.Collections.Generic; + +namespace CgfConverter.CryEngineCore; + +/// +/// DBA metadata chunk for Star Citizen #ivo DBA files. +/// Chunk ID: 0xF7351608 (IvoDBAMetadata) +/// Contains animation metadata entries (44 bytes each) and path string table. +/// +public class ChunkIvoDBAMetadata : Chunk +{ + /// Number of animations in the library. + public uint AnimCount { get; set; } + + /// Metadata entries for each animation (44 bytes each). + public List Entries { get; set; } = []; + + /// Animation path strings (null-terminated). + public List AnimPaths { get; set; } = []; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata_900.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata_900.cs new file mode 100644 index 00000000..a6ab049b --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata_900.cs @@ -0,0 +1,60 @@ +using CgfConverter.Models.Structs; +using Extensions; +using System.IO; +using System.Text; + +namespace CgfConverter.CryEngineCore.Chunks; + +/// +/// DBA metadata chunk version 0x900. +/// Contains animation metadata entries (44 bytes each) and path string table. +/// +internal sealed class ChunkIvoDBAMetadata_900 : ChunkIvoDBAMetadata +{ + public override void Read(BinaryReader b) + { + base.Read(b); + + AnimCount = b.ReadUInt32(); + + // Read metadata entries (44 bytes each) + // Layout: Flags(4), FPS(2), NumControllers(2), Unknown1(4), Unknown2(4), StartRotation(16), StartPosition(12) + for (int i = 0; i < AnimCount; i++) + { + var entry = new IvoDBAMetaEntry + { + Flags = b.ReadUInt32(), + FramesPerSecond = b.ReadUInt16(), + NumControllers = b.ReadUInt16(), + Unknown1 = b.ReadUInt32(), + Unknown2 = b.ReadUInt32(), + StartRotation = b.ReadQuaternion(), + StartPosition = b.ReadVector3() + }; + Entries.Add(entry); + } + + // Read string table - null-terminated animation paths + for (int i = 0; i < AnimCount; i++) + { + var path = ReadNullTerminatedString(b); + AnimPaths.Add(path); + } + } + + private static string ReadNullTerminatedString(BinaryReader b) + { + var sb = new StringBuilder(); + while (true) + { + byte c = b.ReadByte(); + if (c == 0) + break; + sb.Append((char)c); + } + return sb.ToString(); + } + + public override string ToString() => + $"ChunkIvoDBAMetadata_900: AnimCount={AnimCount}"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata_901.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata_901.cs new file mode 100644 index 00000000..8cc79cd6 --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoDBAMetadata_901.cs @@ -0,0 +1,83 @@ +using CgfConverter.Models.Structs; +using CgfConverter.Utilities; +using Extensions; +using System.IO; +using System.Text; + +namespace CgfConverter.CryEngineCore.Chunks; + +/// +/// DBA metadata chunk version 0x901. +/// Contains animation metadata entries (44 bytes each) and path string table. +/// +internal sealed class ChunkIvoDBAMetadata_901 : ChunkIvoDBAMetadata +{ + public override void Read(BinaryReader b) + { + base.Read(b); + + long startOffset = b.BaseStream.Position; + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAMetadata_901: Reading at offset 0x{startOffset:X}"); + + AnimCount = b.ReadUInt32(); + + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAMetadata_901: AnimCount={AnimCount}"); + + // Read metadata entries (44 bytes each) + // Layout: Flags(4), FPS(2), NumControllers(2), Unknown1(4), Unknown2(4), StartRotation(16), StartPosition(12) + for (int i = 0; i < AnimCount; i++) + { + long entryOffset = b.BaseStream.Position; + var entry = new IvoDBAMetaEntry + { + Flags = b.ReadUInt32(), + FramesPerSecond = b.ReadUInt16(), + NumControllers = b.ReadUInt16(), + Unknown1 = b.ReadUInt32(), + Unknown2 = b.ReadUInt32(), + StartRotation = b.ReadQuaternion(), + StartPosition = b.ReadVector3() + }; + Entries.Add(entry); + + var rot = entry.StartRotation; + var pos = entry.StartPosition; + HelperMethods.Log(LogLevelEnum.Debug, + $"ChunkIvoDBAMetadata_901: Entry[{i}] @ 0x{entryOffset:X}: " + + $"Flags=0x{entry.Flags:X}, FPS={entry.FramesPerSecond}, Controllers={entry.NumControllers}, " + + $"Unknown1=0x{entry.Unknown1:X}, Unknown2=0x{entry.Unknown2:X}, " + + $"StartRot=({rot.X:F3}, {rot.Y:F3}, {rot.Z:F3}, {rot.W:F3}), " + + $"StartPos=({pos.X:F3}, {pos.Y:F3}, {pos.Z:F3})"); + } + + // Read string table - null-terminated animation paths + long stringTableOffset = b.BaseStream.Position; + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAMetadata_901: String table at offset 0x{stringTableOffset:X}"); + + for (int i = 0; i < AnimCount; i++) + { + var path = ReadNullTerminatedString(b); + AnimPaths.Add(path); + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAMetadata_901: AnimPath[{i}] = \"{path}\""); + } + + long endOffset = b.BaseStream.Position; + HelperMethods.Log(LogLevelEnum.Debug, $"ChunkIvoDBAMetadata_901: Finished at offset 0x{endOffset:X}, read {endOffset - startOffset} bytes"); + } + + private static string ReadNullTerminatedString(BinaryReader b) + { + var sb = new StringBuilder(); + while (true) + { + byte c = b.ReadByte(); + if (c == 0) + break; + sb.Append((char)c); + } + return sb.ToString(); + } + + public override string ToString() => + $"ChunkIvoDBAMetadata_901: AnimCount={AnimCount}"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkIvoSkinMesh_900.cs b/CgfConverter/CryEngineCore/Chunks/ChunkIvoSkinMesh_900.cs index 50bd8956..b75f5238 100644 --- a/CgfConverter/CryEngineCore/Chunks/ChunkIvoSkinMesh_900.cs +++ b/CgfConverter/CryEngineCore/Chunks/ChunkIvoSkinMesh_900.cs @@ -137,40 +137,50 @@ public override void Read(BinaryReader b) break; case DatastreamType.IVOTANGENTS: bytesPerElement = b.ReadUInt32(); - Datastream tangents = new( - DatastreamType.IVOTANGENTS, - meshDetails.NumberOfVertices, - bytesPerElement, - new Quaternion[meshDetails.NumberOfVertices]); - Datastream bitangents = new( - DatastreamType.IVOTANGENTS, - meshDetails.NumberOfVertices, - bytesPerElement, - new Quaternion[meshDetails.NumberOfVertices]); - if (bytesPerElement == 8) { + // 8-byte format uses smallest-three quaternion encoding for TBN matrix + Datastream tangentNormals = new( + DatastreamType.NORMALS, + meshDetails.NumberOfVertices, + bytesPerElement, + new Vector3[meshDetails.NumberOfVertices]); + for (int i = 0; i < meshDetails.NumberOfVertices; i++) { - tangents.Data[i] = b.ReadQuaternion(InputType.SNorm); + var frame = b.ReadIvoTangentFrame(); + var (normal, _, _) = frame.Decode(); + tangentNormals.Data[i] = normal; } + Normals = tangentNormals; } - else if (tangents.BytesPerElement == 16) + else if (bytesPerElement == 16) { + Datastream tangents = new( + DatastreamType.IVOTANGENTS, + meshDetails.NumberOfVertices, + bytesPerElement, + new Quaternion[meshDetails.NumberOfVertices]); + Datastream bitangents = new( + DatastreamType.IVOTANGENTS, + meshDetails.NumberOfVertices, + bytesPerElement, + new Quaternion[meshDetails.NumberOfVertices]); + for (int i = 0; i < meshDetails.NumberOfVertices; i++) { tangents.Data[i] = b.ReadQuaternion(InputType.SNorm); bitangents.Data[i] = b.ReadQuaternion(InputType.SNorm); } + Tangents = tangents; + BiTangents = bitangents; } else - throw new NotSupportedException($"Unsupported tangents format: {tangents.BytesPerElement}"); - Tangents = tangents; - BiTangents = bitangents; + throw new NotSupportedException($"Unsupported tangents format: {bytesPerElement}"); b.AlignTo(8); break; case DatastreamType.IVOQTANGENTS: - // For Ivo files, these are qtangents using SNORM (int16 I think). 8 bytes. + // IVOQTANGENTS uses the same smallest-three format as IVOTANGENTS when 8 bytes bytesPerElement = b.ReadUInt32(); Datastream qtangents = new( DatastreamType.IVOTANGENTS, @@ -184,18 +194,19 @@ public override void Read(BinaryReader b) new Vector3[meshDetails.NumberOfVertices]); if (qtangents.BytesPerElement == 8) { + // Use IvoTangentFrame for 8-byte format (smallest-three encoding) for (int i = 0; i < meshDetails.NumberOfVertices; i++) { - Quaternion q = b.ReadQuaternion(InputType.SNorm); - qtangents.Data[i] = q; - normals2.Data[i] = q.GetNormalFromQTangent(); + var frame = b.ReadIvoTangentFrame(); + var (normal, _, _) = frame.Decode(); + normals2.Data[i] = normal; } } else if (qtangents.BytesPerElement == 16) { for (int i = 0; i < meshDetails.NumberOfVertices; i++) { - // TODO: Finish this or ignore. + // 16-byte format uses direct quaternion Quaternion q = b.ReadQuaternion(InputType.Single); qtangents.Data[i] = q; normals2.Data[i] = q.GetNormalFromQTangent(); @@ -303,9 +314,36 @@ public override void Read(BinaryReader b) b.AlignTo(8); Colors = colors; break; - case DatastreamType.IVOUNKNOWN: + case DatastreamType.IVOSIMPLEBONEMAP: + // Simple bone mapping: single ushort bone index per vertex + // Used for rigid attachment meshes where each vertex is influenced by a single bone + // Weight is implied 1.0 (100%) since there's only one influence bytesPerElement = b.ReadUInt32(); - SkipBytes(b, bytesPerElement * meshDetails.NumberOfVertices); + if (bytesPerElement == 2) + { + Datastream simpleBoneMaps = new( + DatastreamType.IVOSIMPLEBONEMAP, + meshDetails.NumberOfVertices, + bytesPerElement, + new MeshBoneMapping[meshDetails.NumberOfVertices]); + for (int i = 0; i < meshDetails.NumberOfVertices; i++) + { + ushort boneIndex = b.ReadUInt16(); + MeshBoneMapping bm = new() + { + BoneInfluenceCount = 1, + BoneIndex = [boneIndex, 0, 0, 0], + Weight = [1.0f, 0, 0, 0] + }; + simpleBoneMaps.Data[i] = bm; + } + BoneMappings = simpleBoneMaps; + } + else + { + HelperMethods.Log(LogLevelEnum.Warning, $"Unexpected simple bone map bytes per element: {bytesPerElement}"); + SkipBytes(b, bytesPerElement * meshDetails.NumberOfVertices); + } b.AlignTo(8); break; default: diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkMotionParameters.cs b/CgfConverter/CryEngineCore/Chunks/ChunkMotionParameters.cs new file mode 100644 index 00000000..7a58e39b --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkMotionParameters.cs @@ -0,0 +1,49 @@ +using System.Numerics; + +namespace CgfConverter.CryEngineCore; + +/// +/// Motion Parameters chunk (0x3002) - Contains animation timing and motion data. +/// Similar to MotionParams905 struct in DBA files, but as a standalone chunk in CAF files. +/// +public abstract class ChunkMotionParameters : Chunk +{ + public uint AssetFlags { get; internal set; } + public uint Compression { get; internal set; } + public int TicksPerFrame { get; internal set; } + public float SecsPerTick { get; internal set; } + public int Start { get; internal set; } + public int End { get; internal set; } + public float MoveSpeed { get; internal set; } + public float TurnSpeed { get; internal set; } + public float AssetTurn { get; internal set; } + public float Distance { get; internal set; } + public float Slope { get; internal set; } + + // StartLocation (QuatT = Quaternion + Vector3) + public Quaternion StartLocationQ { get; internal set; } + public Vector3 StartLocationT { get; internal set; } + + // EndLocation (QuatT = Quaternion + Vector3) + public Quaternion EndLocationQ { get; internal set; } + public Vector3 EndLocationT { get; internal set; } + + // Foot plant timing data + public float LHeelStart { get; internal set; } + public float LHeelEnd { get; internal set; } + public float LToe0Start { get; internal set; } + public float LToe0End { get; internal set; } + public float RHeelStart { get; internal set; } + public float RHeelEnd { get; internal set; } + public float RToe0Start { get; internal set; } + public float RToe0End { get; internal set; } + + /// Duration in seconds based on ticks. + public float Duration => (End - Start) * SecsPerTick; + + /// Frame count. + public int FrameCount => TicksPerFrame > 0 ? (End - Start) / TicksPerFrame : 0; + + public override string ToString() => + $"ChunkMotionParameters: Start={Start}, End={End}, TicksPerFrame={TicksPerFrame}, SecsPerTick={SecsPerTick}, Duration={Duration:F3}s"; +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkMotionParameters_925.cs b/CgfConverter/CryEngineCore/Chunks/ChunkMotionParameters_925.cs new file mode 100644 index 00000000..85fdee1e --- /dev/null +++ b/CgfConverter/CryEngineCore/Chunks/ChunkMotionParameters_925.cs @@ -0,0 +1,46 @@ +using System.IO; +using Extensions; + +namespace CgfConverter.CryEngineCore; + +/// +/// Motion Parameters chunk version 0x925 (2341 decimal). +/// 0x84 bytes (132 decimal) of motion parameter data. +/// +internal sealed class ChunkMotionParameters_925 : ChunkMotionParameters +{ + public override void Read(BinaryReader b) + { + base.Read(b); + + AssetFlags = b.ReadUInt32(); + Compression = b.ReadUInt32(); + TicksPerFrame = b.ReadInt32(); + SecsPerTick = b.ReadSingle(); + Start = b.ReadInt32(); + End = b.ReadInt32(); + MoveSpeed = b.ReadSingle(); + TurnSpeed = b.ReadSingle(); + AssetTurn = b.ReadSingle(); + Distance = b.ReadSingle(); + Slope = b.ReadSingle(); + + // StartLocation (QuatT: Quaternion 16 bytes + Vector3 12 bytes = 28 bytes) + StartLocationQ = b.ReadQuaternion(); + StartLocationT = b.ReadVector3(); + + // EndLocation (QuatT: Quaternion 16 bytes + Vector3 12 bytes = 28 bytes) + EndLocationQ = b.ReadQuaternion(); + EndLocationT = b.ReadVector3(); + + // Foot plant timing + LHeelStart = b.ReadSingle(); + LHeelEnd = b.ReadSingle(); + LToe0Start = b.ReadSingle(); + LToe0End = b.ReadSingle(); + RHeelStart = b.ReadSingle(); + RHeelEnd = b.ReadSingle(); + RToe0Start = b.ReadSingle(); + RToe0End = b.ReadSingle(); + } +} diff --git a/CgfConverter/CryEngineCore/Chunks/ChunkNodeMeshCombo_900.cs b/CgfConverter/CryEngineCore/Chunks/ChunkNodeMeshCombo_900.cs index 2afdda17..b91ee4f8 100644 --- a/CgfConverter/CryEngineCore/Chunks/ChunkNodeMeshCombo_900.cs +++ b/CgfConverter/CryEngineCore/Chunks/ChunkNodeMeshCombo_900.cs @@ -51,7 +51,7 @@ public override void Read(BinaryReader b) MaterialIndices.Add(b.ReadUInt16()); } - NodeNames = GetNullSeparatedStrings(NumberOfNodes, b); + NodeNames = GetNullSeparatedStrings(NumberOfNodes, StringTableSize, b); // There is more data after here but it's unknown. } } diff --git a/CgfConverter/CryEngineCore/Model.cs b/CgfConverter/CryEngineCore/Model.cs index 2a57bfd2..841578de 100644 --- a/CgfConverter/CryEngineCore/Model.cs +++ b/CgfConverter/CryEngineCore/Model.cs @@ -156,7 +156,7 @@ private void ReadFileHeader(BinaryReader b) return; } - throw new NotSupportedException($"Unsupported FileS ignature {FileSignature}"); + throw new NotSupportedException($"Unsupported File Signature {FileSignature}"); } private void CreateDummyRootNode() @@ -201,6 +201,9 @@ private void ReadChunks(BinaryReader reader) { foreach (ChunkHeader chunkHeaderItem in chunkHeaders) { + Utilities.HelperMethods.Log(Utilities.LogLevelEnum.Debug, + $"[{Path.GetFileName(FileName)}] Reading chunk: Type={chunkHeaderItem.ChunkType}, Version=0x{chunkHeaderItem.Version:X}, ID={chunkHeaderItem.ID}, Offset=0x{chunkHeaderItem.Offset:X}, Size={chunkHeaderItem.Size}"); + var chunk = Chunk.New(chunkHeaderItem.ChunkType, chunkHeaderItem.Version); ChunkMap[chunkHeaderItem.ID] = chunk; @@ -216,7 +219,7 @@ private void ReadChunks(BinaryReader reader) // Add Bones to the model. We are assuming there is only one CompiledBones chunk per file. if (chunkHeaderItem.ChunkType == ChunkType.CompiledBones || chunkHeaderItem.ChunkType == ChunkType.CompiledBonesSC || - //chunkHeaderItem.ChunkType == ChunkType.CompiledBonesIvo || + chunkHeaderItem.ChunkType == ChunkType.CompiledBones_Ivo || chunkHeaderItem.ChunkType == ChunkType.CompiledBones_Ivo2) { Bones = chunk as ChunkCompiledBones; diff --git a/CgfConverter/Enums/Enums.cs b/CgfConverter/Enums/Enums.cs index 475896cb..d10a960e 100644 --- a/CgfConverter/Enums/Enums.cs +++ b/CgfConverter/Enums/Enums.cs @@ -82,7 +82,8 @@ public enum ChunkType : uint // complete BonesBoxes = 0xAAFC0004, // unknown chunk FoliageInfo = 0xAAFC0005, // unknown chunk GlobalAnimationHeaderCAF = 0xAAFC0007, - + MotionParams = 0x3002, + // Star Citizen versions NodeSC = 0xCCCC100B, CompiledBonesSC = 0xCCCC1000, @@ -108,6 +109,12 @@ public enum ChunkType : uint // complete BShapesGPU = 0x57A3BEFD, BShapes = 0x875CCB28, + // Star Citizen #ivo animation chunks + IvoAnimInfo = 0x4733C6ED, // Animation info chunk (CAF), v901 + IvoCAFData = 0xA9496CB5, // #caf animation data + IvoDBAData = 0x194FBC50, // #dba animation data blocks (was SpeedInfoSC) + IvoDBAMetadata = 0xF7351608, // DBA metadata/string table + BinaryXmlDataSC = 0xcccbf004, } @@ -204,7 +211,7 @@ public enum DatastreamType : uint IVOVERTSUVS = 0x91329AE9, IVOVERTSUVS2 = 0xB3A70D5E, IVOBONEMAP32 = 0x6ECA3708, // Objects\Characters\Human\heads\male\npc\male01\male01_t2_head.skinm - IVOUNKNOWN = 0x9D51C5EE, // box.cgfm. 2 bytes, all zeros, numvertices + IVOSIMPLEBONEMAP = 0x9D51C5EE, // Simple bone mapping: ushort bone index per vertex, weight implied 1.0 (rigid attachment meshes) } public enum PhysicsPrimitiveType : uint diff --git a/CgfConverter/Models/CafAnimation.cs b/CgfConverter/Models/CafAnimation.cs new file mode 100644 index 00000000..b5635179 --- /dev/null +++ b/CgfConverter/Models/CafAnimation.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace CgfConverter.Models; + +/// +/// Represents a single animation from a CAF file. +/// Aggregates all bone controllers into a unified animation structure. +/// +public class CafAnimation +{ + /// Name of the animation (from chrparams or filename). + public required string Name { get; init; } + + /// Source file path. + public required string FilePath { get; init; } + + /// Seconds per tick from timing chunk. + public float SecsPerTick { get; set; } = 1f / 4800f; // Default: 4800 ticks/sec + + /// Ticks per frame from timing chunk. + public int TicksPerFrame { get; set; } = 160; // Default: 30fps + + /// Start frame of the animation. + public int StartFrame { get; set; } + + /// End frame of the animation. + public int EndFrame { get; set; } + + /// + /// Whether this is an additive animation (stores deltas from rest pose). + /// Additive animations need to be converted to absolute transforms for export. + /// + public bool IsAdditive { get; set; } + + /// + /// Per-bone animation tracks, keyed by controller ID (CRC32 of bone name). + /// + public Dictionary BoneTracks { get; } = []; + + /// + /// Mapping from controller ID to bone name (from BoneNameList chunk). + /// + public Dictionary ControllerIdToBoneName { get; } = []; + + /// + /// Gets the duration in frames. + /// + public int DurationFrames => EndFrame - StartFrame; + + /// + /// Gets the duration in seconds. + /// + public float DurationSeconds => DurationFrames * TicksPerFrame * SecsPerTick; +} + +/// +/// Animation track for a single bone. +/// +public class BoneTrack +{ + /// Controller ID (CRC32 of bone name). + public uint ControllerId { get; init; } + + /// Rotation keyframe times (in ticks or frames depending on source). + public List RotationKeyTimes { get; init; } = []; + + /// Position keyframe times (in ticks or frames depending on source). + public List PositionKeyTimes { get; init; } = []; + + /// Position keyframes. + public List Positions { get; init; } = []; + + /// Rotation keyframes. + public List Rotations { get; init; } = []; + + /// + /// Legacy property for backward compatibility. Returns rotation key times. + /// + public List KeyTimes => RotationKeyTimes.Count > 0 ? RotationKeyTimes : PositionKeyTimes; +} diff --git a/CgfConverter/Models/CompiledBone.cs b/CgfConverter/Models/CompiledBone.cs index 3c1f94c7..9d29a878 100644 --- a/CgfConverter/Models/CompiledBone.cs +++ b/CgfConverter/Models/CompiledBone.cs @@ -19,7 +19,7 @@ public sealed class CompiledBone public int OffsetParent { get; set; } // offset to the parent in number of CompiledBone structs (584 bytes) public int OffsetChild { get; set; } // Offset to the first child to this bone in number of CompiledBone structs. Don't use this. Not in Ivo files. public int NumberOfChildren { get; set; } // Number of children to this bone - public int ObjectNodeIndex { get; set; } // Points to index of NodeMeshCombo chunk (Ivo file) + public int ObjectNodeIndex { get; set; } = -1; // Points to index of NodeMeshCombo chunk (Ivo file only, -1 = not set) public int ParentIndex { get; set; } // For 0x900, we don't have parent offset. // Calculated values public Matrix4x4 BindPoseMatrix { get; set; } // This is the WorldToBone matrix for library_controllers @@ -58,18 +58,36 @@ public void ReadCompiledBone_800(BinaryReader b) public void ReadCompiledBone_801(BinaryReader b) { // Reads just a single 324 byte entry of a bone. + // Unlike 0x800 which stores both W2B and B2W matrices, 0x801 only stores B2W (boneToWorld). + // We must compute W2B by inverting B2W. ControllerID = b.ReadUInt32(); // Bone controller. Can be 0xFFFFFFFF LimbId = b.ReadUInt32(); PhysicsGeometry = new PhysicsGeometry[2]; - PhysicsGeometry[0].ReadPhysicsGeometry(b); // LOD 0 is the physics of alive body, + PhysicsGeometry[0].ReadPhysicsGeometry(b); // LOD 0 is the physics of alive body, PhysicsGeometry[1].ReadPhysicsGeometry(b); // LOD 1 is the physics of a dead body BoneName = b.ReadFString(48); OffsetParent = b.ReadInt32(); NumberOfChildren = b.ReadInt32(); OffsetChild = b.ReadInt32(); - LocalTransformMatrix = b.ReadMatrix3x4(); - BindPoseMatrix = LocalTransformMatrix.ConvertToTransformMatrix(); - WorldTransformMatrix = new(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0); + + // The matrix stored in 0x801 is B2W (boneToWorld), not W2B like in 0x800 + WorldTransformMatrix = b.ReadMatrix3x4(); + + // Compute W2B (worldToBone) by inverting B2W - this is the bind pose matrix + var boneToWorld = WorldTransformMatrix.ConvertToTransformMatrix(); + if (Matrix4x4.Invert(boneToWorld, out var worldToBone)) + { + BindPoseMatrix = worldToBone; + } + else + { + // Fallback to identity if inversion fails + BindPoseMatrix = Matrix4x4.Identity; + } + + // LocalTransformMatrix will be computed after parent relationships are established + // For now, use the B2W matrix (actual local will be computed in ChunkCompiledBones_801) + LocalTransformMatrix = WorldTransformMatrix; } public void ReadCompiledBone_900(BinaryReader b) diff --git a/CgfConverter/Models/GeometryInfo.cs b/CgfConverter/Models/GeometryInfo.cs index 48735bf6..c9b88129 100644 --- a/CgfConverter/Models/GeometryInfo.cs +++ b/CgfConverter/Models/GeometryInfo.cs @@ -14,6 +14,7 @@ public sealed record GeometryInfo public Datastream? Indices { get; set; } public Datastream? Vertices { get; set; } public Datastream? UVs { get; set; } + public Datastream? UVs2 { get; set; } // Second UV layer (for games supporting multiple UV sets) public Datastream? Normals { get; set; } public Datastream? Colors { get; set; } public Datastream? VertUVs { get; set; } diff --git a/CgfConverter/Models/Materials/Texture.cs b/CgfConverter/Models/Materials/Texture.cs index caaa2e5f..0ca1758c 100644 --- a/CgfConverter/Models/Materials/Texture.cs +++ b/CgfConverter/Models/Materials/Texture.cs @@ -96,6 +96,7 @@ public MapTypeEnum Map { // Backwards-compatible names "Normal" => MapTypeEnum.Normals, + "Normalmap" => MapTypeEnum.Normals, "GlossNormalA" => MapTypeEnum.Smoothness, "Height" => MapTypeEnum.Height, diff --git a/CgfConverter/Models/Shaders/MaterialRule.cs b/CgfConverter/Models/Shaders/MaterialRule.cs new file mode 100644 index 00000000..4533364c --- /dev/null +++ b/CgfConverter/Models/Shaders/MaterialRule.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +namespace CgfConverter.Models.Shaders; + +/// +/// Represents an interpreted shader rule to apply to a material during USD export. +/// Generated by the ShaderRulesEngine based on active StringGenMask flags. +/// +public class MaterialRule +{ + /// The shader property flag that triggered this rule (e.g., "%ALPHAGLOW") + public string PropertyName { get; set; } = string.Empty; + + /// Human-readable description of what this rule does + public string Description { get; set; } = string.Empty; + + /// Rule type/category for processing + public RuleType Type { get; set; } + + /// Which texture map this rule affects (if applicable) + public string? SourceTexture { get; set; } + + /// Which channel from the source texture (e.g., "alpha", "rgb", "r", "g", "b") + public string? SourceChannel { get; set; } + + /// Target input on the material node (e.g., "emissiveColor", "opacity", "roughness") + public string? TargetInput { get; set; } + + /// Additional metadata or parameters for complex rules + public Dictionary? Metadata { get; set; } + + public override string ToString() => $"{PropertyName}: {Description}"; +} + +/// +/// Categories of material rules for processing +/// +public enum RuleType +{ + /// Route a texture channel to a material input (e.g., diffuse.alpha → emissive) + ChannelRouting, + + /// Enable a specific texture map (e.g., environment map, gloss map) + TextureEnable, + + /// Enable vertex color usage + VertexColors, + + /// Modify material property or add special handling + PropertyModifier, + + /// Unknown or unsupported rule type + Unknown +} diff --git a/CgfConverter/Models/Shaders/ShaderDefinition.cs b/CgfConverter/Models/Shaders/ShaderDefinition.cs new file mode 100644 index 00000000..783ad0ce --- /dev/null +++ b/CgfConverter/Models/Shaders/ShaderDefinition.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace CgfConverter.Models.Shaders; + +/// +/// Represents a complete CryEngine shader definition loaded from a .ext file. +/// Contains all property definitions that control how materials using this shader are interpreted. +/// +public class ShaderDefinition +{ + /// Name of the shader (e.g., "MechCockpit", "Illum", "Glass") + public string ShaderName { get; set; } = string.Empty; + + /// Shader version from the .ext file (e.g., "1.00", "2.00") + public string? Version { get; set; } + + /// Whether the shader declares UsesCommonGlobalFlags (informational only) + public bool UsesCommonGlobalFlags { get; set; } + + /// + /// All properties defined in this shader, indexed by property name. + /// Key is the property name (e.g., "%ALPHAGLOW"), value is the property definition. + /// + public Dictionary Properties { get; set; } = new(); + + /// Gets a property by name, returns null if not found + public ShaderProperty? GetProperty(string name) + { + Properties.TryGetValue(name, out var property); + return property; + } + + public override string ToString() => $"{ShaderName} v{Version} ({Properties.Count} properties)"; +} diff --git a/CgfConverter/Models/Shaders/ShaderProperty.cs b/CgfConverter/Models/Shaders/ShaderProperty.cs new file mode 100644 index 00000000..465a6293 --- /dev/null +++ b/CgfConverter/Models/Shaders/ShaderProperty.cs @@ -0,0 +1,34 @@ +namespace CgfConverter.Models.Shaders; + +/// +/// Represents a single property definition from a CryEngine shader .ext file. +/// Properties define how texture channels and material attributes are interpreted. +/// +public class ShaderProperty +{ + /// Property flag name (e.g., "%ALPHAGLOW", "%ENVIRONMENT_MAP") + public string Name { get; set; } = string.Empty; + + /// Hex mask value used in GenMask (e.g., 0x2000, 0x80) + public long Mask { get; set; } + + /// Short property name shown in editor (e.g., "Glow in Diffuse alpha") + public string? Property { get; set; } + + /// + /// Description of what the property does + /// (e.g., "Use alpha channel of diffuse texture for glow") + /// + public string? Description { get; set; } + + /// Texture dependency to set when this property is enabled (e.g., "$TEX_Gloss") + public string? DependencySet { get; set; } + + /// Texture dependency to reset when this property is enabled (e.g., "$TEX_EnvCM") + public string? DependencyReset { get; set; } + + /// Whether this property is hidden in the editor UI + public bool Hidden { get; set; } + + public override string ToString() => $"{Name} (Mask: 0x{Mask:X})"; +} diff --git a/CgfConverter/Models/Structs/IvoAnimationStructs.cs b/CgfConverter/Models/Structs/IvoAnimationStructs.cs new file mode 100644 index 00000000..db206759 --- /dev/null +++ b/CgfConverter/Models/Structs/IvoAnimationStructs.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; + +namespace CgfConverter.Models.Structs; + +/// +/// Position data format types for Ivo animations. +/// Determined by the high byte of PosFormatFlags. +/// +public enum IvoPositionFormat : byte +{ + /// No position data. + None = 0x00, + + /// Float Vector3 (12 bytes per key), no header. + FloatVector3 = 0xC0, + + /// SNORM with 24-byte header, all channels present (6 bytes per key). + SNormFull = 0xC1, + + /// SNORM with 24-byte header, packed active channels only. + SNormPacked = 0xC2 +} + +/// +/// Helper methods for Ivo animation data processing. +/// +public static class IvoAnimationHelpers +{ + /// FLT_MAX value used as sentinel for inactive channels. + public const float FltMaxSentinel = 3.4028235e+38f; + + /// + /// Gets the position format type from format flags. + /// + public static IvoPositionFormat GetPositionFormat(ushort posFormatFlags) + { + if (posFormatFlags == 0) + return IvoPositionFormat.None; + + byte highByte = (byte)((posFormatFlags >> 8) & 0xFF); + return highByte switch + { + 0xC0 => IvoPositionFormat.FloatVector3, + 0xC1 => IvoPositionFormat.SNormFull, + 0xC2 => IvoPositionFormat.SNormPacked, + _ => IvoPositionFormat.None + }; + } + + /// + /// Gets the time format from format flags (low nibble). + /// 0x00 = ubyte time array, 0x02 = uint16 time with 8-byte header. + /// + public static byte GetTimeFormat(ushort formatFlags) => (byte)(formatFlags & 0x0F); + + /// + /// Decompresses a SNORM int16 value to float using scale factor. + /// Formula: (snormValue / 32767.0f) * scale + /// + public static float DecompressSNorm(short snormValue, float scale) + => (snormValue / 32767.0f) * scale; + + /// + /// Checks if a channel is active (not masked with FLT_MAX sentinel). + /// + public static bool IsChannelActive(float channelMaskValue) + => channelMaskValue < FltMaxSentinel; + + /// + /// Reads time keys from a binary reader based on format flags. + /// + /// Binary reader positioned at time data. + /// Number of keys. + /// Format flags (low nibble determines time format). + /// List of time values. + public static List ReadTimeKeys(BinaryReader b, int count, ushort formatFlags) + { + var times = new List(count); + byte timeFormat = GetTimeFormat(formatFlags); + + if (timeFormat == 0x00) + { + // 0x40: ubyte time array + for (int t = 0; t < count; t++) + times.Add(b.ReadByte()); + } + else + { + // 0x42: 8-byte time header (startTime uint16, endTime uint16, marker uint32) + ushort startTime = b.ReadUInt16(); + ushort endTime = b.ReadUInt16(); + b.ReadUInt32(); // marker + + // For single-key animations, just use start time + if (count == 1) + { + times.Add(startTime); + } + else + { + // Interpolate times between start and end + for (int t = 0; t < count; t++) + { + float normalized = count > 1 ? (float)t / (count - 1) : 0; + times.Add(startTime + normalized * (endTime - startTime)); + } + } + } + + return times; + } + + /// + /// Reads rotation keyframes (uncompressed quaternions). + /// + /// Binary reader positioned at rotation data. + /// Number of rotation keys. + /// List of quaternion rotations. + public static List ReadRotationKeys(BinaryReader b, int count) + { + var rotations = new List(count); + + // Ivo CAF/DBA uses uncompressed quaternions (16 bytes each: x, y, z, w) + for (int i = 0; i < count; i++) + { + float x = b.ReadSingle(); + float y = b.ReadSingle(); + float z = b.ReadSingle(); + float w = b.ReadSingle(); + rotations.Add(new Quaternion(x, y, z, w)); + } + + return rotations; + } + + /// + /// Reads position keyframes based on format flags. + /// + /// Binary reader positioned at position data. + /// Number of position keys. + /// Position format flags (high byte determines format). + /// List of position vectors, or empty list if format unknown. + public static List ReadPositionKeys(BinaryReader b, int count, ushort formatFlags) + { + var positions = new List(count); + var format = GetPositionFormat(formatFlags); + + switch (format) + { + case IvoPositionFormat.FloatVector3: + // 0xC0xx: Float Vector3 (12 bytes per key), no header + for (int i = 0; i < count; i++) + { + float x = b.ReadSingle(); + float y = b.ReadSingle(); + float z = b.ReadSingle(); + positions.Add(new Vector3(x, y, z)); + } + break; + + case IvoPositionFormat.SNormFull: + // 0xC1xx: SNORM with 24-byte header, all channels (6 bytes per key) + { + // Read 24-byte header: channelMask (12 bytes) + scale (12 bytes) + Vector3 channelMask = ReadVector3(b); + Vector3 scale = ReadVector3(b); + + for (int i = 0; i < count; i++) + { + short sx = b.ReadInt16(); + short sy = b.ReadInt16(); + short sz = b.ReadInt16(); + + float x = DecompressSNorm(sx, scale.X); + float y = DecompressSNorm(sy, scale.Y); + float z = DecompressSNorm(sz, scale.Z); + + positions.Add(new Vector3(x, y, z)); + } + } + break; + + case IvoPositionFormat.SNormPacked: + // 0xC2xx: SNORM with 24-byte header, packed active channels only + { + // Read 24-byte header: channelMask (12 bytes) + scale (12 bytes) + Vector3 channelMask = ReadVector3(b); + Vector3 scale = ReadVector3(b); + + bool xActive = IsChannelActive(channelMask.X); + bool yActive = IsChannelActive(channelMask.Y); + bool zActive = IsChannelActive(channelMask.Z); + + for (int i = 0; i < count; i++) + { + float x = 0, y = 0, z = 0; + + if (xActive) + { + short sx = b.ReadInt16(); + x = DecompressSNorm(sx, scale.X); + } + if (yActive) + { + short sy = b.ReadInt16(); + y = DecompressSNorm(sy, scale.Y); + } + if (zActive) + { + short sz = b.ReadInt16(); + z = DecompressSNorm(sz, scale.Z); + } + + positions.Add(new Vector3(x, y, z)); + } + } + break; + + default: + // Unknown format - return empty list + break; + } + + return positions; + } + + /// + /// Reads a Vector3 from binary reader (3 floats). + /// + private static Vector3 ReadVector3(BinaryReader b) + { + float x = b.ReadSingle(); + float y = b.ReadSingle(); + float z = b.ReadSingle(); + return new Vector3(x, y, z); + } +} + +/// +/// Block header for #caf and #dba animation blocks (12 bytes). +/// +public struct IvoAnimBlockHeader +{ + /// Signature: "#caf" or "#dba". + public string Signature { get; set; } + + /// Number of bones in this animation (CryEngine supports up to 1024). + public ushort BoneCount { get; set; } + + /// Magic number (0xAA55 for DBA, 0xFFFF for CAF). + public ushort Magic { get; set; } + + /// Total size of block data after header. + public uint DataSize { get; set; } +} + +/// +/// Controller entry for per-bone animation data (24 bytes). +/// Structure has separate rotation track (12 bytes) and position track (12 bytes). +/// All offsets are relative to the START of this controller entry (not the keyframe data start). +/// +/// +/// Format flags breakdown (low nibble determines time format: 0=ubyte, 2=uint16 header): +/// - Rotation: 0x8040 = ubyte time array, uncompressed quaternions +/// 0x8042 = uint16 time header (8 bytes), uncompressed quaternions +/// - Position: 0xC040 = ubyte time, numPosKeys positions +/// 0xC142 = uint16 time header (8 bytes), 2 positions +/// 0xC242 = uint16 time header (8 bytes), data header (8 bytes), 1 position +/// 0x0000 = no position track +/// +public struct IvoAnimControllerEntry +{ + // Rotation track info (12 bytes) + + /// Number of rotation keyframes. + public ushort NumRotKeys { get; set; } + + /// Rotation format flags. 0x8042 = standard rotation track. + public ushort RotFormatFlags { get; set; } + + /// Offset to rotation time keys (relative to controller start). + public uint RotTimeOffset { get; set; } + + /// Offset to rotation quaternion data (relative to controller start). + public uint RotDataOffset { get; set; } + + // Position track info (12 bytes) + + /// Number of position keyframes. Meaning varies by format flag. + public ushort NumPosKeys { get; set; } + + /// Position format flags. See remarks for valid values. + public ushort PosFormatFlags { get; set; } + + /// Offset to position time keys (relative to controller start). + public uint PosTimeOffset { get; set; } + + /// Offset to position vector data (relative to controller start). + public uint PosDataOffset { get; set; } + + /// Returns true if this controller has position data. + public readonly bool HasPosition => PosFormatFlags != 0; + + /// Returns true if this controller has rotation data. + public readonly bool HasRotation => RotFormatFlags != 0; +} + +/// +/// DBA metadata entry for a single animation in the library (44 bytes). +/// +/// +/// Layout per 010 template: +/// - Flags (4 bytes) +/// - FramesPerSecond (2 bytes) +/// - NumControllers (2 bytes) +/// - Unknown1 (4 bytes) +/// - Unknown2 (4 bytes) +/// - StartRotation (16 bytes) +/// - StartPosition (12 bytes) +/// Total: 44 bytes +/// +public struct IvoDBAMetaEntry +{ + /// Animation flags (usually 2). + public uint Flags { get; set; } + + /// Frames per second (typically 30). + public ushort FramesPerSecond { get; set; } + + /// Number of bone controllers. + public ushort NumControllers { get; set; } + + /// Unknown value (often 0). + public uint Unknown1 { get; set; } + + /// Unknown value (varies: 17, 25, etc.). + public uint Unknown2 { get; set; } + + /// Reference pose rotation. + public Quaternion StartRotation { get; set; } + + /// Reference pose position. + public Vector3 StartPosition { get; set; } +} + +/// +/// Parsed animation block from #caf or #dba data. +/// +public class IvoAnimationBlock +{ + /// Block header. + public IvoAnimBlockHeader Header { get; set; } + + /// Bone hash array (CRC32 of bone names). + public uint[] BoneHashes { get; set; } = []; + + /// Controller entries for each bone. + public IvoAnimControllerEntry[] Controllers { get; set; } = []; + + /// File offsets where each controller starts (for relative offset calculations). + public long[] ControllerOffsets { get; set; } = []; + + /// Parsed rotation keyframes per bone (bone hash -> rotations). + public Dictionary> Rotations { get; set; } = []; + + /// Parsed position keyframes per bone (bone hash -> positions). + public Dictionary> Positions { get; set; } = []; + + /// Parsed rotation key times per bone (bone hash -> times). + public Dictionary> RotationTimes { get; set; } = []; + + /// Parsed position key times per bone (bone hash -> times). + public Dictionary> PositionTimes { get; set; } = []; +} diff --git a/CgfConverter/Models/Structs/Matrix3x4.cs b/CgfConverter/Models/Structs/Matrix3x4.cs index c09dcf48..1f4b8197 100644 --- a/CgfConverter/Models/Structs/Matrix3x4.cs +++ b/CgfConverter/Models/Structs/Matrix3x4.cs @@ -139,6 +139,35 @@ public readonly Matrix4x4 ConvertToLocalTransformMatrix() return m; } + /// + /// Converts Matrix3x4 to Matrix4x4 by moving translation from column 4 to row 4. + /// Does NOT transpose the rotation matrix - keeps rotation as-is. + /// Use this for USD restTransforms where you need translation in M41/42/43. + /// + public readonly Matrix4x4 ConvertToUsdTransformMatrix() + { + var m = new Matrix4x4 + { + M11 = M11, + M12 = M12, + M13 = M13, + M14 = 0, + M21 = M21, + M22 = M22, + M23 = M23, + M24 = 0, + M31 = M31, + M32 = M32, + M33 = M33, + M34 = 0, + M41 = M14, // Move translation from column 4 to row 4 + M42 = M24, + M43 = M34, + M44 = 1 + }; + return m; + } + /// /// Returns a boolean indicating whether this matrix instance is equal to the other given matrix. /// diff --git a/CgfConverter/Models/Structs/Structs.cs b/CgfConverter/Models/Structs/Structs.cs index bf55546e..aa308bea 100644 --- a/CgfConverter/Models/Structs/Structs.cs +++ b/CgfConverter/Models/Structs/Structs.cs @@ -503,7 +503,8 @@ public static implicit operator Quaternion(SmallTreeDWORDQuat value) shift += 10; } - comp[maxComponentIndex] = (float)Math.Sqrt(1.0f - sqrsumm); + // Clamp to avoid NaN from sqrt of negative number (can happen with precision issues) + comp[maxComponentIndex] = (float)Math.Sqrt(Math.Max(0.0f, 1.0f - sqrsumm)); return new Quaternion(comp[0], comp[1], comp[2], comp[3]); } } @@ -559,7 +560,8 @@ public static implicit operator Quaternion(SmallTree48BitQuat value) shift += 15; } - comp[maxComponentIndex] = (float)Math.Sqrt(1.0f - sqrsumm); + // Clamp to avoid NaN from sqrt of negative number (can happen with precision issues) + comp[maxComponentIndex] = (float)Math.Sqrt(Math.Max(0.0f, 1.0f - sqrsumm)); return new Quaternion(comp[0], comp[1], comp[2], comp[3]); } } @@ -619,7 +621,8 @@ public static implicit operator Quaternion(SmallTree64BitQuat value) shift += 20; } - comp[maxComponentIndex] = (float)Math.Sqrt(1.0f - sqrsumm); + // Clamp to avoid NaN from sqrt of negative number (can happen with precision issues) + comp[maxComponentIndex] = (float)Math.Sqrt(Math.Max(0.0f, 1.0f - sqrsumm)); return new Quaternion(comp[0], comp[1], comp[2], comp[3]); } } @@ -701,7 +704,156 @@ public static implicit operator Quaternion(SmallTree64BitExtQuat value) } } - comp[maxComponentIndex] = (float)Math.Sqrt(1.0f - sqrsumm); + // Clamp to avoid NaN from sqrt of negative number (can happen with precision issues) + comp[maxComponentIndex] = (float)Math.Sqrt(Math.Max(0.0f, 1.0f - sqrsumm)); return new Quaternion(comp[0], comp[1], comp[2], comp[3]); } } + +/// +/// 8-byte tangent frame format used in Star Citizen's #ivo geometry files. +/// Encodes a full TBN (Tangent-Bitangent-Normal) matrix using smallest-three quaternion compression. +/// +public struct IvoTangentFrame +{ + private const float SCALE = 722.0f; + + public ushort Word0; + public ushort Word1; + public ushort Word2; + public ushort Word3; + + /// + /// Decodes the tangent frame into TBN vectors. + /// + /// A tuple containing the Normal, Tangent, and Bitangent vectors. + public readonly (Vector3 Normal, Vector3 Tangent, Vector3 Bitangent) Decode() + { + // Combine words into 32-bit values + uint value1 = Word0 | ((uint)Word1 << 16); + uint value2 = Word2 | ((uint)Word3 << 16); + + // Extract dropped index and raw components from first value (10-10-10-2 format) + int droppedIndex = (int)((value1 >> 30) & 0x3); + int aRaw = (int)(value1 & 0x3FF); + int bRaw = (int)((value1 >> 10) & 0x3FF); + int cRaw = (int)((value1 >> 20) & 0x3FF); + + // Sign-extend 10-bit values (two's complement) + if (aRaw > 511) aRaw -= 1024; + if (bRaw > 511) bRaw -= 1024; + if (cRaw > 511) cRaw -= 1024; + + // Normalize to float range [-0.707, 0.707] + float a = aRaw / SCALE; + float b = bRaw / SCALE; + float c = cRaw / SCALE; + + // Reconstruct dropped component using unit quaternion constraint + float sumSq = a * a + b * b + c * c; + float d = sumSq < 1.0f ? MathF.Sqrt(1.0f - sumSq) : 0.0f; + + // Build quaternion based on dropped index (which component was omitted) + float qx, qy, qz, qw; + switch (droppedIndex) + { + case 0: qx = d; qy = a; qz = b; qw = c; break; // X was dropped + case 1: qx = a; qy = d; qz = b; qw = c; break; // Y was dropped + case 2: qx = a; qy = b; qz = d; qw = c; break; // Z was dropped + default: qx = a; qy = b; qz = c; qw = d; break; // W was dropped + } + + // Extract TBN matrix columns from quaternion + // Tangent (first column of rotation matrix) + float Tx = 1.0f - 2.0f * (qy * qy + qz * qz); + float Ty = 2.0f * (qx * qy + qw * qz); + float Tz = 2.0f * (qx * qz - qw * qy); + + // Bitangent (second column of rotation matrix) + float Bx = 2.0f * (qx * qy - qw * qz); + float By = 1.0f - 2.0f * (qx * qx + qz * qz); + float Bz = 2.0f * (qy * qz + qw * qx); + + // Normal (third column of rotation matrix) + float Nx = 2.0f * (qx * qz + qw * qy); + float Ny = 2.0f * (qy * qz - qw * qx); + float Nz = 1.0f - 2.0f * (qx * qx + qy * qy); + + Vector3 tangent = new(Tx, Ty, Tz); + Vector3 bitangent = new(Bx, By, Bz); + Vector3 normalColumn = new(Nx, Ny, Nz); + + // Extract column selector from second value (bits 30-31) + int columnSelector = (int)((value2 >> 30) & 0x3); + + // Select final normal based on column selector + Vector3 normal; + switch (columnSelector) + { + case 0: + // Negative Bitangent + normal = -bitangent; + break; + case 1: + // Heuristic: use -Normal if more horizontal, else -Tangent + if (MathF.Abs(Nz) < 0.5f) + normal = -normalColumn; + else + normal = -tangent; + break; + case 2: + // Positive Normal (direct) + normal = normalColumn; + break; + case 3: + // Negative Tangent + normal = -tangent; + break; + default: + normal = normalColumn; + break; + } + + return (normal, tangent, bitangent); + } + + /// + /// Decodes with diagnostic information for debugging. + /// + public readonly string GetDiagnostics() + { + uint value1 = Word0 | ((uint)Word1 << 16); + uint value2 = Word2 | ((uint)Word3 << 16); + + int droppedIndex = (int)((value1 >> 30) & 0x3); + int aRaw = (int)(value1 & 0x3FF); + int bRaw = (int)((value1 >> 10) & 0x3FF); + int cRaw = (int)((value1 >> 20) & 0x3FF); + + // Sign-extend + int aSigned = aRaw > 511 ? aRaw - 1024 : aRaw; + int bSigned = bRaw > 511 ? bRaw - 1024 : bRaw; + int cSigned = cRaw > 511 ? cRaw - 1024 : cRaw; + + float a = aSigned / SCALE; + float b = bSigned / SCALE; + float c = cSigned / SCALE; + float sumSq = a * a + b * b + c * c; + float d = sumSq < 1.0f ? MathF.Sqrt(1.0f - sumSq) : 0.0f; + + float qx, qy, qz, qw; + switch (droppedIndex) + { + case 0: qx = d; qy = a; qz = b; qw = c; break; + case 1: qx = a; qy = d; qz = b; qw = c; break; + case 2: qx = a; qy = b; qz = d; qw = c; break; + default: qx = a; qy = b; qz = c; qw = d; break; + } + + int colSelector = (int)((value2 >> 30) & 0x3); + + return $"dropped={droppedIndex}, raw=({aRaw},{bRaw},{cRaw}), signed=({aSigned},{bSigned},{cSigned}), " + + $"abc=({a:F4},{b:F4},{c:F4}), d={d:F4}, sumSq={sumSq:F4}, " + + $"quat=({qx:F4},{qy:F4},{qz:F4},{qw:F4}), colSel={colSelector}"; + } +} diff --git a/CgfConverter/Parsers/ShaderExtParser.cs b/CgfConverter/Parsers/ShaderExtParser.cs new file mode 100644 index 00000000..3c67ac6c --- /dev/null +++ b/CgfConverter/Parsers/ShaderExtParser.cs @@ -0,0 +1,239 @@ +using CgfConverter.Models.Shaders; +using CgfConverter.Utils; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; + +namespace CgfConverter.Parsers; + +/// +/// Parser for CryEngine shader .ext files. +/// Extracts property definitions that control material interpretation. +/// +public class ShaderExtParser +{ + private readonly TaggedLogger _log; + + public ShaderExtParser(TaggedLogger logger) + { + _log = logger; + } + + /// + /// Parse a shader .ext file and return the shader definition. + /// + /// Path to the .ext file + /// Parsed shader definition, or null if parsing fails + public ShaderDefinition? ParseShaderFile(string filePath) + { + if (!File.Exists(filePath)) + { + _log.D($"Shader file not found: {filePath}"); + return null; + } + + try + { + var shaderName = Path.GetFileNameWithoutExtension(filePath); + var content = File.ReadAllText(filePath); + + return ParseShaderContent(shaderName, content); + } + catch (Exception ex) + { + _log.D($"Error parsing shader file {filePath}: {ex.Message}"); + return null; + } + } + + /// + /// Parse shader content string into a shader definition. + /// + public ShaderDefinition ParseShaderContent(string shaderName, string content) + { + var shader = new ShaderDefinition { ShaderName = shaderName }; + + // Extract version + var versionMatch = Regex.Match(content, @"Version\s*\(([^)]+)\)", RegexOptions.IgnoreCase); + if (versionMatch.Success) + shader.Version = versionMatch.Groups[1].Value.Trim(); + + // Check for UsesCommonGlobalFlags + shader.UsesCommonGlobalFlags = content.Contains("UsesCommonGlobalFlags", StringComparison.OrdinalIgnoreCase); + + // Extract all Property blocks + var properties = ParsePropertyBlocks(content); + foreach (var property in properties) + { + if (!string.IsNullOrEmpty(property.Name)) + shader.Properties[property.Name] = property; + } + + _log.D($"Parsed shader {shaderName}: {shader.Properties.Count} properties"); + return shader; + } + + /// + /// Extract all Property {} blocks from shader content. + /// + private List ParsePropertyBlocks(string content) + { + var properties = new List(); + + // Find all Property blocks using regex + // Match: Property\n{\n ... content ...\n} + var propertyBlockPattern = @"Property\s*\{([^}]+)\}"; + var matches = Regex.Matches(content, propertyBlockPattern, RegexOptions.Singleline); + + foreach (Match match in matches) + { + var blockContent = match.Groups[1].Value; + var property = ParsePropertyBlock(blockContent); + if (property != null) + properties.Add(property); + } + + return properties; + } + + /// + /// Parse a single Property block content. + /// + private ShaderProperty? ParsePropertyBlock(string blockContent) + { + var property = new ShaderProperty(); + + // Parse each line in the block + var lines = blockContent.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + foreach (var line in lines) + { + var trimmed = line.Trim(); + + // Skip comments + if (trimmed.StartsWith("//")) + continue; + + // Parse Name = value + if (TryParseKeyValue(trimmed, "Name", out var name)) + { + property.Name = name.Trim(); + } + // Parse Mask = 0xHEX + else if (TryParseKeyValue(trimmed, "Mask", out var mask)) + { + property.Mask = ParseHexValue(mask); + } + // Parse Property (display name) + else if (TryParseKeyValue(trimmed, "Property", out var propName)) + { + property.Property = propName.Trim(); + } + // Parse Description + else if (TryParseKeyValue(trimmed, "Description", out var description)) + { + property.Description = description.Trim(); + } + // Parse DependencySet + else if (TryParseKeyValue(trimmed, "DependencySet", out var depSet)) + { + property.DependencySet = depSet.Trim(); + } + // Parse DependencyReset + else if (TryParseKeyValue(trimmed, "DependencyReset", out var depReset)) + { + property.DependencyReset = depReset.Trim(); + } + // Parse Hidden + else if (trimmed.Equals("Hidden", StringComparison.OrdinalIgnoreCase)) + { + property.Hidden = true; + } + } + + // Only return valid properties with a name + return string.IsNullOrEmpty(property.Name) ? null : property; + } + + /// + /// Try to parse a "Key = Value" or "Key (Value)" line. + /// + private bool TryParseKeyValue(string line, string key, out string value) + { + value = string.Empty; + + // Try "Key = Value" format + var equalsPattern = $@"^\s*{key}\s*=\s*(.+)$"; + var match = Regex.Match(line, equalsPattern, RegexOptions.IgnoreCase); + + if (match.Success) + { + value = match.Groups[1].Value.Trim(); + return true; + } + + // Try "Key (Value)" format + var parensPattern = $@"^\s*{key}\s*\(([^)]*)\)"; + match = Regex.Match(line, parensPattern, RegexOptions.IgnoreCase); + + if (match.Success) + { + value = match.Groups[1].Value.Trim(); + return true; + } + + return false; + } + + /// + /// Parse hex value string (e.g., "0x2000", "0x80") to long. + /// + private long ParseHexValue(string hexString) + { + hexString = hexString.Trim(); + + // Remove "0x" prefix if present + if (hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + hexString = hexString.Substring(2); + + // Parse as hex + if (long.TryParse(hexString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) + return value; + + _log.D($"Failed to parse hex value: {hexString}"); + return 0; + } + + /// + /// Load all shader definitions from a directory. + /// + /// Directory containing .ext files + /// Dictionary of shader definitions keyed by shader name (case-insensitive) + public Dictionary LoadShadersFromDirectory(string shadersDirectory) + { + var shaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (!Directory.Exists(shadersDirectory)) + { + _log.D($"Shaders directory not found: {shadersDirectory}"); + return shaders; + } + + var extFiles = Directory.GetFiles(shadersDirectory, "*.ext", SearchOption.TopDirectoryOnly); + _log.D($"Loading {extFiles.Length} shader files from {shadersDirectory}"); + + foreach (var extFile in extFiles) + { + var shader = ParseShaderFile(extFile); + if (shader != null) + { + shaders[shader.ShaderName] = shader; + } + } + + _log.D($"Loaded {shaders.Count} shader definitions"); + return shaders; + } +} diff --git a/CgfConverter/Renderers/Collada/ColladaModelRenderer.Animation.cs b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Animation.cs new file mode 100644 index 00000000..8f9acaa2 --- /dev/null +++ b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Animation.cs @@ -0,0 +1,466 @@ +using CgfConverter.CryEngineCore; +using CgfConverter.Renderers.Collada.Collada; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Animation; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Data_Flow; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Metadata; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Parameters; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Scene; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Technique_Common; +using CgfConverter.Renderers.Collada.Collada.Enums; +using CgfConverter.Renderers.Collada.Collada.Types; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text; +using static CgfConverter.Utilities.HelperMethods; + +namespace CgfConverter.Renderers.Collada; + +/// +/// ColladaModelRenderer partial class - Animation export support +/// +public partial class ColladaModelRenderer +{ + public void WriteLibrary_Animations() + { + if (controllerIdToBoneName.Count == 0) + { + Log.D("No bone mappings available for animation export"); + return; + } + + var animationLibrary = new ColladaLibraryAnimations(); + var allAnimations = new List(); + + // Find all the 905 controller chunks from animation files (.dba) + foreach (var animChunk in _cryData.Animations + .SelectMany(x => x.ChunkMap.Values.OfType()) + .ToList()) + { + if (animChunk?.Animations is null || animChunk.Animations.Count == 0) + continue; + + foreach (var animation in animChunk.Animations) + { + var animationName = CleanAnimationName(animation.Name); + var boneAnimations = CreateMatrixAnimationsForClip(animation, animChunk, animationName); + + if (boneAnimations.Count > 0) + { + // Create root animation containing all bone animations for this clip + var rootAnimation = new ColladaAnimation + { + Name = animationName, + ID = $"{animationName}_animation", + Animation = boneAnimations.ToArray() + }; + allAnimations.Add(rootAnimation); + + Log.D($"Created animation: {animationName} with {boneAnimations.Count} bone channels"); + } + } + } + + if (allAnimations.Count > 0) + { + animationLibrary.Animation = allAnimations.ToArray(); + DaeObject.Library_Animations = animationLibrary; + + Log.I($"Exported {allAnimations.Count} animation(s)"); + } + } + + /// + /// Creates matrix-based animations for all bones in a single animation clip. + /// Uses float4x4 matrices with channel target "/transform" for Blender compatibility. + /// + private List CreateMatrixAnimationsForClip( + ChunkController_905.Animation animation, + ChunkController_905 animChunk, + string animationName) + { + var boneAnimations = new List(); + + foreach (var controller in animation.Controllers) + { + if (!controllerIdToBoneName.TryGetValue(controller.ControllerID, out var boneName)) + { + Log.D($"Animation[{animationName}]: Controller 0x{controller.ControllerID:X08} not found in skeleton"); + continue; + } + + if (!controller.HasPosTrack && !controller.HasRotTrack) + continue; + + // Get rest transform for this bone (fallback when no position/rotation track exists) + var (restPosition, restRotation) = controllerIdToRestTransform.TryGetValue(controller.ControllerID, out var rest) + ? rest + : (Vector3.Zero, Quaternion.Identity); + + var boneAnim = CreateBoneMatrixAnimation(controller, animChunk, boneName, animationName, restPosition, restRotation); + if (boneAnim is not null) + { + boneAnimations.Add(boneAnim); + } + } + + return boneAnimations; + } + + /// + /// Creates a matrix-based animation for a single bone. + /// Outputs float4x4 matrices targeting the bone's "transform" SID. + /// + private ColladaAnimation? CreateBoneMatrixAnimation( + ChunkController_905.CControllerInfo controller, + ChunkController_905 animChunk, + string boneName, + string animationName, + Vector3 restPosition, + Quaternion restRotation) + { + // Collect all unique frame times from both position and rotation tracks + var frameTimes = new SortedSet(); + + if (controller.HasPosTrack && animChunk.KeyTimes is not null) + { + foreach (var t in animChunk.KeyTimes[controller.PosKeyTimeTrack]) + frameTimes.Add(t); + } + + if (controller.HasRotTrack && animChunk.KeyTimes is not null) + { + foreach (var t in animChunk.KeyTimes[controller.RotKeyTimeTrack]) + frameTimes.Add(t); + } + + if (frameTimes.Count == 0) + return null; + + var frameList = frameTimes.ToList(); + var startFrame = frameList[0]; + var keyframeCount = frameList.Count; + + // Build time and matrix output strings + var timeValues = new StringBuilder(); + var matrixValues = new StringBuilder(); + + foreach (var frame in frameList) + { + // Time in seconds (normalized to start at 0) + var timeInSeconds = (frame - startFrame) / 30f; + timeValues.Append($"{timeInSeconds:F6} "); + + // Sample position and rotation at this frame, using rest pose as fallback + var position = SamplePositionAtFrame(controller, animChunk, frame, restPosition); + var rotation = SampleRotationAtFrame(controller, animChunk, frame, restRotation); + + // Build transform matrix from position and rotation + // IMPORTANT: CryEngine stores translation in column 4 (M14, M24, M34), NOT row 4 + // Do NOT use matrix.Translation which sets M41, M42, M43 + var matrix = Matrix4x4.CreateFromQuaternion(rotation); + matrix.M14 = position.X; + matrix.M24 = position.Y; + matrix.M34 = position.Z; + + // Append matrix values (row-major for Collada) + matrixValues.Append(CreateStringFromMatrix4x4(matrix) + " "); + } + + var animId = $"{animationName}_{boneName}"; + + // Time source + var timeSource = new ColladaSource + { + ID = $"{animId}_time", + Float_Array = new ColladaFloatArray + { + ID = $"{animId}_time_array", + Count = keyframeCount, + Value_As_String = timeValues.ToString().TrimEnd() + }, + Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor + { + Source = $"#{animId}_time_array", + Count = (uint)keyframeCount, + Stride = 1, + Param = [new ColladaParam { Name = "TIME", Type = "float" }] + } + } + }; + + // Output source (float4x4 matrices) + var outputSource = new ColladaSource + { + ID = $"{animId}_output", + Float_Array = new ColladaFloatArray + { + ID = $"{animId}_output_array", + Count = keyframeCount * 16, + Value_As_String = matrixValues.ToString().TrimEnd() + }, + Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor + { + Source = $"#{animId}_output_array", + Count = (uint)keyframeCount, + Stride = 16, + Param = [new ColladaParam { Name = "TRANSFORM", Type = "float4x4" }] + } + } + }; + + // Interpolation source + var interpolationSource = new ColladaSource + { + ID = $"{animId}_interpolation", + Name_Array = new ColladaNameArray + { + ID = $"{animId}_interpolation_array", + Count = keyframeCount, + Value_Pre_Parse = string.Join(" ", Enumerable.Repeat("LINEAR", keyframeCount)) + }, + Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor + { + Source = $"#{animId}_interpolation_array", + Count = (uint)keyframeCount, + Stride = 1, + Param = [new ColladaParam { Name = "INTERPOLATION", Type = "name" }] + } + } + }; + + // Sampler + var sampler = new ColladaSampler + { + ID = $"{animId}_sampler", + Input = + [ + new ColladaInputUnshared { Semantic = ColladaInputSemantic.INPUT, source = $"#{animId}_time" }, + new ColladaInputUnshared { Semantic = ColladaInputSemantic.OUTPUT, source = $"#{animId}_output" }, + new ColladaInputUnshared { Semantic = ColladaInputSemantic.INTERPOLATION, source = $"#{animId}_interpolation" } + ] + }; + + // Channel targeting the bone's transform SID (critical: use /transform not /matrix) + var channel = new ColladaChannel + { + Source = $"#{animId}_sampler", + Target = $"{boneName}/transform" + }; + + return new ColladaAnimation + { + ID = $"{animId}_animation", + Name = $"{boneName}_transform", + Source = [timeSource, outputSource, interpolationSource], + Sampler = [sampler], + Channel = [channel] + }; + } + + /// + /// Samples position at a given frame using linear interpolation. + /// Falls back to rest position if no position track exists. + /// + private static Vector3 SamplePositionAtFrame( + ChunkController_905.CControllerInfo controller, + ChunkController_905 animChunk, + float frame, + Vector3 restPosition) + { + if (!controller.HasPosTrack || animChunk.KeyTimes is null || animChunk.KeyPositions is null) + return restPosition; + + var keyTimes = animChunk.KeyTimes[controller.PosKeyTimeTrack]; + var keyPositions = animChunk.KeyPositions[controller.PosTrack]; + + if (keyTimes.Count == 0 || keyPositions.Count == 0) + return Vector3.Zero; + + // Find surrounding keyframes + int i = 0; + while (i < keyTimes.Count - 1 && keyTimes[i + 1] <= frame) + i++; + + if (i >= keyPositions.Count) + return keyPositions[^1]; + + if (i == keyTimes.Count - 1 || keyTimes[i] >= frame) + return keyPositions[i]; + + // Linear interpolation + float t0 = keyTimes[i]; + float t1 = keyTimes[i + 1]; + float alpha = (frame - t0) / (t1 - t0); + + return Vector3.Lerp(keyPositions[i], keyPositions[Math.Min(i + 1, keyPositions.Count - 1)], alpha); + } + + /// + /// Samples rotation at a given frame using spherical linear interpolation. + /// Falls back to rest rotation if no rotation track exists. + /// + private static Quaternion SampleRotationAtFrame( + ChunkController_905.CControllerInfo controller, + ChunkController_905 animChunk, + float frame, + Quaternion restRotation) + { + if (!controller.HasRotTrack || animChunk.KeyTimes is null || animChunk.KeyRotations is null) + return restRotation; + + var keyTimes = animChunk.KeyTimes[controller.RotKeyTimeTrack]; + var keyRotations = animChunk.KeyRotations[controller.RotTrack]; + + if (keyTimes.Count == 0 || keyRotations.Count == 0) + return Quaternion.Identity; + + // Find surrounding keyframes + int i = 0; + while (i < keyTimes.Count - 1 && keyTimes[i + 1] <= frame) + i++; + + if (i >= keyRotations.Count) + return keyRotations[^1]; + + if (i == keyTimes.Count - 1 || keyTimes[i] >= frame) + return keyRotations[i]; + + // Spherical linear interpolation + float t0 = keyTimes[i]; + float t1 = keyTimes[i + 1]; + float alpha = (frame - t0) / (t1 - t0); + + return Quaternion.Slerp(keyRotations[i], keyRotations[Math.Min(i + 1, keyRotations.Count - 1)], alpha); + } + + /// + /// Cleans animation name for use as Collada ID. + /// + private static string CleanAnimationName(string name) + { + var cleanName = Path.GetFileNameWithoutExtension(name); + // Replace invalid characters with underscores + return cleanName.Replace(' ', '_').Replace('/', '_').Replace('\\', '_'); + } + + /// + /// Exports individual animation files for Blender NLA workflow. + /// Blender's Collada importer merges all animations into one action, so we export + /// each animation as a separate file with skeleton + that animation only. + /// + private void ExportAnimationFiles() + { + if (_cryData.Animations is null || _cryData.Animations.Count == 0) + return; + + if (controllerIdToBoneName.Count == 0) + return; + + // Collect all animations with their data + var allAnimations = new List<(string name, ColladaAnimation animation)>(); + + foreach (var animChunk in _cryData.Animations + .SelectMany(x => x.ChunkMap.Values.OfType()) + .ToList()) + { + if (animChunk?.Animations is null || animChunk.Animations.Count == 0) + continue; + + foreach (var animation in animChunk.Animations) + { + var animationName = CleanAnimationName(animation.Name); + var boneAnimations = CreateMatrixAnimationsForClip(animation, animChunk, animationName); + + if (boneAnimations.Count > 0) + { + var rootAnimation = new ColladaAnimation + { + Name = animationName, + ID = $"{animationName}_animation", + Animation = boneAnimations.ToArray() + }; + allAnimations.Add((animationName, rootAnimation)); + } + } + } + + // Skip if no animations + if (allAnimations.Count == 0) + return; + + Log.D($"Exporting {allAnimations.Count} separate animation files for Blender NLA workflow..."); + + var baseFileName = Path.GetFileNameWithoutExtension(daeOutputFile.FullName); + var outputDir = daeOutputFile.DirectoryName ?? "."; + + foreach (var (animName, animation) in allAnimations) + { + var animFileName = $"{baseFileName}_anim_{animName}.dae"; + var animFilePath = Path.Combine(outputDir, animFileName); + + var animDoc = CreateAnimationOnlyDoc(animation); + + using (var writer = new StreamWriter(animFilePath)) + { + serializer.Serialize(writer, animDoc); + } + + Log.D($" Exported: {animFileName}"); + } + } + + /// + /// Creates a minimal Collada document containing only skeleton + single animation. + /// This is for Blender import workflow where each animation needs its own file. + /// + private ColladaDoc CreateAnimationOnlyDoc(ColladaAnimation animation) + { + var animDoc = new ColladaDoc + { + Collada_Version = "1.4.1" + }; + + // Asset metadata + animDoc.Asset = new ColladaAsset + { + Created = DateTime.Now, + Modified = DateTime.Now, + Up_Axis = "Z_UP", + Unit = new ColladaAssetUnit { Meter = 1.0, Name = "meter" }, + Title = animation.Name + }; + + // Scene reference + animDoc.Scene = new ColladaScene + { + Visual_Scene = new ColladaInstanceVisualScene { URL = "#Scene" } + }; + + // Copy the skeleton structure from the main document's visual scene + // We need the skeleton nodes for the animation to target + if (DaeObject.Library_Visual_Scene?.Visual_Scene?.Length > 0) + { + animDoc.Library_Visual_Scene = new ColladaLibraryVisualScenes + { + Visual_Scene = DaeObject.Library_Visual_Scene.Visual_Scene + }; + } + + // Add just this animation + animDoc.Library_Animations = new ColladaLibraryAnimations + { + Animation = new[] { animation } + }; + + return animDoc; + } +} diff --git a/CgfConverter/Renderers/Collada/ColladaModelRenderer.Geometry.cs b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Geometry.cs new file mode 100644 index 00000000..ee42f780 --- /dev/null +++ b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Geometry.cs @@ -0,0 +1,429 @@ +using CgfConverter.Collada; +using CgfConverter.CryEngineCore; +using CgfConverter.Models; +using CgfConverter.Models.Structs; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Data_Flow; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Geometry; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Parameters; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Technique_Common; +using CgfConverter.Renderers.Collada.Collada.Enums; +using CgfConverter.Renderers.Collada.Collada.Types; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using static CgfConverter.Utilities.HelperMethods; + +namespace CgfConverter.Renderers.Collada; + +/// +/// ColladaModelRenderer partial class - Geometry creation and mesh processing +/// +public partial class ColladaModelRenderer +{ + public void WriteLibrary_Geometries() + { + WriteGeometries(); + } + + public void WriteGeometries() + { + ColladaLibraryGeometries libraryGeometries = new(); + + // Make a list for all the geometries objects we will need. Will convert to array at end. Define the array here as well + // We have to define a Geometry for EACH meshsubset in the meshsubsets, since the mesh can contain multiple materials + List geometryList = []; + + // For each of the nodes, we need to write the geometry. + foreach (ChunkNode nodeChunk in _cryData.Nodes) + { + if (_args.IsNodeNameExcluded(nodeChunk.Name)) + { + Log.D($"Excluding node {nodeChunk.Name}"); + continue; + } + + if (nodeChunk.MeshData is not ChunkMesh meshChunk) + continue; + + if (meshChunk.GeometryInfo is null) // $physics node + continue; + + // Create a geometry object. Use the chunk ID for the geometry ID + // Create all the materials used by this chunk. + // Make the mesh object. This will have 3 or 4 sources, 1 vertices, and 1 or more Triangles (with material ID) + // If the Object ID of Node chunk points to a Helper or a Controller, place an empty. + var subsets = meshChunk.GeometryInfo.GeometrySubsets; + Datastream? indices = meshChunk.GeometryInfo.Indices; + Datastream? uvs = meshChunk.GeometryInfo.UVs; + Datastream? verts = meshChunk.GeometryInfo.Vertices; + Datastream? vertsUvs = meshChunk.GeometryInfo.VertUVs; + Datastream? normals = meshChunk.GeometryInfo.Normals; + Datastream? colors = meshChunk.GeometryInfo.Colors; + + if (verts is null && vertsUvs is null) // There is no vertex data for this node. Skip. + continue; + + // geometry is a Geometry object for each meshsubset. + ColladaGeometry geometry = new() + { + Name = nodeChunk.Name, + ID = nodeChunk.Name + "-mesh" + }; + ColladaMesh colladaMesh = new(); + geometry.Mesh = colladaMesh; + + ColladaSource[] source = new ColladaSource[4]; // 4 possible source types. + ColladaSource posSource = new(); + ColladaSource normSource = new(); + ColladaSource uvSource = new(); + ColladaSource colorSource = new(); + source[0] = posSource; + source[1] = normSource; + source[2] = uvSource; + source[3] = colorSource; + posSource.ID = nodeChunk.Name + "-mesh-pos"; + posSource.Name = nodeChunk.Name + "-pos"; + normSource.ID = nodeChunk.Name + "-mesh-norm"; + normSource.Name = nodeChunk.Name + "-norm"; + uvSource.ID = nodeChunk.Name + "-mesh-UV"; + uvSource.Name = nodeChunk.Name + "-UV"; + colorSource.ID = nodeChunk.Name + "-mesh-color"; + colorSource.Name = nodeChunk.Name + "-color"; + + ColladaVertices vertices = new() { ID = nodeChunk.Name + "-vertices" }; + geometry.Mesh.Vertices = vertices; + ColladaInputUnshared[] inputshared = new ColladaInputUnshared[4]; + vertices.Input = inputshared; + + ColladaInputUnshared posInput = new() { Semantic = ColladaInputSemantic.POSITION }; + ColladaInputUnshared normInput = new() { Semantic = ColladaInputSemantic.NORMAL }; + ColladaInputUnshared uvInput = new() { Semantic = ColladaInputSemantic.TEXCOORD }; + ColladaInputUnshared colorInput = new() { Semantic = ColladaInputSemantic.COLOR }; + + posInput.source = "#" + posSource.ID; + normInput.source = "#" + normSource.ID; + uvInput.source = "#" + uvSource.ID; + colorInput.source = "#" + colorSource.ID; + inputshared[0] = posInput; + + ColladaFloatArray floatArrayVerts = new(); + ColladaFloatArray floatArrayNormals = new(); + ColladaFloatArray floatArrayUVs = new(); + ColladaFloatArray floatArrayColors = new(); + + StringBuilder vertString = new(); + StringBuilder normString = new(); + StringBuilder uvString = new(); + StringBuilder colorString = new(); + + var numberOfElements = nodeChunk.MeshData.GeometryInfo.GeometrySubsets.Sum(x => x.NumVertices); + + if (verts is not null) // Will be null if it's using VertsUVs. + { + int numVerts = (int)verts.NumElements; + + floatArrayVerts.ID = posSource.ID + "-array"; + floatArrayVerts.Digits = 6; + floatArrayVerts.Magnitude = 38; + floatArrayVerts.Count = numVerts * 3; + floatArrayUVs.ID = uvSource.ID + "-array"; + floatArrayUVs.Digits = 6; + floatArrayUVs.Magnitude = 38; + floatArrayUVs.Count = numVerts * 2; + floatArrayNormals.ID = normSource.ID + "-array"; + floatArrayNormals.Digits = 6; + floatArrayNormals.Magnitude = 38; + floatArrayNormals.Count = numVerts * 3; + floatArrayColors.ID = colorSource.ID + "-array"; + floatArrayColors.Digits = 6; + floatArrayColors.Magnitude = 38; + floatArrayColors.Count = numVerts * 4; + + var hasNormals = normals is not null; + var hasUVs = uvs is not null; + var hasColors = colors is not null; + for (uint j = 0; j < numVerts; j++) + { + var normal = hasNormals ? normals.Data[j] : DefaultNormal; + var uv = hasUVs ? uvs.Data[j] : DefaultUV; + var color = hasColors ? colors.Data[j] : DefaultColor; + vertString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} ", verts.Data[j].X, verts.Data[j].Y, verts.Data[j].Z); + normString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} ", Safe(normal.X), Safe(normal.Y), Safe(normal.Z)); + colorString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} {3:F6} ", color.R, color.G, color.B, color.A); + uvString.AppendFormat(culture, "{0:F6} {1:F6} ", Safe(uv.U), 1 - Safe(uv.V)); + } + } + else // VertsUV structure. Pull out verts, colors and UVs from vertsUvs. + { + floatArrayVerts.ID = posSource.ID + "-array"; + floatArrayVerts.Digits = 6; + floatArrayVerts.Magnitude = 38; + floatArrayVerts.Count = numberOfElements * 3; + floatArrayUVs.ID = uvSource.ID + "-array"; + floatArrayUVs.Digits = 6; + floatArrayUVs.Magnitude = 38; + floatArrayUVs.Count = numberOfElements * 2; + floatArrayNormals.ID = normSource.ID + "-array"; + floatArrayNormals.Digits = 6; + floatArrayNormals.Magnitude = 38; + floatArrayNormals.Count = numberOfElements * 3; + floatArrayColors.ID = colorSource.ID + "-array"; + floatArrayColors.Digits = 6; + floatArrayColors.Magnitude = 38; + floatArrayColors.Count = numberOfElements * 4; + + var multiplerVector = _cryData.IsIvoFile + ? Vector3.Abs((meshChunk.MinBound - meshChunk.MaxBound) / 2f) + : Vector3.One; + + if (multiplerVector.X < 1) multiplerVector.X = 1; + if (multiplerVector.Y < 1) multiplerVector.Y = 1; + if (multiplerVector.Z < 1) multiplerVector.Z = 1; + Vector3 scalingVector = Vector3.One; + + if (meshChunk.ScalingVectors is not null) + { + scalingVector = Vector3.Abs((meshChunk.ScalingVectors.Max - meshChunk.ScalingVectors.Min) / 2f); + if (scalingVector.X < 1) scalingVector.X = 1; + if (scalingVector.Y < 1) scalingVector.Y = 1; + if (scalingVector.Z < 1) scalingVector.Z = 1; + } + + var boundaryBoxCenter = _cryData.IsIvoFile + ? (meshChunk.MinBound + meshChunk.MaxBound) / 2f + : Vector3.Zero; + + var scalingBoxCenter = meshChunk.ScalingVectors is not null ? (meshChunk.ScalingVectors.Max + meshChunk.ScalingVectors.Min) / 2f : Vector3.Zero; + var hasNormals = normals is not null; + var useScalingBox = _cryData.InputFile + .EndsWith("cga") || _cryData.InputFile.EndsWith("cgf") + && meshChunk.ScalingVectors is not null; + + // Create Vertices, UV, normals and colors string + foreach (var subset in meshChunk.GeometryInfo.GeometrySubsets ?? []) + { + for (int i = subset.FirstVertex; i < subset.NumVertices + subset.FirstVertex; i++) + { + Vector3 vert = vertsUvs.Data[i].Vertex; + + if (!_cryData.InputFile.EndsWith("skin") && !_cryData.InputFile.EndsWith("chr")) + { + if (meshChunk.ScalingVectors is null) + vert = (vert * multiplerVector) + boundaryBoxCenter; + else + vert = (vert * scalingVector) + scalingBoxCenter; + } + + vertString.AppendFormat("{0:F6} {1:F6} {2:F6} ", Safe(vert.X), Safe(vert.Y), Safe(vert.Z)); + colorString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} {3:F6} ", vertsUvs.Data[i].Color.R, vertsUvs.Data[i].Color.G, vertsUvs.Data[i].Color.B, vertsUvs.Data[i].Color.A); + uvString.AppendFormat("{0:F6} {1:F6} ", Safe(vertsUvs.Data[i].UV.U), Safe(1 - vertsUvs.Data[i].UV.V)); + + var normal = hasNormals ? normals.Data[i] : DefaultNormal; + normString.AppendFormat("{0:F6} {1:F6} {2:F6} ", Safe(normal.X), Safe(normal.Y), Safe(normal.Z)); + } + } + } + + CleanNumbers(vertString); + CleanNumbers(normString); + CleanNumbers(uvString); + CleanNumbers(colorString); + + #region Create the triangles node. + var numberOfMeshSubsets = subsets.Count; + var triangles = new ColladaTriangles[numberOfMeshSubsets]; + geometry.Mesh.Triangles = triangles; + + for (int j = 0; j < numberOfMeshSubsets; j++) // Need to make a new Triangles entry for each submesh. + { + triangles[j] = new ColladaTriangles + { + Count = subsets[j].NumIndices / 3, + Material = GetMaterialName(nodeChunk.MaterialFileName, nodeChunk.Materials.SubMaterials[subsets[j].MatID].Name) + "-material" + }; + + // Create the inputs. vertex, normal, texcoord, color + int inputCount = 3; + if (colors is not null || vertsUvs is not null) + inputCount++; + + triangles[j].Input = new ColladaInputShared[inputCount]; + + triangles[j].Input[0] = new ColladaInputShared + { + Semantic = ColladaInputSemantic.VERTEX, + Offset = 0, + source = "#" + vertices.ID + }; + triangles[j].Input[1] = new ColladaInputShared + { + Semantic = ColladaInputSemantic.NORMAL, + Offset = 1, + source = "#" + normSource.ID + }; + triangles[j].Input[2] = new ColladaInputShared + { + Semantic = ColladaInputSemantic.TEXCOORD, + Offset = 2, + source = "#" + uvSource.ID + }; + + int nextInputID = 3; + if (colors is not null || vertsUvs is not null) + { + triangles[j].Input[nextInputID] = new ColladaInputShared + { + Semantic = ColladaInputSemantic.COLOR, + Offset = nextInputID, + source = "#" + colorSource.ID + }; + nextInputID++; + } + + // Create the P node for the Triangles. + StringBuilder p = new(); + string formatString; + if (colors is not null || vertsUvs is not null) + formatString = "{0} {0} {0} {0} {1} {1} {1} {1} {2} {2} {2} {2} "; + else + formatString = "{0} {0} {0} {1} {1} {1} {2} {2} {2} "; + + var offsetStart = 0; + for (int q = 0; q < meshChunk.GeometryInfo.GeometrySubsets.IndexOf(subsets[j]); q++) + { + offsetStart += meshChunk.GeometryInfo.GeometrySubsets[q].NumVertices; + } + + for (var k = subsets[j].FirstIndex; k < (subsets[j].FirstIndex + subsets[j].NumIndices); k += 3) + { + var firstGlobalIndex = indices.Data[subsets[j].FirstIndex]; + uint localIndex0 = (uint)((indices.Data[k] - firstGlobalIndex) + offsetStart); + uint localIndex1 = (uint)((indices.Data[k + 1] - firstGlobalIndex) + offsetStart); + uint localIndex2 = (uint)((indices.Data[k + 2] - firstGlobalIndex) + offsetStart); + + p.AppendFormat(formatString, localIndex0, localIndex1, localIndex2); + } + triangles[j].P = new ColladaIntArrayString + { + Value_As_String = p.ToString().TrimEnd() + }; + } + + #endregion + + #region Create the source float_array nodes. Vertex, normal, UV. May need color as well. + + floatArrayVerts.Value_As_String = vertString.ToString().TrimEnd(); + floatArrayNormals.Value_As_String = normString.ToString().TrimEnd(); + floatArrayUVs.Value_As_String = uvString.ToString().TrimEnd(); + floatArrayColors.Value_As_String = colorString.ToString(); + + source[0].Float_Array = floatArrayVerts; + source[1].Float_Array = floatArrayNormals; + source[2].Float_Array = floatArrayUVs; + source[3].Float_Array = floatArrayColors; + geometry.Mesh.Source = source; + + // create the technique_common for each of these + posSource.Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor() + }; + posSource.Technique_Common.Accessor.Source = "#" + floatArrayVerts.ID; + posSource.Technique_Common.Accessor.Stride = 3; + posSource.Technique_Common.Accessor.Count = (uint)numberOfElements; + ColladaParam[] paramPos = new ColladaParam[3]; + paramPos[0] = new ColladaParam(); + paramPos[1] = new ColladaParam(); + paramPos[2] = new ColladaParam(); + paramPos[0].Name = "X"; + paramPos[0].Type = "float"; + paramPos[1].Name = "Y"; + paramPos[1].Type = "float"; + paramPos[2].Name = "Z"; + paramPos[2].Type = "float"; + posSource.Technique_Common.Accessor.Param = paramPos; + + normSource.Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor + { + Source = "#" + floatArrayNormals.ID, + Stride = 3, + Count = (uint)numberOfElements + } + }; + ColladaParam[] paramNorm = new ColladaParam[3]; + paramNorm[0] = new ColladaParam(); + paramNorm[1] = new ColladaParam(); + paramNorm[2] = new ColladaParam(); + paramNorm[0].Name = "X"; + paramNorm[0].Type = "float"; + paramNorm[1].Name = "Y"; + paramNorm[1].Type = "float"; + paramNorm[2].Name = "Z"; + paramNorm[2].Type = "float"; + normSource.Technique_Common.Accessor.Param = paramNorm; + + uvSource.Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor + { + Source = "#" + floatArrayUVs.ID, + Stride = 2 + } + }; + + uvSource.Technique_Common.Accessor.Count = (uint)numberOfElements; + + ColladaParam[] paramUV = new ColladaParam[2]; + paramUV[0] = new ColladaParam(); + paramUV[1] = new ColladaParam(); + paramUV[0].Name = "S"; + paramUV[0].Type = "float"; + paramUV[1].Name = "T"; + paramUV[1].Type = "float"; + uvSource.Technique_Common.Accessor.Param = paramUV; + + if (colors is not null || vertsUvs is not null) + { + colorSource.Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor() + }; + colorSource.Technique_Common.Accessor.Source = "#" + floatArrayColors.ID; + colorSource.Technique_Common.Accessor.Stride = 4; + colorSource.Technique_Common.Accessor.Count = (uint)numberOfElements; + ColladaParam[] paramColor = new ColladaParam[4]; + paramColor[0] = new ColladaParam(); + paramColor[1] = new ColladaParam(); + paramColor[2] = new ColladaParam(); + paramColor[3] = new ColladaParam(); + paramColor[0].Name = "R"; + paramColor[0].Type = "float"; + paramColor[1].Name = "G"; + paramColor[1].Type = "float"; + paramColor[2].Name = "B"; + paramColor[2].Type = "float"; + paramColor[3].Name = "A"; + paramColor[3].Type = "float"; + colorSource.Technique_Common.Accessor.Param = paramColor; + } + + geometryList.Add(geometry); + + #endregion + + // There is no geometry for a helper or controller node. Can skip the rest. + // Sanity checks + var vertcheck = vertString.ToString().TrimEnd().Split(' '); + var normcheck = normString.ToString().TrimEnd().Split(' '); + var colorcheck = colorString.ToString().TrimEnd().Split(' '); + var uvcheck = uvString.ToString().TrimEnd().Split(' '); + + } + libraryGeometries.Geometry = geometryList.ToArray(); + DaeObject.Library_Geometries = libraryGeometries; + } +} diff --git a/CgfConverter/Renderers/Collada/ColladaModelRenderer.Materials.cs b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Materials.cs new file mode 100644 index 00000000..6ab4d8d5 --- /dev/null +++ b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Materials.cs @@ -0,0 +1,324 @@ +using CgfConverter.Collada; +using CgfConverter.Models.Materials; +using CgfConverter.Renderers.Collada.Collada.Collada_B_Rep.Surfaces; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Extensibility; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Lighting; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Parameters; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Custom_Types; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Effects; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Materials; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Profiles.COMMON; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Rendering; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Technique_Common; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Texturing; +using CgfConverter.Renderers.Collada.Collada.Enums; +using CgfConverter.Renderers.Collada.Collada.Types; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using static Extensions.FileHandlingExtensions; +using static DDSUnsplitter.Library.DDSUnsplitter; + +namespace CgfConverter.Renderers.Collada; + +/// +/// ColladaModelRenderer partial class - Material creation and texture handling +/// +public partial class ColladaModelRenderer +{ + private void CreateMaterials() + { + List colladaMaterials = []; + List colladaEffects = []; + if (DaeObject.Library_Materials?.Material is null) + { + DaeObject.Library_Materials = new(); + DaeObject.Library_Materials.Material = []; + } + + foreach (var matKey in _cryData.Materials.Keys) + { + foreach (var subMat in _cryData.Materials[matKey].SubMaterials ?? []) + { + // Check to see if the collada object already has a material with this name. If not, add. + var matNames = DaeObject.Library_Materials.Material.Select(c => c.Name); + if (!matNames.Contains(subMat.Name)) + { + colladaMaterials.Add(AddMaterialToMaterialLibrary(matKey, subMat)); + var effect = CreateColladaEffect(matKey, subMat); + if (effect is not null) + colladaEffects.Add(effect); + } + // If there are matlayers, add these too + foreach (var matLayer in subMat.MatLayers?.Layers ?? []) + { + int index = Array.IndexOf(subMat.MatLayers?.Layers ?? [], matLayer); + colladaMaterials.Add(AddMaterialToMaterialLibrary(matLayer.Path ?? "unknown mat layer", subMat.SubMaterials[index])); + var effect = CreateColladaEffect(matKey, subMat); + if (effect is not null) + colladaEffects.Add(CreateColladaEffect(matLayer.Path ?? "unknown mat layer", subMat.SubMaterials[index])); + } + } + } + + int arraySize = DaeObject.Library_Materials.Material.Length; + Array.Resize(ref DaeObject.Library_Materials.Material, DaeObject.Library_Materials.Material.Length + colladaMaterials.Count); + colladaMaterials.CopyTo(DaeObject.Library_Materials.Material, arraySize); + + if (DaeObject.Library_Effects.Effect is null) + DaeObject.Library_Effects.Effect = colladaEffects.ToArray(); + else + { + int effectsArraySize = DaeObject.Library_Effects.Effect.Length; + Array.Resize(ref DaeObject.Library_Effects.Effect, DaeObject.Library_Effects.Effect.Length + colladaEffects.Count); + colladaEffects.CopyTo(DaeObject.Library_Effects.Effect, effectsArraySize); + } + } + + public ColladaEffect? CreateColladaEffect(string matKey, Material subMat) + { + if (subMat is null) return null; + //TODO: Change this so that it creates an effect for each Shader type. Parameters will need to be passed in from the material. + var effectName = GetMaterialName(matKey, subMat.Name ?? "unknown"); + + ColladaEffect colladaEffect = new() + { + ID = effectName + "-effect", + Name = effectName + }; + + // create the profile_common for the effect + List profiles = new(); + ColladaProfileCOMMON profile = new(); + profile.Technique = new() { sID = effectName + "-technique" }; + profiles.Add(profile); + + // Create a list for the new_params + List newparams = new(); + if (subMat.Textures is not null && !_args.NoTextures) + { + for (int j = 0; j < subMat.Textures.Length; j++) + { + // Add the Surface node + ColladaNewParam texSurface = new() + { + sID = effectName + "_" + subMat.Textures[j].Map + "-surface" + }; + ColladaSurface surface = new(); + texSurface.Surface = surface; + surface.Init_From = new ColladaInitFrom(); + texSurface.Surface.Type = "2D"; + texSurface.Surface.Init_From = new ColladaInitFrom + { + Uri = effectName + "_" + subMat.Textures[j].Map + }; + + // Add the Sampler node + ColladaNewParam texSampler = new() + { + sID = effectName + "_" + subMat.Textures[j].Map + "-sampler" + }; + ColladaSampler2D sampler2D = new(); + texSampler.Sampler2D = sampler2D; + texSampler.Sampler2D.Source = texSurface.sID; + + newparams.Add(texSurface); + newparams.Add(texSampler); + } + } + + #region Create the Technique + // Make the techniques for the profile + ColladaEffectTechniqueCOMMON technique = new(); + ColladaPhong phong = new(); + technique.Phong = phong; + technique.sID = effectName + "-technique"; + profile.Technique = technique; + + phong.Diffuse = new ColladaFXCommonColorOrTextureType(); + phong.Specular = new ColladaFXCommonColorOrTextureType(); + + // Add all the emissive, etc features to the phong + // Need to check if a texture exists. If so, refer to the sampler. Should be a imageList = []; + int numberOfTextures = submat?.Textures?.Length ?? 0; + + for (int i = 0; i < numberOfTextures; i++) + { + // For each texture in the material, we make a new object and add it to the list. + var name = GetMaterialName(matKey, submat.Name); + ColladaImage image = new() + { + ID = name + "_" + submat.Textures[i].Map, + Name = name + "_" + submat.Textures[i].Map, + Init_From = new ColladaInitFrom() + }; + // TODO: Refactor to use fully qualified path, and to use Pack File System. + var textureFile = ResolveTextureFile(submat.Textures[i].File, _args.PackFileSystem, [_cryData.ObjectDir]); + + if (_args.PngTextures && File.Exists(Path.ChangeExtension(textureFile, ".png"))) + textureFile = Path.ChangeExtension(textureFile, ".png"); + else if (_args.TgaTextures && File.Exists(Path.ChangeExtension(textureFile, ".tga"))) + textureFile = Path.ChangeExtension(textureFile, ".tga"); + else if (_args.TiffTextures && File.Exists(Path.ChangeExtension(textureFile, ".tif"))) + textureFile = Path.ChangeExtension(textureFile, ".tif"); + try + { + if (_args.UnsplitTextures) + { + Log.I($"Combining texture file {textureFile}"); + Combine(textureFile); + } + } + catch (Exception ex) + { + Log.W($"Error combining texture {textureFile}: {ex.Message}"); + } + + textureFile = Path.GetRelativePath(daeOutputFile.DirectoryName, textureFile); + + textureFile = textureFile.Replace(" ", @"%20"); + image.Init_From.Uri = textureFile; + imageList.Add(image); + } + + ColladaImage[] images = imageList.ToArray(); + if (DaeObject.Library_Images.Image is null) + DaeObject.Library_Images.Image = images; + else + { + int arraySize = DaeObject.Library_Images.Image.Length; + Array.Resize(ref DaeObject.Library_Images.Image, DaeObject.Library_Images.Image.Length + images.Length); + images.CopyTo(DaeObject.Library_Images.Image, arraySize); + } + } + + private string GetMaterialName(string matKey, string submatName) + { + // material name is _mtl_ + var matfileName = Path.GetFileNameWithoutExtension(matKey); + + return $"{matfileName}_mtl_{submatName}".Replace(' ', '_'); + } +} diff --git a/CgfConverter/Renderers/Collada/ColladaModelRenderer.Nodes.cs b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Nodes.cs new file mode 100644 index 00000000..96d2dd96 --- /dev/null +++ b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Nodes.cs @@ -0,0 +1,240 @@ +using CgfConverter.CryEngineCore; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Controller; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Geometry; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Scene; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Transform; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Materials; +using CgfConverter.Renderers.Collada.Collada.Collada_FX.Technique_Common; +using CgfConverter.Renderers.Collada.Collada.Enums; +using System.Collections.Generic; +using System.Linq; + +namespace CgfConverter.Renderers.Collada; + +/// +/// ColladaModelRenderer partial class - Visual scene and node hierarchy creation +/// +public partial class ColladaModelRenderer +{ + public void WriteLibrary_VisualScenes() + { + ColladaLibraryVisualScenes libraryVisualScenes = new(); + + List visualScenes = []; + ColladaVisualScene visualScene = new(); + List nodes = []; + + // THERE CAN BE MULTIPLE ROOT NODES IN EACH FILE! Check to see if the parentnodeid ~0 and be sure to add a node for it. + List positionNodes = []; + List positionRoots = _cryData.Nodes.Where(a => a.ParentNodeID == ~0).ToList(); + foreach (ChunkNode root in positionRoots) + { + positionNodes.Add(CreateNode(root, false)); + } + nodes.AddRange(positionNodes.ToArray()); + + visualScene.Node = nodes.ToArray(); + visualScene.ID = "Scene"; + visualScenes.Add(visualScene); + + libraryVisualScenes.Visual_Scene = visualScenes.ToArray(); + DaeObject.Library_Visual_Scene = libraryVisualScenes; + } + + public void WriteLibrary_VisualScenesWithSkeleton() + { + ColladaLibraryVisualScenes libraryVisualScenes = new(); + + List visualScenes = []; + ColladaVisualScene visualScene = new(); + List nodes = []; + + List positionRoots = _cryData.Nodes.Where(a => a.ParentNodeID == ~0).ToList(); + + // Check to see if there is a CompiledBones chunk. If so, add a Node. + if (_cryData.Chunks.Any(a => a.ChunkType == ChunkType.CompiledBones || + a.ChunkType == ChunkType.CompiledBonesSC || + a.ChunkType == ChunkType.CompiledBones_Ivo || + a.ChunkType == ChunkType.CompiledBones_Ivo2)) + { + ColladaNode boneNode = CreateJointNode(_cryData.SkinningInfo.RootBone); + nodes.Add(boneNode); + } + + var hasGeometry = _cryData.Nodes.Any(x => x.MeshData is not null); + + if (hasGeometry) + { + foreach (var node in positionRoots) + { + var colladaNode = CreateNode(node, true); + colladaNode.Instance_Controller = new ColladaInstanceController[1]; + colladaNode.Instance_Controller[0] = new ColladaInstanceController + { + URL = "#Controller", + Skeleton = new ColladaSkeleton[1] + }; + + var skeleton = colladaNode.Instance_Controller[0].Skeleton[0] = new ColladaSkeleton(); + skeleton.Value = $"#{_cryData.SkinningInfo.CompiledBones[0].BoneName}".Replace(' ', '_'); + colladaNode.Instance_Controller[0].Bind_Material = new ColladaBindMaterial[1]; + ColladaBindMaterial bindMaterial = colladaNode.Instance_Controller[0].Bind_Material[0] = new ColladaBindMaterial(); + + // Create an Instance_Material for each material + bindMaterial.Technique_Common = new ColladaTechniqueCommonBindMaterial(); + colladaNode.Instance_Controller[0].Bind_Material[0].Technique_Common.Instance_Material = CreateInstanceMaterials(node); + + foreach (ChunkNode child in node.Children) + CreateChildNodes(child, true); + + nodes.Add(colladaNode); + } + } + + visualScene.Node = nodes.ToArray(); + visualScene.ID = "Scene"; + visualScenes.Add(visualScene); + + libraryVisualScenes.Visual_Scene = visualScenes.ToArray(); + DaeObject.Library_Visual_Scene = libraryVisualScenes; + } + + private ColladaInstanceMaterialGeometry[] CreateInstanceMaterials(ChunkNode node) + { + List instanceMaterials = []; + + var matIndices = node.MeshData?.GeometryInfo?.GeometrySubsets?.Select(x => x.MatID) ?? []; + + foreach (var index in matIndices) + { + var matName = GetMaterialName(node.MaterialFileName, node.Materials.SubMaterials[index].Name); + ColladaInstanceMaterialGeometry instanceMaterial = new(); + instanceMaterial.Target = $"#{matName}-material"; + instanceMaterial.Symbol = $"{matName}-material"; + instanceMaterials.Add(instanceMaterial); + } + + return instanceMaterials.ToArray(); + } + + private ColladaNode CreateNode(ChunkNode nodeChunk, bool isControllerNode) + { + ColladaNode colladaNode = new(); + + string nodeName = nodeChunk.Name; + int nodeID = nodeChunk.ID; + + if (nodeChunk.ChunkHelper is not null || nodeChunk.MeshData?.GeometryInfo is null) + colladaNode = CreateSimpleNode(nodeChunk, isControllerNode); + else + { + ColladaGeometry geometryLibraryObject = DaeObject.Library_Geometries.Geometry.Where(a => a.Name == nodeChunk.Name).FirstOrDefault(); + ChunkMesh geometryMesh = nodeChunk.MeshData; + colladaNode = CreateGeometryNode(nodeChunk, geometryMesh, isControllerNode); + } + + colladaNode.node = CreateChildNodes(nodeChunk, isControllerNode); + return colladaNode; + } + + /// This will be used to make the Collada node element for Node chunks that point to Helper Chunks and MeshPhysics + private ColladaNode CreateSimpleNode(ChunkNode nodeChunk, bool isControllerNode) + { + // This will be used to make the Collada node element for Node chunks that point to Helper Chunks and MeshPhysics + ColladaNode colladaNode = new() + { + Type = ColladaNodeType.NODE, + Name = nodeChunk.Name, + ID = nodeChunk.Name + }; + + ColladaMatrix matrix = new() + { + sID = "transform", + Value_As_String = CreateStringFromMatrix4x4(nodeChunk.LocalTransform) + }; + colladaNode.Matrix = new ColladaMatrix[1] { matrix }; + + colladaNode.node = CreateChildNodes(nodeChunk, isControllerNode); + return colladaNode; + } + + /// Used by CreateNode and CreateSimpleNodes to create all the child nodes for the given node. + private ColladaNode[]? CreateChildNodes(ChunkNode nodeChunk, bool isControllerNode) + { + List childNodes = []; + foreach (ChunkNode childNodeChunk in nodeChunk.Children) + { + if (_args.IsNodeNameExcluded(childNodeChunk.Name)) + { + Log.D($"Excluding child node {childNodeChunk.Name}"); + continue; + } + + ColladaNode childNode = CreateNode(childNodeChunk, isControllerNode); + childNodes.Add(childNode); + } + return childNodes.ToArray(); + } + + private ColladaNode CreateGeometryNode(ChunkNode nodeChunk, ChunkMesh tmpMeshChunk, bool isControllerNode) + { + ColladaNode colladaNode = new(); + var meshSubsets = nodeChunk.MeshData.GeometryInfo.GeometrySubsets; + var nodeType = ColladaNodeType.NODE; + colladaNode.Type = nodeType; + colladaNode.Name = nodeChunk.Name; + colladaNode.ID = nodeChunk.Name; + + // Make the lists necessary for this Node. + List bindMaterials = []; + List matrices = []; + ColladaMatrix matrix = new() + { + Value_As_String = CreateStringFromMatrix4x4(nodeChunk.LocalTransform), + sID = "transform" + }; + + matrices.Add(matrix); // we can have multiple matrices, but only need one since there is only one per Node chunk anyway + colladaNode.Matrix = matrices.ToArray(); + + // Each node will have one instance geometry, although it could be a list. + if (!isControllerNode) + { + List instanceGeometries = []; + ColladaInstanceGeometry instanceGeometry = new() + { + Name = nodeChunk.Name, + URL = "#" + nodeChunk.Name + "-mesh" // this is the ID of the geometry. + }; + ColladaBindMaterial bindMaterial = new() + { + Technique_Common = new ColladaTechniqueCommonBindMaterial + { + Instance_Material = new ColladaInstanceMaterialGeometry[meshSubsets.Count] + } + }; + bindMaterials.Add(bindMaterial); + instanceGeometry.Bind_Material = bindMaterials.ToArray(); + instanceGeometries.Add(instanceGeometry); + + colladaNode.Instance_Geometry = instanceGeometries.ToArray(); + colladaNode.Instance_Geometry[0].Bind_Material[0].Technique_Common.Instance_Material = CreateInstanceMaterials(nodeChunk); + } + + return colladaNode; + } + + /// Adds the scene element to the Collada document. + private void WriteScene() + { + ColladaScene scene = new(); + ColladaInstanceVisualScene visualScene = new() + { + URL = "#Scene", + Name = "Scene" + }; + scene.Visual_Scene = visualScene; + DaeObject.Scene = scene; + } +} diff --git a/CgfConverter/Renderers/Collada/ColladaModelRenderer.Skeleton.cs b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Skeleton.cs new file mode 100644 index 00000000..ba2556eb --- /dev/null +++ b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Skeleton.cs @@ -0,0 +1,304 @@ +using CgfConverter.Collada; +using CgfConverter.Models; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Controller; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Data_Flow; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Extensibility; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Parameters; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Scene; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Technique_Common; +using CgfConverter.Renderers.Collada.Collada.Collada_Core.Transform; +using CgfConverter.Renderers.Collada.Collada.Enums; +using CgfConverter.Renderers.Collada.Collada.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using static CgfConverter.Utilities.HelperMethods; + +namespace CgfConverter.Renderers.Collada; + +/// +/// ColladaModelRenderer partial class - Skeleton/controller and bone handling +/// +public partial class ColladaModelRenderer +{ + private void WriteLibrary_Controllers() + { + if (DaeObject.Library_Geometries.Geometry.Length != 0) + { + ColladaLibraryControllers libraryController = new(); + + // There can be multiple controllers in the controller library. But for Cryengine files, there is only one rig. + // So if a rig exists, make that the controller. This applies mostly to .chr files, which will have a rig and may have geometry. + ColladaController controller = new() { ID = "Controller" }; + // Create the skin object and assign to the controller + ColladaSkin skin = new() + { + source = "#" + DaeObject.Library_Geometries.Geometry[0].ID, + Bind_Shape_Matrix = new ColladaFloatArrayString() + }; + skin.Bind_Shape_Matrix.Value_As_String = CreateStringFromMatrix4x4(Matrix4x4.Identity); // We will assume the BSM is the identity matrix for now + + // Create the 3 sources for this controller: joints, bind poses, and weights + skin.Source = new ColladaSource[3]; + + // Populate the data. + // Need to map the exterior vertices (geometry) to the int vertices. Or use the Bone Map datastream if it exists (check HasBoneMapDatastream). + #region Joints Source + ColladaSource jointsSource = new() + { + ID = "Controller-joints", + Name_Array = new ColladaNameArray() + { + ID = "Controller-joints-array", + Count = _cryData.SkinningInfo.CompiledBones.Count, + } + }; + StringBuilder boneNames = new(); + for (int i = 0; i < _cryData.SkinningInfo.CompiledBones.Count; i++) + { + boneNames.Append(_cryData.SkinningInfo.CompiledBones[i].BoneName.Replace(' ', '_') + " "); + } + jointsSource.Name_Array.Value_Pre_Parse = boneNames.ToString().TrimEnd(); + jointsSource.Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor + { + Source = "#Controller-joints-array", + Count = (uint)_cryData.SkinningInfo.CompiledBones.Count, + Stride = 1 + } + }; + skin.Source[0] = jointsSource; + #endregion + + #region Bind Pose Array Source + ColladaSource bindPoseArraySource = new() + { + ID = "Controller-bind_poses", + Float_Array = new() + { + ID = "Controller-bind_poses-array", + Count = _cryData.SkinningInfo.CompiledBones.Count * 16, + Value_As_String = GetBindPoseArray(_cryData.SkinningInfo.CompiledBones) + }, + Technique_Common = new ColladaTechniqueCommonSource + { + Accessor = new ColladaAccessor + { + Source = "#Controller-bind_poses-array", + Count = (uint)_cryData.SkinningInfo.CompiledBones.Count, + Stride = 16, + } + } + }; + bindPoseArraySource.Technique_Common.Accessor.Param = new ColladaParam[1]; + bindPoseArraySource.Technique_Common.Accessor.Param[0] = new ColladaParam + { + Name = "TRANSFORM", + Type = "float4x4" + }; + skin.Source[1] = bindPoseArraySource; + #endregion + + #region Weights Source + var skinningInfo = _cryData.SkinningInfo; + var nodeChunk = _cryData.RootNode; + + ColladaSource weightArraySource = new() + { + ID = "Controller-weights", + Technique_Common = new ColladaTechniqueCommonSource() + }; + ColladaAccessor accessor = weightArraySource.Technique_Common.Accessor = new ColladaAccessor(); + + weightArraySource.Float_Array = new ColladaFloatArray() + { + ID = "Controller-weights-array", + }; + + var numberOfWeights = skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count; + var boneInfluenceCount = + nodeChunk.MeshData?.GeometryInfo?.BoneMappings?.Data[0].BoneInfluenceCount == 8 + ? 8 + : 4; + + var boneMappingData = skinningInfo.IntVertices is null + ? nodeChunk.MeshData?.GeometryInfo?.BoneMappings.Data.ToList() + : skinningInfo.Ext2IntMap + .Select(x => skinningInfo.IntVertices[x]) + .Select(x => x.BoneMapping) + .ToList(); + + if (boneMappingData is null) return; + + StringBuilder weights = new(); + weightArraySource.Float_Array.Count = numberOfWeights; + for (int i = 0; i < numberOfWeights; i++) + { + for (int j = 0; j < boneInfluenceCount; j++) + { + weights.Append(boneMappingData[i].Weight[j].ToString() + " "); + } + } + ; + accessor.Count = (uint)(numberOfWeights * boneInfluenceCount); + + CleanNumbers(weights); + weightArraySource.Float_Array.Value_As_String = weights.ToString().TrimEnd(); + // Add technique_common part. + accessor.Source = "#Controller-weights-array"; + accessor.Stride = 1; + accessor.Param = new ColladaParam[1]; + accessor.Param[0] = new ColladaParam + { + Name = "WEIGHT", + Type = "float" + }; + skin.Source[2] = weightArraySource; + + #endregion + + #region Joints + skin.Joints = new ColladaJoints + { + Input = new ColladaInputUnshared[2] + }; + skin.Joints.Input[0] = new ColladaInputUnshared + { + Semantic = new ColladaInputSemantic() + }; + skin.Joints.Input[0].Semantic = ColladaInputSemantic.JOINT; + skin.Joints.Input[0].source = "#Controller-joints"; + skin.Joints.Input[1] = new ColladaInputUnshared + { + Semantic = new ColladaInputSemantic() + }; + skin.Joints.Input[1].Semantic = ColladaInputSemantic.INV_BIND_MATRIX; + skin.Joints.Input[1].source = "#Controller-bind_poses"; + #endregion + + #region Vertex Weights + ColladaVertexWeights vertexWeights = skin.Vertex_Weights = new(); + vertexWeights.Count = (int)numberOfWeights; + skin.Vertex_Weights.Input = new ColladaInputShared[2]; + ColladaInputShared jointSemantic = skin.Vertex_Weights.Input[0] = new(); + jointSemantic.Semantic = ColladaInputSemantic.JOINT; + jointSemantic.source = "#Controller-joints"; + jointSemantic.Offset = 0; + ColladaInputShared weightSemantic = skin.Vertex_Weights.Input[1] = new(); + weightSemantic.Semantic = ColladaInputSemantic.WEIGHT; + weightSemantic.source = "#Controller-weights"; + weightSemantic.Offset = 1; + StringBuilder vCount = new(); + for (int i = 0; i < numberOfWeights; i++) + { + vCount.Append($"{boneInfluenceCount} "); + } + ; + vertexWeights.VCount = new ColladaIntArrayString + { + Value_As_String = vCount.ToString().TrimEnd() + }; + StringBuilder vertices = new(); + int index = 0; + + for (int i = 0; i < numberOfWeights; i++) + { + vertices.Append(boneMappingData[i].BoneIndex[0] + " " + index + " "); + vertices.Append(boneMappingData[i].BoneIndex[1] + " " + (index + 1) + " "); + vertices.Append(boneMappingData[i].BoneIndex[2] + " " + (index + 2) + " "); + vertices.Append(boneMappingData[i].BoneIndex[3] + " " + (index + 3) + " "); + if (boneInfluenceCount == 8) + { + vertices.Append(boneMappingData[i].BoneIndex[4] + " " + (index + 4) + " "); + vertices.Append(boneMappingData[i].BoneIndex[5] + " " + (index + 5) + " "); + vertices.Append(boneMappingData[i].BoneIndex[6] + " " + (index + 6) + " "); + vertices.Append(boneMappingData[i].BoneIndex[7] + " " + (index + 7) + " "); + index += 4; + } + index += 4; + } + vertexWeights.V = new ColladaIntArrayString { Value_As_String = vertices.ToString().TrimEnd() }; + #endregion + + // create the extra element for the FCOLLADA profile + controller.Extra = new ColladaExtra[1]; + controller.Extra[0] = new ColladaExtra + { + Technique = new ColladaTechnique[1] + }; + controller.Extra[0].Technique[0] = new ColladaTechnique + { + profile = "FCOLLADA", + UserProperties = "SkinController" + }; + + // Add the parts to their parents + controller.Skin = skin; + libraryController.Controller = new ColladaController[1]; + libraryController.Controller[0] = controller; + DaeObject.Library_Controllers = libraryController; + } + } + + private ColladaNode CreateJointNode(CompiledBone bone) + { + var boneName = bone.BoneName.Replace(' ', '_'); + + ColladaNode tmpNode = new() + { + ID = boneName, // ID, name and sID must be the same or the controller can't seem to find the bone. + Name = boneName, + sID = boneName, + Type = ColladaNodeType.JOINT + }; + if (bone.ControllerID != -1 && bone.ControllerID != uint.MaxValue) + { + controllerIdToBoneName.Add(bone.ControllerID, boneName); // Use sanitized name to match joint node ID + + // Store rest transform for animation fallback (bones without position/rotation tracks) + Matrix4x4 localMatrix4x4 = bone.LocalTransformMatrix.ConvertToTransformMatrix(); + Matrix4x4.Decompose(localMatrix4x4, out _, out var restRotation, out var restPosition); + controllerIdToRestTransform.Add(bone.ControllerID, (restPosition, restRotation)); + } + + Matrix4x4 localMatrix = bone.LocalTransformMatrix.ConvertToTransformMatrix(); + + // Use matrix with sid="transform" for Blender compatibility + // Blender's Collada importer expects this exact SID for animation channel targeting + tmpNode.Matrix = + [ + new ColladaMatrix + { + sID = "transform", + Value_As_String = CreateStringFromMatrix4x4(localMatrix) + } + ]; + + // Recursively call this for each of the child bones to this bone. + if (bone.NumberOfChildren > 0) + { + List childNodes = []; + var allChildBones = _cryData.SkinningInfo?.GetChildBones(bone) ?? []; + foreach (CompiledBone childBone in allChildBones) + { + childNodes.Add(CreateJointNode(childBone)); + } + tmpNode.node = childNodes.ToArray(); + } + return tmpNode; + } + + /// Retrieves the worldtobone (bind pose matrix) for the bone. + private static string GetBindPoseArray(List compiledBones) + { + StringBuilder value = new(); + for (int i = 0; i < compiledBones.Count; i++) + { + value.Append(CreateStringFromMatrix4x4(compiledBones[i].BindPoseMatrix) + " "); + } + return value.ToString().TrimEnd(); + } +} diff --git a/CgfConverter/Renderers/Collada/ColladaModelRenderer.Utilities.cs b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Utilities.cs new file mode 100644 index 00000000..35db9bb1 --- /dev/null +++ b/CgfConverter/Renderers/Collada/ColladaModelRenderer.Utilities.cs @@ -0,0 +1,51 @@ +using System.Numerics; +using System.Text; +using static CgfConverter.Utilities.HelperMethods; + +namespace CgfConverter.Renderers.Collada; + +/// +/// ColladaModelRenderer partial class - Utility methods for string formatting +/// +public partial class ColladaModelRenderer +{ + private static string CreateStringFromVector3(Vector3 vector) + { + StringBuilder vectorValues = new(); + vectorValues.AppendFormat("{0:F6} {1:F6} {2:F6}", vector.X, vector.Y, vector.Z); + CleanNumbers(vectorValues); + return vectorValues.ToString(); + } + + private static string CreateStringFromVector4(Vector4 vector) + { + StringBuilder vectorValues = new(); + vectorValues.AppendFormat("{0:F6} {1:F6} {2:F6} {3:F6}", vector.X, vector.Y, vector.Z, vector.W); + CleanNumbers(vectorValues); + return vectorValues.ToString(); + } + + private static string CreateStringFromMatrix4x4(Matrix4x4 matrix) + { + StringBuilder matrixValues = new(); + matrixValues.AppendFormat("{0:F6} {1:F6} {2:F6} {3:F6} {4:F6} {5:F6} {6:F6} {7:F6} {8:F6} {9:F6} {10:F6} {11:F6} {12:F6} {13:F6} {14:F6} {15:F6}", + matrix.M11, + matrix.M12, + matrix.M13, + matrix.M14, + matrix.M21, + matrix.M22, + matrix.M23, + matrix.M24, + matrix.M31, + matrix.M32, + matrix.M33, + matrix.M34, + matrix.M41, + matrix.M42, + matrix.M43, + matrix.M44); + CleanNumbers(matrixValues); + return matrixValues.ToString(); + } +} diff --git a/CgfConverter/Renderers/Collada/ColladaModelRenderer.cs b/CgfConverter/Renderers/Collada/ColladaModelRenderer.cs index 0f5ac7ee..9da5905a 100644 --- a/CgfConverter/Renderers/Collada/ColladaModelRenderer.cs +++ b/CgfConverter/Renderers/Collada/ColladaModelRenderer.cs @@ -1,32 +1,8 @@ -using CgfConverter.Collada; using CgfConverter.CryEngineCore; -using CgfConverter.Models; -using CgfConverter.Models.Materials; using CgfConverter.Models.Structs; using CgfConverter.Renderers.Collada.Collada; -using CgfConverter.Renderers.Collada.Collada.Collada_B_Rep.Surfaces; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Animation; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Controller; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Data_Flow; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Extensibility; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Geometry; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Lighting; using CgfConverter.Renderers.Collada.Collada.Collada_Core.Metadata; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Parameters; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Scene; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Technique_Common; -using CgfConverter.Renderers.Collada.Collada.Collada_Core.Transform; -using CgfConverter.Renderers.Collada.Collada.Collada_FX.Custom_Types; -using CgfConverter.Renderers.Collada.Collada.Collada_FX.Effects; -using CgfConverter.Renderers.Collada.Collada.Collada_FX.Materials; -using CgfConverter.Renderers.Collada.Collada.Collada_FX.Profiles.COMMON; -using CgfConverter.Renderers.Collada.Collada.Collada_FX.Rendering; -using CgfConverter.Renderers.Collada.Collada.Collada_FX.Technique_Common; -using CgfConverter.Renderers.Collada.Collada.Collada_FX.Texturing; -using CgfConverter.Renderers.Collada.Collada.Enums; -using CgfConverter.Renderers.Collada.Collada.Types; using CgfConverter.Utils; -using Extensions; using System; using System.Collections.Generic; using System.Globalization; @@ -34,16 +10,22 @@ using System.Linq; using System.Numerics; using System.Reflection; -using System.Text; -using System.Xml; using System.Xml.Serialization; -using static CgfConverter.Utilities.HelperMethods; -using static DDSUnsplitter.Library.DDSUnsplitter; -using static Extensions.FileHandlingExtensions; namespace CgfConverter.Renderers.Collada; -public class ColladaModelRenderer : IRenderer +/// +/// ColladaModelRenderer - Main orchestration class for Collada (.dae) export. +/// Split into partial classes for maintainability: +/// - ColladaModelRenderer.cs (this file) - Main orchestration, constructor, Render() +/// - ColladaModelRenderer.Animation.cs - Animation export +/// - ColladaModelRenderer.Materials.cs - Material and texture handling +/// - ColladaModelRenderer.Geometry.cs - Mesh/geometry creation +/// - ColladaModelRenderer.Skeleton.cs - Controller/bone/skinning +/// - ColladaModelRenderer.Nodes.cs - Visual scene and node hierarchy +/// - ColladaModelRenderer.Utilities.cs - String formatting helpers +/// +public partial class ColladaModelRenderer : IRenderer { protected readonly ArgsHandler _args; protected readonly CryEngine _cryData; @@ -59,6 +41,7 @@ public class ColladaModelRenderer : IRenderer private static readonly IRGBA DefaultColor = new(1.0f, 1.0f, 1.0f, 1.0f); private readonly Dictionary controllerIdToBoneName = new(); + private readonly Dictionary controllerIdToRestTransform = new(); private readonly TaggedLogger Log; @@ -85,6 +68,12 @@ public int Render() writer.Close(); Log.D("End of Write Collada. Export complete."); + + // Export separate animation files for Blender compatibility + // Blender's Collada importer merges all animations into one action, + // so we export each animation as a separate file for NLA workflow + ExportAnimationFiles(); + return 1; } @@ -117,9 +106,9 @@ public void GenerateDaeObject() else WriteLibrary_VisualScenes(); - // Write animations - //if (_cryData.Animations is not null) - // WriteLibrary_Animations(); + // Note: Animations are exported to separate files for Blender compatibility. + // Blender's Collada importer merges all animations into one action, so we + // don't include animations in the main geometry file. See ExportAnimationFiles(). } protected void WriteColladaRoot(string version) @@ -157,1429 +146,4 @@ protected void WriteAsset() DaeObject.Asset = asset; DaeObject.Asset.Contributor = contributors; } - - public void WriteLibrary_Animations() - { - var animationLibrary = new ColladaLibraryAnimations(); - // Find all the 905 controller chunks - foreach (var animChunk in _cryData.Animations - .SelectMany(x => x.ChunkMap.Values.OfType()) - .ToList()) - { - var names = animChunk?.Animations?.Select(x => Path.GetFileNameWithoutExtension(x.Name)).ToArray(); - animationLibrary.Animation = new ColladaAnimation[animChunk?.Animations?.Count ?? 0]; - if (animationLibrary.Animation.Length == 0) - continue; - - for (int i = 0; i < animChunk.Animations.Count; i++) - { - var animation = animChunk.Animations[i]; - var animationName = Path.GetFileNameWithoutExtension(animation.Name); - - var colladaAnimation = new ColladaAnimation // Root animation object. Controller animations go here. - { - Name = animationName, - ID = $"{animationName}_animation", - Animation = new ColladaAnimation[animation.Controllers.Count] - }; - - // Add an animation for each controller - var controllerAnimations = new List(); - for (int j = 0; j < animation.Controllers.Count; j++) - { - // Each controller can have up to 2 sub Animations, one for position and one for rotation - var controllerInfo = animation.Controllers[j]; - var controllerBoneName = controllerIdToBoneName[controllerInfo.ControllerID]; - var controllerIdBase = $"{controllerBoneName}_{controllerInfo.ControllerID}"; - - if (animation.Controllers[j].HasPosTrack) - controllerAnimations.Add(CreateAnimation(controllerInfo, "translate", animChunk)); - - if (animation.Controllers[j].HasRotTrack) - controllerAnimations.Add(CreateAnimation(controllerInfo, "rotate", animChunk)); - - colladaAnimation.Animation[j] = new ColladaAnimation - { - Name = controllerIdBase, - ID = $"{controllerIdBase}_animation", - Animation = controllerAnimations.ToArray() - }; - controllerAnimations.Clear(); - } - animationLibrary.Animation[i] = colladaAnimation; - } - } - - DaeObject.Library_Animations = animationLibrary; - } - - private ColladaAnimation CreateAnimation( - ChunkController_905.CControllerInfo controllerInfo, - string animType, - ChunkController_905 animationChunk) - { - var controllerBoneName = controllerIdToBoneName[controllerInfo.ControllerID]; - var controllerIdBase = $"{controllerBoneName}_{controllerInfo.ControllerID}_{animType}"; - - var numberOfTimeFrames = animType == "rotate" - ? animationChunk.KeyTimes[controllerInfo.RotKeyTimeTrack].Count - : animationChunk.KeyTimes[controllerInfo.PosKeyTimeTrack].Count; - - var pathName = animType == "rotate" ? "rotation" : "location"; - - var inputSource = new ColladaSource // Time - { - ID = $"{controllerIdBase}_input", - Name = $"{controllerIdBase}_input" - }; - var outputSource = new ColladaSource // transform - { - ID = $"{controllerIdBase}_output", - Name = $"{controllerIdBase}_output" - }; - var interpolationSource = new ColladaSource // interpolation - { - ID = $"{controllerIdBase}_interpolation", - Name = $"{controllerIdBase}_interpolation" - }; - var sampler = new ColladaSampler - { - ID = $"{controllerIdBase}_sampler_{animType}", - Input = - [ - new ColladaInputUnshared - { - Semantic = ColladaInputSemantic.INPUT, - source = $"#{controllerIdBase}_input" - }, - new ColladaInputUnshared - { - Semantic = ColladaInputSemantic.OUTPUT, - source = $"#{controllerIdBase}_output" - }, - new ColladaInputUnshared - { - Semantic = ColladaInputSemantic.INTERPOLATION, - source = $"#{controllerIdBase}_interpolation" - } - ] - }; - var channel = new ColladaChannel - { - Source = $"#{controllerIdBase}_sampler_{animType}", - Target = $"{controllerBoneName}/matrix" - }; - var controllerAnimation = new ColladaAnimation - { - Name = controllerIdBase, - ID = $"{controllerIdBase}_animation", - Source = new ColladaSource[3] { inputSource, outputSource, interpolationSource }, - Channel = new ColladaChannel[1] { channel }, - Sampler = new ColladaSampler[1] { sampler }, - }; - - // Create the time source - var timeArray = new ColladaFloatArray - { - ID = $"{controllerBoneName}_{controllerInfo.ControllerID}_{animType}_time_array", - Count = numberOfTimeFrames, - Value_As_String = string.Join(" ", - animType == "rotate" - ? animationChunk.KeyTimes[controllerInfo.RotKeyTimeTrack].Select(x => x / 30f) - : animationChunk.KeyTimes[controllerInfo.PosKeyTimeTrack].Select(x => x / 30f)) - }; - - inputSource.Float_Array = timeArray; - inputSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor - { - Source = $"#{controllerBoneName}_{controllerInfo.ControllerID}_{animType}_time_array", - Count = (uint)timeArray.Count, - Stride = 1, - Param = [new ColladaParam { Name = "TIME", Type = "float" }] - } - }; - - // Create the transform source - var numberOfElements = animType == "rotate" ? 4 : 3; - - var colladaParams = new ColladaParam[1]; - if (animType == "rotate") - colladaParams[0] = new ColladaParam { Name = "AXISANGLE", Type = "vector4" }; - else - colladaParams[0] = new ColladaParam { Name = "TRANSLATE", Type = "vector3" }; - - var positionArray = new ColladaFloatArray - { - ID = $"{controllerBoneName}_{controllerInfo.ControllerID}_{animType}_array", - Count = numberOfTimeFrames * numberOfElements, - Value_As_String = string.Join(" ", - animType == "rotate" - ? animationChunk.KeyRotations[controllerInfo.RotTrack].Select(x => CreateStringFromVector4(x.ToAxisAngle())) - : animationChunk.KeyPositions[controllerInfo.PosTrack].Select(x => CreateStringFromVector3(x))) - }; - outputSource.Float_Array = positionArray; - //var paramType = animType == "rotate" ? "vector4" : "vector3"; - outputSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor - { - Source = $"#{controllerBoneName}_{controllerInfo.ControllerID}_{animType}_array", - Count = (uint)numberOfTimeFrames, - Stride = (uint)numberOfElements, - Param = colladaParams - } - }; - - // Create the interpolation source (all LINEAR) - var interpolationArray = new ColladaNameArray - { - ID = $"{controllerBoneName}_{controllerInfo.ControllerID}_{animType}_interpolation_array", - Count = numberOfTimeFrames, - Value_Pre_Parse = string.Join(' ', Enumerable.Repeat("LINEAR", numberOfTimeFrames)) - }; - interpolationSource.Name_Array = interpolationArray; - interpolationSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor - { - Source = $"#{controllerBoneName}_{controllerInfo.ControllerID}_{animType}_interpolation_array", - Count = (uint)numberOfTimeFrames, - Stride = 1, - Param = [new ColladaParam { Name = "INTERPOLATION", Type = "name" }] - } - }; - - return controllerAnimation; - } - - public ColladaEffect? CreateColladaEffect(string matKey, Material subMat) - { - if (subMat is null) return null; - //TODO: Change this so that it creates an effect for each Shader type. Parameters will need to be passed in from the material. - var effectName = GetMaterialName(matKey, subMat.Name ?? "unknown"); - - ColladaEffect colladaEffect = new() - { - ID = effectName + "-effect", - Name = effectName - }; - - // create the profile_common for the effect - List profiles = new(); - ColladaProfileCOMMON profile = new(); - profile.Technique = new() { sID = effectName + "-technique" }; - profiles.Add(profile); - - // Create a list for the new_params - List newparams = new(); - if (subMat.Textures is not null && !_args.NoTextures) - { - for (int j = 0; j < subMat.Textures.Length; j++) - { - // Add the Surface node - ColladaNewParam texSurface = new() - { - sID = effectName + "_" + subMat.Textures[j].Map + "-surface" - }; - ColladaSurface surface = new(); - texSurface.Surface = surface; - surface.Init_From = new ColladaInitFrom(); - texSurface.Surface.Type = "2D"; - texSurface.Surface.Init_From = new ColladaInitFrom - { - Uri = effectName + "_" + subMat.Textures[j].Map - }; - - // Add the Sampler node - ColladaNewParam texSampler = new() - { - sID = effectName + "_" + subMat.Textures[j].Map + "-sampler" - }; - ColladaSampler2D sampler2D = new(); - texSampler.Sampler2D = sampler2D; - texSampler.Sampler2D.Source = texSurface.sID; - - newparams.Add(texSurface); - newparams.Add(texSampler); - } - } - - #region Create the Technique - // Make the techniques for the profile - ColladaEffectTechniqueCOMMON technique = new(); - ColladaPhong phong = new(); - technique.Phong = phong; - technique.sID = effectName + "-technique"; - profile.Technique = technique; - - phong.Diffuse = new ColladaFXCommonColorOrTextureType(); - phong.Specular = new ColladaFXCommonColorOrTextureType(); - - // Add all the emissive, etc features to the phong - // Need to check if a texture exists. If so, refer to the sampler. Should be a geometryList = []; - - // For each of the nodes, we need to write the geometry. - foreach (ChunkNode nodeChunk in _cryData.Nodes) - { - if (_args.IsNodeNameExcluded(nodeChunk.Name)) - { - Log.D($"Excluding node {nodeChunk.Name}"); - continue; - } - - if (nodeChunk.MeshData is not ChunkMesh meshChunk) - continue; - - if (meshChunk.GeometryInfo is null) // $physics node - continue; - - // Create a geometry object. Use the chunk ID for the geometry ID - // Create all the materials used by this chunk. - // Make the mesh object. This will have 3 or 4 sources, 1 vertices, and 1 or more Triangles (with material ID) - // If the Object ID of Node chunk points to a Helper or a Controller, place an empty. - var subsets = meshChunk.GeometryInfo.GeometrySubsets; - Datastream? indices = meshChunk.GeometryInfo.Indices; - Datastream? uvs = meshChunk.GeometryInfo.UVs; - Datastream? verts = meshChunk.GeometryInfo.Vertices; - Datastream? vertsUvs = meshChunk.GeometryInfo.VertUVs; - Datastream? normals = meshChunk.GeometryInfo.Normals; - Datastream? colors = meshChunk.GeometryInfo.Colors; - - if (verts is null && vertsUvs is null) // There is no vertex data for this node. Skip. - continue; - - // geometry is a Geometry object for each meshsubset. - ColladaGeometry geometry = new() - { - Name = nodeChunk.Name, - ID = nodeChunk.Name + "-mesh" - }; - ColladaMesh colladaMesh = new(); - geometry.Mesh = colladaMesh; - - ColladaSource[] source = new ColladaSource[4]; // 4 possible source types. - ColladaSource posSource = new(); - ColladaSource normSource = new(); - ColladaSource uvSource = new(); - ColladaSource colorSource = new(); - source[0] = posSource; - source[1] = normSource; - source[2] = uvSource; - source[3] = colorSource; - posSource.ID = nodeChunk.Name + "-mesh-pos"; - posSource.Name = nodeChunk.Name + "-pos"; - normSource.ID = nodeChunk.Name + "-mesh-norm"; - normSource.Name = nodeChunk.Name + "-norm"; - uvSource.ID = nodeChunk.Name + "-mesh-UV"; - uvSource.Name = nodeChunk.Name + "-UV"; - colorSource.ID = nodeChunk.Name + "-mesh-color"; - colorSource.Name = nodeChunk.Name + "-color"; - - ColladaVertices vertices = new() { ID = nodeChunk.Name + "-vertices" }; - geometry.Mesh.Vertices = vertices; - ColladaInputUnshared[] inputshared = new ColladaInputUnshared[4]; - vertices.Input = inputshared; - - ColladaInputUnshared posInput = new() { Semantic = ColladaInputSemantic.POSITION }; - ColladaInputUnshared normInput = new() { Semantic = ColladaInputSemantic.NORMAL }; - ColladaInputUnshared uvInput = new() { Semantic = ColladaInputSemantic.TEXCOORD }; - ColladaInputUnshared colorInput = new() { Semantic = ColladaInputSemantic.COLOR }; - - posInput.source = "#" + posSource.ID; - normInput.source = "#" + normSource.ID; - uvInput.source = "#" + uvSource.ID; - colorInput.source = "#" + colorSource.ID; - inputshared[0] = posInput; - - ColladaFloatArray floatArrayVerts = new(); - ColladaFloatArray floatArrayNormals = new(); - ColladaFloatArray floatArrayUVs = new(); - ColladaFloatArray floatArrayColors = new(); - - StringBuilder vertString = new(); - StringBuilder normString = new(); - StringBuilder uvString = new(); - StringBuilder colorString = new(); - - var numberOfElements = nodeChunk.MeshData.GeometryInfo.GeometrySubsets.Sum(x => x.NumVertices); - - if (verts is not null) // Will be null if it's using VertsUVs. - { - int numVerts = (int)verts.NumElements; - - floatArrayVerts.ID = posSource.ID + "-array"; - floatArrayVerts.Digits = 6; - floatArrayVerts.Magnitude = 38; - floatArrayVerts.Count = numVerts * 3; - floatArrayUVs.ID = uvSource.ID + "-array"; - floatArrayUVs.Digits = 6; - floatArrayUVs.Magnitude = 38; - floatArrayUVs.Count = numVerts * 2; - floatArrayNormals.ID = normSource.ID + "-array"; - floatArrayNormals.Digits = 6; - floatArrayNormals.Magnitude = 38; - floatArrayNormals.Count = numVerts * 3; - floatArrayColors.ID = colorSource.ID + "-array"; - floatArrayColors.Digits = 6; - floatArrayColors.Magnitude = 38; - floatArrayColors.Count = numVerts * 4; - - var hasNormals = normals is not null; - var hasUVs = uvs is not null; - var hasColors = colors is not null; - for (uint j = 0; j < numVerts; j++) - { - var normal = hasNormals ? normals.Data[j] : DefaultNormal; - var uv = hasUVs ? uvs.Data[j] : DefaultUV; - var color = hasColors ? colors.Data[j] : DefaultColor; - vertString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} ", verts.Data[j].X, verts.Data[j].Y, verts.Data[j].Z); - normString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} ", Safe(normal.X), Safe(normal.Y), Safe(normal.Z)); - colorString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} {3:F6} ", color.R, color.G, color.B, color.A); - uvString.AppendFormat(culture, "{0:F6} {1:F6} ", Safe(uv.U), 1 - Safe(uv.V)); - } - } - else // VertsUV structure. Pull out verts, colors and UVs from vertsUvs. - { - floatArrayVerts.ID = posSource.ID + "-array"; - floatArrayVerts.Digits = 6; - floatArrayVerts.Magnitude = 38; - floatArrayVerts.Count = numberOfElements * 3; - floatArrayUVs.ID = uvSource.ID + "-array"; - floatArrayUVs.Digits = 6; - floatArrayUVs.Magnitude = 38; - floatArrayUVs.Count = numberOfElements * 2; - floatArrayNormals.ID = normSource.ID + "-array"; - floatArrayNormals.Digits = 6; - floatArrayNormals.Magnitude = 38; - floatArrayNormals.Count = numberOfElements * 3; - floatArrayColors.ID = colorSource.ID + "-array"; - floatArrayColors.Digits = 6; - floatArrayColors.Magnitude = 38; - floatArrayColors.Count = numberOfElements * 4; - - var multiplerVector = _cryData.IsIvoFile - ? Vector3.Abs((meshChunk.MinBound - meshChunk.MaxBound) / 2f) - : Vector3.One; - - if (multiplerVector.X < 1) multiplerVector.X = 1; - if (multiplerVector.Y < 1) multiplerVector.Y = 1; - if (multiplerVector.Z < 1) multiplerVector.Z = 1; - Vector3 scalingVector = Vector3.One; - - if (meshChunk.ScalingVectors is not null) - { - scalingVector = Vector3.Abs((meshChunk.ScalingVectors.Max - meshChunk.ScalingVectors.Min) / 2f); - if (scalingVector.X < 1) scalingVector.X = 1; - if (scalingVector.Y < 1) scalingVector.Y = 1; - if (scalingVector.Z < 1) scalingVector.Z = 1; - } - - var boundaryBoxCenter = _cryData.IsIvoFile - ? (meshChunk.MinBound + meshChunk.MaxBound) / 2f - : Vector3.Zero; - - var scalingBoxCenter = meshChunk.ScalingVectors is not null ? (meshChunk.ScalingVectors.Max + meshChunk.ScalingVectors.Min) / 2f : Vector3.Zero; - var hasNormals = normals is not null; - var useScalingBox = _cryData.InputFile - .EndsWith("cga") || _cryData.InputFile.EndsWith("cgf") - && meshChunk.ScalingVectors is not null; - - // Create Vertices, UV, normals and colors string - foreach (var subset in meshChunk.GeometryInfo.GeometrySubsets ?? []) - { - for (int i = subset.FirstVertex; i < subset.NumVertices + subset.FirstVertex; i++) - { - Vector3 vert = vertsUvs.Data[i].Vertex; - - if (!_cryData.InputFile.EndsWith("skin") && !_cryData.InputFile.EndsWith("chr")) - { - if (meshChunk.ScalingVectors is null) - vert = (vert * multiplerVector) + boundaryBoxCenter; - else - vert = (vert * scalingVector) + scalingBoxCenter; - } - - vertString.AppendFormat("{0:F6} {1:F6} {2:F6} ", Safe(vert.X), Safe(vert.Y), Safe(vert.Z)); - colorString.AppendFormat(culture, "{0:F6} {1:F6} {2:F6} {3:F6} ", vertsUvs.Data[i].Color.R, vertsUvs.Data[i].Color.G, vertsUvs.Data[i].Color.B, vertsUvs.Data[i].Color.A); - uvString.AppendFormat("{0:F6} {1:F6} ", Safe(vertsUvs.Data[i].UV.U), Safe(1 - vertsUvs.Data[i].UV.V)); - - var normal = hasNormals ? normals.Data[i] : DefaultNormal; - normString.AppendFormat("{0:F6} {1:F6} {2:F6} ", Safe(normal.X), Safe(normal.Y), Safe(normal.Z)); - } - } - } - - CleanNumbers(vertString); - CleanNumbers(normString); - CleanNumbers(uvString); - CleanNumbers(colorString); - - #region Create the triangles node. - var numberOfMeshSubsets = subsets.Count; - var triangles = new ColladaTriangles[numberOfMeshSubsets]; - geometry.Mesh.Triangles = triangles; - - for (int j = 0; j < numberOfMeshSubsets; j++) // Need to make a new Triangles entry for each submesh. - { - triangles[j] = new ColladaTriangles - { - Count = subsets[j].NumIndices / 3, - Material = GetMaterialName(nodeChunk.MaterialFileName, nodeChunk.Materials.SubMaterials[subsets[j].MatID].Name) + "-material" - }; - - // Create the inputs. vertex, normal, texcoord, color - int inputCount = 3; - if (colors is not null || vertsUvs is not null) - inputCount++; - - triangles[j].Input = new ColladaInputShared[inputCount]; - - triangles[j].Input[0] = new ColladaInputShared - { - Semantic = ColladaInputSemantic.VERTEX, - Offset = 0, - source = "#" + vertices.ID - }; - triangles[j].Input[1] = new ColladaInputShared - { - Semantic = ColladaInputSemantic.NORMAL, - Offset = 1, - source = "#" + normSource.ID - }; - triangles[j].Input[2] = new ColladaInputShared - { - Semantic = ColladaInputSemantic.TEXCOORD, - Offset = 2, - source = "#" + uvSource.ID - }; - - int nextInputID = 3; - if (colors is not null || vertsUvs is not null) - { - triangles[j].Input[nextInputID] = new ColladaInputShared - { - Semantic = ColladaInputSemantic.COLOR, - Offset = nextInputID, - source = "#" + colorSource.ID - }; - nextInputID++; - } - - // Create the P node for the Triangles. - StringBuilder p = new(); - string formatString; - if (colors is not null || vertsUvs is not null) - formatString = "{0} {0} {0} {0} {1} {1} {1} {1} {2} {2} {2} {2} "; - else - formatString = "{0} {0} {0} {1} {1} {1} {2} {2} {2} "; - - var offsetStart = 0; - for (int q = 0; q < meshChunk.GeometryInfo.GeometrySubsets.IndexOf(subsets[j]); q++) - { - offsetStart += meshChunk.GeometryInfo.GeometrySubsets[q].NumVertices; - } - - for (var k = subsets[j].FirstIndex; k < (subsets[j].FirstIndex + subsets[j].NumIndices); k += 3) - { - var firstGlobalIndex = indices.Data[subsets[j].FirstIndex]; - uint localIndex0 = (uint)((indices.Data[k] - firstGlobalIndex) + offsetStart); - uint localIndex1 = (uint)((indices.Data[k + 1] - firstGlobalIndex) + offsetStart); - uint localIndex2 = (uint)((indices.Data[k + 2] - firstGlobalIndex) + offsetStart); - - p.AppendFormat(formatString, localIndex0, localIndex1, localIndex2); - } - triangles[j].P = new ColladaIntArrayString - { - Value_As_String = p.ToString().TrimEnd() - }; - } - - #endregion - - #region Create the source float_array nodes. Vertex, normal, UV. May need color as well. - - floatArrayVerts.Value_As_String = vertString.ToString().TrimEnd(); - floatArrayNormals.Value_As_String = normString.ToString().TrimEnd(); - floatArrayUVs.Value_As_String = uvString.ToString().TrimEnd(); - floatArrayColors.Value_As_String = colorString.ToString(); - - source[0].Float_Array = floatArrayVerts; - source[1].Float_Array = floatArrayNormals; - source[2].Float_Array = floatArrayUVs; - source[3].Float_Array = floatArrayColors; - geometry.Mesh.Source = source; - - // create the technique_common for each of these - posSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor() - }; - posSource.Technique_Common.Accessor.Source = "#" + floatArrayVerts.ID; - posSource.Technique_Common.Accessor.Stride = 3; - posSource.Technique_Common.Accessor.Count = (uint)numberOfElements; - ColladaParam[] paramPos = new ColladaParam[3]; - paramPos[0] = new ColladaParam(); - paramPos[1] = new ColladaParam(); - paramPos[2] = new ColladaParam(); - paramPos[0].Name = "X"; - paramPos[0].Type = "float"; - paramPos[1].Name = "Y"; - paramPos[1].Type = "float"; - paramPos[2].Name = "Z"; - paramPos[2].Type = "float"; - posSource.Technique_Common.Accessor.Param = paramPos; - - normSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor - { - Source = "#" + floatArrayNormals.ID, - Stride = 3, - Count = (uint)numberOfElements - } - }; - ColladaParam[] paramNorm = new ColladaParam[3]; - paramNorm[0] = new ColladaParam(); - paramNorm[1] = new ColladaParam(); - paramNorm[2] = new ColladaParam(); - paramNorm[0].Name = "X"; - paramNorm[0].Type = "float"; - paramNorm[1].Name = "Y"; - paramNorm[1].Type = "float"; - paramNorm[2].Name = "Z"; - paramNorm[2].Type = "float"; - normSource.Technique_Common.Accessor.Param = paramNorm; - - uvSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor - { - Source = "#" + floatArrayUVs.ID, - Stride = 2 - } - }; - - uvSource.Technique_Common.Accessor.Count = (uint)numberOfElements; - - ColladaParam[] paramUV = new ColladaParam[2]; - paramUV[0] = new ColladaParam(); - paramUV[1] = new ColladaParam(); - paramUV[0].Name = "S"; - paramUV[0].Type = "float"; - paramUV[1].Name = "T"; - paramUV[1].Type = "float"; - uvSource.Technique_Common.Accessor.Param = paramUV; - - if (colors is not null || vertsUvs is not null) - { - colorSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor() - }; - colorSource.Technique_Common.Accessor.Source = "#" + floatArrayColors.ID; - colorSource.Technique_Common.Accessor.Stride = 4; - colorSource.Technique_Common.Accessor.Count = (uint)numberOfElements; - ColladaParam[] paramColor = new ColladaParam[4]; - paramColor[0] = new ColladaParam(); - paramColor[1] = new ColladaParam(); - paramColor[2] = new ColladaParam(); - paramColor[3] = new ColladaParam(); - paramColor[0].Name = "R"; - paramColor[0].Type = "float"; - paramColor[1].Name = "G"; - paramColor[1].Type = "float"; - paramColor[2].Name = "B"; - paramColor[2].Type = "float"; - paramColor[3].Name = "A"; - paramColor[3].Type = "float"; - colorSource.Technique_Common.Accessor.Param = paramColor; - } - - geometryList.Add(geometry); - - #endregion - - // There is no geometry for a helper or controller node. Can skip the rest. - // Sanity checks - var vertcheck = vertString.ToString().TrimEnd().Split(' '); - var normcheck = normString.ToString().TrimEnd().Split(' '); - var colorcheck = colorString.ToString().TrimEnd().Split(' '); - var uvcheck = uvString.ToString().TrimEnd().Split(' '); - - } - libraryGeometries.Geometry = geometryList.ToArray(); - DaeObject.Library_Geometries = libraryGeometries; - } - - private void CreateMaterials() - { - List colladaMaterials = []; - List colladaEffects = []; - if (DaeObject.Library_Materials?.Material is null) - { - DaeObject.Library_Materials = new(); - DaeObject.Library_Materials.Material = []; - } - - foreach (var matKey in _cryData.Materials.Keys) - { - foreach (var subMat in _cryData.Materials[matKey].SubMaterials ?? []) - { - // Check to see if the collada object already has a material with this name. If not, add. - var matNames = DaeObject.Library_Materials.Material.Select(c => c.Name); - if (!matNames.Contains(subMat.Name)) - { - colladaMaterials.Add(AddMaterialToMaterialLibrary(matKey, subMat)); - var effect = CreateColladaEffect(matKey, subMat); - if (effect is not null) - colladaEffects.Add(effect); - } - // If there are matlayers, add these too - foreach (var matLayer in subMat.MatLayers?.Layers ?? []) - { - int index = Array.IndexOf(subMat.MatLayers?.Layers ?? [], matLayer); - colladaMaterials.Add(AddMaterialToMaterialLibrary(matLayer.Path ?? "unknown mat layer", subMat.SubMaterials[index])); - var effect = CreateColladaEffect(matKey, subMat); - if (effect is not null) - colladaEffects.Add(CreateColladaEffect(matLayer.Path ?? "unknown mat layer", subMat.SubMaterials[index])); - } - } - } - - int arraySize = DaeObject.Library_Materials.Material.Length; - Array.Resize(ref DaeObject.Library_Materials.Material, DaeObject.Library_Materials.Material.Length + colladaMaterials.Count); - colladaMaterials.CopyTo(DaeObject.Library_Materials.Material, arraySize); - - if (DaeObject.Library_Effects.Effect is null) - DaeObject.Library_Effects.Effect = colladaEffects.ToArray(); - else - { - int effectsArraySize = DaeObject.Library_Effects.Effect.Length; - Array.Resize(ref DaeObject.Library_Effects.Effect, DaeObject.Library_Effects.Effect.Length + colladaEffects.Count); - colladaEffects.CopyTo(DaeObject.Library_Effects.Effect, effectsArraySize); - } - } - - private ColladaMaterial AddMaterialToMaterialLibrary(string matKey, Material submat) - { - var matName = GetMaterialName(matKey, submat?.Name ?? "unknown"); - ColladaMaterial material = new() - { - Instance_Effect = new ColladaInstanceEffect(), - Name = matName, - ID = matName + "-material" - }; - material.Instance_Effect.URL = "#" + matName + "-effect"; - - if (!_args.NoTextures) - AddTexturesToTextureLibrary(matKey, submat); - return material; - } - - private void AddTexturesToTextureLibrary(string matKey, Material submat) - { - List imageList = []; - int numberOfTextures = submat?.Textures?.Length ?? 0; - - for (int i = 0; i < numberOfTextures; i++) - { - // For each texture in the material, we make a new object and add it to the list. - var name = GetMaterialName(matKey, submat.Name); - ColladaImage image = new() - { - ID = name + "_" + submat.Textures[i].Map, - Name = name + "_" + submat.Textures[i].Map, - Init_From = new ColladaInitFrom() - }; - // TODO: Refactor to use fully qualified path, and to use Pack File System. - var textureFile = ResolveTextureFile(submat.Textures[i].File, _args.PackFileSystem, [_cryData.ObjectDir]); - - if (_args.PngTextures && File.Exists(Path.ChangeExtension(textureFile, ".png"))) - textureFile = Path.ChangeExtension(textureFile, ".png"); - else if (_args.TgaTextures && File.Exists(Path.ChangeExtension(textureFile, ".tga"))) - textureFile = Path.ChangeExtension(textureFile, ".tga"); - else if (_args.TiffTextures && File.Exists(Path.ChangeExtension(textureFile, ".tif"))) - textureFile = Path.ChangeExtension(textureFile, ".tif"); - try - { - if (_args.UnsplitTextures) - { - Log.I($"Combining texture file {textureFile}"); - Combine(textureFile); - } - } - catch (Exception ex) - { - Log.W($"Error combining texture {textureFile}: {ex.Message}"); - } - - textureFile = Path.GetRelativePath(daeOutputFile.DirectoryName, textureFile); - - textureFile = textureFile.Replace(" ", @"%20"); - image.Init_From.Uri = textureFile; - imageList.Add(image); - } - - ColladaImage[] images = imageList.ToArray(); - if (DaeObject.Library_Images.Image is null) - DaeObject.Library_Images.Image = images; - else - { - int arraySize = DaeObject.Library_Images.Image.Length; - Array.Resize(ref DaeObject.Library_Images.Image, DaeObject.Library_Images.Image.Length + images.Length); - images.CopyTo(DaeObject.Library_Images.Image, arraySize); - } - } - - private void WriteLibrary_Controllers() - { - if (DaeObject.Library_Geometries.Geometry.Length != 0) - { - ColladaLibraryControllers libraryController = new(); - - // There can be multiple controllers in the controller library. But for Cryengine files, there is only one rig. - // So if a rig exists, make that the controller. This applies mostly to .chr files, which will have a rig and may have geometry. - ColladaController controller = new() { ID = "Controller" }; - // Create the skin object and assign to the controller - ColladaSkin skin = new() - { - source = "#" + DaeObject.Library_Geometries.Geometry[0].ID, - Bind_Shape_Matrix = new ColladaFloatArrayString() - }; - skin.Bind_Shape_Matrix.Value_As_String = CreateStringFromMatrix4x4(Matrix4x4.Identity); // We will assume the BSM is the identity matrix for now - - // Create the 3 sources for this controller: joints, bind poses, and weights - skin.Source = new ColladaSource[3]; - - // Populate the data. - // Need to map the exterior vertices (geometry) to the int vertices. Or use the Bone Map datastream if it exists (check HasBoneMapDatastream). - #region Joints Source - ColladaSource jointsSource = new() - { - ID = "Controller-joints", - Name_Array = new ColladaNameArray() - { - ID = "Controller-joints-array", - Count = _cryData.SkinningInfo.CompiledBones.Count, - } - }; - StringBuilder boneNames = new(); - for (int i = 0; i < _cryData.SkinningInfo.CompiledBones.Count; i++) - { - boneNames.Append(_cryData.SkinningInfo.CompiledBones[i].BoneName.Replace(' ', '_') + " "); - } - jointsSource.Name_Array.Value_Pre_Parse = boneNames.ToString().TrimEnd(); - jointsSource.Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor - { - Source = "#Controller-joints-array", - Count = (uint)_cryData.SkinningInfo.CompiledBones.Count, - Stride = 1 - } - }; - skin.Source[0] = jointsSource; - #endregion - - #region Bind Pose Array Source - ColladaSource bindPoseArraySource = new() - { - ID = "Controller-bind_poses", - Float_Array = new() - { - ID = "Controller-bind_poses-array", - Count = _cryData.SkinningInfo.CompiledBones.Count * 16, - Value_As_String = GetBindPoseArray(_cryData.SkinningInfo.CompiledBones) - }, - Technique_Common = new ColladaTechniqueCommonSource - { - Accessor = new ColladaAccessor - { - Source = "#Controller-bind_poses-array", - Count = (uint)_cryData.SkinningInfo.CompiledBones.Count, - Stride = 16, - } - } - }; - bindPoseArraySource.Technique_Common.Accessor.Param = new ColladaParam[1]; - bindPoseArraySource.Technique_Common.Accessor.Param[0] = new ColladaParam - { - Name = "TRANSFORM", - Type = "float4x4" - }; - skin.Source[1] = bindPoseArraySource; - #endregion - - #region Weights Source - var skinningInfo = _cryData.SkinningInfo; - var nodeChunk = _cryData.RootNode; - - ColladaSource weightArraySource = new() - { - ID = "Controller-weights", - Technique_Common = new ColladaTechniqueCommonSource() - }; - ColladaAccessor accessor = weightArraySource.Technique_Common.Accessor = new ColladaAccessor(); - - weightArraySource.Float_Array = new ColladaFloatArray() - { - ID = "Controller-weights-array", - }; - - var numberOfWeights = skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count; - var boneInfluenceCount = - nodeChunk.MeshData?.GeometryInfo?.BoneMappings?.Data[0].BoneInfluenceCount == 8 - ? 8 - : 4; - - var boneMappingData = skinningInfo.IntVertices is null - ? nodeChunk.MeshData?.GeometryInfo?.BoneMappings.Data.ToList() - : skinningInfo.Ext2IntMap - .Select(x => skinningInfo.IntVertices[x]) - .Select(x => x.BoneMapping) - .ToList(); - - if (boneMappingData is null) return; - - StringBuilder weights = new(); - weightArraySource.Float_Array.Count = numberOfWeights; - for (int i = 0; i < numberOfWeights; i++) - { - for (int j = 0; j < boneInfluenceCount; j++) - { - weights.Append(boneMappingData[i].Weight[j].ToString() + " "); - } - } - ; - accessor.Count = (uint)(numberOfWeights * boneInfluenceCount); - - CleanNumbers(weights); - weightArraySource.Float_Array.Value_As_String = weights.ToString().TrimEnd(); - // Add technique_common part. - accessor.Source = "#Controller-weights-array"; - accessor.Stride = 1; - accessor.Param = new ColladaParam[1]; - accessor.Param[0] = new ColladaParam - { - Name = "WEIGHT", - Type = "float" - }; - skin.Source[2] = weightArraySource; - - #endregion - - #region Joints - skin.Joints = new ColladaJoints - { - Input = new ColladaInputUnshared[2] - }; - skin.Joints.Input[0] = new ColladaInputUnshared - { - Semantic = new ColladaInputSemantic() - }; - skin.Joints.Input[0].Semantic = ColladaInputSemantic.JOINT; - skin.Joints.Input[0].source = "#Controller-joints"; - skin.Joints.Input[1] = new ColladaInputUnshared - { - Semantic = new ColladaInputSemantic() - }; - skin.Joints.Input[1].Semantic = ColladaInputSemantic.INV_BIND_MATRIX; - skin.Joints.Input[1].source = "#Controller-bind_poses"; - #endregion - - #region Vertex Weights - ColladaVertexWeights vertexWeights = skin.Vertex_Weights = new(); - vertexWeights.Count = (int)numberOfWeights; - skin.Vertex_Weights.Input = new ColladaInputShared[2]; - ColladaInputShared jointSemantic = skin.Vertex_Weights.Input[0] = new(); - jointSemantic.Semantic = ColladaInputSemantic.JOINT; - jointSemantic.source = "#Controller-joints"; - jointSemantic.Offset = 0; - ColladaInputShared weightSemantic = skin.Vertex_Weights.Input[1] = new(); - weightSemantic.Semantic = ColladaInputSemantic.WEIGHT; - weightSemantic.source = "#Controller-weights"; - weightSemantic.Offset = 1; - StringBuilder vCount = new(); - for (int i = 0; i < numberOfWeights; i++) - { - vCount.Append($"{boneInfluenceCount} "); - } - ; - vertexWeights.VCount = new ColladaIntArrayString - { - Value_As_String = vCount.ToString().TrimEnd() - }; - StringBuilder vertices = new(); - int index = 0; - - for (int i = 0; i < numberOfWeights; i++) - { - vertices.Append(boneMappingData[i].BoneIndex[0] + " " + index + " "); - vertices.Append(boneMappingData[i].BoneIndex[1] + " " + (index + 1) + " "); - vertices.Append(boneMappingData[i].BoneIndex[2] + " " + (index + 2) + " "); - vertices.Append(boneMappingData[i].BoneIndex[3] + " " + (index + 3) + " "); - if (boneInfluenceCount == 8) - { - vertices.Append(boneMappingData[i].BoneIndex[4] + " " + (index + 4) + " "); - vertices.Append(boneMappingData[i].BoneIndex[5] + " " + (index + 5) + " "); - vertices.Append(boneMappingData[i].BoneIndex[6] + " " + (index + 6) + " "); - vertices.Append(boneMappingData[i].BoneIndex[7] + " " + (index + 7) + " "); - index += 4; - } - index += 4; - } - vertexWeights.V = new ColladaIntArrayString { Value_As_String = vertices.ToString().TrimEnd() }; - #endregion - - // create the extra element for the FCOLLADA profile - controller.Extra = new ColladaExtra[1]; - controller.Extra[0] = new ColladaExtra - { - Technique = new ColladaTechnique[1] - }; - controller.Extra[0].Technique[0] = new ColladaTechnique - { - profile = "FCOLLADA", - UserProperties = "SkinController" - }; - - // Add the parts to their parents - controller.Skin = skin; - libraryController.Controller = new ColladaController[1]; - libraryController.Controller[0] = controller; - DaeObject.Library_Controllers = libraryController; - } - } - - public void WriteLibrary_VisualScenes() - { - ColladaLibraryVisualScenes libraryVisualScenes = new(); - - List visualScenes = []; - ColladaVisualScene visualScene = new(); - List nodes = []; - - // THERE CAN BE MULTIPLE ROOT NODES IN EACH FILE! Check to see if the parentnodeid ~0 and be sure to add a node for it. - List positionNodes = []; - List positionRoots = _cryData.Nodes.Where(a => a.ParentNodeID == ~0).ToList(); - foreach (ChunkNode root in positionRoots) - { - positionNodes.Add(CreateNode(root, false)); - } - nodes.AddRange(positionNodes.ToArray()); - - visualScene.Node = nodes.ToArray(); - visualScene.ID = "Scene"; - visualScenes.Add(visualScene); - - libraryVisualScenes.Visual_Scene = visualScenes.ToArray(); - DaeObject.Library_Visual_Scene = libraryVisualScenes; - } - - public void WriteLibrary_VisualScenesWithSkeleton() - { - ColladaLibraryVisualScenes libraryVisualScenes = new(); - - List visualScenes = []; - ColladaVisualScene visualScene = new(); - List nodes = []; - - List positionRoots = _cryData.Nodes.Where(a => a.ParentNodeID == ~0).ToList(); - - // Check to see if there is a CompiledBones chunk. If so, add a Node. - if (_cryData.Chunks.Any(a => a.ChunkType == ChunkType.CompiledBones || - a.ChunkType == ChunkType.CompiledBonesSC || - a.ChunkType == ChunkType.CompiledBones_Ivo2)) - { - ColladaNode boneNode = CreateJointNode(_cryData.SkinningInfo.RootBone); - nodes.Add(boneNode); - } - - var hasGeometry = _cryData.Nodes.Any(x => x.MeshData is not null); - - if (hasGeometry) - { - foreach (var node in positionRoots) - { - var colladaNode = CreateNode(node, true); - colladaNode.Instance_Controller = new ColladaInstanceController[1]; - colladaNode.Instance_Controller[0] = new ColladaInstanceController - { - URL = "#Controller", - Skeleton = new ColladaSkeleton[1] - }; - - var skeleton = colladaNode.Instance_Controller[0].Skeleton[0] = new ColladaSkeleton(); - skeleton.Value = $"#{_cryData.SkinningInfo.CompiledBones[0].BoneName}".Replace(' ', '_'); - colladaNode.Instance_Controller[0].Bind_Material = new ColladaBindMaterial[1]; - ColladaBindMaterial bindMaterial = colladaNode.Instance_Controller[0].Bind_Material[0] = new ColladaBindMaterial(); - - // Create an Instance_Material for each material - bindMaterial.Technique_Common = new ColladaTechniqueCommonBindMaterial(); - colladaNode.Instance_Controller[0].Bind_Material[0].Technique_Common.Instance_Material = CreateInstanceMaterials(node); - - foreach (ChunkNode child in node.Children) - CreateChildNodes(child, true); - - nodes.Add(colladaNode); - } - } - - visualScene.Node = nodes.ToArray(); - visualScene.ID = "Scene"; - visualScenes.Add(visualScene); - - libraryVisualScenes.Visual_Scene = visualScenes.ToArray(); - DaeObject.Library_Visual_Scene = libraryVisualScenes; - } - - private ColladaInstanceMaterialGeometry[] CreateInstanceMaterials(ChunkNode node) - { - List instanceMaterials = []; - - var matIndices = node.MeshData?.GeometryInfo?.GeometrySubsets?.Select(x => x.MatID) ?? []; - - foreach (var index in matIndices) - { - var matName = GetMaterialName(node.MaterialFileName, node.Materials.SubMaterials[index].Name); - ColladaInstanceMaterialGeometry instanceMaterial = new(); - instanceMaterial.Target = $"#{matName}-material"; - instanceMaterial.Symbol = $"{matName}-material"; - instanceMaterials.Add(instanceMaterial); - } - - return instanceMaterials.ToArray(); - } - - private ColladaNode CreateNode(ChunkNode nodeChunk, bool isControllerNode) - { - ColladaNode colladaNode = new(); - - string nodeName = nodeChunk.Name; - int nodeID = nodeChunk.ID; - - if (nodeChunk.ChunkHelper is not null || nodeChunk.MeshData?.GeometryInfo is null) - colladaNode = CreateSimpleNode(nodeChunk, isControllerNode); - else - { - ColladaGeometry geometryLibraryObject = DaeObject.Library_Geometries.Geometry.Where(a => a.Name == nodeChunk.Name).FirstOrDefault(); - ChunkMesh geometryMesh = nodeChunk.MeshData; - colladaNode = CreateGeometryNode(nodeChunk, geometryMesh, isControllerNode); - } - - colladaNode.node = CreateChildNodes(nodeChunk, isControllerNode); - return colladaNode; - } - - /// This will be used to make the Collada node element for Node chunks that point to Helper Chunks and MeshPhysics - private ColladaNode CreateSimpleNode(ChunkNode nodeChunk, bool isControllerNode) - { - // This will be used to make the Collada node element for Node chunks that point to Helper Chunks and MeshPhysics - ColladaNode colladaNode = new() - { - Type = ColladaNodeType.NODE, - Name = nodeChunk.Name, - ID = nodeChunk.Name - }; - - ColladaMatrix matrix = new() - { - sID = "transform", - Value_As_String = CreateStringFromMatrix4x4(nodeChunk.LocalTransform) - }; - colladaNode.Matrix = new ColladaMatrix[1] { matrix }; - - colladaNode.node = CreateChildNodes(nodeChunk, isControllerNode); - return colladaNode; - } - - /// Used by CreateNode and CreateSimpleNodes to create all the child nodes for the given node. - private ColladaNode[]? CreateChildNodes(ChunkNode nodeChunk, bool isControllerNode) - { - List childNodes = []; - foreach (ChunkNode childNodeChunk in nodeChunk.Children) - { - if (_args.IsNodeNameExcluded(childNodeChunk.Name)) - { - Log.D($"Excluding child node {childNodeChunk.Name}"); - continue; - } - - ColladaNode childNode = CreateNode(childNodeChunk, isControllerNode); - childNodes.Add(childNode); - } - return childNodes.ToArray(); - } - - private ColladaNode CreateJointNode(CompiledBone bone) - { - var boneName = bone.BoneName.Replace(' ', '_'); - - ColladaNode tmpNode = new() - { - ID = boneName, // ID, name and sID must be the same or the controller can't seem to find the bone. - Name = boneName, - sID = boneName, - Type = ColladaNodeType.JOINT - }; - if (bone.ControllerID != -1 && bone.ControllerID != uint.MaxValue) - controllerIdToBoneName.Add(bone.ControllerID, bone.BoneName); - - Matrix4x4 localMatrix = bone.LocalTransformMatrix.ConvertToTransformMatrix(); - - ColladaMatrix matrix = new(); - List matrices = []; - matrix.Value_As_String = CreateStringFromMatrix4x4(localMatrix); - matrix.sID = "matrix"; - matrices.Add(matrix); - tmpNode.Matrix = matrices.ToArray(); - - // Recursively call this for each of the child bones to this bone. - if (bone.NumberOfChildren > 0) - { - List childNodes = []; - var allChildBones = _cryData.SkinningInfo?.GetChildBones(bone) ?? []; - foreach (CompiledBone childBone in allChildBones) - { - childNodes.Add(CreateJointNode(childBone)); - } - tmpNode.node = childNodes.ToArray(); - } - return tmpNode; - } - - private ColladaNode CreateGeometryNode(ChunkNode nodeChunk, ChunkMesh tmpMeshChunk, bool isControllerNode) - { - ColladaNode colladaNode = new(); - var meshSubsets = nodeChunk.MeshData.GeometryInfo.GeometrySubsets; - var nodeType = ColladaNodeType.NODE; - colladaNode.Type = nodeType; - colladaNode.Name = nodeChunk.Name; - colladaNode.ID = nodeChunk.Name; - - // Make the lists necessary for this Node. - List bindMaterials = []; - List matrices = []; - ColladaMatrix matrix = new() - { - Value_As_String = CreateStringFromMatrix4x4(nodeChunk.LocalTransform), - sID = "transform" - }; - - matrices.Add(matrix); // we can have multiple matrices, but only need one since there is only one per Node chunk anyway - colladaNode.Matrix = matrices.ToArray(); - - // Each node will have one instance geometry, although it could be a list. - if (!isControllerNode) - { - List instanceGeometries = []; - ColladaInstanceGeometry instanceGeometry = new() - { - Name = nodeChunk.Name, - URL = "#" + nodeChunk.Name + "-mesh" // this is the ID of the geometry. - }; - ColladaBindMaterial bindMaterial = new() - { - Technique_Common = new ColladaTechniqueCommonBindMaterial - { - Instance_Material = new ColladaInstanceMaterialGeometry[meshSubsets.Count] - } - }; - bindMaterials.Add(bindMaterial); - instanceGeometry.Bind_Material = bindMaterials.ToArray(); - instanceGeometries.Add(instanceGeometry); - - colladaNode.Instance_Geometry = instanceGeometries.ToArray(); - colladaNode.Instance_Geometry[0].Bind_Material[0].Technique_Common.Instance_Material = CreateInstanceMaterials(nodeChunk); - } - - return colladaNode; - } - - /// Retrieves the worldtobone (bind pose matrix) for the bone. - private static string GetBindPoseArray(List compiledBones) - { - StringBuilder value = new(); - for (int i = 0; i < compiledBones.Count; i++) - { - value.Append(CreateStringFromMatrix4x4(compiledBones[i].BindPoseMatrix) + " "); - } - return value.ToString().TrimEnd(); - } - - /// Adds the scene element to the Collada document. - private void WriteScene() - { - ColladaScene scene = new(); - ColladaInstanceVisualScene visualScene = new() - { - URL = "#Scene", - Name = "Scene" - }; - scene.Visual_Scene = visualScene; - DaeObject.Scene = scene; - } - - private string GetMaterialName(string matKey, string submatName) - { - // material name is _mtl_ - var matfileName = Path.GetFileNameWithoutExtension(matKey); - - return $"{matfileName}_mtl_{submatName}".Replace(' ', '_'); - } - - private static string CreateStringFromVector3(Vector3 vector) - { - StringBuilder vectorValues = new(); - vectorValues.AppendFormat("{0:F6} {1:F6} {2:F6}", vector.X, vector.Y, vector.Z); - CleanNumbers(vectorValues); - return vectorValues.ToString(); - } - - private static string CreateStringFromVector4(Vector4 vector) - { - StringBuilder vectorValues = new(); - vectorValues.AppendFormat("{0:F6} {1:F6} {2:F6} {3:F6}", vector.X, vector.Y, vector.Z, vector.W); - CleanNumbers(vectorValues); - return vectorValues.ToString(); - } - - private static string CreateStringFromMatrix4x4(Matrix4x4 matrix) - { - StringBuilder matrixValues = new(); - matrixValues.AppendFormat("{0:F6} {1:F6} {2:F6} {3:F6} {4:F6} {5:F6} {6:F6} {7:F6} {8:F6} {9:F6} {10:F6} {11:F6} {12:F6} {13:F6} {14:F6} {15:F6}", - matrix.M11, - matrix.M12, - matrix.M13, - matrix.M14, - matrix.M21, - matrix.M22, - matrix.M23, - matrix.M24, - matrix.M31, - matrix.M32, - matrix.M33, - matrix.M34, - matrix.M41, - matrix.M42, - matrix.M43, - matrix.M44); - CleanNumbers(matrixValues); - return matrixValues.ToString(); - } } diff --git a/CgfConverter/Renderers/Collada/Models/Collada_Core/Animation/ColladaAnimation.cs b/CgfConverter/Renderers/Collada/Models/Collada_Core/Animation/ColladaAnimation.cs index 52d1e732..8e071d2a 100644 --- a/CgfConverter/Renderers/Collada/Models/Collada_Core/Animation/ColladaAnimation.cs +++ b/CgfConverter/Renderers/Collada/Models/Collada_Core/Animation/ColladaAnimation.cs @@ -17,21 +17,23 @@ public partial class ColladaAnimation [XmlAttribute("name")] public string Name; + // Element order matters for Collada spec compliance! + // Correct order: asset, animation, source, sampler, channel, extra + + [XmlElement(ElementName = "asset")] + public ColladaAsset Asset; [XmlElement(ElementName = "animation")] public ColladaAnimation[] Animation; - [XmlElement(ElementName = "channel")] - public ColladaChannel[] Channel; - [XmlElement(ElementName = "source")] public ColladaSource[] Source; [XmlElement(ElementName = "sampler")] public ColladaSampler[] Sampler; - [XmlElement(ElementName = "asset")] - public ColladaAsset Asset; + [XmlElement(ElementName = "channel")] + public ColladaChannel[] Channel; [XmlElement(ElementName = "extra")] public ColladaExtra[] Extra; diff --git a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Animation.cs b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Animation.cs index deccd573..3ef0ac4e 100644 --- a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Animation.cs +++ b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Animation.cs @@ -1,8 +1,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using CgfConverter.CryEngineCore; +using CgfConverter.Models; +using CgfConverter.Models.Structs; using CgfConverter.Renderers.Gltf.Models; +using CgfConverter.Utilities; namespace CgfConverter.Renderers.Gltf; @@ -124,6 +128,28 @@ private int WriteAnimations( $"animation/rotation/{i}", -1, null, animChunk.KeyRotations[i].Select(SwapAxesForAnimations).ToArray())); + // Debug: log turret_arm rotation (CtrlID = 0x9384FC75) + foreach (var anim in animChunk.Animations) + { + foreach (var controller in anim.Controllers) + { + if (controller.ControllerID == 0x9384FC75 && controller.HasRotTrack) + { + var rawRot = animChunk.KeyRotations[controller.RotTrack][0]; + var swappedRot = SwapAxesForAnimations(rawRot); + Log.I($"glTF DBA turret_arm frame 0: raw quat = ({rawRot.X:F6}, {rawRot.Y:F6}, {rawRot.Z:F6}, {rawRot.W:F6})"); + Log.I($"glTF DBA turret_arm frame 0: swapped quat = ({swappedRot.X:F6}, {swappedRot.Y:F6}, {swappedRot.Z:F6}, {swappedRot.W:F6})"); + var angle = 2 * System.Math.Acos(System.Math.Clamp(rawRot.W, -1, 1)) * 180 / System.Math.PI; + var axis = new Vector3(rawRot.X, rawRot.Y, rawRot.Z); + if (axis.LengthSquared() > 0.0001f) + axis = Vector3.Normalize(axis); + Log.I($"glTF DBA turret_arm frame 0: raw axis=({axis.X:F3}, {axis.Y:F3}, {axis.Z:F3}), angle={angle:F2}°"); + goto doneLogging; // Only log once + } + } + } + doneLogging:; + var names = GltfRendererUtilities.StripCommonParentPaths( animChunk.Animations.Select(x => Path.ChangeExtension(x.Name, null)).ToList()); @@ -146,4 +172,408 @@ private int WriteAnimations( return numAnimationsWritten; } + + /// + /// Writes CAF animations (from .caf files and .cal animation lists) to glTF. + /// + private int WriteCafAnimations( + IEnumerable? cafAnimations, + IReadOnlyDictionary controllerIdToNodeIndex) + { + if (cafAnimations is null) + return 0; + + var cafList = cafAnimations.ToList(); + if (cafList.Count == 0) + return 0; + + var numAnimationsWritten = 0; + + foreach (var cafAnim in cafList) + { + if (!CreateCafAnimation(out var newAnimation, cafAnim, controllerIdToNodeIndex)) + continue; + + AddAnimation(newAnimation); + numAnimationsWritten++; + } + + return numAnimationsWritten; + } + + /// + /// Creates a glTF animation from a CAF animation. + /// + private bool CreateCafAnimation( + out GltfAnimation newAnimation, + CafAnimation cafAnim, + IReadOnlyDictionary controllerIdToNodeIndex) + { + var cleanName = Path.GetFileNameWithoutExtension(cafAnim.Name); + newAnimation = new GltfAnimation { Name = cleanName }; + + foreach (var (controllerId, track) in cafAnim.BoneTracks) + { + // Try to find node by controller ID + if (!controllerIdToNodeIndex.TryGetValue(controllerId, out var nodeIndex)) + { + // Try using bone name from CAF's bone name list + if (cafAnim.ControllerIdToBoneName.TryGetValue(controllerId, out var boneName)) + { + // Search for matching node by name - this requires iterating controllerIdToNodeIndex + // Since we don't have reverse mapping, log and skip + Log.D("CAF[{0}]: Controller 0x{1:X08} ({2}) not found in skeleton", + cafAnim.Name, controllerId, boneName); + } + else + { + Log.D("CAF[{0}]: Controller 0x{1:X08} not found in skeleton", + cafAnim.Name, controllerId); + } + continue; + } + + // Create position animation channel if we have position data + if (track.Positions.Count > 0) + { + var keyTimes = track.PositionKeyTimes.Count > 0 + ? track.PositionKeyTimes + : track.KeyTimes; + + if (keyTimes.Count > 0) + { + // Normalize key times to seconds (assuming 30fps if times look like frame numbers) + var startTime = keyTimes[0]; + var timeAccessor = AddAccessor( + $"caf/{cleanName}/pos_time/{controllerId:X08}", -1, null, + keyTimes.Select(t => (t - startTime) / 30f).ToArray()); + + var posAccessor = AddAccessor( + $"caf/{cleanName}/pos/{controllerId:X08}", -1, null, + track.Positions.Select(SwapAxesForPosition).ToArray()); + + newAnimation.Samplers.Add(new GltfAnimationSampler + { + Input = timeAccessor, + Output = posAccessor, + Interpolation = GltfAnimationSamplerInterpolation.Linear, + }); + newAnimation.Channels.Add(new GltfAnimationChannel + { + Sampler = newAnimation.Samplers.Count - 1, + Target = new GltfAnimationChannelTarget + { + Node = nodeIndex, + Path = GltfAnimationChannelTargetPath.Translation, + }, + }); + } + } + + // Create rotation animation channel if we have rotation data + if (track.Rotations.Count > 0) + { + var keyTimes = track.RotationKeyTimes.Count > 0 + ? track.RotationKeyTimes + : track.KeyTimes; + + if (keyTimes.Count > 0) + { + // Normalize key times to seconds + var startTime = keyTimes[0]; + var timeAccessor = AddAccessor( + $"caf/{cleanName}/rot_time/{controllerId:X08}", -1, null, + keyTimes.Select(t => (t - startTime) / 30f).ToArray()); + + var rotAccessor = AddAccessor( + $"caf/{cleanName}/rot/{controllerId:X08}", -1, null, + track.Rotations.Select(SwapAxesForAnimations).ToArray()); + + newAnimation.Samplers.Add(new GltfAnimationSampler + { + Input = timeAccessor, + Output = rotAccessor, + Interpolation = GltfAnimationSamplerInterpolation.Linear, + }); + newAnimation.Channels.Add(new GltfAnimationChannel + { + Sampler = newAnimation.Samplers.Count - 1, + Target = new GltfAnimationChannelTarget + { + Node = nodeIndex, + Path = GltfAnimationChannelTargetPath.Rotation, + }, + }); + } + } + } + + return newAnimation.Samplers.Count > 0 && newAnimation.Channels.Count > 0; + } + + /// + /// Writes Ivo DBA animations (from Star Citizen #ivo .dba files) to glTF. + /// Ivo DBA uses ChunkIvoDBAData + ChunkIvoDBAMetadata instead of ChunkController_905. + /// + /// IMPORTANT: Ivo animations store values as DELTAS from rest pose, not absolute + /// local transforms. glTF expects absolute joint-local transforms. + /// We must convert: absolute = rest + delta (translation), absolute = rest * delta (rotation) + /// + private int WriteIvoDbaAnimations( + IEnumerable? animationContainers, + IReadOnlyDictionary controllerIdToNodeIndex, + SkinningInfo? skinningInfo = null) + { + if (animationContainers is null) + return 0; + + // Build rest pose mappings for converting Ivo deltas to absolute transforms + // Ivo animations store deltas from rest pose, but glTF expects absolute local transforms + var controllerIdToRestTranslation = new Dictionary(); + var controllerIdToRestRotation = new Dictionary(); + + if (skinningInfo?.CompiledBones is not null) + { + BuildIvoRestPoseMappings(skinningInfo, controllerIdToRestTranslation, controllerIdToRestRotation); + } + + var numAnimationsWritten = 0; + + foreach (var animModel in animationContainers) + { + var ivoDbaData = animModel.ChunkMap.Values.OfType().FirstOrDefault(); + var ivoDbaMetadata = animModel.ChunkMap.Values.OfType().FirstOrDefault(); + + if (ivoDbaData is null || ivoDbaData.AnimationBlocks.Count == 0) + continue; + + Log.D("Found Ivo DBA with {0} animation blocks", ivoDbaData.AnimationBlocks.Count); + + var animNames = ivoDbaMetadata?.AnimPaths ?? []; + + for (int i = 0; i < ivoDbaData.AnimationBlocks.Count; i++) + { + var block = ivoDbaData.AnimationBlocks[i]; + var animName = i < animNames.Count + ? Path.GetFileNameWithoutExtension(animNames[i]) + : $"animation_{i}"; + + if (!CreateIvoDbaAnimation(out var newAnimation, block, animName, controllerIdToNodeIndex, + controllerIdToRestTranslation, controllerIdToRestRotation)) + continue; + + AddAnimation(newAnimation); + numAnimationsWritten++; + Log.D("Created Ivo DBA animation: {0}", animName); + } + } + + return numAnimationsWritten; + } + + /// + /// Builds rest pose translation and rotation mappings for Ivo animation conversion. + /// Rest pose values are in CryEngine space (before axis swap). + /// + private void BuildIvoRestPoseMappings( + SkinningInfo skinningInfo, + Dictionary controllerIdToRestTranslation, + Dictionary controllerIdToRestRotation) + { + foreach (var bone in skinningInfo.CompiledBones) + { + // Compute local transform in CryEngine space (same as skeleton creation) + Matrix4x4 localMatrix; + + if (bone.ParentBone == null) + { + if (!Matrix4x4.Invert(bone.BindPoseMatrix, out localMatrix)) + { + localMatrix = Matrix4x4.Identity; + } + } + else + { + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var childBoneToWorld)) + { + localMatrix = bone.ParentBone.BindPoseMatrix * childBoneToWorld; + } + else + { + localMatrix = Matrix4x4.Identity; + } + } + + // Extract translation from column 4 (CryEngine convention: M14, M24, M34) + var restTranslation = new Vector3(localMatrix.M14, localMatrix.M24, localMatrix.M34); + + // Extract rotation from the 3x3 rotation part + // Build a proper rotation matrix by normalizing the 3x3 part + var rotMatrix = new Matrix4x4( + localMatrix.M11, localMatrix.M12, localMatrix.M13, 0, + localMatrix.M21, localMatrix.M22, localMatrix.M23, 0, + localMatrix.M31, localMatrix.M32, localMatrix.M33, 0, + 0, 0, 0, 1); + + Quaternion restRotation; + if (Matrix4x4.Decompose(rotMatrix, out _, out restRotation, out _)) + { + // Success + } + else + { + restRotation = Quaternion.Identity; + } + + // Store by controller ID + controllerIdToRestTranslation[bone.ControllerID] = restTranslation; + controllerIdToRestRotation[bone.ControllerID] = restRotation; + + // Also store by CRC32 hash of bone name for Ivo matching + if (!string.IsNullOrEmpty(bone.BoneName)) + { + var crc32 = Crc32CryEngine.Compute(bone.BoneName); + controllerIdToRestTranslation.TryAdd(crc32, restTranslation); + controllerIdToRestRotation.TryAdd(crc32, restRotation); + + var crc32Lower = Crc32CryEngine.Compute(bone.BoneName.ToLowerInvariant()); + if (crc32Lower != crc32) + { + controllerIdToRestTranslation.TryAdd(crc32Lower, restTranslation); + controllerIdToRestRotation.TryAdd(crc32Lower, restRotation); + } + } + } + } + + /// + /// Creates a glTF animation from an Ivo DBA animation block. + /// Converts Ivo delta values to absolute transforms before axis swap. + /// + private bool CreateIvoDbaAnimation( + out GltfAnimation newAnimation, + IvoAnimationBlock block, + string animName, + IReadOnlyDictionary controllerIdToNodeIndex, + IReadOnlyDictionary controllerIdToRestTranslation, + IReadOnlyDictionary controllerIdToRestRotation) + { + newAnimation = new GltfAnimation { Name = animName }; + + foreach (var boneHash in block.BoneHashes) + { + // Try to find node by bone hash (CRC32 of bone name) + if (!controllerIdToNodeIndex.TryGetValue(boneHash, out var nodeIndex)) + { + Log.D("IvoDBA[{0}]: Bone hash 0x{1:X08} not found in skeleton", animName, boneHash); + continue; + } + + // Get rest pose for this bone (in CryEngine space) + var restTranslation = controllerIdToRestTranslation.TryGetValue(boneHash, out var restT) + ? restT + : Vector3.Zero; + var restRotation = controllerIdToRestRotation.TryGetValue(boneHash, out var restR) + ? restR + : Quaternion.Identity; + + // Get rotation and position data for this bone + block.Rotations.TryGetValue(boneHash, out var rotations); + block.RotationTimes.TryGetValue(boneHash, out var rotTimes); + block.Positions.TryGetValue(boneHash, out var positions); + block.PositionTimes.TryGetValue(boneHash, out var posTimes); + + // Create position animation channel if we have position data + if (positions is not null && positions.Count > 0) + { + var keyTimes = posTimes ?? []; + if (keyTimes.Count == 0 && positions.Count > 0) + { + // Generate uniform time distribution if no explicit times + keyTimes = Enumerable.Range(0, positions.Count).Select(t => (float)t).ToList(); + } + + if (keyTimes.Count > 0) + { + var startTime = keyTimes[0]; + var timeAccessor = AddAccessor( + $"ivo_dba/{animName}/pos_time/{boneHash:X08}", -1, null, + keyTimes.Select(t => (t - startTime) / 30f).ToArray()); + + // Ivo DBA stores ABSOLUTE local positions, not deltas + // Just swap axes for glTF coordinate system + var absolutePositions = positions + .Select(SwapAxesForPosition) + .ToArray(); + + var posAccessor = AddAccessor( + $"ivo_dba/{animName}/pos/{boneHash:X08}", -1, null, + absolutePositions); + + newAnimation.Samplers.Add(new GltfAnimationSampler + { + Input = timeAccessor, + Output = posAccessor, + Interpolation = GltfAnimationSamplerInterpolation.Linear, + }); + newAnimation.Channels.Add(new GltfAnimationChannel + { + Sampler = newAnimation.Samplers.Count - 1, + Target = new GltfAnimationChannelTarget + { + Node = nodeIndex, + Path = GltfAnimationChannelTargetPath.Translation, + }, + }); + } + } + + // Create rotation animation channel if we have rotation data + if (rotations is not null && rotations.Count > 0) + { + var keyTimes = rotTimes ?? []; + if (keyTimes.Count == 0 && rotations.Count > 0) + { + // Generate uniform time distribution if no explicit times + keyTimes = Enumerable.Range(0, rotations.Count).Select(t => (float)t).ToList(); + } + + if (keyTimes.Count > 0) + { + var startTime = keyTimes[0]; + var timeAccessor = AddAccessor( + $"ivo_dba/{animName}/rot_time/{boneHash:X08}", -1, null, + keyTimes.Select(t => (t - startTime) / 30f).ToArray()); + + // Ivo DBA stores ABSOLUTE local rotations, not deltas + // Just swap axes for glTF coordinate system + var absoluteRotations = rotations + .Select(SwapAxesForAnimations) + .ToArray(); + + var rotAccessor = AddAccessor( + $"ivo_dba/{animName}/rot/{boneHash:X08}", -1, null, + absoluteRotations); + + newAnimation.Samplers.Add(new GltfAnimationSampler + { + Input = timeAccessor, + Output = rotAccessor, + Interpolation = GltfAnimationSamplerInterpolation.Linear, + }); + newAnimation.Channels.Add(new GltfAnimationChannel + { + Sampler = newAnimation.Samplers.Count - 1, + Target = new GltfAnimationChannelTarget + { + Node = nodeIndex, + Path = GltfAnimationChannelTargetPath.Rotation, + }, + }); + } + } + } + + return newAnimation.Samplers.Count > 0 && newAnimation.Channels.Count > 0; + } } diff --git a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Buffers.cs b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Buffers.cs index fdcd7721..ef2104c0 100644 --- a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Buffers.cs +++ b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Buffers.cs @@ -11,6 +11,7 @@ using CgfConverter.Renderers.MaterialTextures; using Newtonsoft.Json; using SixLabors.ImageSharp; +using static DDSUnsplitter.Library.DDSUnsplitter; namespace CgfConverter.Renderers.Gltf; @@ -496,16 +497,36 @@ private int AddTexture(string? baseName, int width, int height, MaterialTexture if (!Args.EmbedTextures && materialTexture.Key is FileMaterialTextureKey fileKey) { string originalPath = fileKey.Path; + + // Handle Armored Warfare split DDS files (e.g., chicken_d.dds.1) + // Strip the .1 suffix before changing extension to avoid chicken_d.dds.dds + if (originalPath.EndsWith(".1")) + originalPath = originalPath[..^2]; // Remove ".1" + + // Unsplit textures if requested (combines .dds.1, .dds.2, etc. into single .dds) + if (Args.UnsplitTextures) + { + try + { + Log.I($"Combining texture file {originalPath}"); + Combine(originalPath); + } + catch (Exception ex) + { + Log.W($"Error combining texture {originalPath}: {ex.Message}"); + } + } + string extension = ".dds"; // Default - + // Determine the extension based on ArgsHandler settings if (Args.PngTextures) extension = ".png"; else if (Args.TiffTextures) extension = ".tif"; else if (Args.TgaTextures) extension = ".tga"; - + // Use the original path with the appropriate extension string uri = Path.ChangeExtension(originalPath, extension); - + // Add the texture to the glTF file with the URI return AddTexture(baseName, GetMimeTypeForExtension(extension), null, uri); } diff --git a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Geometry.cs b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Geometry.cs index 5e3e7d6e..0084ef23 100644 --- a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Geometry.cs +++ b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Geometry.cs @@ -21,6 +21,15 @@ protected void CreateGltfNodeInto(List nodes, CryEngine cryData, bool omitS if (cryData.MaterialFiles is not null) WriteMaterial(cryData.MaterialFiles.FirstOrDefault(), cryData.Materials.Values.FirstOrDefault()); + // For Ivo format with skinning, create skeleton first and attach meshes to skeleton nodes + // This ensures geometry moves with the skeleton + if (!omitSkins && cryData.SkinningInfo is { HasSkinningInfo: true } skinningInfo + && HasObjectNodeIndexMappings(skinningInfo)) + { + CreateIvoSkeletonWithMeshes(nodes, cryData, skinningInfo); + return; + } + // THERE CAN BE MULTIPLE ROOT NODES IN EACH FILE! Check to see if the parentnodeid ~0 and be sure to add a node for it. List positionNodes = []; List positionRoots = cryData.Nodes.Where(a => a.ParentNodeID == ~0).ToList(); @@ -36,6 +45,377 @@ protected void CreateGltfNodeInto(List nodes, CryEngine cryData, bool omitS } } + /// + /// Check if skinning info has ObjectNodeIndex mappings (Ivo format). + /// + private static bool HasObjectNodeIndexMappings(SkinningInfo skinningInfo) + { + if (skinningInfo.CompiledBones is null || skinningInfo.CompiledBones.Count == 0) + return false; + + // If any bone has a valid ObjectNodeIndex, we have Ivo format + return skinningInfo.CompiledBones.Any(b => b.ObjectNodeIndex >= 0); + } + + /// + /// Creates skeleton and mesh hierarchy for Ivo format. + /// Skeleton provides the armature for animation, meshes are attached with skinning. + /// + private void CreateIvoSkeletonWithMeshes(List nodes, CryEngine cryData, SkinningInfo skinningInfo) + { + var controllerIdToNodeIndex = new Dictionary(); + var boneIndexToNodeIndex = new Dictionary(); + + // Build mapping from ObjectNodeIndex (ChunkNode index) to bone index + var nodeIndexToBoneIndex = new Dictionary(); + for (int boneIndex = 0; boneIndex < skinningInfo.CompiledBones.Count; boneIndex++) + { + var bone = skinningInfo.CompiledBones[boneIndex]; + if (bone.ObjectNodeIndex >= 0) + { + nodeIndexToBoneIndex[bone.ObjectNodeIndex] = boneIndex; + } + } + + // Get all ChunkNodes indexed by their position (for mesh lookup) + var allNodes = cryData.Nodes; + var nodeIndexToChunkNode = new Dictionary(); + for (int i = 0; i < allNodes.Count; i++) + { + nodeIndexToChunkNode[i] = allNodes[i]; + } + + // === STEP 1: Create skeleton (bone nodes) === + for (int boneIndex = 0; boneIndex < skinningInfo.CompiledBones.Count; boneIndex++) + { + var bone = skinningInfo.CompiledBones[boneIndex]; + + // Compute local transform + Matrix4x4 localMatrix; + + if (bone.ParentBone == null) + { + if (!Matrix4x4.Invert(bone.BindPoseMatrix, out localMatrix)) + { + Log.W("CompiledBone[{0}]: Failed to invert BindPoseMatrix for root", bone.BoneName); + localMatrix = Matrix4x4.Identity; + } + } + else + { + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var childBoneToWorld)) + { + localMatrix = bone.ParentBone.BindPoseMatrix * childBoneToWorld; + } + else + { + Log.W("CompiledBone[{0}]: Failed to invert BindPoseMatrix", bone.BoneName); + localMatrix = Matrix4x4.Identity; + } + } + + // Transpose and swap axes for glTF coordinate system (Y-up) + var matrix = SwapAxes(Matrix4x4.Transpose(localMatrix)); + if (!Matrix4x4.Decompose(matrix, out var scale, out var rotation, out var translation)) + { + Log.W("CompiledBone[{0}]: BindPoseMatrix is not decomposable", bone.BoneName); + scale = Vector3.One; + rotation = Quaternion.Identity; + translation = Vector3.Zero; + } + + var boneNode = new GltfNode + { + Name = bone.BoneName, + Scale = (scale - Vector3.One).LengthSquared() > 0.000001 + ? new List { scale.X, scale.Y, scale.Z } + : null, + Translation = translation != Vector3.Zero + ? new List { translation.X, translation.Y, translation.Z } + : null, + Rotation = rotation != Quaternion.Identity + ? new List { rotation.X, rotation.Y, rotation.Z, rotation.W } + : null + }; + + var nodeIndex = AddNode(boneNode); + boneIndexToNodeIndex[boneIndex] = nodeIndex; + controllerIdToNodeIndex[bone.ControllerID] = nodeIndex; + + // Also add CRC32 hash of bone name for Ivo animation matching + // Ivo animations use bone hashes (CRC32) instead of controller IDs + if (!string.IsNullOrEmpty(bone.BoneName)) + { + var crc32Original = Crc32CryEngine.Compute(bone.BoneName); + controllerIdToNodeIndex.TryAdd(crc32Original, nodeIndex); + + var crc32Lower = Crc32CryEngine.Compute(bone.BoneName.ToLowerInvariant()); + if (crc32Lower != crc32Original) + controllerIdToNodeIndex.TryAdd(crc32Lower, nodeIndex); + } + } + + // Set up bone parent-child relationships + for (int boneIndex = 0; boneIndex < skinningInfo.CompiledBones.Count; boneIndex++) + { + var bone = skinningInfo.CompiledBones[boneIndex]; + var nodeIndex = boneIndexToNodeIndex[boneIndex]; + + if (bone.ParentBone == null) + { + nodes.Add(nodeIndex); + } + else + { + var parentBoneIndex = skinningInfo.CompiledBones.IndexOf(bone.ParentBone); + if (parentBoneIndex >= 0 && boneIndexToNodeIndex.TryGetValue(parentBoneIndex, out var parentNodeIndex)) + { + Root.Nodes[parentNodeIndex].Children.Add(nodeIndex); + } + else + { + Log.W("Bone[{0}]: Parent bone not found, treating as root", bone.BoneName); + nodes.Add(nodeIndex); + } + } + } + + // === STEP 2: Attach meshes to skeleton nodes with skinning === + // Create a single shared skin for all meshes (named after the model) + GltfSkin? sharedSkin = null; + int? sharedSkinIndex = null; + + foreach (var kvp in nodeIndexToBoneIndex) + { + var chunkNodeIndex = kvp.Key; + var boneIndex = kvp.Value; + + if (!nodeIndexToChunkNode.TryGetValue(chunkNodeIndex, out var cryNode)) + continue; + + if (cryNode.MeshData?.GeometryInfo is null) + continue; + + var boneNodeIndex = boneIndexToNodeIndex[boneIndex]; + var boneNode = Root.Nodes[boneNodeIndex]; + + var accessors = new GltfMeshPrimitiveAttributes(); + var meshChunk = cryNode.MeshData; + + if (!WriteMeshOrLogError(out var gltfMesh, cryData, boneNode, cryNode, meshChunk!, accessors)) + continue; + + boneNode.Mesh = AddMesh(gltfMesh); + + // Create shared skin on first mesh + if (sharedSkin == null) + { + var geometrySubsets = meshChunk?.GeometryInfo?.GeometrySubsets; + bool usePerSubsetExtraction = meshChunk?.GeometryInfo?.VertUVs is not null; + + if (WriteSkinningDataOnly(out sharedSkin, out var weights, out var joints, boneNode, skinningInfo, + controllerIdToNodeIndex, geometrySubsets, usePerSubsetExtraction)) + { + // Use model name for skin instead of node name + sharedSkin.Name = $"{cryData.Name}/skin"; + sharedSkinIndex = AddSkin(sharedSkin); + + boneNode.Skin = sharedSkinIndex; + foreach (var prim in gltfMesh.Primitives) + { + prim.Attributes.Joints0 = joints; + prim.Attributes.Weights0 = weights; + } + } + } + else if (sharedSkinIndex.HasValue) + { + // Reuse shared skin for subsequent meshes + var geometrySubsets = meshChunk?.GeometryInfo?.GeometrySubsets; + bool usePerSubsetExtraction = meshChunk?.GeometryInfo?.VertUVs is not null; + + if (WriteSkinningDataOnly(out _, out var weights, out var joints, boneNode, skinningInfo, + controllerIdToNodeIndex, geometrySubsets, usePerSubsetExtraction)) + { + boneNode.Skin = sharedSkinIndex; + foreach (var prim in gltfMesh.Primitives) + { + prim.Attributes.Joints0 = joints; + prim.Attributes.Weights0 = weights; + } + } + } + } + + // Write animations using the skeleton mapping + _ = WriteAnimations(cryData.Animations, controllerIdToNodeIndex); + _ = WriteCafAnimations(cryData.CafAnimations, controllerIdToNodeIndex); + _ = WriteIvoDbaAnimations(cryData.Animations, controllerIdToNodeIndex, skinningInfo); + } + + /// + /// Adds mesh to an existing node (used for Ivo format where meshes are attached to skeleton bones). + /// + private void AddMeshToExistingNode( + CryEngine cryData, + ChunkNode cryNode, + GltfNode gltfNode, + Dictionary controllerIdToNodeIndex, + SkinningInfo skinningInfo) + { + var accessors = new GltfMeshPrimitiveAttributes(); + var meshChunk = cryNode.MeshData; + + if (!WriteMeshOrLogError(out var gltfMesh, cryData, gltfNode, cryNode, meshChunk!, accessors)) + return; + + gltfNode.Mesh = AddMesh(gltfMesh); + + var geometrySubsets = meshChunk?.GeometryInfo?.GeometrySubsets; + bool usePerSubsetExtraction = meshChunk?.GeometryInfo?.VertUVs is not null; + + if (WriteSkinningDataOnly(out var newSkin, out var weights, out var joints, gltfNode, skinningInfo, + controllerIdToNodeIndex, geometrySubsets, usePerSubsetExtraction)) + { + gltfNode.Skin = AddSkin(newSkin); + foreach (var prim in gltfMesh.Primitives) + { + prim.Attributes.Joints0 = joints; + prim.Attributes.Weights0 = weights; + } + } + } + + /// + /// Writes skinning data (weights, joints, inverse bind matrices) without creating skeleton nodes. + /// Used when skeleton nodes are already created separately. + /// + private bool WriteSkinningDataOnly( + out GltfSkin newSkin, + out int weights, + out int joints, + GltfNode rootNode, + SkinningInfo skinningInfo, + IDictionary controllerIdToNodeIndex, + List? geometrySubsets, + bool usePerSubsetExtraction) + { + newSkin = null!; + weights = joints = 0; + + var baseName = $"{rootNode.Name}/bone/weight"; + + if (usePerSubsetExtraction) + { + var subsets = geometrySubsets ?? []; + var numberOfElements = subsets.Sum(x => x.NumVertices); + + weights = + GetAccessorOrDefault(baseName, 0, numberOfElements) + ?? AddAccessor(baseName, -1, null, + skinningInfo.IntVertices is null + ? subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.BoneMappings[i]) + .Select(x => new Vector4( + x.Weight[0], x.Weight[1], x.Weight[2], x.Weight[3]))) + .ToArray() + : subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.IntVertices[skinningInfo.Ext2IntMap[i]]) + .Select(x => new Vector4( + x.BoneMapping.Weight[0], x.BoneMapping.Weight[1], x.BoneMapping.Weight[2], x.BoneMapping.Weight[3]))) + .ToArray()); + } + else + { + var skinCount = skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count; + + weights = + GetAccessorOrDefault(baseName, 0, skinCount) + ?? AddAccessor(baseName, -1, null, + skinningInfo.IntVertices is null + ? skinningInfo.BoneMappings + .Select(x => new Vector4( + x.Weight[0], x.Weight[1], x.Weight[2], x.Weight[3])) + .ToArray() + : skinningInfo.Ext2IntMap + .Select(x => skinningInfo.IntVertices[x]) + .Select(x => new Vector4( + x.BoneMapping.Weight[0], x.BoneMapping.Weight[1], x.BoneMapping.Weight[2], x.BoneMapping.Weight[3])) + .ToArray()); + } + + baseName = $"{rootNode.Name}/bone/joint"; + if (usePerSubsetExtraction) + { + var subsets = geometrySubsets ?? []; + var numberOfElements = subsets.Sum(x => x.NumVertices); + + joints = + GetAccessorOrDefault(baseName, 0, numberOfElements) + ?? AddAccessor( + baseName, + -1, + null, + skinningInfo is { HasIntToExtMapping: true, IntVertices: { } } + ? subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.IntVertices[skinningInfo.Ext2IntMap[i]]) + .Select(x => new TypedVec4( + x.BoneMapping.BoneIndex[0], x.BoneMapping.BoneIndex[1], x.BoneMapping.BoneIndex[2], x.BoneMapping.BoneIndex[3]))) + .ToArray() + : subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.BoneMappings[i]) + .Select(x => new TypedVec4( + (ushort)x.BoneIndex[0], (ushort)x.BoneIndex[1], (ushort)x.BoneIndex[2], + (ushort)x.BoneIndex[3]))) + .ToArray()); + } + else + { + var skinCount = skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count; + + joints = + GetAccessorOrDefault(baseName, 0, skinCount) + ?? AddAccessor( + baseName, + -1, + null, + skinningInfo is { HasIntToExtMapping: true, IntVertices: { } } + ? skinningInfo.Ext2IntMap + .Select(x => skinningInfo.IntVertices[x]) + .Select(x => new TypedVec4( + x.BoneMapping.BoneIndex[0], x.BoneMapping.BoneIndex[1], x.BoneMapping.BoneIndex[2], x.BoneMapping.BoneIndex[3])) + .ToArray() + : skinningInfo.BoneMappings + .Select(x => new TypedVec4( + (ushort)x.BoneIndex[0], (ushort)x.BoneIndex[1], (ushort)x.BoneIndex[2], + (ushort)x.BoneIndex[3])) + .ToArray()); + } + + baseName = $"{rootNode.Name}/inverseBindMatrix"; + var inverseBindMatricesAccessor = + GetAccessorOrDefault(baseName, 0, skinningInfo.CompiledBones.Count) + ?? AddAccessor(baseName, -1, null, + skinningInfo.CompiledBones.Select(x => SwapAxes(Matrix4x4.Transpose(x.BindPoseMatrix))).ToArray()); + + newSkin = new GltfSkin + { + InverseBindMatrices = inverseBindMatricesAccessor, + Joints = skinningInfo.CompiledBones.Select(x => controllerIdToNodeIndex[x.ControllerID]).ToList(), + Name = $"{rootNode.Name}/skin", + Skeleton = controllerIdToNodeIndex.Values.Min() // Root bone node + }; + return true; + } + protected bool CreateGltfNode( [MaybeNullWhen(false)] out GltfNode node, CryEngine cryData, @@ -63,6 +443,22 @@ protected bool CreateGltfNode( CryEngine cryData, ChunkNode cryNode, bool omitSkins) + { + // Create shared dictionary for skeleton - will be populated by first skinned mesh + // and reused by subsequent meshes to avoid duplicate skeletons + var controllerIdToNodeIndex = new Dictionary(); + return CreateGltfNode(out node, cryData, cryNode, omitSkins, controllerIdToNodeIndex); + } + + /// + /// Recursive method to add a gltf node to the nodes array with shared skeleton dictionary. + /// + private bool CreateGltfNode( + [MaybeNullWhen(false)] out GltfNode node, + CryEngine cryData, + ChunkNode cryNode, + bool omitSkins, + Dictionary controllerIdToNodeIndex) { if (Args.IsNodeNameExcluded(cryNode.Name)) { @@ -71,18 +467,18 @@ protected bool CreateGltfNode( return false; } - var controllerIdToNodeIndex = new Dictionary(); - // Create this node and add to GltfRoot.Nodes - var rotationQuat = Quaternion.CreateFromRotationMatrix(cryNode.LocalTransform); - var translation = cryNode.LocalTransform.GetTranslation(); + // Use the transformed matrix directly to preserve correct coordinate transformation + // This avoids issues with TRS decomposition when axes are swapped and negated + // Note: CryEngine uses row-major with translation in last row (row-vector convention) + // glTF uses column-major with translation in last column (column-vector convention) + // MatrixToGltfList outputs in column-major format which naturally transposes + var transformedMatrix = SwapAxes(cryNode.LocalTransform); node = new GltfNode { Name = cryNode.Name, - Translation = SwapAxesForPosition(translation).ToGltfList(), - Rotation = SwapAxesForLayout(rotationQuat).ToGltfList(), - Scale = Vector3.One.ToGltfList() + Matrix = MatrixToGltfList(transformedMatrix) }; // Add mesh if needed @@ -96,12 +492,17 @@ protected bool CreateGltfNode( } if (!omitSkins) + { _ = WriteAnimations(cryData.Animations, controllerIdToNodeIndex); + _ = WriteCafAnimations(cryData.CafAnimations, controllerIdToNodeIndex); + _ = WriteIvoDbaAnimations(cryData.Animations, controllerIdToNodeIndex, cryData.SkinningInfo); + } // For each child, recursively call this method to add the child to GltfRoot.Nodes. + // Pass the shared controllerIdToNodeIndex so all meshes use the same skeleton. foreach (ChunkNode cryChildNode in cryNode.Children) { - if (!CreateGltfNode(out GltfNode? childNode, cryData, cryChildNode, omitSkins)) + if (!CreateGltfNode(out GltfNode? childNode, cryData, cryChildNode, omitSkins, controllerIdToNodeIndex)) continue; node.Children.Add(Root.Nodes.Count); @@ -124,8 +525,12 @@ private void AddMesh(CryEngine cryData, ChunkNode cryNode, GltfNode gltfNode, Di if (cryData.SkinningInfo is { HasSkinningInfo: true } skinningInfo) { + var geometrySubsets = meshChunk?.GeometryInfo?.GeometrySubsets; + // Ivo format (VertUVs) requires per-subset extraction; traditional format (Vertices) uses raw arrays + bool usePerSubsetExtraction = meshChunk?.GeometryInfo?.VertUVs is not null; + if (WriteSkinOrLogError(out var newSkin, out var weights, out var joints, cryData, gltfNode, skinningInfo, - controllerIdToNodeIndex)) + controllerIdToNodeIndex, geometrySubsets, usePerSubsetExtraction)) { gltfNode.Skin = AddSkin(newSkin); foreach (var prim in gltfMesh.Primitives) @@ -146,7 +551,9 @@ private bool WriteSkinOrLogError( CryEngine cryData, GltfNode rootNode, SkinningInfo skinningInfo, - IDictionary controllerIdToNodeIndex) + IDictionary controllerIdToNodeIndex, + List? geometrySubsets, + bool usePerSubsetExtraction) { if (!skinningInfo.HasSkinningInfo) throw new ArgumentException("HasSkinningInfo must be true", nameof(skinningInfo)); @@ -159,46 +566,121 @@ private bool WriteSkinOrLogError( var nodeChunk = cryData.RootNode; var boneMappingData = nodeChunk.MeshData?.GeometryInfo?.BoneMappings; - weights = - GetAccessorOrDefault(baseName, 0, - skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count) - ?? AddAccessor(baseName, -1, null, - skinningInfo.IntVertices is null - ? skinningInfo.BoneMappings - .Select(x => new Vector4( - x.Weight[0], x.Weight[1], x.Weight[2], x.Weight[3])) - .ToArray() - : skinningInfo.Ext2IntMap - .Select(x => skinningInfo.IntVertices[x]) - .Select(x => new Vector4( - x.BoneMapping.Weight[0], x.BoneMapping.Weight[1], x.BoneMapping.Weight[2], x.BoneMapping.Weight[3])) - .ToArray()); - - var boneIdToBindPoseMatrices = new Dictionary(); - foreach (var bone in skinningInfo.CompiledBones) - { - var boneId = skinningInfo.CompiledBones.IndexOf(bone); // parent bone id is always 0 - var parentBone = boneId == 0 ? bone : skinningInfo.CompiledBones[boneId + bone.OffsetParent]; - var parentBoneId = skinningInfo.CompiledBones.IndexOf(parentBone); - var matrix = boneIdToBindPoseMatrices[boneId] = bone.BindPoseMatrix; - - if (bone.OffsetParent != 0 && bone.OffsetParent != 0xffffffff) - { - if (!Matrix4x4.Invert(boneIdToBindPoseMatrices[parentBoneId], out var parentMat)) - return Log.E("CompiledBone[{0}/{1}]: Failed to invert BindPoseMatrix.", - rootNode.Name, bone.ParentBone?.BoneName); + if (usePerSubsetExtraction) + { + // Ivo format: extract weights per-subset to match per-subset vertex extraction + var subsets = geometrySubsets ?? []; + var numberOfElements = subsets.Sum(x => x.NumVertices); + + weights = + GetAccessorOrDefault(baseName, 0, numberOfElements) + ?? AddAccessor(baseName, -1, null, + skinningInfo.IntVertices is null + ? subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.BoneMappings[i]) + .Select(x => new Vector4( + x.Weight[0], x.Weight[1], x.Weight[2], x.Weight[3]))) + .ToArray() + : subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.IntVertices[skinningInfo.Ext2IntMap[i]]) + .Select(x => new Vector4( + x.BoneMapping.Weight[0], x.BoneMapping.Weight[1], x.BoneMapping.Weight[2], x.BoneMapping.Weight[3]))) + .ToArray()); + } + else + { + // Traditional format: use raw skinning data (like USD renderer) + var skinCount = skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count; + + weights = + GetAccessorOrDefault(baseName, 0, skinCount) + ?? AddAccessor(baseName, -1, null, + skinningInfo.IntVertices is null + ? skinningInfo.BoneMappings + .Select(x => new Vector4( + x.Weight[0], x.Weight[1], x.Weight[2], x.Weight[3])) + .ToArray() + : skinningInfo.Ext2IntMap + .Select(x => skinningInfo.IntVertices[x]) + .Select(x => new Vector4( + x.BoneMapping.Weight[0], x.BoneMapping.Weight[1], x.BoneMapping.Weight[2], x.BoneMapping.Weight[3])) + .ToArray()); + } + + // Build bone index to node index mapping + // If skeleton already exists (controllerIdToNodeIndex is populated), reuse those nodes + var boneIndexToNodeIndex = new Dictionary(); + bool skeletonAlreadyExists = controllerIdToNodeIndex.Count > 0; - matrix *= parentMat; + for (int boneIndex = 0; boneIndex < skinningInfo.CompiledBones.Count; boneIndex++) + { + var bone = skinningInfo.CompiledBones[boneIndex]; + + // Check if this bone's node already exists (from a previous mesh) + if (controllerIdToNodeIndex.TryGetValue(bone.ControllerID, out int existingNodeIndex)) + { + boneIndexToNodeIndex[boneIndex] = existingNodeIndex; + continue; } - if (!Matrix4x4.Invert(matrix, out matrix)) - return Log.E("CompiledBone[{0}/{1}]: Failed to invert BindPoseMatrix.", - rootNode.Name, bone.BoneName); + // Compute local transform using same approach as USD renderer + Matrix4x4 localMatrix; - matrix = SwapAxes(Matrix4x4.Transpose(matrix)); + if (bone.ParentBone == null) + { + // Root bone: local = world, invert BindPoseMatrix to get boneToWorld + if (!Matrix4x4.Invert(bone.BindPoseMatrix, out localMatrix)) + { + Log.W("CompiledBone[{0}/{1}]: Failed to invert BindPoseMatrix for root", + rootNode.Name, bone.BoneName); + localMatrix = Matrix4x4.Identity; + } + } + else + { + // Child bone: compute local transform relative to parent + // localTransform = parentWorldToBone * childBoneToWorld + // = parent.BindPoseMatrix * inverse(child.BindPoseMatrix) + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var childBoneToWorld)) + { + localMatrix = bone.ParentBone.BindPoseMatrix * childBoneToWorld; + } + else + { + Log.W("CompiledBone[{0}/{1}]: Failed to invert BindPoseMatrix", + rootNode.Name, bone.BoneName); + localMatrix = Matrix4x4.Identity; + } + } + + // Transpose and swap axes for glTF coordinate system (Y-up) + var matrix = SwapAxes(Matrix4x4.Transpose(localMatrix)); if (!Matrix4x4.Decompose(matrix, out var scale, out var rotation, out var translation)) - return Log.E("CompiledBone[{0}/{1}]: BindPoseMatrix is not decomposable.", - rootNode.Name, bone.BoneName); + { + Log.W("CompiledBone[{0}/{1}]: BindPoseMatrix is not decomposable", rootNode.Name, bone.BoneName); + scale = Vector3.One; + rotation = Quaternion.Identity; + translation = Vector3.Zero; + } + + // Debug: log turret_arm skeleton transform (CtrlID = 0x9384FC75) + if (bone.ControllerID == 0x9384FC75) + { + Log.I($"glTF skeleton turret_arm localMatrix (before transpose/swap):"); + Log.I($" [{localMatrix.M11:F6}, {localMatrix.M12:F6}, {localMatrix.M13:F6}, {localMatrix.M14:F6}]"); + Log.I($" [{localMatrix.M21:F6}, {localMatrix.M22:F6}, {localMatrix.M23:F6}, {localMatrix.M24:F6}]"); + Log.I($" [{localMatrix.M31:F6}, {localMatrix.M32:F6}, {localMatrix.M33:F6}, {localMatrix.M34:F6}]"); + Log.I($" [{localMatrix.M41:F6}, {localMatrix.M42:F6}, {localMatrix.M43:F6}, {localMatrix.M44:F6}]"); + if (Matrix4x4.Decompose(localMatrix, out _, out var rawRot, out _)) + { + Log.I($"glTF skeleton turret_arm raw rotation (from localMatrix): ({rawRot.X:F6}, {rawRot.Y:F6}, {rawRot.Z:F6}, {rawRot.W:F6})"); + } + Log.I($"glTF skeleton turret_arm final rotation (after swap): ({rotation.X:F6}, {rotation.Y:F6}, {rotation.Z:F6}, {rotation.W:F6})"); + } var boneNode = new GltfNode { @@ -214,36 +696,115 @@ skinningInfo.IntVertices is null ? new List { rotation.X, rotation.Y, rotation.Z, rotation.W } : null }; - controllerIdToNodeIndex[bone.ControllerID] = AddNode(boneNode); + var nodeIndex = AddNode(boneNode); + boneIndexToNodeIndex[boneIndex] = nodeIndex; + controllerIdToNodeIndex[bone.ControllerID] = nodeIndex; - if (bone.ParentControllerIndex == 0) - CurrentScene.Nodes.Add(controllerIdToNodeIndex[bone.ControllerID]); - else - Root.Nodes[controllerIdToNodeIndex[parentBone.ControllerID]].Children - .Add(controllerIdToNodeIndex[bone.ControllerID]); + // Also add CRC32 hash of bone name for Ivo animation matching + // Ivo animations use bone hashes (CRC32) instead of controller IDs + if (!string.IsNullOrEmpty(bone.BoneName)) + { + var crc32Original = Crc32CryEngine.Compute(bone.BoneName); + controllerIdToNodeIndex.TryAdd(crc32Original, nodeIndex); + + var crc32Lower = Crc32CryEngine.Compute(bone.BoneName.ToLowerInvariant()); + if (crc32Lower != crc32Original) + controllerIdToNodeIndex.TryAdd(crc32Lower, nodeIndex); + } + } + + // Only set up parent-child relationships if we created new skeleton nodes + if (!skeletonAlreadyExists) + { + for (int boneIndex = 0; boneIndex < skinningInfo.CompiledBones.Count; boneIndex++) + { + var bone = skinningInfo.CompiledBones[boneIndex]; + var nodeIndex = boneIndexToNodeIndex[boneIndex]; + + if (bone.ParentBone == null) + { + // Root bone + CurrentScene.Nodes.Add(nodeIndex); + } + else + { + // Find parent bone index + var parentBoneIndex = skinningInfo.CompiledBones.IndexOf(bone.ParentBone); + if (parentBoneIndex >= 0 && boneIndexToNodeIndex.TryGetValue(parentBoneIndex, out var parentNodeIndex)) + { + if (parentNodeIndex != nodeIndex) + { + Root.Nodes[parentNodeIndex].Children.Add(nodeIndex); + } + else + { + Log.W("Bone[{0}]: Self-reference detected, treating as root", bone.BoneName); + CurrentScene.Nodes.Add(nodeIndex); + } + } + else + { + Log.W("Bone[{0}]: Parent bone '{1}' not found in bone list, treating as root", + bone.BoneName, bone.ParentBone.BoneName); + CurrentScene.Nodes.Add(nodeIndex); + } + } + } } baseName = $"{rootNode.Name}/bone/joint"; - joints = - GetAccessorOrDefault( - baseName, - 0, - skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count) - ?? AddAccessor( - baseName, - -1, - null, - skinningInfo is { HasIntToExtMapping: true, IntVertices: { } } - ? skinningInfo.Ext2IntMap - .Select(x => skinningInfo.IntVertices[x]) - .Select(x => new TypedVec4( - x.BoneMapping.BoneIndex[0], x.BoneMapping.BoneIndex[1], x.BoneMapping.BoneIndex[2], x.BoneMapping.BoneIndex[3])) - .ToArray() - : skinningInfo.BoneMappings - .Select(x => new TypedVec4( - (ushort)x.BoneIndex[0], (ushort)x.BoneIndex[1], (ushort)x.BoneIndex[2], - (ushort)x.BoneIndex[3])) - .ToArray()); + if (usePerSubsetExtraction) + { + // Ivo format: extract joints per-subset to match per-subset vertex extraction + var subsets = geometrySubsets ?? []; + var numberOfElements = subsets.Sum(x => x.NumVertices); + + joints = + GetAccessorOrDefault(baseName, 0, numberOfElements) + ?? AddAccessor( + baseName, + -1, + null, + skinningInfo is { HasIntToExtMapping: true, IntVertices: { } } + ? subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.IntVertices[skinningInfo.Ext2IntMap[i]]) + .Select(x => new TypedVec4( + x.BoneMapping.BoneIndex[0], x.BoneMapping.BoneIndex[1], x.BoneMapping.BoneIndex[2], x.BoneMapping.BoneIndex[3]))) + .ToArray() + : subsets + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => skinningInfo.BoneMappings[i]) + .Select(x => new TypedVec4( + (ushort)x.BoneIndex[0], (ushort)x.BoneIndex[1], (ushort)x.BoneIndex[2], + (ushort)x.BoneIndex[3]))) + .ToArray()); + } + else + { + // Traditional format: use raw skinning data (like USD renderer) + var skinCount = skinningInfo.IntVertices is null ? skinningInfo.BoneMappings.Count : skinningInfo.Ext2IntMap.Count; + + joints = + GetAccessorOrDefault(baseName, 0, skinCount) + ?? AddAccessor( + baseName, + -1, + null, + skinningInfo is { HasIntToExtMapping: true, IntVertices: { } } + ? skinningInfo.Ext2IntMap + .Select(x => skinningInfo.IntVertices[x]) + .Select(x => new TypedVec4( + x.BoneMapping.BoneIndex[0], x.BoneMapping.BoneIndex[1], x.BoneMapping.BoneIndex[2], x.BoneMapping.BoneIndex[3])) + .ToArray() + : skinningInfo.BoneMappings + .Select(x => new TypedVec4( + (ushort)x.BoneIndex[0], (ushort)x.BoneIndex[1], (ushort)x.BoneIndex[2], + (ushort)x.BoneIndex[3])) + .ToArray()); + } baseName = $"{rootNode.Name}/inverseBindMatrix"; var inverseBindMatricesAccessor = @@ -300,10 +861,15 @@ private bool WriteMeshOrLogError( string baseName; + // Track whether we're using per-subset extraction (Ivo format) or raw arrays (traditional) + bool usePerSubsetExtraction = vertsUvs is not null; + if (verts is not null || vertsUvs is not null) { if (verts is not null) { + // Traditional format: use raw vertex array directly (like USD renderer) + // No per-subset extraction needed - indices reference vertices directly baseName = $"{gltfNode.Name}/vertex"; accessors.Position = GetAccessorOrDefault(baseName, 0, verts.Data.Length) @@ -320,7 +886,7 @@ uvs is null ?? AddAccessor($"{nodeChunk.Name}/uv", -1, GltfBufferViewTarget.ArrayBuffer, uvs.Data); } } - else // VertsUVs. + else // VertsUVs (Ivo format) - requires per-subset extraction { baseName = $"{gltfNode.Name}/vertex"; @@ -389,64 +955,96 @@ uvs is null var normalsArray = normals?.Data; baseName = $"{gltfNode.Name}/normal"; - accessors.Normal = normalsArray is null - ? null - : GetAccessorOrDefault(baseName, 0, normalsArray.Length) - ?? AddAccessor(baseName, -1, GltfBufferViewTarget.ArrayBuffer, - (meshChunk.GeometryInfo.GeometrySubsets ?? []) - .SelectMany(subset => Enumerable - .Range(subset.FirstVertex, subset.NumVertices) - .Select(i => SwapAxesForPosition(normalsArray[i]))) - .ToArray()); + if (usePerSubsetExtraction) + { + // Ivo format: extract normals per-subset + accessors.Normal = normalsArray is null + ? null + : GetAccessorOrDefault(baseName, 0, normalsArray.Length) + ?? AddAccessor(baseName, -1, GltfBufferViewTarget.ArrayBuffer, + (meshChunk.GeometryInfo.GeometrySubsets ?? []) + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => SwapAxesForPosition(normalsArray[i]))) + .ToArray()); + } + else + { + // Traditional format: use raw normals array + accessors.Normal = normalsArray is null + ? null + : GetAccessorOrDefault(baseName, 0, normalsArray.Length) + ?? AddAccessor(baseName, -1, GltfBufferViewTarget.ArrayBuffer, + normalsArray.Select(SwapAxesForPosition).ToArray()); + } baseName = $"{gltfNode.Name}/colors"; - accessors.Color0 = colors is null - ? null - : (GetAccessorOrDefault(baseName, 0, colors.Data.Length) - ?? AddAccessor( - baseName, - -1, - GltfBufferViewTarget.ArrayBuffer, - (meshChunk.GeometryInfo.GeometrySubsets ?? []) - .SelectMany(subset => Enumerable - .Range(subset.FirstVertex, subset.NumVertices) - .Select(i => new Vector4(colors.Data[i].R, colors.Data[i].G, colors.Data[i].B, colors.Data[i].A) / 255f)) - .ToArray())); + if (usePerSubsetExtraction) + { + // Ivo format: extract colors per-subset + accessors.Color0 = colors is null + ? null + : (GetAccessorOrDefault(baseName, 0, colors.Data.Length) + ?? AddAccessor( + baseName, + -1, + GltfBufferViewTarget.ArrayBuffer, + (meshChunk.GeometryInfo.GeometrySubsets ?? []) + .SelectMany(subset => Enumerable + .Range(subset.FirstVertex, subset.NumVertices) + .Select(i => new Vector4(colors.Data[i].R, colors.Data[i].G, colors.Data[i].B, colors.Data[i].A) / 255f)) + .ToArray())); + } + else + { + // Traditional format: use raw colors array + accessors.Color0 = colors is null + ? null + : (GetAccessorOrDefault(baseName, 0, colors.Data.Length) + ?? AddAccessor( + baseName, + -1, + GltfBufferViewTarget.ArrayBuffer, + colors.Data.Select(c => new Vector4(c.R, c.G, c.B, c.A) / 255f).ToArray())); + } baseName = $"${gltfNode.Name}/tangent"; } baseName = $"${gltfNode.Name}/index"; - var remappedIndices = new uint[indices.Data.Length]; - // Create a map of global indices to local indices - var localIndexMap = new Dictionary(); - uint currentOffset = 0; + uint[] finalIndices; - foreach (var subset in meshChunk.GeometryInfo.GeometrySubsets ?? []) + if (usePerSubsetExtraction) { - var firstGlobalIndex = indices.Data[subset.FirstIndex]; + // Ivo format: remap indices per-subset to reference extracted vertex positions. + // Each subset's indices reference original vertices [FirstVertex, FirstVertex+NumVertices) + // which get extracted to positions [currentOffset, currentOffset+NumVertices) + var remappedIndices = new uint[indices.Data.Length]; + uint currentOffset = 0; - for (int i = 0; i < subset.NumIndices; i++) + foreach (var subset in meshChunk.GeometryInfo.GeometrySubsets ?? []) { - uint globalIndex = indices.Data[subset.FirstIndex + i]; - uint localIndex = (uint)((globalIndex - firstGlobalIndex) + currentOffset); + var firstGlobalIndex = indices.Data[subset.FirstIndex]; - // Map the global index to its local counterpart - localIndexMap[globalIndex] = localIndex; - } + for (int i = 0; i < subset.NumIndices; i++) + { + uint globalIndex = indices.Data[subset.FirstIndex + i]; + uint localIndex = (uint)((globalIndex - firstGlobalIndex) + currentOffset); + remappedIndices[subset.FirstIndex + i] = localIndex; + } - currentOffset += (uint)subset.NumVertices; + currentOffset += (uint)subset.NumVertices; + } + finalIndices = remappedIndices; } - - for (int i = 0; i < indices.Data.Length; i++) + else { - remappedIndices[i] = localIndexMap.TryGetValue(indices.Data[i], out uint localIndex) - ? localIndex - : indices.Data[i]; // Fallback to original index if not found in map + // Traditional format: use raw indices directly (like USD renderer) + finalIndices = indices.Data; } var indexBufferView = GetBufferViewOrDefault(baseName) ?? - AddBufferView(baseName, remappedIndices, GltfBufferViewTarget.ElementArrayBuffer); + AddBufferView(baseName, finalIndices, GltfBufferViewTarget.ElementArrayBuffer); newMesh = new GltfMesh { diff --git a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Material.cs b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Material.cs index 25558752..4ec2003b 100644 --- a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Material.cs +++ b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.Material.cs @@ -42,6 +42,7 @@ public WrittenMaterial( GltfMaterial.AlphaMode = GltfMaterialAlphaMode.Mask; GltfMaterial.AlphaCutoff = 1f; // Fully transparent GltfMaterial.PbrMetallicRoughness.BaseColorFactor = Float4Transparent; + Index = renderer.AddMaterial(GltfMaterial); return; } diff --git a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.SwapAxes.cs b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.SwapAxes.cs index f2e935d9..4faf656b 100644 --- a/CgfConverter/Renderers/Gltf/BaseGltfRenderer.SwapAxes.cs +++ b/CgfConverter/Renderers/Gltf/BaseGltfRenderer.SwapAxes.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Collections.Generic; +using System.Numerics; namespace CgfConverter.Renderers.Gltf; @@ -15,46 +16,67 @@ public partial class BaseGltfRenderer * where positive X-axis points to the right, positive Y-axis away from the viewer and positive Z-axis points up. * In the context of characters this means that positive X is right, positive Y is forward, and positive Z is up. * + * Blender glTF import comment (blender_gltf.py): + * # glTF: right = +X, forward = -Z, up = +Y + * # glTF after Yup2Zup: right = +X, forward = +Y, up = +Z + * # Blender: right = +X, forward = -Z, up = +Y + * + * To match USD export in Blender (where models face +Y after import): + * We map CryEngine +Y (forward) → glTF -Z, so after Blender's Y-to-Z conversion, + * the model faces Blender +Y (matching our USD export which uses upAxis="Z"). + * * glTF cryengine - * Right -X +X + * Right +X +X (same, no flip needed for consistency with Blender) * Up +Y +Z - * Forward +Z +Y + * Forward -Z +Y (CryEngine +Y → glTF -Z for Blender compatibility) */ - // Originals from Soreepeong - //private static Vector3 SwapAxes(Vector3 val) => new(val.X, val.Z, val.Y); - - //private static Quaternion SwapAxes(Quaternion val) => new(val.X, val.Z, val.Y, -val.W); + // Original mapping (model faces Blender -Y after import): + // protected static Vector3 SwapAxesForPosition(Vector3 val) => new(-val.X, val.Z, val.Y); - //private static Matrix4x4 SwapAxes(Matrix4x4 val) => new( - // val.M11, val.M13, val.M12, val.M14, - // val.M31, val.M33, val.M32, val.M34, - // val.M21, val.M23, val.M22, val.M24, - // val.M41, val.M43, val.M42, val.M44); - - protected static Vector3 SwapAxesForPosition(Vector3 val) => new(-val.X, val.Z, val.Y); - // protected static Vector3 SwapAxesForPosition(Vector3 val) => new(val.X, val.Z, -val.Y); + // Corrected mapping (model faces Blender +Y after import, matching USD): + // CryEngine (X, Y, Z) → glTF (X, Z, -Y) so that: + // - CryEngine +X (right) → glTF +X + // - CryEngine +Z (up) → glTF +Y (up) + // - CryEngine +Y (forward) → glTF -Z → Blender +Y (forward) + protected static Vector3 SwapAxesForPosition(Vector3 val) => new(val.X, val.Z, -val.Y); protected static Vector3 SwapAxesForScale(Vector3 val) => new(val.X, val.Z, val.Y); - protected static Vector4 SwapAxesForTangent(Vector4 val) => new(-val.X, val.Z, val.Y, val.W); + // Tangent vector transforms like position: (x, y, z, w) → (x, z, -y, w) + protected static Vector4 SwapAxesForTangent(Vector4 val) => new(val.X, val.Z, -val.Y, val.W); - protected static Quaternion SwapAxesForAnimations(Quaternion val) => new(-val.X, val.Z, val.Y, val.W); + // Quaternion rotation axis transforms like a vector: (qx, qy, qz, qw) → (qx, qz, -qy, qw) + protected static Quaternion SwapAxesForAnimations(Quaternion val) => new(val.X, val.Z, -val.Y, val.W); - // orig. Just need to rotate 180 around z axis, which swaps Z and -W - //protected static Quaternion SwapAxesForLayout(Quaternion val) => new(-val.Y, val.W, val.Z, val.X); - protected static Quaternion SwapAxesForLayout(Quaternion val) => new(-val.X, -val.Z, val.Y, val.W); + // Layout quaternion uses same transformation as animations + protected static Quaternion SwapAxesForLayout(Quaternion val) => new(val.X, val.Z, -val.Y, val.W); // M': swapped matrix - // T: swap matrix = new Matrix4x4(-1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1) - // T^-1: inverse of swap matrix (= T, for this specific configuration) - // M' = T @ M @ T^-1 = + // T: swap matrix for (X, Y, Z) → (X, Z, -Y): + // | 1 0 0 0 | + // | 0 0 1 0 | + // | 0 -1 0 0 | + // | 0 0 0 1 | + // M' = T @ M @ T^-1 protected static Matrix4x4 SwapAxes(Matrix4x4 val) => new( - val.M11, -val.M13, -val.M12, -val.M14, - -val.M31, val.M33, val.M32, val.M34, - -val.M21, val.M23, val.M22, val.M24, - -val.M41, val.M43, val.M42, val.M44); + val.M11, val.M13, -val.M12, val.M14, + val.M31, val.M33, -val.M32, val.M34, + -val.M21, -val.M23, val.M22, -val.M24, + val.M41, val.M43, -val.M42, val.M44); + + /// + /// Converts a Matrix4x4 to glTF's column-major format (16-element list). + /// glTF expects matrices in column-major order: [m00, m10, m20, m30, m01, m11, m21, m31, ...] + /// + protected static List MatrixToGltfList(Matrix4x4 m) => + [ + m.M11, m.M21, m.M31, m.M41, // Column 0 + m.M12, m.M22, m.M32, m.M42, // Column 1 + m.M13, m.M23, m.M33, m.M43, // Column 2 + m.M14, m.M24, m.M34, m.M44 // Column 3 + ]; diff --git a/CgfConverter/Renderers/Gltf/GltfModelRenderer.cs b/CgfConverter/Renderers/Gltf/GltfModelRenderer.cs index 346b6dfa..4c9652e2 100644 --- a/CgfConverter/Renderers/Gltf/GltfModelRenderer.cs +++ b/CgfConverter/Renderers/Gltf/GltfModelRenderer.cs @@ -25,8 +25,8 @@ public GltfRoot GenerateGltfObject() // Create the root object. Reset("Scene"); - var extension = Path.GetExtension(_cryData.InputFile); - bool omitSkins = !(extension == ".chr" || extension == ".skin"); + // Only omit skins if there's no skinning data available + bool omitSkins = _cryData.SkinningInfo is not { HasSkinningInfo: true }; // For each root node in the crydata, add to the scene nodes. CreateGltfNodeInto(CurrentScene.Nodes, _cryData, omitSkins); diff --git a/CgfConverter/Renderers/Gltf/Models/GltfNode.cs b/CgfConverter/Renderers/Gltf/Models/GltfNode.cs index 5da55e62..d3e2bbbb 100644 --- a/CgfConverter/Renderers/Gltf/Models/GltfNode.cs +++ b/CgfConverter/Renderers/Gltf/Models/GltfNode.cs @@ -18,6 +18,13 @@ public class GltfNode [JsonProperty("children", NullValueHandling = NullValueHandling.Ignore)] public List Children = new(); + /// + /// 4x4 transformation matrix in column-major order. + /// When present, rotation/scale/translation are ignored (per glTF spec). + /// + [JsonProperty("matrix", NullValueHandling = NullValueHandling.Ignore)] + public List? Matrix; + [JsonProperty("rotation", NullValueHandling = NullValueHandling.Ignore)] public List? Rotation; diff --git a/CgfConverter/Renderers/USD/Models/UsdColor3f.cs b/CgfConverter/Renderers/USD/Models/UsdColor3f.cs index 1b7c6d9a..24a1ba0f 100644 --- a/CgfConverter/Renderers/USD/Models/UsdColor3f.cs +++ b/CgfConverter/Renderers/USD/Models/UsdColor3f.cs @@ -25,8 +25,12 @@ public override string Serialize(int indentLevel) sb.Append($"color3f {Name}"); else { - var stringValue = FormatStringValue($"<{Value}>"); - sb.Append($"color3f {Name} = {stringValue}"); + // Connection paths (starting with ")) + sb.Append($"color3f {Name} = {Value}"); + else + sb.Append($"color3f {Name} = ({Value})"); } return sb.ToString(); diff --git a/CgfConverter/Renderers/USD/Models/UsdFloat.cs b/CgfConverter/Renderers/USD/Models/UsdFloat.cs new file mode 100644 index 00000000..50e85ef6 --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdFloat.cs @@ -0,0 +1,38 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +public class UsdFloat : UsdAttribute +{ + public string? Value { get; set; } + + public UsdFloat(string name, string? value = null, bool isUniform = false) : base(name, isUniform) + { + Value = value; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + + if (IsUniform) + sb.Append("uniform "); + + if (Value is null) + sb.Append($"float {Name}"); + else + { + // Connection paths (starting with ")) + sb.Append($"float {Name} = {Value}"); + else + sb.Append($"float {Name} = {Value}"); + } + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdFloat3f.cs b/CgfConverter/Renderers/USD/Models/UsdFloat3f.cs index 62991142..c5f8b8de 100644 --- a/CgfConverter/Renderers/USD/Models/UsdFloat3f.cs +++ b/CgfConverter/Renderers/USD/Models/UsdFloat3f.cs @@ -25,8 +25,15 @@ public override string Serialize(int indentLevel) sb.Append($"float3 {Name}"); else { - var stringValue = FormatStringValue($"<{Value}>"); - sb.Append($"float3 {Name} = {stringValue}"); + // Connection paths (starting with ")) + sb.Append($"float3 {Name} = {Value}"); + else + { + var stringValue = FormatStringValue($"<{Value}>"); + sb.Append($"float3 {Name} = {stringValue}"); + } } diff --git a/CgfConverter/Renderers/USD/Models/UsdFloatArray.cs b/CgfConverter/Renderers/USD/Models/UsdFloatArray.cs new file mode 100644 index 00000000..17708d2b --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdFloatArray.cs @@ -0,0 +1,57 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents an array of floats for USD. +/// +public class UsdFloatArray : UsdAttribute +{ + public List Values { get; set; } + public int? ElementSize { get; set; } + public string? Interpolation { get; set; } + + public UsdFloatArray(string name, List values, int? elementSize = null, string? interpolation = null, bool isUniform = false) + : base(name, isUniform) + { + Values = values; + ElementSize = elementSize; + Interpolation = interpolation; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + + if (IsUniform) + sb.Append("uniform "); + + sb.Append($"float[] {Name} = ["); + sb.Append(string.Join(", ", Values)); + sb.Append("]"); + + if (ElementSize.HasValue || Interpolation != null) + { + sb.AppendLine(" ("); + if (ElementSize.HasValue) + { + sb.AppendIndent(indentLevel + 1); + sb.AppendLine($"elementSize = {ElementSize.Value}"); + } + if (Interpolation != null) + { + sb.AppendIndent(indentLevel + 1); + sb.AppendLine($"interpolation = \"{Interpolation}\""); + } + sb.AppendIndent(indentLevel); + sb.Append(")"); + } + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdHeader.cs b/CgfConverter/Renderers/USD/Models/UsdHeader.cs index 6ed4dfda..0bf87b4f 100644 --- a/CgfConverter/Renderers/USD/Models/UsdHeader.cs +++ b/CgfConverter/Renderers/USD/Models/UsdHeader.cs @@ -37,7 +37,13 @@ public string Serialize() sb.AppendLine("("); sb.AppendLine($" defaultPrim = \"{DefaultPrim}\""); sb.AppendLine($" doc = \"{Doc}\""); + if (EndTimeCode.HasValue) + sb.AppendLine($" endTimeCode = {EndTimeCode.Value}"); sb.AppendLine($" metersPerUnit = {MetersPerUnit}"); + if (StartTimeCode.HasValue) + sb.AppendLine($" startTimeCode = {StartTimeCode.Value}"); + if (TimeCodesPerSecond.HasValue) + sb.AppendLine($" timeCodesPerSecond = {TimeCodesPerSecond.Value}"); sb.AppendLine($" upAxis = \"{UpAxis}\""); sb.AppendLine(")"); return sb.ToString(); diff --git a/CgfConverter/Renderers/USD/Models/UsdIntArray.cs b/CgfConverter/Renderers/USD/Models/UsdIntArray.cs new file mode 100644 index 00000000..5c322de8 --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdIntArray.cs @@ -0,0 +1,57 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents an array of integers for USD. +/// +public class UsdIntArray : UsdAttribute +{ + public List Values { get; set; } + public int? ElementSize { get; set; } + public string? Interpolation { get; set; } + + public UsdIntArray(string name, List values, int? elementSize = null, string? interpolation = null, bool isUniform = false) + : base(name, isUniform) + { + Values = values; + ElementSize = elementSize; + Interpolation = interpolation; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + + if (IsUniform) + sb.Append("uniform "); + + sb.Append($"int[] {Name} = ["); + sb.Append(string.Join(", ", Values)); + sb.Append("]"); + + if (ElementSize.HasValue || Interpolation != null) + { + sb.AppendLine(" ("); + if (ElementSize.HasValue) + { + sb.AppendIndent(indentLevel + 1); + sb.AppendLine($"elementSize = {ElementSize.Value}"); + } + if (Interpolation != null) + { + sb.AppendIndent(indentLevel + 1); + sb.AppendLine($"interpolation = \"{Interpolation}\""); + } + sb.AppendIndent(indentLevel); + sb.Append(")"); + } + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdMatrix4d.cs b/CgfConverter/Renderers/USD/Models/UsdMatrix4d.cs index 3f767287..3e3ada00 100644 --- a/CgfConverter/Renderers/USD/Models/UsdMatrix4d.cs +++ b/CgfConverter/Renderers/USD/Models/UsdMatrix4d.cs @@ -1,18 +1,23 @@ -using CgfConverter.Renderers.USD.Attributes; +using CgfConverter.Renderers.USD.Attributes; using Extensions; +using System; using System.Numerics; using System.Text; namespace CgfConverter.Renderers.USD.Models; +/// +/// Represents a single 4x4 matrix for USD. +/// Used for geomBindTransform. +/// public class UsdMatrix4d : UsdAttribute { - public Matrix4x4 Value { get; set; } + public Matrix4x4 Matrix { get; set; } - public UsdMatrix4d(string name, Matrix4x4 value, bool isUniform = false) + public UsdMatrix4d(string name, Matrix4x4 matrix, bool isUniform = false) : base(name, isUniform) { - Value = value; + Matrix = matrix; } public override string Serialize(int indentLevel) @@ -23,13 +28,30 @@ public override string Serialize(int indentLevel) if (IsUniform) sb.Append("uniform "); - sb.Append($"matrix4d {Name} = ("); - sb.Append($" ({Value.M11:F7}, {Value.M12:F7}, {Value.M13:F7}, {Value.M14:F7}),"); - sb.Append($" ({Value.M21:F7}, {Value.M22:F7}, {Value.M23:F7}, {Value.M24:F7}),"); - sb.Append($" ({Value.M31:F7}, {Value.M32:F7}, {Value.M33:F7}, {Value.M34:F7}),"); - sb.Append($" ({Value.M41:F7}, {Value.M42:F7}, {Value.M43:F7}, {Value.M44:F7}) )"); - sb.CleanNumbers(); + var m = Matrix; + + // USD matrices are row-major format + sb.Append($"matrix4d {Name} = ( "); + sb.Append($"({FormatMatrixValue(m.M11)}, {FormatMatrixValue(m.M12)}, {FormatMatrixValue(m.M13)}, {FormatMatrixValue(m.M14)}), "); + sb.Append($"({FormatMatrixValue(m.M21)}, {FormatMatrixValue(m.M22)}, {FormatMatrixValue(m.M23)}, {FormatMatrixValue(m.M24)}), "); + sb.Append($"({FormatMatrixValue(m.M31)}, {FormatMatrixValue(m.M32)}, {FormatMatrixValue(m.M33)}, {FormatMatrixValue(m.M34)}), "); + sb.Append($"({FormatMatrixValue(m.M41)}, {FormatMatrixValue(m.M42)}, {FormatMatrixValue(m.M43)}, {FormatMatrixValue(m.M44)}) )"); return sb.ToString(); } + + /// + /// Formats a matrix value for USD output. + /// Values less than 1e-8 are rounded to zero. + /// Other values are formatted with max 6 decimal places. + /// + private static string FormatMatrixValue(float value) + { + // Round very small values to zero + if (Math.Abs(value) < 1e-8f) + return "0"; + + // Format with max 6 decimal places, stripping trailing zeros + return value.ToString("0.######"); + } } diff --git a/CgfConverter/Renderers/USD/Models/UsdMatrix4dArray.cs b/CgfConverter/Renderers/USD/Models/UsdMatrix4dArray.cs new file mode 100644 index 00000000..386f9e17 --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdMatrix4dArray.cs @@ -0,0 +1,69 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents an array of 4x4 matrices for USD. +/// Used for bindTransforms and restTransforms in skeletons. +/// +public class UsdMatrix4dArray : UsdAttribute +{ + public List Matrices { get; set; } + + public UsdMatrix4dArray(string name, List matrices, bool isUniform = false) + : base(name, isUniform) + { + Matrices = matrices; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + + if (IsUniform) + sb.Append("uniform "); + + sb.Append($"matrix4d[] {Name} = ["); + + for (int i = 0; i < Matrices.Count; i++) + { + var m = Matrices[i]; + + // USD matrices are row-major format + // Clean up very small values to avoid scientific notation clutter + sb.Append($"( ({FormatMatrixValue(m.M11)}, {FormatMatrixValue(m.M12)}, {FormatMatrixValue(m.M13)}, {FormatMatrixValue(m.M14)}), "); + sb.Append($"({FormatMatrixValue(m.M21)}, {FormatMatrixValue(m.M22)}, {FormatMatrixValue(m.M23)}, {FormatMatrixValue(m.M24)}), "); + sb.Append($"({FormatMatrixValue(m.M31)}, {FormatMatrixValue(m.M32)}, {FormatMatrixValue(m.M33)}, {FormatMatrixValue(m.M34)}), "); + sb.Append($"({FormatMatrixValue(m.M41)}, {FormatMatrixValue(m.M42)}, {FormatMatrixValue(m.M43)}, {FormatMatrixValue(m.M44)}) )"); + + if (i < Matrices.Count - 1) + sb.Append(", "); + } + + sb.Append("]"); + + return sb.ToString(); + } + + /// + /// Formats a matrix value for USD output. + /// Values less than 1e-8 are rounded to zero. + /// Other values are formatted with max 6 decimal places. + /// + private static string FormatMatrixValue(float value) + { + // Round very small values to zero + if (Math.Abs(value) < 1e-8f) + return "0"; + + // Format with max 6 decimal places, stripping trailing zeros + // 0.###### means: integer part required, up to 6 optional decimal digits + return value.ToString("0.######"); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdRelationship.cs b/CgfConverter/Renderers/USD/Models/UsdRelationship.cs new file mode 100644 index 00000000..b2545b43 --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdRelationship.cs @@ -0,0 +1,30 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents a USD relationship (rel). +/// Used for skel:skeleton relationships between meshes and skeletons. +/// +public class UsdRelationship : UsdAttribute +{ + public string Target { get; set; } + + public UsdRelationship(string name, string target) + : base(name, false) + { + Target = target; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + + sb.Append($"rel {Name} = {Target}"); + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdSkelAnimation.cs b/CgfConverter/Renderers/USD/Models/UsdSkelAnimation.cs new file mode 100644 index 00000000..9a39f2cf --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdSkelAnimation.cs @@ -0,0 +1,22 @@ +using CgfConverter.Renderers.USD.Attributes; +using System.Collections.Generic; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// SkelAnimation prim contains animation data for skeletal animation. +/// Stores joint transforms as time-sampled arrays. +/// +[UsdElement("SkelAnimation")] +public class UsdSkelAnimation : UsdPrim +{ + public UsdSkelAnimation(string name, List? properties = null) : base(name, properties) + { + } + + public override string Serialize(int indentLevel) + { + // Serialization handled by UsdSerializer using reflection + return string.Empty; + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdSkelRoot.cs b/CgfConverter/Renderers/USD/Models/UsdSkelRoot.cs index a16762b7..e1c494be 100644 --- a/CgfConverter/Renderers/USD/Models/UsdSkelRoot.cs +++ b/CgfConverter/Renderers/USD/Models/UsdSkelRoot.cs @@ -1,11 +1,22 @@ -namespace CgfConverter.Renderers.USD.Models; +using CgfConverter.Renderers.USD.Attributes; +using System.Collections.Generic; +namespace CgfConverter.Renderers.USD.Models; + +/// +/// SkelRoot prim marks the scope of skeletal data. +/// Must be parent to both Skeleton and skinned Meshes. +/// +[UsdElement("SkelRoot")] public class UsdSkelRoot : UsdPrim { - public UsdSkelRoot(string name) : base(name) { } + public UsdSkelRoot(string name, List? properties = null) : base(name, properties) + { + } public override string Serialize(int indentLevel) { - throw new System.NotImplementedException(); + // Serialization handled by UsdSerializer using reflection + return string.Empty; } } diff --git a/CgfConverter/Renderers/USD/Models/UsdSkeleton.cs b/CgfConverter/Renderers/USD/Models/UsdSkeleton.cs new file mode 100644 index 00000000..0784b022 --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdSkeleton.cs @@ -0,0 +1,33 @@ +using CgfConverter.Renderers.USD.Attributes; +using System.Collections.Generic; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Skeleton prim contains joint hierarchy and bind pose data. +/// Requires SkelBindingAPI schema. +/// +[UsdElement("Skeleton")] +public class UsdSkeleton : UsdPrim +{ + public UsdSkeleton(string name, List? properties = null) : base(name, properties) + { + // Add SkelBindingAPI schema by default + if (properties == null) + { + Properties = new List + { + new UsdProperty(new Dictionary + { + ["apiSchemas"] = "[\"SkelBindingAPI\"]" + }, true) + }; + } + } + + public override string Serialize(int indentLevel) + { + // Serialization handled by UsdSerializer using reflection + return string.Empty; + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdTimeSampledFloat3Array.cs b/CgfConverter/Renderers/USD/Models/UsdTimeSampledFloat3Array.cs new file mode 100644 index 00000000..1f1d5edf --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdTimeSampledFloat3Array.cs @@ -0,0 +1,58 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents time-sampled float3 array for skeletal animation translations. +/// USD format: float3[] translations.timeSamples = { 0: [(x, y, z), ...], 0.033: [...], ... } +/// +public class UsdTimeSampledFloat3Array : UsdAttribute +{ + /// + /// Time samples mapping frame time (in seconds) to arrays of Vector3 for all joints. + /// + public SortedDictionary> TimeSamples { get; set; } + + public UsdTimeSampledFloat3Array(string name, SortedDictionary> timeSamples) + : base(name, false) + { + TimeSamples = timeSamples; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + sb.AppendLine($"float3[] {Name}.timeSamples = {{"); + + foreach (var kvp in TimeSamples) + { + sb.AppendIndent(indentLevel + 1); + sb.Append(kvp.Key.ToString("0", CultureInfo.InvariantCulture)); + sb.Append(": ["); + + bool first = true; + foreach (var v in kvp.Value) + { + if (!first) sb.Append(", "); + first = false; + // Use fixed-point notation to avoid scientific notation + sb.AppendFormat(CultureInfo.InvariantCulture, + "({0:0.######}, {1:0.######}, {2:0.######})", + v.X, v.Y, v.Z); + } + + sb.AppendLine("],"); + } + + sb.AppendIndent(indentLevel); + sb.Append("}"); + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdTimeSampledHalf3Array.cs b/CgfConverter/Renderers/USD/Models/UsdTimeSampledHalf3Array.cs new file mode 100644 index 00000000..1a9c6e9f --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdTimeSampledHalf3Array.cs @@ -0,0 +1,58 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents time-sampled half3 array for skeletal animation scales. +/// USD format: half3[] scales.timeSamples = { 0: [(x, y, z), ...], 1: [...], ... } +/// Note: USD SkelAnimation requires scales to be half3[] (16-bit precision). +/// +public class UsdTimeSampledHalf3Array : UsdAttribute +{ + /// + /// Time samples mapping frame number to arrays of Vector3 for all joints. + /// + public SortedDictionary> TimeSamples { get; set; } + + public UsdTimeSampledHalf3Array(string name, SortedDictionary> timeSamples) + : base(name, false) + { + TimeSamples = timeSamples; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + sb.AppendLine($"half3[] {Name}.timeSamples = {{"); + + foreach (var kvp in TimeSamples) + { + sb.AppendIndent(indentLevel + 1); + sb.Append(kvp.Key.ToString("0", CultureInfo.InvariantCulture)); + sb.Append(": ["); + + bool first = true; + foreach (var v in kvp.Value) + { + if (!first) sb.Append(", "); + first = false; + sb.AppendFormat(CultureInfo.InvariantCulture, + "({0:0.######}, {1:0.######}, {2:0.######})", + v.X, v.Y, v.Z); + } + + sb.AppendLine("],"); + } + + sb.AppendIndent(indentLevel); + sb.Append("}"); + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdTimeSampledQuatfArray.cs b/CgfConverter/Renderers/USD/Models/UsdTimeSampledQuatfArray.cs new file mode 100644 index 00000000..46a3dce7 --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdTimeSampledQuatfArray.cs @@ -0,0 +1,60 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents time-sampled quaternion array for skeletal animation rotations. +/// USD format: quatf[] rotations.timeSamples = { 0: [(w, x, y, z), ...], 0.033: [...], ... } +/// Note: USD quaternions are stored as (real, i, j, k) = (w, x, y, z) +/// +public class UsdTimeSampledQuatfArray : UsdAttribute +{ + /// + /// Time samples mapping frame time (in seconds) to arrays of quaternions for all joints. + /// + public SortedDictionary> TimeSamples { get; set; } + + public UsdTimeSampledQuatfArray(string name, SortedDictionary> timeSamples) + : base(name, false) + { + TimeSamples = timeSamples; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + sb.AppendLine($"quatf[] {Name}.timeSamples = {{"); + + foreach (var kvp in TimeSamples) + { + sb.AppendIndent(indentLevel + 1); + sb.Append(kvp.Key.ToString("0", CultureInfo.InvariantCulture)); + sb.Append(": ["); + + bool first = true; + foreach (var q in kvp.Value) + { + if (!first) sb.Append(", "); + first = false; + // USD quaternion format: (real, i, j, k) = (w, x, y, z) + // Use fixed-point notation to avoid scientific notation + sb.AppendFormat(CultureInfo.InvariantCulture, + "({0:0.######}, {1:0.######}, {2:0.######}, {3:0.######})", + q.W, q.X, q.Y, q.Z); + } + + sb.AppendLine("],"); + } + + sb.AppendIndent(indentLevel); + sb.Append("}"); + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/Models/UsdToken.cs b/CgfConverter/Renderers/USD/Models/UsdToken.cs index b77c1d44..b9e0afea 100644 --- a/CgfConverter/Renderers/USD/Models/UsdToken.cs +++ b/CgfConverter/Renderers/USD/Models/UsdToken.cs @@ -52,4 +52,6 @@ private string FormatValue(T value) return value.ToString(); } } + + public override string ToString() => Serialize(0); } diff --git a/CgfConverter/Renderers/USD/Models/UsdTokenArray.cs b/CgfConverter/Renderers/USD/Models/UsdTokenArray.cs new file mode 100644 index 00000000..e37d14d8 --- /dev/null +++ b/CgfConverter/Renderers/USD/Models/UsdTokenArray.cs @@ -0,0 +1,41 @@ +using CgfConverter.Renderers.USD.Attributes; +using Extensions; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CgfConverter.Renderers.USD.Models; + +/// +/// Represents an array of tokens for USD. +/// Used for joint names in skeletons. +/// +public class UsdTokenArray : UsdAttribute +{ + public List Tokens { get; set; } + + public UsdTokenArray(string name, List tokens, bool isUniform = false) + : base(name, isUniform) + { + Tokens = tokens; + } + + public override string Serialize(int indentLevel) + { + var sb = new StringBuilder(); + sb.AppendIndent(indentLevel); + + if (IsUniform) + sb.Append("uniform "); + + sb.Append($"token[] {Name} = ["); + + // Each token is quoted + var quotedTokens = Tokens.Select(t => $"\"{t}\""); + sb.Append(string.Join(", ", quotedTokens)); + + sb.Append("]"); + + return sb.ToString(); + } +} diff --git a/CgfConverter/Renderers/USD/ShaderRulesEngine.cs b/CgfConverter/Renderers/USD/ShaderRulesEngine.cs new file mode 100644 index 00000000..30877f76 --- /dev/null +++ b/CgfConverter/Renderers/USD/ShaderRulesEngine.cs @@ -0,0 +1,218 @@ +using CgfConverter.Models.Materials; +using CgfConverter.Models.Shaders; +using CgfConverter.Utils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CgfConverter.Renderers.USD; + +/// +/// Interprets shader StringGenMask flags and generates material rules for USD export. +/// Converts CryEngine shader semantics into USD material node connections. +/// +public class ShaderRulesEngine +{ + private readonly TaggedLogger _log; + + public ShaderRulesEngine(TaggedLogger logger) + { + _log = logger; + } + + /// + /// Generate material rules from a material's shader definition and StringGenMask. + /// + /// The material to process + /// The shader definition (can be null if shader not found) + /// List of material rules to apply + public List GenerateRules(Material material, ShaderDefinition? shaderDef) + { + var rules = new List(); + + if (shaderDef == null) + { + _log.D($"No shader definition found for material {material.Name}, using default behavior"); + return rules; + } + + if (string.IsNullOrEmpty(material.StringGenMask)) + { + _log.D($"Material {material.Name} has no StringGenMask, using default behavior"); + return rules; + } + + // Parse StringGenMask into individual flags (split by '%', filter empty) + var flags = material.StringGenMask + .Split('%', StringSplitOptions.RemoveEmptyEntries) + .Select(f => $"%{f.Trim()}") // Re-add % prefix + .ToList(); + + _log.D($"Processing {flags.Count} shader flags for material {material.Name}"); + + foreach (var flag in flags) + { + var property = shaderDef.GetProperty(flag); + if (property == null) + { + _log.D($"Unknown shader property: {flag} (shader: {shaderDef.ShaderName})"); + continue; + } + + var rule = CreateRuleFromProperty(property); + if (rule != null) + { + rules.Add(rule); + _log.D($" {flag}: {rule.Description}"); + } + } + + return rules; + } + + /// + /// Create a material rule from a shader property definition. + /// + private MaterialRule? CreateRuleFromProperty(ShaderProperty property) + { + var rule = new MaterialRule + { + PropertyName = property.Name, + Description = property.Description ?? property.Property ?? property.Name + }; + + // Map property names to specific rules + switch (property.Name.ToUpperInvariant()) + { + // Alpha channel routing + case "%ALPHAGLOW": + rule.Type = RuleType.ChannelRouting; + rule.SourceTexture = "Diffuse"; + rule.SourceChannel = "alpha"; + rule.TargetInput = "emissiveColor"; + break; + + case "%GLOSS_DIFFUSEALPHA": + rule.Type = RuleType.ChannelRouting; + rule.SourceTexture = "Diffuse"; + rule.SourceChannel = "alpha"; + rule.TargetInput = "specular"; // Specular mask + break; + + case "%SPECULARPOW_GLOSSALPHA": + rule.Type = RuleType.ChannelRouting; + rule.SourceTexture = "Specular"; + rule.SourceChannel = "alpha"; + rule.TargetInput = "roughness"; // Gloss map (invert for roughness) + break; + + // Texture enables + case "%ENVIRONMENT_MAP": + rule.Type = RuleType.TextureEnable; + rule.SourceTexture = "Environment"; + rule.TargetInput = "environment"; + break; + + case "%GLOSS_MAP": + rule.Type = RuleType.TextureEnable; + rule.SourceTexture = "Gloss"; + rule.TargetInput = "roughness"; + break; + + case "%DETAIL_BUMP_MAPPING": + case "%DETAIL_TEXTURE_IS_SET": + rule.Type = RuleType.TextureEnable; + rule.SourceTexture = "Detail"; + rule.TargetInput = "detail"; + break; + + // Vertex colors + case "%VERTCOLORS": + rule.Type = RuleType.VertexColors; + break; + + // Properties we can ignore for USD export + case "%ANISO_SPECULAR": + case "%OFFSETBUMPMAPPING": + case "%PARALLAX_OCCLUSION_MAPPING": + case "%DISPLACEMENT_MAPPING": + case "%PHONG_TESSELLATION": + case "%PN_TESSELLATION": + case "%TESSELLATION": + case "%DECAL": + case "%BLENDLAYER": + case "%DIRTLAYER": + case "%ALPHAMASK_DETAILMAP": + case "%DETAIL_TEXTURE_IS_NORMALMAP": + rule.Type = RuleType.PropertyModifier; + rule.Metadata = new Dictionary { ["ignore"] = true }; + _log.D($"Ignoring unsupported property: {property.Name}"); + return null; // Don't add to rules list + + // Glass-specific + case "%DIRT_MAP": + case "%TINT_MAP": + case "%TINT_COLOR_MAP": + case "%BLUR_REFRACTION": + case "%DEPTH_FOG": + case "%UNLIT": + case "%BILINEAR_FP16": + rule.Type = RuleType.PropertyModifier; + rule.Metadata = new Dictionary { ["shader_specific"] = true }; + _log.D($"Skipping shader-specific property: {property.Name}"); + return null; + + // Unknown property + default: + _log.D($"Unhandled shader property: {property.Name}"); + rule.Type = RuleType.Unknown; + return null; + } + + return rule; + } + + /// + /// Check if a rule type should prevent default alpha-to-opacity connection. + /// + public bool ShouldOverrideDefaultAlphaConnection(List rules) + { + // If %ALPHAGLOW is present, don't connect diffuse alpha to opacity + return rules.Any(r => r.PropertyName == "%ALPHAGLOW"); + } + + /// + /// Get the target input for a texture channel based on active rules. + /// + /// Active material rules + /// Texture map type (e.g., "Diffuse", "Specular") + /// Channel name (e.g., "alpha", "rgb") + /// Target USD input name, or null if no rule applies + public string? GetChannelTarget(List rules, string textureType, string channel) + { + var rule = rules.FirstOrDefault(r => + r.Type == RuleType.ChannelRouting && + r.SourceTexture?.Equals(textureType, StringComparison.OrdinalIgnoreCase) == true && + r.SourceChannel?.Equals(channel, StringComparison.OrdinalIgnoreCase) == true); + + return rule?.TargetInput; + } + + /// + /// Check if a texture type is enabled by shader rules. + /// + public bool IsTextureEnabled(List rules, string textureType) + { + return rules.Any(r => + r.Type == RuleType.TextureEnable && + r.SourceTexture?.Equals(textureType, StringComparison.OrdinalIgnoreCase) == true); + } + + /// + /// Check if vertex colors should be used based on shader rules. + /// + public bool UseVertexColors(List rules) + { + return rules.Any(r => r.Type == RuleType.VertexColors); + } +} diff --git a/CgfConverter/Renderers/USD/UsdRenderer.Animation.cs b/CgfConverter/Renderers/USD/UsdRenderer.Animation.cs new file mode 100644 index 00000000..6e894c86 --- /dev/null +++ b/CgfConverter/Renderers/USD/UsdRenderer.Animation.cs @@ -0,0 +1,1250 @@ +using CgfConverter.CryEngineCore; +using CgfConverter.Models; +using CgfConverter.Models.Structs; +using CgfConverter.Renderers.USD.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; + +namespace CgfConverter.Renderers.USD; + +/// +/// UsdRenderer partial class - Skeletal animation support +/// +public partial class UsdRenderer +{ + /// + /// Creates animation prims from CryEngine animation data. + /// Returns a list of SkelAnimation prims to be added to the scene. + /// Also updates the USD header with animation timeline info. + /// + private List CreateAnimations(Dictionary controllerIdToJointPath, UsdHeader header) + { + var animations = new List(); + int maxEndFrame = 0; + + // Process DBA animations (ChunkController_905 for non-Ivo, ChunkIvoDBAData for Ivo) + if (_cryData.Animations is not null && _cryData.Animations.Count > 0) + { + foreach (var animModel in _cryData.Animations) + { + // Try standard DBA format first (ChunkController_905) + var animChunks = animModel.ChunkMap.Values.OfType().ToList(); + + foreach (var animChunk in animChunks) + { + if (animChunk.Animations is null) + continue; + + // Get stripped names for cleaner animation names + var names = StripCommonParentPaths( + animChunk.Animations.Select(x => Path.ChangeExtension(x.Name, null)).ToList()); + + foreach (var (anim, name) in animChunk.Animations.Zip(names)) + { + var (skelAnim, endFrame) = CreateSkelAnimation(anim, animChunk, controllerIdToJointPath, name); + if (skelAnim is not null) + { + animations.Add(skelAnim); + maxEndFrame = Math.Max(maxEndFrame, endFrame); + Log.D($"Created DBA animation: {name} ({endFrame} frames)"); + } + } + } + + // Try Ivo DBA format (ChunkIvoDBAData + ChunkIvoDBAMetadata) + var ivoDbaData = animModel.ChunkMap.Values.OfType().FirstOrDefault(); + var ivoDbaMetadata = animModel.ChunkMap.Values.OfType().FirstOrDefault(); + + if (ivoDbaData is not null && ivoDbaData.AnimationBlocks.Count > 0) + { + Log.D($"Found Ivo DBA with {ivoDbaData.AnimationBlocks.Count} animation blocks"); + + // Get animation names from metadata if available + var animNames = ivoDbaMetadata?.AnimPaths ?? []; + var animEntries = ivoDbaMetadata?.Entries ?? []; + + // Log animation names from metadata + if (animNames.Count > 0) + { + Log.D($"Ivo DBA metadata has {animNames.Count} animation paths:"); + for (int j = 0; j < animNames.Count; j++) + { + Log.D($" [{j}] {animNames[j]}"); + } + } + else + Log.D("Ivo DBA has no metadata animation paths"); + + int ivoSuccessCount = 0; + int ivoFailCount = 0; + + for (int i = 0; i < ivoDbaData.AnimationBlocks.Count; i++) + { + var block = ivoDbaData.AnimationBlocks[i]; + var animName = i < animNames.Count + ? Path.GetFileNameWithoutExtension(animNames[i]) + : $"animation_{i}"; + var metaEntry = i < animEntries.Count ? animEntries[i] : (IvoDBAMetaEntry?)null; + + Log.D($"Processing Ivo DBA animation block {i}: '{animName}'"); + + var (skelAnim, endFrame) = CreateSkelAnimationFromIvoDBA( + block, animName, metaEntry, controllerIdToJointPath); + + if (skelAnim is not null) + { + animations.Add(skelAnim); + maxEndFrame = Math.Max(maxEndFrame, endFrame); + Log.D($"SUCCESS: Created Ivo DBA animation: {animName} ({endFrame} frames)"); + ivoSuccessCount++; + } + else + { + Log.D($"FAILED: Could not create Ivo DBA animation: {animName}"); + ivoFailCount++; + } + } + + Log.D($"Ivo DBA summary: {ivoSuccessCount} succeeded, {ivoFailCount} failed out of {ivoDbaData.AnimationBlocks.Count} blocks"); + } + } + } + + // Process CAF animations + if (_cryData.CafAnimations is not null && _cryData.CafAnimations.Count > 0) + { + foreach (var cafAnim in _cryData.CafAnimations) + { + var (skelAnim, endFrame) = CreateSkelAnimationFromCaf(cafAnim, controllerIdToJointPath); + if (skelAnim is not null) + { + animations.Add(skelAnim); + maxEndFrame = Math.Max(maxEndFrame, endFrame); + Log.D($"Created CAF animation: {cafAnim.Name} ({endFrame} frames)"); + } + } + } + + if (animations.Count == 0) + { + Log.D("No animations found in CryEngine data"); + return animations; + } + + // Set header timeline info if we have animations + header.TimeCodesPerSecond = 30; + header.StartTimeCode = 0; + header.EndTimeCode = maxEndFrame; + + Log.D($"Created {animations.Count} animation(s)"); + return animations; + } + + /// + /// Creates a SkelAnimation prim from a CafAnimation. + /// Handles both absolute and additive animations - additive animations are converted + /// to absolute by applying deltas to the rest pose. + /// + private (UsdSkelAnimation? skelAnim, int endFrame) CreateSkelAnimationFromCaf( + CafAnimation cafAnim, + Dictionary controllerIdToJointPath) + { + // Build rest pose mappings + var jointPathToRestTranslation = BuildRestTranslationMapping(); + var jointPathToRestRotation = BuildRestRotationMapping(); + + Log.D($"CAF[{cafAnim.Name}]: Rest translation mapping has {jointPathToRestTranslation.Count} entries"); + if (jointPathToRestTranslation.Count > 0) + { + var first = jointPathToRestTranslation.First(); + Log.D($"CAF[{cafAnim.Name}]: First rest translation: '{first.Key}' = {first.Value}"); + } + + bool isAdditive = cafAnim.IsAdditive; + if (isAdditive) + Log.D($"CAF[{cafAnim.Name}]: Converting additive animation to absolute"); + + // Map controller IDs to joint paths + var animatedJoints = new List(); + var tracksByJointPath = new Dictionary(); + + foreach (var (controllerId, track) in cafAnim.BoneTracks) + { + // Try to find joint path by controller ID + if (!controllerIdToJointPath.TryGetValue(controllerId, out var jointPath)) + { + // Try using bone name from CAF's bone name list + if (cafAnim.ControllerIdToBoneName.TryGetValue(controllerId, out var boneName)) + { + // Search for matching joint path by bone name + jointPath = controllerIdToJointPath.Values + .FirstOrDefault(jp => jp.EndsWith("/" + boneName) || jp == boneName); + } + + if (jointPath is null) + { + Log.D($"CAF[{cafAnim.Name}]: Controller 0x{controllerId:X08} not found in skeleton"); + continue; + } + } + + animatedJoints.Add(jointPath); + tracksByJointPath[jointPath] = track; + } + + if (animatedJoints.Count == 0) + { + Log.W($"CAF[{cafAnim.Name}]: No valid bone tracks found"); + return (null, 0); + } + + // Debug: Check how many animated joints have rest translations + int foundRestCount = animatedJoints.Count(jp => jointPathToRestTranslation.ContainsKey(jp)); + Log.D($"CAF[{cafAnim.Name}]: {animatedJoints.Count} animated joints, {foundRestCount} have rest translations"); + if (animatedJoints.Count > 0 && foundRestCount == 0) + { + Log.W($"CAF[{cafAnim.Name}]: No rest translations found for animated joints!"); + Log.D($"CAF[{cafAnim.Name}]: First animated joint: '{animatedJoints[0]}'"); + if (jointPathToRestTranslation.Count > 0) + Log.D($"CAF[{cafAnim.Name}]: First rest key: '{jointPathToRestTranslation.Keys.First()}'"); + } + + // Calculate frame range + int startFrame = cafAnim.StartFrame; + int endFrame = cafAnim.EndFrame; + + // If timing chunk didn't have valid range, calculate from keyframes + if (endFrame <= startFrame) + { + foreach (var track in tracksByJointPath.Values) + { + if (track.KeyTimes.Count > 0) + { + endFrame = Math.Max(endFrame, (int)track.KeyTimes.Max()); + } + } + } + + // Normalize frame range + int durationFrames = endFrame - startFrame; + if (durationFrames <= 0) + durationFrames = 1; + + // Collect all unique frame numbers + var allFrames = new SortedSet(); + foreach (var track in tracksByJointPath.Values) + { + foreach (var t in track.KeyTimes) + { + int frame = (int)t - startFrame; + if (frame >= 0 && frame <= durationFrames) + allFrames.Add(frame); + } + } + + // Ensure we have at least frame 0 and end frame + allFrames.Add(0); + allFrames.Add(durationFrames); + + // Build time-sampled arrays + var translationSamples = new SortedDictionary>(); + var rotationSamples = new SortedDictionary>(); + var scaleSamples = new SortedDictionary>(); + + foreach (var frame in allFrames) + { + var translations = new List(); + var rotations = new List(); + var scales = new List(); + + foreach (var jointPath in animatedJoints) + { + var track = tracksByJointPath[jointPath]; + + // Get rest pose for this joint + bool foundRest = jointPathToRestTranslation.TryGetValue(jointPath, out var restT); + var restTranslation = foundRest ? restT : Vector3.Zero; + var restRotation = jointPathToRestRotation.TryGetValue(jointPath, out var restR) + ? restR + : Quaternion.Identity; + + // Sample position and rotation at this frame + // CAF positions are absolute local transforms, not deltas from rest pose + // For bones WITH position animation: use animation data directly + // For bones WITHOUT position animation: SampleCafPosition returns restTranslation + Vector3 position = SampleCafPosition(track, frame + startFrame, restTranslation); + Quaternion rotation = SampleCafRotation(track, frame + startFrame); + + if (isAdditive) + { + // Additive animations: rotation is also a delta from rest + rotation = restRotation * rotation; + } + + translations.Add(position); + rotations.Add(rotation); + + // CAF animations don't typically have scale + scales.Add(Vector3.One); + } + + translationSamples[frame] = translations; + rotationSamples[frame] = rotations; + scaleSamples[frame] = scales; + } + + // Create the SkelAnimation prim + var cleanName = CleanPathString(cafAnim.Name); + var skelAnim = new UsdSkelAnimation(cleanName); + + skelAnim.Attributes.Add(new UsdTokenArray("joints", animatedJoints, isUniform: true)); + skelAnim.Attributes.Add(new UsdTimeSampledFloat3Array("translations", translationSamples)); + skelAnim.Attributes.Add(new UsdTimeSampledQuatfArray("rotations", rotationSamples)); + skelAnim.Attributes.Add(new UsdTimeSampledHalf3Array("scales", scaleSamples)); + + return (skelAnim, durationFrames); + } + + /// + /// Creates a SkelAnimation prim from an Ivo DBA animation block. + /// Ivo DBA uses bone hashes (CRC32) instead of controller IDs. + /// + /// Ivo animations may store values as deltas from a reference pose (StartPosition/StartRotation + /// in metadata), or as absolute values. The metadata's StartPosition/StartRotation appears to + /// be for the root bone reference. + /// + private (UsdSkelAnimation? skelAnim, int endFrame) CreateSkelAnimationFromIvoDBA( + IvoAnimationBlock block, + string animName, + IvoDBAMetaEntry? metaEntry, + Dictionary controllerIdToJointPath) + { + // Get reference pose from metadata (for root bone) and skeleton rest pose (for other bones) + var refPosition = metaEntry?.StartPosition ?? Vector3.Zero; + var refRotation = metaEntry?.StartRotation ?? Quaternion.Identity; + + Log.D($"IvoDBA[{animName}]: Reference pose from metadata: pos=({refPosition.X:F3}, {refPosition.Y:F3}, {refPosition.Z:F3}), rot=({refRotation.X:F3}, {refRotation.Y:F3}, {refRotation.Z:F3}, {refRotation.W:F3})"); + + // Build rest pose mappings for bones without animation + var jointPathToRestTranslation = BuildRestTranslationMapping(); + var jointPathToRestRotation = BuildRestRotationMapping(); + + // Map bone hashes to joint paths + var animatedJoints = new List(); + var tracksByJointPath = new Dictionary? rotations, List? rotTimes, + List? positions, List? posTimes)>(); + + int matchedBones = 0; + int unmatchedBones = 0; + int bonesWithData = 0; + var unmatchedHashes = new List(); + + Log.D($"IvoDBA[{animName}]: Processing {block.BoneHashes.Length} bone hashes, skeleton has {controllerIdToJointPath.Count} controller IDs"); + + foreach (var boneHash in block.BoneHashes) + { + // Try to find joint path by bone hash (same as controller ID for Ivo) + if (!controllerIdToJointPath.TryGetValue(boneHash, out var jointPath)) + { + unmatchedBones++; + unmatchedHashes.Add(boneHash); + continue; + } + + matchedBones++; + + block.Rotations.TryGetValue(boneHash, out var rotations); + block.RotationTimes.TryGetValue(boneHash, out var rotTimes); + block.Positions.TryGetValue(boneHash, out var positions); + block.PositionTimes.TryGetValue(boneHash, out var posTimes); + + // Only add if there's actual animation data + if ((rotations is not null && rotations.Count > 0) || + (positions is not null && positions.Count > 0)) + { + bonesWithData++; + animatedJoints.Add(jointPath); + tracksByJointPath[jointPath] = (rotations, rotTimes, positions, posTimes); + } + } + + Log.D($"IvoDBA[{animName}]: Matched {matchedBones}/{block.BoneHashes.Length} bones, {bonesWithData} have animation data, {unmatchedBones} unmatched"); + + if (unmatchedBones > 0 && unmatchedBones <= 10) + { + // Log individual unmatched hashes if there aren't too many + foreach (var hash in unmatchedHashes) + { + Log.D($"IvoDBA[{animName}]: Unmatched bone hash 0x{hash:X08}"); + } + } + else if (unmatchedBones > 10) + { + // Just log the first few if there are many + Log.D($"IvoDBA[{animName}]: First 5 unmatched hashes: {string.Join(", ", unmatchedHashes.Take(5).Select(h => $"0x{h:X08}"))}"); + } + + if (animatedJoints.Count == 0) + { + Log.W($"IvoDBA[{animName}]: No valid bone tracks found (matched={matchedBones}, withData={bonesWithData}, unmatched={unmatchedBones})"); + return (null, 0); + } + + // Calculate frame range from all tracks + var allFrames = new SortedSet(); + foreach (var (_, (_, rotTimes, _, posTimes)) in tracksByJointPath) + { + if (rotTimes is not null) + { + foreach (var t in rotTimes) + allFrames.Add((int)t); + } + if (posTimes is not null) + { + foreach (var t in posTimes) + allFrames.Add((int)t); + } + } + + // Ensure at least one frame + if (allFrames.Count == 0) + allFrames.Add(0); + + int endFrame = allFrames.Count > 0 ? allFrames.Max() : 0; + + // Build time-sampled arrays + var translationSamples = new SortedDictionary>(); + var rotationSamples = new SortedDictionary>(); + var scaleSamples = new SortedDictionary>(); + + // Log first bone's values to diagnose if Ivo stores absolute or delta values + bool loggedFirstBone = false; + + foreach (var frame in allFrames) + { + var translations = new List(); + var rotations = new List(); + var scales = new List(); + + foreach (var jointPath in animatedJoints) + { + var (trackRots, rotTimes, trackPos, posTimes) = tracksByJointPath[jointPath]; + + // Get rest pose for this joint + var restTranslation = jointPathToRestTranslation.TryGetValue(jointPath, out var restT) + ? restT + : Vector3.Zero; + var restRotation = jointPathToRestRotation.TryGetValue(jointPath, out var restR) + ? restR + : Quaternion.Identity; + + // Sample position - Ivo DBA stores position DELTAS (not absolute) + // Evidence: laser cannon barrel animation shows small delta values (~0.045) + // compared to rest position (~0.87). Bolt in p4ar also confirms this. + Vector3 position; + if (trackPos is not null && trackPos.Count > 0) + { + var animDelta = SampleIvoPositionDelta(trackPos, posTimes, frame); + position = restTranslation + animDelta; + } + else + { + // No position animation - use rest translation for skeleton structure + position = restTranslation; + } + + // Sample rotation - Ivo DBA stores ABSOLUTE rotations (not deltas) + // Evidence: magAttach in p4ar rifle has rest rotation (0.1248, 0, 0, 0.9922), + // and animation frames 0-11 store the SAME value, not identity. + // If it were delta, identity would mean "no change". Storing rest value = absolute. + Quaternion rotation; + if (trackRots is not null && trackRots.Count > 0) + { + // Use animation value directly - it's the absolute local rotation + rotation = Quaternion.Normalize(SampleIvoRotationDelta(trackRots, rotTimes, frame)); + } + else + { + // No rotation animation - use rest rotation for skeleton structure + rotation = restRotation; + } + + translations.Add(position); + rotations.Add(rotation); + scales.Add(Vector3.One); + } + + translationSamples[frame] = translations; + rotationSamples[frame] = rotations; + scaleSamples[frame] = scales; + } + + // Create the SkelAnimation prim + var cleanName = CleanPathString(animName); + var skelAnim = new UsdSkelAnimation(cleanName); + + skelAnim.Attributes.Add(new UsdTokenArray("joints", animatedJoints, isUniform: true)); + skelAnim.Attributes.Add(new UsdTimeSampledFloat3Array("translations", translationSamples)); + skelAnim.Attributes.Add(new UsdTimeSampledQuatfArray("rotations", rotationSamples)); + skelAnim.Attributes.Add(new UsdTimeSampledHalf3Array("scales", scaleSamples)); + + Log.D($"IvoDBA[{animName}]: Created animation with {animatedJoints.Count} joints, {allFrames.Count} frames"); + return (skelAnim, endFrame); + } + + /// + /// Samples position delta from Ivo animation data at a given frame. + /// Returns the animation delta value (NOT absolute position). + /// Caller must add rest translation to convert to absolute: absolute = rest + delta + /// + private static Vector3 SampleIvoPositionDelta(List positions, List? keyTimes, int frame) + { + if (positions.Count == 0) + return Vector3.Zero; + + if (positions.Count == 1 || keyTimes is null || keyTimes.Count == 0) + return positions[0]; + + // Find surrounding keyframes + int i = 0; + while (i < keyTimes.Count - 1 && keyTimes[i + 1] <= frame) + i++; + + if (i >= positions.Count) + return positions[^1]; + + if (i == keyTimes.Count - 1 || keyTimes[i] >= frame) + return i < positions.Count ? positions[i] : positions[^1]; + + // Linear interpolation + float t0 = keyTimes[i]; + float t1 = keyTimes[i + 1]; + float alpha = (frame - t0) / (t1 - t0); + + int i1 = Math.Min(i + 1, positions.Count - 1); + return Vector3.Lerp(positions[i], positions[i1], alpha); + } + + /// + /// Samples rotation delta from Ivo animation data at a given frame. + /// Returns the animation delta value (NOT absolute rotation). + /// Caller must multiply by rest rotation to convert to absolute: absolute = rest * delta + /// + private static Quaternion SampleIvoRotationDelta(List rotations, List? keyTimes, int frame) + { + if (rotations.Count == 0) + return Quaternion.Identity; + + if (rotations.Count == 1 || keyTimes is null || keyTimes.Count == 0) + return rotations[0]; + + // Find surrounding keyframes + int i = 0; + while (i < keyTimes.Count - 1 && keyTimes[i + 1] <= frame) + i++; + + if (i >= rotations.Count) + return rotations[^1]; + + if (i == keyTimes.Count - 1 || keyTimes[i] >= frame) + return i < rotations.Count ? rotations[i] : rotations[^1]; + + // Spherical linear interpolation + float t0 = keyTimes[i]; + float t1 = keyTimes[i + 1]; + float alpha = (frame - t0) / (t1 - t0); + + int i1 = Math.Min(i + 1, rotations.Count - 1); + return Quaternion.Slerp(rotations[i], rotations[i1], alpha); + } + + /// + /// Builds a mapping from joint path to rest pose translation. + /// Used for bones without position animation to maintain their skeleton structure. + /// + private Dictionary BuildRestTranslationMapping() + { + var mapping = new Dictionary(); + + if (_bonePathMap is null) + { + Log.W("BuildRestTranslationMapping: _bonePathMap is null!"); + return mapping; + } + + Log.D($"BuildRestTranslationMapping: Processing {_bonePathMap.Count} bones"); + int zeroCount = 0; + + foreach (var (bone, jointPath) in _bonePathMap) + { + // Compute rest translation: local position of this bone relative to parent + Matrix4x4 restMatrix; + + if (bone.ParentBone == null) + { + // Root bone: local = world, invert BindPoseMatrix to get boneToWorld + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var boneToWorld)) + { + restMatrix = boneToWorld; + } + else + { + restMatrix = Matrix4x4.Identity; + } + } + else + { + // Child bone: compute local transform relative to parent + // localTransform = parentWorldToBone * childBoneToWorld + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var childBoneToWorld)) + { + restMatrix = bone.ParentBone.BindPoseMatrix * childBoneToWorld; + } + else + { + restMatrix = Matrix4x4.Identity; + } + } + + // Extract translation from the rest matrix + // CryEngine matrices store translation in column 4 (M14, M24, M34), not row 4. + var translation = new Vector3(restMatrix.M14, restMatrix.M24, restMatrix.M34); + + // Sanity check: if translation values are garbage (very large), use zero + // This can happen if the skeleton matrices are corrupted or incompatible + if (Math.Abs(translation.X) > 1e6 || Math.Abs(translation.Y) > 1e6 || Math.Abs(translation.Z) > 1e6 || + float.IsNaN(translation.X) || float.IsNaN(translation.Y) || float.IsNaN(translation.Z) || + float.IsInfinity(translation.X) || float.IsInfinity(translation.Y) || float.IsInfinity(translation.Z)) + { + Log.W($" Bone '{bone.BoneName}': garbage translation {translation}, using zero"); + translation = Vector3.Zero; + } + + if (translation == Vector3.Zero) + zeroCount++; + + mapping[jointPath] = translation; + } + + Log.D($"BuildRestTranslationMapping: {zeroCount}/{mapping.Count} bones have zero translation"); + return mapping; + } + + /// + /// Builds a mapping from joint path to rest pose rotation. + /// Used for converting additive animations to absolute. + /// + private Dictionary BuildRestRotationMapping() + { + var mapping = new Dictionary(); + + if (_bonePathMap is null) + return mapping; + + foreach (var (bone, jointPath) in _bonePathMap) + { + // Compute rest rotation: local rotation of this bone relative to parent + Matrix4x4 restMatrix; + + if (bone.ParentBone == null) + { + // Root bone: local = world, invert BindPoseMatrix to get boneToWorld + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var boneToWorld)) + { + restMatrix = boneToWorld; + } + else + { + restMatrix = Matrix4x4.Identity; + } + } + else + { + // Child bone: compute local transform relative to parent + // localTransform = parentWorldToBone * childBoneToWorld + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var childBoneToWorld)) + { + restMatrix = bone.ParentBone.BindPoseMatrix * childBoneToWorld; + } + else + { + restMatrix = Matrix4x4.Identity; + } + } + + // Extract rotation from the rest matrix + if (Matrix4x4.Decompose(restMatrix, out _, out var rotation, out _)) + { + mapping[jointPath] = rotation; + + // Debug: compare non-transposed vs transposed rest rotation for turret_arm + if (bone.ControllerID == 0x9384FC75) + { + var transposedMatrix = Matrix4x4.Transpose(restMatrix); + if (Matrix4x4.Decompose(transposedMatrix, out _, out var transposedRot, out _)) + { + Log.I($"turret_arm rest rotation comparison:"); + Log.I($" Non-transposed (animation): ({rotation.X:F6}, {rotation.Y:F6}, {rotation.Z:F6}, {rotation.W:F6})"); + Log.I($" Transposed (skeleton): ({transposedRot.X:F6}, {transposedRot.Y:F6}, {transposedRot.Z:F6}, {transposedRot.W:F6})"); + Log.I($" Conjugate of non-transposed: ({-rotation.X:F6}, {-rotation.Y:F6}, {-rotation.Z:F6}, {rotation.W:F6})"); + } + } + } + else + { + mapping[jointPath] = Quaternion.Identity; + } + } + + return mapping; + } + + /// + /// Samples position from a CAF bone track at a given frame. + /// If no position keyframes exist, uses the rest translation to maintain skeleton structure. + /// + /// The bone animation track. + /// The frame number to sample. + /// The rest pose translation to use when no position animation exists. + private static Vector3 SampleCafPosition(BoneTrack track, float frame, Vector3 restTranslation) + { + // If no position animation, use rest pose translation to maintain skeleton structure + if (track.Positions.Count == 0) + return restTranslation; + + var keyTimes = track.PositionKeyTimes; + + if (track.Positions.Count == 1 || keyTimes.Count == 0) + { + var pos = track.Positions[0]; + return IsValidPosition(pos) ? pos : restTranslation; + } + + // Find surrounding keyframes using position key times + int i = 0; + while (i < keyTimes.Count - 1 && keyTimes[i + 1] <= frame) + i++; + + if (i >= track.Positions.Count) + { + var pos = track.Positions[^1]; + return IsValidPosition(pos) ? pos : restTranslation; + } + + if (i == keyTimes.Count - 1 || keyTimes[i] >= frame) + { + var pos = i < track.Positions.Count ? track.Positions[i] : track.Positions[^1]; + return IsValidPosition(pos) ? pos : restTranslation; + } + + // Linear interpolation + float t0 = keyTimes[i]; + float t1 = keyTimes[i + 1]; + float alpha = (frame - t0) / (t1 - t0); + + int i1 = Math.Min(i + 1, track.Positions.Count - 1); + var p0 = track.Positions[i]; + var p1 = track.Positions[i1]; + + // If either position is invalid, fall back to rest translation + if (!IsValidPosition(p0) || !IsValidPosition(p1)) + return restTranslation; + + return Vector3.Lerp(p0, p1, alpha); + } + + /// + /// Checks if a position vector has valid (non-garbage) values. + /// Positions in character animations should be small (within a few meters). + /// + private static bool IsValidPosition(Vector3 pos) + { + const float maxValidPosition = 1e6f; + return Math.Abs(pos.X) < maxValidPosition && + Math.Abs(pos.Y) < maxValidPosition && + Math.Abs(pos.Z) < maxValidPosition && + !float.IsNaN(pos.X) && !float.IsNaN(pos.Y) && !float.IsNaN(pos.Z) && + !float.IsInfinity(pos.X) && !float.IsInfinity(pos.Y) && !float.IsInfinity(pos.Z); + } + + /// + /// Samples rotation from a CAF bone track at a given frame. + /// + private static Quaternion SampleCafRotation(BoneTrack track, float frame) + { + if (track.Rotations.Count == 0) + return Quaternion.Identity; + + var keyTimes = track.RotationKeyTimes; + + if (track.Rotations.Count == 1 || keyTimes.Count == 0) + return track.Rotations[0]; + + // Find surrounding keyframes using rotation key times + int i = 0; + while (i < keyTimes.Count - 1 && keyTimes[i + 1] <= frame) + i++; + + if (i >= track.Rotations.Count) + return track.Rotations[^1]; + + if (i == keyTimes.Count - 1 || keyTimes[i] >= frame) + return i < track.Rotations.Count ? track.Rotations[i] : track.Rotations[^1]; + + // Spherical linear interpolation + float t0 = keyTimes[i]; + float t1 = keyTimes[i + 1]; + float alpha = (frame - t0) / (t1 - t0); + + int i1 = Math.Min(i + 1, track.Rotations.Count - 1); + return Quaternion.Slerp(track.Rotations[i], track.Rotations[i1], alpha); + } + + /// + /// Creates a single SkelAnimation prim from a CryEngine Animation struct. + /// Handles both absolute and additive animations - additive animations are converted + /// to absolute by applying deltas to the rest pose. + /// Returns the animation prim and the end frame number. + /// + private (UsdSkelAnimation? skelAnim, int endFrame) CreateSkelAnimation( + ChunkController_905.Animation anim, + ChunkController_905 animChunk, + Dictionary controllerIdToJointPath, + string animName) + { + // Check if this is an additive animation + bool isAdditive = (anim.MotionParams.AssetFlags & ChunkController_905.AssetFlags.Additive) != 0; + if (isAdditive) + Log.D($"DBA[{animName}]: Converting additive animation to absolute"); + + // Build rest pose mappings - needed for: + // 1. Additive animation conversion + // 2. Bones without position animation (must use rest translation to maintain skeleton structure) + var jointPathToRestTranslation = BuildRestTranslationMapping(); + var jointPathToRestRotation = isAdditive ? BuildRestRotationMapping() : null; + + // Collect all joint paths that have animation in this clip + var animatedJoints = new List(); + var controllersByJointPath = new Dictionary(); + + foreach (var controller in anim.Controllers) + { + if (!controllerIdToJointPath.TryGetValue(controller.ControllerID, out var jointPath)) + { + Log.D($"Animation[{animName}]: Controller 0x{controller.ControllerID:X08} not found in skeleton"); + continue; + } + + animatedJoints.Add(jointPath); + controllersByJointPath[jointPath] = controller; + } + + if (animatedJoints.Count == 0) + { + Log.W($"Animation[{animName}]: No valid controllers found"); + return (null, 0); + } + + // Collect all unique frame numbers across all tracks + // CryEngine keyTimes are already in frames (at 30fps) + var allFrames = new SortedSet(); + foreach (var controller in anim.Controllers) + { + if (controller.HasPosTrack && animChunk.KeyTimes is not null) + { + var times = animChunk.KeyTimes[controller.PosKeyTimeTrack]; + var startTime = times.Count > 0 ? (int)times[0] : 0; + foreach (var t in times) + allFrames.Add((int)t - startTime); + } + if (controller.HasRotTrack && animChunk.KeyTimes is not null) + { + var times = animChunk.KeyTimes[controller.RotKeyTimeTrack]; + var startTime = times.Count > 0 ? (int)times[0] : 0; + foreach (var t in times) + allFrames.Add((int)t - startTime); + } + } + + if (allFrames.Count == 0) + { + Log.W($"Animation[{animName}]: No keyframes found"); + return (null, 0); + } + + int endFrame = allFrames.Max; + + // Build time-sampled arrays using frame numbers + // USD expects all joints' values at each time sample + // USD SkelAnimation requires translations, rotations, AND scales to be valid + var translationSamples = new SortedDictionary>(); + var rotationSamples = new SortedDictionary>(); + var scaleSamples = new SortedDictionary>(); + + foreach (var frame in allFrames) + { + var translations = new List(); + var rotations = new List(); + var scales = new List(); + + foreach (var jointPath in animatedJoints) + { + var controller = controllersByJointPath[jointPath]; + + // Get rest translation for this joint (used when no position animation) + var restTranslation = jointPathToRestTranslation.TryGetValue(jointPath, out var restT) + ? restT + : Vector3.Zero; + + // Get translation for this joint at this frame + // If no position track exists, use rest translation to maintain skeleton structure + Vector3 translation = restTranslation; + if (controller.HasPosTrack && animChunk.KeyTimes is not null && animChunk.KeyPositions is not null) + { + translation = SamplePositionAtFrame( + animChunk.KeyTimes[controller.PosKeyTimeTrack], + animChunk.KeyPositions[controller.PosTrack], + frame); + } + + // Get rotation for this joint at this frame + Quaternion rotation = Quaternion.Identity; + if (controller.HasRotTrack && animChunk.KeyTimes is not null && animChunk.KeyRotations is not null) + { + rotation = SampleRotationAtFrame( + animChunk.KeyTimes[controller.RotKeyTimeTrack], + animChunk.KeyRotations[controller.RotTrack], + frame); + } + + if (isAdditive) + { + // Convert additive deltas to absolute transforms: + // Additive rotation: absolute = rest * delta + // Additive translation: absolute = rest + delta (translation already has rest if no pos track) + var restRotation = jointPathToRestRotation!.TryGetValue(jointPath, out var restR) + ? restR + : Quaternion.Identity; + + rotation = restRotation * rotation; + // For additive with position track, add animation delta to rest + if (controller.HasPosTrack) + translation = restTranslation + translation; + } + + translations.Add(translation); + rotations.Add(rotation); + + // CryEngine animations typically don't have scale, use uniform scale + scales.Add(Vector3.One); + } + + translationSamples[frame] = translations; + rotationSamples[frame] = rotations; + scaleSamples[frame] = scales; + } + + // Create the SkelAnimation prim + var cleanName = CleanPathString(animName); + var skelAnim = new UsdSkelAnimation(cleanName); + + // Add joints array (token[]) + skelAnim.Attributes.Add(new UsdTokenArray("joints", animatedJoints, isUniform: true)); + + // Add time-sampled translations + skelAnim.Attributes.Add(new UsdTimeSampledFloat3Array("translations", translationSamples)); + + // Add time-sampled rotations + skelAnim.Attributes.Add(new UsdTimeSampledQuatfArray("rotations", rotationSamples)); + + // Add time-sampled scales (required by USD SkelAnimation spec, must be half3[]) + skelAnim.Attributes.Add(new UsdTimeSampledHalf3Array("scales", scaleSamples)); + + return (skelAnim, endFrame); + } + + /// + /// Samples position at a given frame using linear interpolation. + /// + private Vector3 SamplePositionAtFrame(List keyTimes, List keyPositions, int frame) + { + if (keyTimes.Count == 0 || keyPositions.Count == 0) + return Vector3.Zero; + + // Normalize times to frame numbers relative to start + float startTime = keyTimes[0]; + var normalizedFrames = keyTimes.Select(t => (int)(t - startTime)).ToList(); + + // Find surrounding keyframes + int i = 0; + while (i < normalizedFrames.Count - 1 && normalizedFrames[i + 1] <= frame) + i++; + + if (i >= keyPositions.Count) + return keyPositions[^1]; + + if (i == normalizedFrames.Count - 1 || normalizedFrames[i] >= frame) + return keyPositions[i]; + + // Linear interpolation + int f0 = normalizedFrames[i]; + int f1 = normalizedFrames[i + 1]; + float alpha = (float)(frame - f0) / (f1 - f0); + + return Vector3.Lerp(keyPositions[i], keyPositions[Math.Min(i + 1, keyPositions.Count - 1)], alpha); + } + + /// + /// Samples rotation at a given frame using spherical linear interpolation. + /// + private Quaternion SampleRotationAtFrame(List keyTimes, List keyRotations, int frame) + { + if (keyTimes.Count == 0 || keyRotations.Count == 0) + return Quaternion.Identity; + + // Normalize times to frame numbers relative to start + float startTime = keyTimes[0]; + var normalizedFrames = keyTimes.Select(t => (int)(t - startTime)).ToList(); + + // Find surrounding keyframes + int i = 0; + while (i < normalizedFrames.Count - 1 && normalizedFrames[i + 1] <= frame) + i++; + + if (i >= keyRotations.Count) + return keyRotations[^1]; + + if (i == normalizedFrames.Count - 1 || normalizedFrames[i] >= frame) + return keyRotations[i]; + + // Spherical linear interpolation + int f0 = normalizedFrames[i]; + int f1 = normalizedFrames[i + 1]; + float alpha = (float)(frame - f0) / (f1 - f0); + + return Quaternion.Slerp(keyRotations[i], keyRotations[Math.Min(i + 1, keyRotations.Count - 1)], alpha); + } + + /// + /// Strips common parent paths from animation names for cleaner display. + /// Based on GltfRendererUtilities.StripCommonParentPaths logic. + /// + private static List StripCommonParentPaths(List paths) + { + if (paths.Count == 0) + return paths; + + // Split all paths into components + var splitPaths = paths.Select(p => p.Replace('\\', '/').Split('/').ToList()).ToList(); + + // Find common prefix length + int commonPrefixLen = 0; + bool done = false; + while (!done) + { + string? commonPart = null; + foreach (var parts in splitPaths) + { + if (commonPrefixLen >= parts.Count) + { + done = true; + break; + } + + if (commonPart is null) + commonPart = parts[commonPrefixLen]; + else if (parts[commonPrefixLen] != commonPart) + { + done = true; + break; + } + } + if (!done) + commonPrefixLen++; + } + + // Return paths with common prefix stripped + return splitPaths + .Select(parts => string.Join("/", parts.Skip(commonPrefixLen))) + .ToList(); + } + + /// + /// Exports individual animation files for Blender NLA workflow. + /// Blender's USD importer only reads the single bound animation, so we export + /// each animation as a separate file with skeleton + that animation bound. + /// + /// Mapping from bone controller IDs to USD joint paths. + /// Ordered list of joint paths for skeleton. + /// Mapping from CompiledBone to joint path. + /// Number of animation files exported. + private int ExportAnimationFiles( + Dictionary controllerIdToJointPath, + List jointPaths, + Dictionary bonePathMap) + { + // Check if we have any animations (DBA or CAF) + bool hasDbaAnimations = _cryData.Animations is not null && _cryData.Animations.Count > 0; + bool hasCafAnimations = _cryData.CafAnimations is not null && _cryData.CafAnimations.Count > 0; + + if (!hasDbaAnimations && !hasCafAnimations) + return 0; + + int exportedCount = 0; + var baseFileName = Path.GetFileNameWithoutExtension(usdOutputFile.FullName); + var outputDir = usdOutputFile.DirectoryName ?? "."; + + // Collect all animations with their metadata + var allAnimations = new List<(string name, UsdSkelAnimation skelAnim, int endFrame)>(); + + // Process DBA animations (both standard ChunkController_905 and Ivo format) + if (hasDbaAnimations) + { + foreach (var animModel in _cryData.Animations!) + { + // Try standard DBA format (ChunkController_905) + var animChunks = animModel.ChunkMap.Values.OfType().ToList(); + + foreach (var animChunk in animChunks) + { + if (animChunk.Animations is null) + continue; + + var names = StripCommonParentPaths( + animChunk.Animations.Select(x => Path.ChangeExtension(x.Name, null)).ToList()); + + foreach (var (anim, name) in animChunk.Animations.Zip(names)) + { + var (skelAnim, endFrame) = CreateSkelAnimation(anim, animChunk, controllerIdToJointPath, name); + if (skelAnim is not null) + { + allAnimations.Add((name, skelAnim, endFrame)); + } + } + } + + // Try Ivo DBA format (ChunkIvoDBAData + ChunkIvoDBAMetadata) + var ivoDbaData = animModel.ChunkMap.Values.OfType().FirstOrDefault(); + var ivoDbaMetadata = animModel.ChunkMap.Values.OfType().FirstOrDefault(); + + if (ivoDbaData is not null && ivoDbaData.AnimationBlocks.Count > 0) + { + var animNames = ivoDbaMetadata?.AnimPaths ?? []; + var animEntries = ivoDbaMetadata?.Entries ?? []; + + for (int i = 0; i < ivoDbaData.AnimationBlocks.Count; i++) + { + var block = ivoDbaData.AnimationBlocks[i]; + var animName = i < animNames.Count + ? Path.GetFileNameWithoutExtension(animNames[i]) + : $"animation_{i}"; + var metaEntry = i < animEntries.Count ? animEntries[i] : (IvoDBAMetaEntry?)null; + + var (skelAnim, endFrame) = CreateSkelAnimationFromIvoDBA( + block, animName, metaEntry, controllerIdToJointPath); + + if (skelAnim is not null) + { + allAnimations.Add((animName, skelAnim, endFrame)); + } + } + } + } + } + + // Process CAF animations + if (hasCafAnimations) + { + foreach (var cafAnim in _cryData.CafAnimations!) + { + var (skelAnim, endFrame) = CreateSkelAnimationFromCaf(cafAnim, controllerIdToJointPath); + if (skelAnim is not null) + { + allAnimations.Add((cafAnim.Name, skelAnim, endFrame)); + } + } + } + + // Skip if only one animation (already in main file) + if (allAnimations.Count <= 1) + { + Log.D("Only one animation found, skipping separate animation file export"); + return 0; + } + + Log.D($"Exporting {allAnimations.Count} animation files for Blender NLA workflow..."); + + // Export each animation as a separate file + foreach (var (animName, skelAnim, endFrame) in allAnimations) + { + var cleanAnimName = CleanPathString(animName); + var animFileName = $"{baseFileName}_anim_{cleanAnimName}.usda"; + var animFilePath = Path.Combine(outputDir, animFileName); + + var animDoc = CreateAnimationOnlyDoc(skelAnim, endFrame, jointPaths, bonePathMap); + + using (var writer = new StreamWriter(animFilePath)) + { + usdSerializer.Serialize(animDoc, writer); + } + + Log.D($" Exported: {animFileName}"); + exportedCount++; + } + + return exportedCount; + } + + /// + /// Creates a USD document containing only skeleton + single animation. + /// This is for Blender import workflow where each animation needs its own file. + /// + private UsdDoc CreateAnimationOnlyDoc( + UsdSkelAnimation skelAnim, + int endFrame, + List jointPaths, + Dictionary bonePathMap) + { + var usdDoc = new UsdDoc + { + Header = new UsdHeader + { + TimeCodesPerSecond = 30, + StartTimeCode = 0, + EndTimeCode = endFrame + } + }; + + // Create root + usdDoc.Prims.Add(new UsdXform("root", "/")); + var rootPrim = usdDoc.Prims[0]; + + // Create skeleton structure (same as main export but without mesh) + var skelRoot = new UsdSkelRoot("Armature"); + var skeleton = new UsdSkeleton("Skeleton"); + + // Add joint names array + skeleton.Attributes.Add(new UsdTokenArray("joints", jointPaths, isUniform: true)); + + // Add bind transforms + var bindTransforms = GetBindTransforms(jointPaths, bonePathMap); + skeleton.Attributes.Add(new UsdMatrix4dArray("bindTransforms", bindTransforms, isUniform: true)); + + // Add rest transforms + var restTransforms = GetRestTransforms(jointPaths, bonePathMap); + skeleton.Attributes.Add(new UsdMatrix4dArray("restTransforms", restTransforms, isUniform: true)); + + // Bind this animation to the skeleton + var animPath = $""; + skeleton.Attributes.Add(new UsdRelationship("skel:animationSource", animPath)); + + skelRoot.Children.Add(skeleton); + skelRoot.Children.Add(skelAnim); + rootPrim.Children.Add(skelRoot); + + return usdDoc; + } +} diff --git a/CgfConverter/Renderers/USD/UsdRenderer.Geometry.cs b/CgfConverter/Renderers/USD/UsdRenderer.Geometry.cs new file mode 100644 index 00000000..1da92e90 --- /dev/null +++ b/CgfConverter/Renderers/USD/UsdRenderer.Geometry.cs @@ -0,0 +1,432 @@ +using CgfConverter.CryEngineCore; +using CgfConverter.Models; +using CgfConverter.Models.Structs; +using CgfConverter.Renderers.USD.Attributes; +using CgfConverter.Renderers.USD.Models; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; + +namespace CgfConverter.Renderers.USD; + +/// +/// UsdRenderer partial class - Geometry and node hierarchy creation +/// +public partial class UsdRenderer +{ + private List CreateNodeHierarchy() + { + // For Ivo format with skinning, use skeleton-based hierarchy to avoid double transforms + // Each mesh should be a direct child of its corresponding bone Xform, not nested under other meshes + if (_cryData.SkinningInfo?.HasSkinningInfo == true && _bonePathMap is not null) + { + return CreateIvoSkeletonNodes(); + } + + // Traditional format: build node hierarchy directly + List rootNodes = _cryData.Nodes.Where(a => a.ParentNodeID == ~0).ToList(); + List nodes = []; + + foreach (ChunkNode root in rootNodes) + { + Log.D("Root node: {0}", root.Name); + nodes.Add(CreateNode(root, $"/root/{root.Name}")); + } + + return nodes; + } + + /// + /// Creates flat mesh list for Ivo format with skeletal skinning. + /// Meshes are placed at SkelRoot level with identity transforms. + /// All positioning comes purely from skeletal skinning (skel:jointIndices/Weights). + /// This avoids double transforms from scene graph inheritance + skinning. + /// + private List CreateIvoSkeletonNodes() + { + var nodes = new List(); + + // Simply create mesh prims for all nodes with geometry + // They will be placed as siblings of Skeleton under SkelRoot + foreach (var cryNode in _cryData.Nodes) + { + var meshPrim = CreateMeshPrim(cryNode); + if (meshPrim is null) + continue; + + // Use identity transform - all positioning comes from skeletal skinning + meshPrim.Attributes.RemoveAll(a => a is UsdMatrix4d m && m.Name == "xformOp:transform"); + meshPrim.Attributes.RemoveAll(a => a is UsdToken> t && t.Name == "xformOpOrder"); + meshPrim.Attributes.Insert(0, new UsdToken>("xformOpOrder", ["xformOp:transform"], true)); + meshPrim.Attributes.Insert(0, new UsdMatrix4d("xformOp:transform", Matrix4x4.Identity)); + + nodes.Add(meshPrim); + Log.D($"Created skinned mesh '{cryNode.Name}' with identity transform"); + } + + return nodes; + } + + /// + /// Creates a USD prim for a CryEngine node. + /// If the node has geometry, returns a Mesh prim with transforms. + /// If no geometry, returns an Xform prim. + /// This avoids unnecessary Xform > Mesh nesting. + /// + private UsdPrim CreateNode(ChunkNode node, string parentPath) + { + string cleanNodeName = CleanPathString(node.Name); + + // Try to create mesh geometry first + var meshPrim = CreateMeshPrim(node); + + // Use Mesh as the node if geometry exists, otherwise use Xform + UsdPrim nodePrim; + if (meshPrim is not null) + { + // Mesh with transforms - no need for Xform wrapper + nodePrim = meshPrim; + // Mesh name was set in CreateMeshPrim, but we need to use node name for hierarchy + nodePrim.Name = cleanNodeName; + } + else + { + // No geometry - use Xform for transform-only nodes + nodePrim = new UsdXform(cleanNodeName, parentPath); + } + + // Add transform attributes + nodePrim.Attributes.Insert(0, new UsdToken>("xformOpOrder", ["xformOp:transform"], true)); + nodePrim.Attributes.Insert(0, new UsdMatrix4d("xformOp:transform", node.Transform)); + + // Get all the children of the node + var children = node.Children; + if (children is not null) + { + // Build path for children based on parent path and this node's name + string nodePath = string.IsNullOrEmpty(parentPath) ? $"/{cleanNodeName}" : $"{parentPath}/{cleanNodeName}"; + foreach (var childNode in children) + { + nodePrim.Children.Add(CreateNode(childNode, nodePath)); + } + } + + return nodePrim; + } + + private UsdPrim? CreateMeshPrim(ChunkNode nodeChunk) + { + if (_args.IsNodeNameExcluded(nodeChunk.Name)) + { + Log.D($"Excluding node {nodeChunk.Name}"); + return null; + } + + if (nodeChunk.MeshData is not ChunkMesh meshChunk) + return null; + + if (meshChunk.GeometryInfo is null) // $physics node + return null; + + string nodeName = nodeChunk.Name; + + var subsets = meshChunk.GeometryInfo.GeometrySubsets; + Datastream? indices = meshChunk.GeometryInfo.Indices; + Datastream? uvs = meshChunk.GeometryInfo.UVs; + Datastream? uvs2 = meshChunk.GeometryInfo.UVs2; // Second UV layer (if available) + Datastream? verts = meshChunk.GeometryInfo.Vertices; + Datastream? vertsUvs = meshChunk.GeometryInfo.VertUVs; + Datastream? normals = meshChunk.GeometryInfo.Normals; + Datastream? colors = meshChunk.GeometryInfo.Colors; + + // Validation checks - same as glTF renderer + if (indices is null) + { + Log.D($"Mesh[{nodeChunk.Name}]: IndicesData is empty."); + return null; + } + if (subsets is null) + { + Log.D($"Mesh[{nodeChunk.Name}]: GeometrySubsets is empty."); + return null; + } + if (verts is null && vertsUvs is null) + { + Log.D($"Mesh[{nodeChunk.Name}]: both VerticesData and VertsUVsData are empty."); + return null; + } + + var numberOfElements = nodeChunk.MeshData.GeometryInfo?.GeometrySubsets?.Sum(x => x.NumVertices) ?? 0; + + UsdMesh meshPrim = new(CleanPathString(nodeChunk.Name)); + + if (verts is not null) + { + int numVerts = (int)verts.NumElements; + var hasNormals = normals is not null; + var hasUVs = uvs is not null; + var hasUVs2 = uvs2 is not null; // Second UV layer + var hasColors = colors is not null; + + meshPrim.Attributes.Add(new UsdBool("doubleSided", true, true)); + meshPrim.Attributes.Add(new UsdVector3dList("extent", [meshChunk.MinBound, meshChunk.MaxBound])); + meshPrim.Attributes.Add(new UsdIntList("faceVertexCounts", [.. Enumerable.Repeat(3, (int)(indices.NumElements / 3))])); + meshPrim.Attributes.Add(new UsdIntList("faceVertexIndices", [.. indices.Data.Select(x => (int)x)])); + meshPrim.Attributes.Add(new UsdPointsList("points", [.. verts.Data])); + + if (hasColors) + meshPrim.Attributes.Add(new UsdColorsList($"{CleanPathString(nodeChunk.Name)}_color", [.. colors.Data])); + if (hasUVs) + meshPrim.Attributes.Add(new UsdTexCoordsList($"{CleanPathString(nodeChunk.Name)}_UV", [.. uvs.Data])); + if (hasUVs2) + meshPrim.Attributes.Add(new UsdTexCoordsList($"{CleanPathString(nodeChunk.Name)}_UV2", [.. uvs2.Data])); + if (hasNormals) + { + // For faceVarying normals, expand the normals array to match faceVertexIndices + // by indexing into the normals using the same indices as vertices + var faceVaryingNormals = indices.Data + .Select(idx => (int)idx < normals.Data.Length ? normals.Data[(int)idx] : Vector3.UnitY) + .ToList(); + meshPrim.Attributes.Add(new UsdNormalsList("normals", faceVaryingNormals)); + } + + meshPrim.Attributes.Add(new UsdToken("subdivisionScheme", "none", true)); + Dictionary matBindingApi = new() { ["apiSchemas"] = "[\"MaterialBindingAPI\"]" }; + meshPrim.Properties = [new UsdProperty(matBindingApi, true)]; + + // Collect face indices per material to avoid duplicate GeomSubset prims + // Multiple mesh subsets may use the same material + var materialFaceIndices = new Dictionary>(); + + foreach (var subset in meshChunk.GeometryInfo.GeometrySubsets ?? []) + { + var index = subset.MatID; + + // Bounds check for material lookup + if (!_cryData.Materials.TryGetValue(nodeChunk.MaterialFileName, out var material) || + material?.SubMaterials is null || + index < 0 || index >= material.SubMaterials.Length) + { + Log.D($"Mesh[{nodeChunk.Name}]: Material index {index} out of bounds or material not found."); + continue; + } + + var matName = GetMaterialName( + Path.GetFileNameWithoutExtension(nodeChunk.MaterialFileName), + material.SubMaterials[index].Name); + var cleanMatName = CleanPathString(matName); + + // Convert vertex index range to face index range + int firstFace = (int)subset.FirstIndex / 3; + int numFaces = (int)subset.NumIndices / 3; + var faceIndices = Enumerable.Range(firstFace, numFaces).Select(i => (uint)i); + + // Merge face indices for subsets using the same material + if (!materialFaceIndices.ContainsKey(cleanMatName)) + materialFaceIndices[cleanMatName] = new List(); + materialFaceIndices[cleanMatName].AddRange(faceIndices); + } + + // Create one GeomSubset per unique material + foreach (var kvp in materialFaceIndices) + { + var cleanMatName = kvp.Key; + var faceIndices = kvp.Value; + + var submeshPrim = new UsdGeomSubset(cleanMatName); + submeshPrim.Attributes.Add(new UsdUIntList("indices", faceIndices)); + submeshPrim.Attributes.Add(new UsdToken("elementType", "face", true)); + submeshPrim.Attributes.Add(new UsdToken("familyName", "materialBind", true)); + meshPrim.Children.Add(submeshPrim); + + // Assign material to submesh + submeshPrim.Properties = [new UsdProperty(matBindingApi, true)]; + submeshPrim.Attributes.Add( + new UsdRelativePath( + "material:binding", + $"/root/_materials/{cleanMatName}")); + } + + // Add skinning data if present (traditional format - no per-subset extraction) + if (_cryData.SkinningInfo?.HasSkinningInfo ?? false) + { + AddSkinningAttributes(meshPrim, nodeChunk, subsets: null); + } + } + else if (vertsUvs is not null) + { + // Handle Ivo format (Star Citizen) - combined vertex/UV/color data + // For Ivo format, we must extract only the vertices used by each subset + // and remap indices from global to local (same approach as Collada/glTF renderers) + var hasNormals = normals is not null; + + // Calculate bounding box scaling for Ivo format + var multiplerVector = Vector3.Abs((meshChunk.MinBound - meshChunk.MaxBound) / 2f); + if (multiplerVector.X < 1) multiplerVector.X = 1; + if (multiplerVector.Y < 1) multiplerVector.Y = 1; + if (multiplerVector.Z < 1) multiplerVector.Z = 1; + var boundaryBoxCenter = (meshChunk.MinBound + meshChunk.MaxBound) / 2f; + + Vector3 scalingVector = Vector3.One; + Vector3 scalingBoxCenter = Vector3.Zero; + bool useScalingBox = false; + + if (meshChunk.ScalingVectors is not null) + { + scalingVector = Vector3.Abs((meshChunk.ScalingVectors.Max - meshChunk.ScalingVectors.Min) / 2f); + if (scalingVector.X < 1) scalingVector.X = 1; + if (scalingVector.Y < 1) scalingVector.Y = 1; + if (scalingVector.Z < 1) scalingVector.Z = 1; + scalingBoxCenter = (meshChunk.ScalingVectors.Max + meshChunk.ScalingVectors.Min) / 2f; + useScalingBox = _cryData.InputFile.EndsWith("cga") || _cryData.InputFile.EndsWith("cgf"); + } + + // Extract vertices, UVs, colors, and normals PER-SUBSET (not all data) + // This is critical for Ivo format where geometry is shared across nodes + var vertices = new List(); + var uvList = new List(); + var colorList = new List(); + var normalsList = new List(); + + foreach (var subset in subsets ?? []) + { + for (int i = subset.FirstVertex; i < subset.FirstVertex + subset.NumVertices; i++) + { + var vertUv = vertsUvs.Data[i]; + + // Apply bounding box scaling to vertices (skip for .skin and .chr files) + Vector3 vertex = vertUv.Vertex; + if (!_cryData.InputFile.EndsWith("skin") && !_cryData.InputFile.EndsWith("chr")) + { + if (useScalingBox) + vertex = (vertex * scalingVector) + scalingBoxCenter; + else + vertex = (vertex * multiplerVector) + boundaryBoxCenter; + } + + vertices.Add(vertex); + uvList.Add(vertUv.UV); + colorList.Add(vertUv.Color); + + if (hasNormals) + normalsList.Add(normals.Data[i]); + } + } + + // Remap indices from global to local + // Each subset's indices point into the global vertex array, but we've extracted + // only the subset vertices concatenated together, so we need to remap + var remappedIndices = new List(); + uint currentOffset = 0; + + foreach (var subset in subsets ?? []) + { + var firstGlobalIndex = indices.Data[subset.FirstIndex]; + + for (int i = 0; i < subset.NumIndices; i++) + { + uint globalIndex = indices.Data[subset.FirstIndex + i]; + int localIndex = (int)((globalIndex - firstGlobalIndex) + currentOffset); + remappedIndices.Add(localIndex); + } + + currentOffset += (uint)subset.NumVertices; + } + + int numFaces = remappedIndices.Count / 3; + + meshPrim.Attributes.Add(new UsdBool("doubleSided", true, true)); + meshPrim.Attributes.Add(new UsdVector3dList("extent", [meshChunk.MinBound, meshChunk.MaxBound])); + meshPrim.Attributes.Add(new UsdIntList("faceVertexCounts", [.. Enumerable.Repeat(3, numFaces)])); + meshPrim.Attributes.Add(new UsdIntList("faceVertexIndices", remappedIndices)); + meshPrim.Attributes.Add(new UsdPointsList("points", vertices)); + + // Add vertex colors from VertUV + meshPrim.Attributes.Add(new UsdColorsList($"{CleanPathString(nodeChunk.Name)}_color", colorList)); + + // Add UVs from VertUV + meshPrim.Attributes.Add(new UsdTexCoordsList($"{CleanPathString(nodeChunk.Name)}_UV", uvList)); + + if (hasNormals) + { + // For faceVarying normals, expand the normals array to match faceVertexIndices + // by indexing into the extracted normals using the remapped indices + var faceVaryingNormals = remappedIndices + .Select(idx => idx >= 0 && idx < normalsList.Count ? normalsList[idx] : Vector3.UnitY) + .ToList(); + meshPrim.Attributes.Add(new UsdNormalsList("normals", faceVaryingNormals)); + } + + meshPrim.Attributes.Add(new UsdToken("subdivisionScheme", "none", true)); + Dictionary matBindingApi = new() { ["apiSchemas"] = "[\"MaterialBindingAPI\"]" }; + meshPrim.Properties = [new UsdProperty(matBindingApi, true)]; + + // Collect face indices per material to avoid duplicate GeomSubset prims + // Multiple mesh subsets may use the same material + var materialFaceIndices = new Dictionary>(); + + // For Ivo format with remapped indices, GeomSubsets use sequential face ranges + // since all indices are now contiguous after remapping + int currentFaceOffset = 0; + foreach (var subset in meshChunk.GeometryInfo.GeometrySubsets ?? []) + { + var index = subset.MatID; + int subsetNumFaces = (int)subset.NumIndices / 3; + + // Bounds check for material lookup + if (!_cryData.Materials.TryGetValue(nodeChunk.MaterialFileName, out var material) || + material?.SubMaterials is null || + index < 0 || index >= material.SubMaterials.Length) + { + Log.D($"Mesh[{nodeChunk.Name}]: Material index {index} out of bounds or material not found."); + // Still need to update face offset even if we skip this subset + currentFaceOffset += subsetNumFaces; + continue; + } + + var matName = GetMaterialName( + Path.GetFileNameWithoutExtension(nodeChunk.MaterialFileName), + material.SubMaterials[index].Name); + var cleanMatName = CleanPathString(matName); + + // Face indices are now sequential after remapping + var faceIndices = Enumerable.Range(currentFaceOffset, subsetNumFaces).Select(i => (uint)i); + currentFaceOffset += subsetNumFaces; + + // Merge face indices for subsets using the same material + if (!materialFaceIndices.ContainsKey(cleanMatName)) + materialFaceIndices[cleanMatName] = new List(); + materialFaceIndices[cleanMatName].AddRange(faceIndices); + } + + // Create one GeomSubset per unique material + foreach (var kvp in materialFaceIndices) + { + var cleanMatName = kvp.Key; + var faceIndices = kvp.Value; + + var submeshPrim = new UsdGeomSubset(cleanMatName); + submeshPrim.Attributes.Add(new UsdUIntList("indices", faceIndices)); + submeshPrim.Attributes.Add(new UsdToken("elementType", "face", true)); + submeshPrim.Attributes.Add(new UsdToken("familyName", "materialBind", true)); + meshPrim.Children.Add(submeshPrim); + + // Assign material to submesh + submeshPrim.Properties = [new UsdProperty(matBindingApi, true)]; + submeshPrim.Attributes.Add( + new UsdRelativePath( + "material:binding", + $"/root/_materials/{cleanMatName}")); + } + + // Add skinning data if present (Ivo format - use per-subset extraction) + if (_cryData.SkinningInfo?.HasSkinningInfo ?? false) + { + AddSkinningAttributes(meshPrim, nodeChunk, subsets); + } + } + + return meshPrim; + } +} diff --git a/CgfConverter/Renderers/USD/UsdRenderer.Materials.cs b/CgfConverter/Renderers/USD/UsdRenderer.Materials.cs new file mode 100644 index 00000000..1f902e42 --- /dev/null +++ b/CgfConverter/Renderers/USD/UsdRenderer.Materials.cs @@ -0,0 +1,297 @@ +using CgfConverter.Models.Materials; +using CgfConverter.Models.Shaders; +using CgfConverter.Renderers.USD.Attributes; +using CgfConverter.Renderers.USD.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using static Extensions.FileHandlingExtensions; + +namespace CgfConverter.Renderers.USD; + +/// +/// UsdRenderer partial class - Material and shader creation +/// +public partial class UsdRenderer +{ + private UsdPrim CreateMaterials() + { + var scope = new UsdScope("_materials"); + var matList = new List(); + var createdMaterialNames = new HashSet(); // Track created materials to avoid duplicates + + foreach (var matKey in _cryData.Materials.Keys) // Each mtl file is a Key + { + var material = _cryData.Materials[matKey]; + if (material?.SubMaterials is null) + { + Log.D($"Skipping material '{matKey}' - no submaterials"); + continue; + } + + foreach (var submat in material.SubMaterials) + { + var matName = GetMaterialName(matKey, submat.Name); + var cleanMatName = CleanPathString(matName); + + // Skip if we've already created a material with this name + if (createdMaterialNames.Contains(cleanMatName)) + { + Log.D($"Skipping duplicate material: {cleanMatName}"); + continue; + } + createdMaterialNames.Add(cleanMatName); + + var usdMat = new UsdMaterial(cleanMatName); + usdMat.Attributes.Add(new UsdToken( + "outputs:surface.connect", + $"")); + usdMat.Children.AddRange(CreateShaders(submat, matKey, cleanMatName)); + matList.Add(usdMat); + } + } + scope.Children.AddRange(matList); + return scope; + } + + private IEnumerable CreateShaders(Material submat, string matKey, string matName) + { + List shaders = []; + + // Look up shader definition and generate rules + ShaderDefinition? shaderDef = null; + List materialRules = []; + + if (!string.IsNullOrEmpty(submat.Shader)) + { + _shaderDefinitions.TryGetValue(submat.Shader, out shaderDef); + materialRules = _shaderRules.GenerateRules(submat, shaderDef); + } + + // Check for Nodraw shader - these materials should be invisible + bool isNodraw = !string.IsNullOrEmpty(submat.Shader) && + submat.Shader.Equals("Nodraw", StringComparison.OrdinalIgnoreCase); + + // Add the PrincipleBSDF shader + var principleBSDF = new UsdShader($"Principled_BSDF"); + principleBSDF.Attributes.Add(new UsdToken("info:id", "UsdPreviewSurface", true)); + + // Check if material rules will override these properties + bool hasAlphaGlow = materialRules.Any(r => r.PropertyName == "%ALPHAGLOW"); + + // Material color properties + if (submat.DiffuseValue is not null) + { + var diffuse = $"{submat.DiffuseValue.Red}, {submat.DiffuseValue.Green}, {submat.DiffuseValue.Blue}"; + principleBSDF.Attributes.Add(new UsdColor3f("inputs:diffuseColor", diffuse)); + } + + if (submat.SpecularValue is not null) + { + var specular = $"{submat.SpecularValue.Red}, {submat.SpecularValue.Green}, {submat.SpecularValue.Blue}"; + principleBSDF.Attributes.Add(new UsdColor3f("inputs:specularColor", specular)); + } + + // Set emissive color from material (even with %ALPHAGLOW) + // Note: %ALPHAGLOW tries to route diffuse alpha to emissive, but USD doesn't support this + // (UsdPreviewSurface emissiveColor is color3f, can't connect float alpha to it) + if (submat.EmissiveValue is not null) + { + var emissive = $"{submat.EmissiveValue.Red}, {submat.EmissiveValue.Green}, {submat.EmissiveValue.Blue}"; + principleBSDF.Attributes.Add(new UsdColor3f("inputs:emissiveColor", emissive)); + } + + // Opacity. Nodraw materials should be fully transparent/invisible + if (isNodraw) + principleBSDF.Attributes.Add(new UsdAttribute("inputs:opacity", 0.0f)); + else if (submat.OpacityValue.HasValue) + principleBSDF.Attributes.Add(new UsdAttribute("inputs:opacity", submat.OpacityValue.Value)); + + // Roughness - convert from Shininess + // Shininess ranges from 0-255, where higher = more shiny (less rough) roughness = (255 - shininess) / 255 + float roughness = Math.Clamp((255.0f - (float)submat.Shininess) / 255.0f, 0.0f, 1.0f); + principleBSDF.Attributes.Add(new UsdAttribute("inputs:roughness", roughness)); + + // Metallic - default to 0 (non-metallic) + principleBSDF.Attributes.Add(new UsdAttribute("inputs:metallic", 0.0f)); + + principleBSDF.Attributes.Add(new UsdAttribute("inputs:clearcoat", 0)); + principleBSDF.Attributes.Add(new UsdAttribute("inputs:clearcoatRoughness", 0.03f)); + principleBSDF.Attributes.Add(new UsdToken("outputs:surface", null, false)); + shaders.Add(principleBSDF); + + if (submat.Textures is null) + return shaders; + + // Track which textures we've already created to avoid duplicates + var createdTextures = new HashSet(); + + foreach (var texture in submat.Textures) + { + // Skip environment maps (cubemaps cause crashes in Blender) + if (texture.Map == Texture.MapTypeEnum.Env) continue; + + // Get the shader name we would create + if (string.IsNullOrEmpty(texture.File)) continue; + + var shaderName = CleanPathString(Path.GetFileNameWithoutExtension(texture.File)); + + // Skip if we've already created a shader for this texture + if (createdTextures.Contains(shaderName)) continue; + + var imageTexture = CreateUsdImageTextureShader(texture, matName); + if (imageTexture is not null) + { + createdTextures.Add(shaderName); + shaders.Add(imageTexture); + + // Apply connections based on texture type and shader rules + ApplyTextureConnections(imageTexture, texture, principleBSDF, matName, materialRules, submat); + } + } + + return shaders; + } + + /// + /// Apply texture connections based on texture type and active shader rules. + /// + private void ApplyTextureConnections( + UsdShader imageTexture, + Texture texture, + UsdShader principleBSDF, + string matName, + List rules, + Material submat) + { + switch (texture.Map) + { + case Texture.MapTypeEnum.Diffuse: + imageTexture.Attributes.Add(new UsdFloat3f("outputs:rgb")); + imageTexture.Attributes.Add(new UsdFloat("outputs:a")); + + // Connect RGB to diffuse color + principleBSDF.Attributes.Add(new UsdColor3f( + $"inputs:diffuseColor.connect", + $"")); + + // Check shader rules for alpha channel routing + var alphaTarget = _shaderRules.GetChannelTarget(rules, "Diffuse", "alpha"); + + if (alphaTarget == "emissiveColor") + { + // %ALPHAGLOW: Cannot directly connect float alpha to color3f emissiveColor in USD + // UsdPreviewSurface doesn't have a separate emissive intensity parameter + // The emissive color is already set to white above, user will see glow in diffuse alpha visually + Log.D($" %ALPHAGLOW: Diffuse alpha controls glow (not directly supported in USD PreviewSurface)"); + // Don't create a connection - it would be invalid USD + } + else if (!_shaderRules.ShouldOverrideDefaultAlphaConnection(rules)) + { + // Default behavior: connect alpha to opacity (unless overridden by rules) + principleBSDF.Attributes.Add(new UsdFloat( + $"inputs:opacity.connect", + $"")); + } + break; + + case Texture.MapTypeEnum.Normals: + imageTexture.Attributes.Add(new UsdFloat3f("outputs:rgb")); + principleBSDF.Attributes.Add(new UsdFloat3f( + $"inputs:normal.connect", + $"")); + break; + + case Texture.MapTypeEnum.Specular: + imageTexture.Attributes.Add(new UsdFloat3f("outputs:rgb")); + imageTexture.Attributes.Add(new UsdFloat("outputs:a")); + + // Connect RGB to specular color + principleBSDF.Attributes.Add(new UsdColor3f( + $"inputs:specularColor.connect", + $"")); + + // Check for %SPECULARPOW_GLOSSALPHA rule (specular alpha → roughness) + var specAlphaTarget = _shaderRules.GetChannelTarget(rules, "Specular", "alpha"); + if (specAlphaTarget == "roughness") + { + Log.D($" Applying %SPECULARPOW_GLOSSALPHA: specular alpha → roughness"); + principleBSDF.Attributes.Add(new UsdFloat( + $"inputs:roughness.connect", + $"")); + } + break; + + case Texture.MapTypeEnum.Opacity: + // In Cryengine, Opacity maps are primarily for translucency/backlighting effects on vegetation + // USD PreviewSurface doesn't support this - only has a single opacity input + // If material has AlphaTest set, the diffuse alpha is used for cutout transparency + // For now, only use Opacity map if there's no diffuse texture (fallback) + var hasDiffuse = submat.Textures.Any(t => t.Map == Texture.MapTypeEnum.Diffuse); + if (!hasDiffuse) + { + // No diffuse texture, use opacity map as transparency mask + imageTexture.Attributes.Add(new UsdFloat("outputs:r")); + principleBSDF.Attributes.Add(new UsdFloat( + $"inputs:opacity.connect", + $"")); + } + else + { + // Diffuse texture exists - its alpha channel handles transparency + // Opacity map is for translucency which USD PreviewSurface doesn't support + Log.D($"Skipping Opacity map for {matName} - USD PreviewSurface doesn't support translucency, using diffuse alpha instead"); + } + break; + + // Additional texture types can be added here + default: + Log.D($"Unhandled texture map type: {texture.Map}"); + break; + } + } + + private UsdShader? CreateUsdImageTextureShader(Texture texture, string matName) + { + if (string.IsNullOrEmpty(texture.File)) + { + Log.D("Texture has no file path specified"); + return null; + } + + // Filter out null/empty data directories + var dataDirs = new List(); + if (!string.IsNullOrEmpty(_args.DataDir)) + dataDirs.Add(_args.DataDir); + + var textureFile = ResolveTextureFile(texture.File, _args.PackFileSystem, dataDirs); + if (File.Exists(textureFile) == false) + { + Log.D("Texture file not found: {0}", texture.File); + return null; + } + var usdImageTexture = new UsdShader(CleanPathString(Path.GetFileNameWithoutExtension(texture.File))); + + usdImageTexture.Attributes.Add(new UsdToken("info:id", "UsdUVTexture", true)); + usdImageTexture.Attributes.Add(new UsdToken("inputs:wrapS", "repeat")); + usdImageTexture.Attributes.Add(new UsdToken("inputs:wrapT", "repeat")); + + // Use forward slashes for USD asset paths (cross-platform compatible) + var normalizedPath = textureFile.Replace('\\', '/'); + usdImageTexture.Attributes.Add(new UsdAsset("file", normalizedPath)); + + var isBumpmap = texture.Map == Texture.MapTypeEnum.Normals ? "raw" : "sRGB"; + usdImageTexture.Attributes.Add(new UsdToken("inputs:sourceColorSpace", isBumpmap)); + + return usdImageTexture; + } + + private static string GetMaterialName(string matKey, string submatName) + { + // material name is _mtl_ + var matfileName = Path.GetFileNameWithoutExtension(submatName); + + return $"{matKey}_mtl_{matfileName}".Replace(' ', '_'); + } +} diff --git a/CgfConverter/Renderers/USD/UsdRenderer.Skeleton.cs b/CgfConverter/Renderers/USD/UsdRenderer.Skeleton.cs new file mode 100644 index 00000000..8e178fd9 --- /dev/null +++ b/CgfConverter/Renderers/USD/UsdRenderer.Skeleton.cs @@ -0,0 +1,388 @@ +using CgfConverter.CryEngineCore; +using CgfConverter.Models.Structs; +using CgfConverter.Renderers.USD.Attributes; +using CgfConverter.Renderers.USD.Models; +using CgfConverter.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace CgfConverter.Renderers.USD; + +/// +/// UsdRenderer partial class - Skeletal animation and skinning +/// +public partial class UsdRenderer +{ + #region Matrix Conversion + + /// + /// Converts a CryEngine matrix to USD format by moving translation from column 4 to row 4. + /// CryEngine stores translation in (M14, M24, M34), USD expects it in (M41, M42, M43). + /// Unlike Transpose(), this preserves the rotation orientation. + /// + private static Matrix4x4 CryEngineToUsdMatrix(Matrix4x4 cryMatrix) + { + return new Matrix4x4( + cryMatrix.M11, cryMatrix.M12, cryMatrix.M13, 0, + cryMatrix.M21, cryMatrix.M22, cryMatrix.M23, 0, + cryMatrix.M31, cryMatrix.M32, cryMatrix.M33, 0, + cryMatrix.M14, cryMatrix.M24, cryMatrix.M34, 1); + } + + #endregion + + #region Skeleton Methods + + /// Creates a USD skeleton hierarchy for skinned meshes. + /// Output mapping from bone controller IDs to USD joint paths for animation binding. + /// Output list of joint paths in order. + /// Output mapping from CompiledBone to joint path. + /// Output mapping from CompiledBones array index to jointPaths array index. + /// The SkelRoot prim containing the skeleton. + private UsdSkelRoot CreateSkeleton( + out Dictionary controllerIdToJointPath, + out List jointPaths, + out Dictionary bonePathMap, + out int[] compiledBoneIndexToJointIndex) + { + var skelRoot = new UsdSkelRoot("Armature"); + var skeleton = new UsdSkeleton("Skeleton"); + + // Build joint paths (hierarchical bone names) + jointPaths = new List(); + bonePathMap = new Dictionary(); + BuildJointPaths(_cryData.SkinningInfo.RootBone, "", jointPaths, bonePathMap); + + // Build controller ID to joint path mapping for animation binding + // Add both stored controller IDs and computed CRC32 hashes of bone names + // (CAF animations may use either for matching) + controllerIdToJointPath = new Dictionary(); + foreach (var kvp in bonePathMap) + { + var bone = kvp.Key; + var jointPath = kvp.Value; + + // Add stored controller ID if valid + if (bone.ControllerID != 0xFFFFFFFF && bone.ControllerID != 0) + controllerIdToJointPath[bone.ControllerID] = jointPath; + + // Also add CRC32 hash of bone name for CAF matching + // Different games use different conventions - add both original case and lowercase CRC32 + if (!string.IsNullOrEmpty(bone.BoneName)) + { + // Original case CRC32 (used by ArcheAge) + var crc32Original = Crc32CryEngine.Compute(bone.BoneName); + Log.D($"Bone '{bone.BoneName}' -> CRC32 = 0x{crc32Original:X08}"); + controllerIdToJointPath.TryAdd(crc32Original, jointPath); + + // Lowercase CRC32 (used by some other games) + var crc32Lower = Crc32CryEngine.Compute(bone.BoneName.ToLowerInvariant()); + if (crc32Lower != crc32Original) + controllerIdToJointPath.TryAdd(crc32Lower, jointPath); + } + } + + // Build mapping from CompiledBones array index to jointPaths array index + // This is critical because: + // - IntSkinVertex.BoneMapping.BoneIndex contains indices into CompiledBones array order + // - USD skel:jointIndices expects indices into jointPaths array order + // - These orders may differ due to depth-first vs linear traversal + var compiledBones = _cryData.SkinningInfo.CompiledBones; + compiledBoneIndexToJointIndex = new int[compiledBones.Count]; + + // Build reverse lookup from path to jointPaths index + var pathToJointIndex = new Dictionary(); + for (int i = 0; i < jointPaths.Count; i++) + { + pathToJointIndex[jointPaths[i]] = i; + } + + // Map each CompiledBone index to its corresponding jointPaths index + for (int compiledIndex = 0; compiledIndex < compiledBones.Count; compiledIndex++) + { + var bone = compiledBones[compiledIndex]; + if (bonePathMap.TryGetValue(bone, out var path) && pathToJointIndex.TryGetValue(path, out var jointIndex)) + compiledBoneIndexToJointIndex[compiledIndex] = jointIndex; + else + { + // Fallback to same index if bone wasn't found (shouldn't happen with valid data) + Log.W($"Bone at index {compiledIndex} ({bone?.BoneName}) not found in joint paths, using index as-is"); + compiledBoneIndexToJointIndex[compiledIndex] = compiledIndex; + } + } + + // Add joint names array + skeleton.Attributes.Add(new UsdTokenArray("joints", jointPaths, isUniform: true)); + + // Add bind transforms (inverse bind pose matrices) + var bindTransforms = GetBindTransforms(jointPaths, bonePathMap); + skeleton.Attributes.Add(new UsdMatrix4dArray("bindTransforms", bindTransforms, isUniform: true)); + + // Add rest transforms (local transform matrices) + var restTransforms = GetRestTransforms(jointPaths, bonePathMap); + skeleton.Attributes.Add(new UsdMatrix4dArray("restTransforms", restTransforms, isUniform: true)); + + skelRoot.Children.Add(skeleton); + return skelRoot; + } + + /// Recursively builds joint path strings in USD format (e.g., "Bip01/bip_01_Pelvis/bip_01_Spine"). + private void BuildJointPaths(CompiledBone? bone, string parentPath, List jointPaths, Dictionary bonePathMap) + { + if (bone == null) return; + + // Cycle detection: skip bones we've already processed + if (bonePathMap.ContainsKey(bone)) return; + + // Clean bone name for USD compliance + string cleanName = CleanPathString(bone.BoneName ?? "bone"); + + // Build the full path for this bone + string bonePath = string.IsNullOrEmpty(parentPath) + ? cleanName + : $"{parentPath}/{cleanName}"; + + jointPaths.Add(bonePath); + bonePathMap[bone] = bonePath; + + // Recursively process children + var childBones = _cryData.SkinningInfo.GetChildBones(bone); + foreach (var childBone in childBones) + { + BuildJointPaths(childBone, bonePath, jointPaths, bonePathMap); + } + } + + /// Gets bind transforms (world-space bone transforms) in joint order. + private List GetBindTransforms(List jointPaths, Dictionary bonePathMap) + { + var bindTransforms = new List(); + + // Build reverse lookup from path to bone + var pathToBone = bonePathMap.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); + + foreach (var jointPath in jointPaths) + { + if (pathToBone.TryGetValue(jointPath, out var bone)) + { + // USD bindTransforms are world-space transforms (bone-to-world) + // CryEngine BindPoseMatrix is worldToBone, so we need to invert it + // Transpose converts from CryEngine convention (translation in column 4) + // to USD convention (translation in row 4) + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var boneToWorld)) + bindTransforms.Add(Matrix4x4.Transpose(boneToWorld)); + else + { + Log.W($"Failed to invert BindPoseMatrix for bone {bone.BoneName}, using identity"); + bindTransforms.Add(Matrix4x4.Identity); + } + } + else + bindTransforms.Add(Matrix4x4.Identity); // Fallback to identity matrix if bone not found + } + + return bindTransforms; + } + + /// Gets rest transforms (local-space bone transforms) in joint order. + private List GetRestTransforms(List jointPaths, Dictionary bonePathMap) + { + var restTransforms = new List(); + + // Build reverse lookup from path to bone + var pathToBone = bonePathMap.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); + + foreach (var jointPath in jointPaths) + { + if (pathToBone.TryGetValue(jointPath, out var bone)) + { + // restTransforms are local-space transforms (bone relative to parent) + // For root bones: use world transform (boneToWorld) + // For child bones: compute relative to parent + + Matrix4x4 restMatrix; + + if (bone.ParentBone == null) + { + // Root bone: local = world, invert BindPoseMatrix to get boneToWorld + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var boneToWorld)) + restMatrix = boneToWorld; + else + { + Log.W($"Failed to invert BindPoseMatrix for root bone {bone.BoneName}"); + restMatrix = Matrix4x4.Identity; + } + } + else + { + // Child bone: compute local transform relative to parent + // localTransform = parentWorldToBone * childBoneToWorld + // = parent.BindPoseMatrix * inverse(child.BindPoseMatrix) + if (Matrix4x4.Invert(bone.BindPoseMatrix, out var childBoneToWorld)) + restMatrix = bone.ParentBone.BindPoseMatrix * childBoneToWorld; + else + { + Log.W($"Failed to invert BindPoseMatrix for bone {bone.BoneName}"); + restMatrix = Matrix4x4.Identity; + } + } + + // Debug: log turret_arm rest transform (CtrlID = 0x9384FC75) + if (bone.ControllerID == 0x9384FC75) + { + Log.I($"USD skeleton turret_arm restMatrix (CryEngine format):"); + Log.I($" [{restMatrix.M11:F6}, {restMatrix.M12:F6}, {restMatrix.M13:F6}, {restMatrix.M14:F6}]"); + Log.I($" [{restMatrix.M21:F6}, {restMatrix.M22:F6}, {restMatrix.M23:F6}, {restMatrix.M24:F6}]"); + Log.I($" [{restMatrix.M31:F6}, {restMatrix.M32:F6}, {restMatrix.M33:F6}, {restMatrix.M34:F6}]"); + Log.I($" [{restMatrix.M41:F6}, {restMatrix.M42:F6}, {restMatrix.M43:F6}, {restMatrix.M44:F6}]"); + if (Matrix4x4.Decompose(restMatrix, out var s, out var r, out var t)) + { + Log.I($"USD skeleton turret_arm rest rotation (from restMatrix): ({r.X:F6}, {r.Y:F6}, {r.Z:F6}, {r.W:F6})"); + var angle = 2 * Math.Acos(Math.Clamp(r.W, -1, 1)) * 180 / Math.PI; + Log.I($"USD skeleton turret_arm rest rotation angle: {angle:F2}°"); + } + } + + // Transpose converts from CryEngine convention (translation in column 4) + // to USD convention (translation in row 4) + restTransforms.Add(Matrix4x4.Transpose(restMatrix)); + } + else + { + Log.W($"Bone not found for joint path: {jointPath}"); + restTransforms.Add(Matrix4x4.Identity); + } + } + + return restTransforms; + } + + /// Adds skinning attributes to a mesh prim for skeletal animation. + /// The mesh prim to add skinning to. + /// The node chunk containing mesh data. + /// Geometry subsets for per-subset vertex extraction (Ivo format). + private void AddSkinningAttributes(UsdMesh meshPrim, ChunkNode nodeChunk, IEnumerable? subsets = null) + { + // Get skinning data + var skinningInfo = _cryData.SkinningInfo; + + // Ensure we have the bone index mapping (set up by CreateSkeleton) + if (_compiledBoneIndexToJointIndex is null) + { + Log.W("Skinning attributes requested but bone index mapping not initialized"); + return; + } + + // Build joint indices and weights arrays FIRST to check if we have actual skinning data + // Use Ext2IntMap if available to map external (mesh) vertices to internal (skinning) vertices + // IMPORTANT: BoneIndex values in skinning data are indices into CompiledBones array, + // but USD expects indices into the joints array (jointPaths). We must remap using + // _compiledBoneIndexToJointIndex to fix bone-to-vertex mapping. + var jointIndices = new List(); + var jointWeights = new List(); + + if (skinningInfo.HasIntToExtMapping && skinningInfo.IntVertices != null) + { + // Use Ext2IntMap to properly map skinning data to mesh vertices + foreach (var extIndex in skinningInfo.Ext2IntMap) + { + var intVertex = skinningInfo.IntVertices[extIndex]; + + // Add bone indices and weights (up to 4 influences per vertex) + for (int i = 0; i < 4; i++) + { + // Remap from CompiledBones index to jointPaths index + int compiledBoneIndex = intVertex.BoneMapping.BoneIndex[i]; + int jointIndex = compiledBoneIndex < _compiledBoneIndexToJointIndex.Length + ? _compiledBoneIndexToJointIndex[compiledBoneIndex] + : compiledBoneIndex; + jointIndices.Add(jointIndex); + jointWeights.Add(intVertex.BoneMapping.Weight[i]); + } + } + } + else if (skinningInfo.BoneMappings != null) + { + // For Ivo format with per-subset vertex extraction, extract bone mappings per-subset + // to match the per-subset vertex extraction in CreateMeshPrim + if (subsets != null) + { + foreach (var subset in subsets) + { + for (int vertexIndex = subset.FirstVertex; vertexIndex < subset.FirstVertex + subset.NumVertices; vertexIndex++) + { + if (vertexIndex >= skinningInfo.BoneMappings.Count) + { + Log.W($"Bone mapping index {vertexIndex} out of bounds (count: {skinningInfo.BoneMappings.Count})"); + // Add default mapping (bone 0 with weight 1) + jointIndices.AddRange([0, 0, 0, 0]); + jointWeights.AddRange([1.0f, 0, 0, 0]); + continue; + } + + var boneMapping = skinningInfo.BoneMappings[vertexIndex]; + for (int i = 0; i < 4; i++) + { + // Remap from CompiledBones index to jointPaths index + int compiledBoneIndex = boneMapping.BoneIndex[i]; + int jointIndex = compiledBoneIndex < _compiledBoneIndexToJointIndex.Length + ? _compiledBoneIndexToJointIndex[compiledBoneIndex] + : compiledBoneIndex; + jointIndices.Add(jointIndex); + jointWeights.Add(boneMapping.Weight[i]); + } + } + } + } + else + { + // Traditional format: use all bone mappings in order + foreach (var boneMapping in skinningInfo.BoneMappings) + { + for (int i = 0; i < 4; i++) + { + // Remap from CompiledBones index to jointPaths index + int compiledBoneIndex = boneMapping.BoneIndex[i]; + int jointIndex = compiledBoneIndex < _compiledBoneIndexToJointIndex.Length + ? _compiledBoneIndexToJointIndex[compiledBoneIndex] + : compiledBoneIndex; + jointIndices.Add(jointIndex); + jointWeights.Add(boneMapping.Weight[i]); + } + } + } + } + else + { + // No skinning data available + return; + } + + // Only add SkelBindingAPI if we have actual skinning data + var skelBindingApi = new Dictionary { ["apiSchemas"] = "[\"SkelBindingAPI\"]" }; + + // Merge with existing properties or create new + if (meshPrim.Properties != null && meshPrim.Properties.Count > 0) + { + // Update existing properties to include SkelBindingAPI + meshPrim.Properties[0].Properties["apiSchemas"] = "[\"MaterialBindingAPI\", \"SkelBindingAPI\"]"; + } + else + meshPrim.Properties = [new UsdProperty(skelBindingApi, true)]; + + // Add geomBindTransform (usually identity matrix) + meshPrim.Attributes.Add(new UsdMatrix4d("primvars:skel:geomBindTransform", Matrix4x4.Identity)); + + // Add skinning arrays with elementSize (influences per vertex) + int elementSize = 4; // CryEngine uses up to 4 bone influences per vertex + meshPrim.Attributes.Add(new UsdIntArray("primvars:skel:jointIndices", jointIndices, elementSize, "vertex")); + meshPrim.Attributes.Add(new UsdFloatArray("primvars:skel:jointWeights", jointWeights, elementSize, "vertex")); + + // Add relationship to skeleton + meshPrim.Attributes.Add(new UsdRelationship("skel:skeleton", "")); + } + + #endregion +} diff --git a/CgfConverter/Renderers/USD/UsdRenderer.cs b/CgfConverter/Renderers/USD/UsdRenderer.cs index 9d39d9a5..6928e004 100644 --- a/CgfConverter/Renderers/USD/UsdRenderer.cs +++ b/CgfConverter/Renderers/USD/UsdRenderer.cs @@ -1,25 +1,32 @@ -using CgfConverter.CryEngineCore; +using CgfConverter.CryEngineCore; using CgfConverter.Models; -using CgfConverter.Models.Materials; -using CgfConverter.Models.Structs; -using CgfConverter.Renderers.USD.Attributes; +using CgfConverter.Models.Shaders; +using CgfConverter.Parsers; using CgfConverter.Renderers.USD.Models; using CgfConverter.Utils; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Numerics; -using static Extensions.FileHandlingExtensions; +using System.Text; namespace CgfConverter.Renderers.USD; -public class UsdRenderer : IRenderer + +/// +/// USD renderer for exporting CryEngine assets to Universal Scene Description format. +/// Organized as partial classes: Main, Materials, Geometry, Skeleton. +/// +public partial class UsdRenderer : IRenderer { protected readonly ArgsHandler _args; protected readonly CryEngine _cryData; private readonly FileInfo usdOutputFile; private UsdSerializer usdSerializer; - private readonly TaggedLogger Log; + protected readonly TaggedLogger Log; + + // Shader system + protected readonly Dictionary _shaderDefinitions; + protected readonly ShaderRulesEngine _shaderRules; public UsdRenderer(ArgsHandler argsHandler, CryEngine cryEngine) { @@ -28,8 +35,48 @@ public UsdRenderer(ArgsHandler argsHandler, CryEngine cryEngine) usdOutputFile = _args.FormatOutputFileName(".usda", _cryData.InputFile); usdSerializer = new UsdSerializer(); Log = _cryData.Log; + + // Initialize shader system + _shaderRules = new ShaderRulesEngine(Log); + _shaderDefinitions = LoadShaderDefinitions(); } + /// + /// Load shader definitions from the Shaders directory. + /// + private Dictionary LoadShaderDefinitions() + { + // Try to find Shaders directory in datadir (objectdir) + if (string.IsNullOrEmpty(_args.DataDir)) + { + Log.D("No datadir specified, shader-based materials disabled"); + return new Dictionary(System.StringComparer.OrdinalIgnoreCase); + } + + var shadersDir = Path.Combine(_args.DataDir, "Shaders"); + if (!Directory.Exists(shadersDir)) + { + Log.D($"Shaders directory not found: {shadersDir}, shader-based materials disabled"); + return new Dictionary(System.StringComparer.OrdinalIgnoreCase); + } + + var parser = new ShaderExtParser(Log); + var shaders = parser.LoadShadersFromDirectory(shadersDir); + + if (shaders.Count > 0) + { + Log.D($"Loaded {shaders.Count} shader definitions from {shadersDir}"); + } + + return shaders; + } + + // Cached skeleton data for multi-file animation export + private Dictionary? _controllerIdToJointPath; + private List? _jointPaths; + private Dictionary? _bonePathMap; + private int[]? _compiledBoneIndexToJointIndex; // Maps CompiledBones array index to jointPaths array index + public int Render() { var usdDoc = GenerateUsdObject(); @@ -40,6 +87,17 @@ public int Render() WriteUsdToFile(usdDoc); + // Export individual animation files for Blender NLA workflow + // (Blender only imports single bound animation, so we export each separately) + if (_controllerIdToJointPath is not null && _jointPaths is not null && _bonePathMap is not null) + { + var animCount = ExportAnimationFiles(_controllerIdToJointPath, _jointPaths, _bonePathMap); + if (animCount > 0) + { + Log.I($"Exported {animCount} separate animation files for Blender NLA workflow"); + } + } + return 0; } @@ -63,334 +121,86 @@ public UsdDoc GenerateUsdObject() usdDoc.Prims.Add(new UsdXform("root", "/")); var rootPrim = usdDoc.Prims[0]; - // Create the node hierarchy as Xforms - rootPrim.Children = CreateNodeHierarchy(); - - rootPrim.Children.Add(CreateMaterials()); + // Check if this model has skeletal animation + bool hasSkeleton = _cryData.SkinningInfo?.HasSkinningInfo ?? false; - return usdDoc; - } - - private UsdPrim CreateMaterials() - { - var scope = new UsdScope("_materials"); - var matList = new List(); - - foreach (var matKey in _cryData.Materials.Keys) // Each mtl file is a Key + if (hasSkeleton) { - foreach (var submat in _cryData.Materials[matKey].SubMaterials) + // Create skeleton hierarchy and cache data for multi-file animation export + Log.D("Model has skeleton with {0} bones", _cryData.SkinningInfo.CompiledBones.Count); + var skelRoot = CreateSkeleton(out _controllerIdToJointPath, out _jointPaths, out _bonePathMap, out _compiledBoneIndexToJointIndex); + rootPrim.Children.Add(skelRoot); + + // Add skinned meshes as children of SkelRoot (siblings of Skeleton) + // All mesh positioning comes from skeletal skinning, not scene graph inheritance + // This avoids double-transform issues where meshes would inherit bone Xform transforms + skelRoot.Children.AddRange(CreateNodeHierarchy()); + + // Create animations if available (DBA or CAF) + // Only include animation in main file if there's exactly one. + // Multiple animations go to separate files for Blender NLA workflow. + bool hasDbaAnimations = _cryData.Animations is not null && _cryData.Animations.Count > 0; + bool hasCafAnimations = _cryData.CafAnimations is not null && _cryData.CafAnimations.Count > 0; + + if (hasDbaAnimations || hasCafAnimations) { - var matName = GetMaterialName(matKey, submat.Name); - var usdMat = new UsdMaterial(CleanPathString(matName)); - usdMat.Attributes.Add(new UsdToken( - "outputs:surface.connect", - $"")); - usdMat.Children.AddRange(CreateShaders(submat, matKey, matName)); - matList.Add(usdMat); - } - } - scope.Children.AddRange(matList); - return scope; - } - - private IEnumerable CreateShaders(Material submat, string matKey, string matName) - { - List shaders = new(); - // Add the PrincipleBSDF shader - var principleBSDF = new UsdShader($"Principled_BSDF"); - principleBSDF.Attributes.Add(new UsdToken("info:id", "UsdPreviewSurface", true)); - principleBSDF.Attributes.Add(new UsdAttribute("inputs:clearcoat", 0)); - principleBSDF.Attributes.Add(new UsdAttribute("inputs:clearcoatRoughness", 0.03f)); - principleBSDF.Attributes.Add(new UsdToken("outputs:surface", null, false)); - shaders.Add(principleBSDF); - - foreach (var texture in submat.Textures) - { - if (texture.Map == Texture.MapTypeEnum.Env) - continue; // Don't add cubemaps as it causes blender to crash - var textureName = Path.ChangeExtension(texture.File, ".dds"); - var imageTexture = CreateUsdImageTextureShader(texture, matName); - if (imageTexture is not null) - { - shaders.Add(imageTexture); - // connect image texture to color input of PrincipledBSDF - if (texture.Map == Texture.MapTypeEnum.Diffuse) + var animations = CreateAnimations(_controllerIdToJointPath, usdDoc.Header); + if (animations.Count == 1) { - imageTexture.Attributes.Add(new UsdFloat3f("outputs:rgb")); - principleBSDF.Attributes.Add(new UsdColor3f( - $"inputs:diffuseColor.connect", - CleanPathString($"/root/_materials/{matName}/{imageTexture.Name}.outputs:rgb"))); + // Single animation - include it in the main file + skelRoot.Children.AddRange(animations); + + var firstAnimName = CleanPathString(animations[0].Name); + var skeleton = skelRoot.Children.OfType().FirstOrDefault(); + if (skeleton is not null) + { + skeleton.Attributes.Add( + new UsdRelationship("skel:animationSource", $"")); + } } - else if (texture.Map == Texture.MapTypeEnum.Normals) + else if (animations.Count > 1) { - imageTexture.Attributes.Add(new UsdFloat3f("outputs:rgb")); + // Multiple animations - they'll be exported to separate files + // Main file gets skeleton + mesh only (no animation bound) + Log.D($"Multiple animations ({animations.Count}) found - excluding from main file, will export separately"); } - //else if (texture.Map == Texture.MapTypeEnum.Env) - //{ - // imageTexture.Attributes.Add(new UsdToken("inputs:type", "cube")); - // imageTexture.Attributes.Add(new UsdColor3f("outputs:rgb", null)); - //} } } - - return shaders; - } - - private UsdShader? CreateUsdImageTextureShader(Texture texture, string matName) - { - var textureFile = ResolveTextureFile(texture.File, _args.PackFileSystem, [_args.DataDir]); - if (File.Exists(textureFile) == false) - { - Log.D("Texture file not found: {0}", texture.File); - return null; - } - var usdImageTexture = new UsdShader(CleanPathString(Path.GetFileNameWithoutExtension(texture.File))); - - usdImageTexture.Attributes.Add(new UsdToken("info:id", "UsdUVTexture", true)); - usdImageTexture.Attributes.Add(new UsdToken("inputs:wrapS", "repeat")); - usdImageTexture.Attributes.Add(new UsdToken("inputs:wrapT", "repeat")); - var texturePath = ResolveTextureFile(texture.File, _args.PackFileSystem, [_args.DataDir]); - usdImageTexture.Attributes.Add(new UsdAsset( - Path.GetFileNameWithoutExtension(texture.File), - CleanPathString(texturePath))); - var isBumpmap = texture.Map == Texture.MapTypeEnum.Normals ? "raw" : "sRGB"; - usdImageTexture.Attributes.Add(new UsdToken("inputs:sourceColorSpace", isBumpmap)); - - return usdImageTexture; - } - - private List CreateNodeHierarchy() - { - List rootNodes = _cryData.Models[0].NodeMap.Values.Where(a => a.ParentNodeID == ~0).ToList(); - List nodes = []; - - foreach (ChunkNode root in rootNodes) + else { - Log.D("Root node: {0}", root.Name); - nodes.Add(CreateNode(root, $"/root/{root.Name}")); + // Create the node hierarchy as Xforms + rootPrim.Children = CreateNodeHierarchy(); } - return nodes; - } - - private UsdXform CreateNode(ChunkNode node, string parentPath) - { - string cleanNodeName = CleanPathString(node.Name); - var xform = new UsdXform(cleanNodeName, parentPath); - - xform.Attributes.Add(new UsdMatrix4d("xformOp:transform", node.Transform)); - xform.Attributes.Add(new UsdToken>("xformOpOrder", ["xformOp:transform"], true)); - // If it's a geometry node, add a UsdMesh - var modelIndex = node._model.IsIvoFile ? 1 : 0; - ChunkNode geometryNode = _cryData.Models.Last().NodeMap.Values.Where(a => a.Name == node.Name).FirstOrDefault(); - - var meshPrim = CreateMeshPrim(node); - if (meshPrim is not null) - xform.Children.Add(meshPrim); - - // Get all the children of the node - var children = node.Children; - if (children is not null) - { - foreach (var childNode in children) - { - xform.Children.Add(CreateNode(childNode, xform.Path)); - } - } + rootPrim.Children.Add(CreateMaterials()); - return xform; + return usdDoc; } - private UsdPrim? CreateMeshPrim(ChunkNode nodeChunk) + /// + /// Clean a string to be a valid USD prim name. + /// USD prim names must start with letter/underscore and contain only letters, digits, and underscores. + /// + protected string CleanPathString(string value) { - // Find the object node that corresponds with this node chunk. If it's - // a mesh chunk, create a collection of UsdMesh prim for each submesh. - // geometryNodeChunk may be the same as nodeChunk for single file models. Otherwise - // it's the matching node to nodeChunk in the second model. - - if (_args.IsNodeNameExcluded(nodeChunk.Name)) - { - Log.D($"Excluding node {nodeChunk.Name}"); - return null; - } - - if (nodeChunk.MeshData is not ChunkMesh meshChunk) - return null; - - if (meshChunk.GeometryInfo is null) // $physics node - return null; - - string nodeName = nodeChunk.Name; + if (string.IsNullOrEmpty(value)) + return "_"; - var subsets = meshChunk.GeometryInfo.GeometrySubsets; - Datastream? indices = meshChunk.GeometryInfo.Indices; - Datastream? uvs = meshChunk.GeometryInfo.UVs; - Datastream? verts = meshChunk.GeometryInfo.Vertices; - Datastream? vertsUvs = meshChunk.GeometryInfo.VertUVs; - Datastream? normals = meshChunk.GeometryInfo.Normals; - Datastream? colors = meshChunk.GeometryInfo.Colors; - - if (verts is null && vertsUvs is null) // There is no vertex data for this node. Skip. - return null; - - List usdMeshes = []; - - if (meshChunk.MeshSubsetsData == 0 - || meshChunk.NumVertices == 0 - || nodeChunk._model.ChunkMap[meshChunk.MeshSubsetsData].ID == 0) - return null; - - var numberOfElements = nodeChunk.MeshData.GeometryInfo?.GeometrySubsets?.Sum(x => x.NumVertices) ?? 0; - - UsdMesh meshPrim = new(CleanPathString(nodeChunk.Name)); - - - - if (verts is not null) + // Replace invalid characters with underscore + var cleaned = new StringBuilder(); + foreach (char c in value) { - int numVerts = (int)verts.NumElements; - var hasNormals = normals is not null; - var hasUVs = uvs is not null; - var hasColors = colors is not null; - - meshPrim.Attributes.Add(new UsdBool("doubleSided", true, true)); - meshPrim.Attributes.Add(new UsdVector3dList("extent", [meshChunk.MinBound, meshChunk.MaxBound])); - meshPrim.Attributes.Add(new UsdIntList("faceVertexCounts", [.. Enumerable.Repeat(3, (int)(indices.NumElements / 3))])); - meshPrim.Attributes.Add(new UsdIntList("faceVertexIndices", [.. indices.Data.Select(x => (int)x)])); - meshPrim.Attributes.Add(new UsdNormalsList("normals", [.. normals.Data])); - meshPrim.Attributes.Add(new UsdPointsList("points", [.. verts.Data])); - meshPrim.Attributes.Add(new UsdColorsList($"{nodeChunk.Name}_color", [.. colors.Data])); - meshPrim.Attributes.Add(new UsdTexCoordsList($"{nodeChunk.Name}_UV", [.. uvs.Data])); - meshPrim.Attributes.Add(new UsdToken("subdivisionScheme", "none", true)); - Dictionary matBindingApi = new() { ["apiSchemas"] = "[\"MaterialBindingAPI\"]" }; - meshPrim.Properties = [new UsdProperty(matBindingApi, true)]; - - foreach (var subset in meshChunk.GeometryInfo.GeometrySubsets ?? []) - { - var index = subset.MatID; - var matName = GetMaterialName( - Path.GetFileNameWithoutExtension(nodeChunk.MaterialFileName), - _cryData.Materials[nodeChunk.MaterialFileName].SubMaterials[index].Name); - // Submesh name should be material name - //var submeshName = - var submeshPrim = new UsdGeomSubset(CleanPathString(matName)); - submeshPrim.Attributes.Add(new UsdUIntList("indices", [.. indices.Data.Skip(subset.FirstIndex).Take(subset.NumIndices)])); - //submeshPrim.Attributes.Add(new UsdToken("familyType", "face", true)); - submeshPrim.Attributes.Add(new UsdToken("elementType", "face", true)); - submeshPrim.Attributes.Add(new UsdToken("familyName", "materialBind", true)); - meshPrim.Children.Add(submeshPrim); - - // Assign material to submesh - - submeshPrim.Properties = [new UsdProperty(matBindingApi, true)]; - submeshPrim.Attributes.Add( - new UsdRelativePath( - "material:binding", - $"/root/_materials/{matName}")); - } + if (char.IsLetterOrDigit(c) || c == '_') + cleaned.Append(c); + else + cleaned.Append('_'); } - else if (vertsUvs is not null) - { - return null; - } - - - - - //var geometryNodeChunk = _cryData.Models.Last().NodeMap.Values.Where(x => x.Name == nodeName).FirstOrDefault(); - //if (geometryNodeChunk is null) - // return null; - - //var meshNodeId = geometryNodeChunk?.ObjectNodeID; - - //var objectNodeChunkType = _cryData.Models.Last().ChunkMap[nodeChunk.ObjectNodeID].GetType(); - //if (!objectNodeChunkType.Name.Contains("ChunkMesh")) - // return null; - - //List usdMeshes = new(); - //var meshChunk = (ChunkMesh)_cryData.Models.Last().ChunkMap[geometryNodeChunk.ObjectNodeID]; - //var meshSubsets = (ChunkMeshSubsets)nodeChunk._model.ChunkMap[meshChunk.MeshSubsetsData]; - //var mtlNameChunk = (ChunkMtlName)_cryData.Models.Last().ChunkMap[nodeChunk.MaterialID]; - //var mtlFileName = mtlNameChunk.Name; - //var key = Path.GetFileNameWithoutExtension(mtlFileName); - //var numberOfSubmeshes = meshSubsets.NumMeshSubset; - - // Get materials for this mesh chunk - //Material[] submats; - //if (_cryData.Materials.ContainsKey(key)) - // submats = _cryData.Materials[key].SubMaterials; - //else - //{ - // submats = _cryData.Materials.FirstOrDefault().Value.SubMaterials; - // mtlFileName = Path.GetFileNameWithoutExtension(_cryData.MaterialFiles.FirstOrDefault()); - //} - //var matName = GetMaterialName(mtlFileName, submats[meshSubsets.MeshSubsets[j].MatID].Name); - - //if (meshChunk.MeshSubsetsData == 0 - // || meshChunk.NumVertices == 0 - // || nodeChunk._model.ChunkMap[meshChunk.MeshSubsetsData].ID == 0) - // return null; - - //// Get datastream chunks for vertices, normals, uvs, indices, colors and tangents - //var vertexChunk = (ChunkDataStream)_cryData.Models.Last().ChunkMap[meshChunk.VerticesData]; - //var normalChunk = (ChunkDataStream)_cryData.Models.Last().ChunkMap[meshChunk.NormalsData]; - //var uvChunk = (ChunkDataStream)_cryData.Models.Last().ChunkMap[meshChunk.UVsData]; - //var indexChunk = (ChunkDataStream)_cryData.Models.Last().ChunkMap[meshChunk.IndicesData]; - //var colorChunk = (ChunkDataStream)_cryData.Models.Last().ChunkMap[meshChunk.ColorsData]; - //var tangentChunk = (ChunkDataStream)_cryData.Models.Last().ChunkMap[meshChunk.TangentsData]; - - ////UsdMesh meshPrim = new(CleanPathString(nodeChunk.Name)); - //meshPrim.Attributes.Add(new UsdBool("doubleSided", true, true)); - //meshPrim.Attributes.Add(new UsdVector3dList("extent", [meshChunk.MinBound, meshChunk.MaxBound])); - //meshPrim.Attributes.Add(new UsdIntList("faceVertexCounts", [.. Enumerable.Repeat(3, (int)indexChunk.NumElements / 3)])); - //meshPrim.Attributes.Add(new UsdIntList("faceVertexIndices", indexChunk.Indices.Select(x => (int)x).ToList())); - //meshPrim.Attributes.Add(new UsdNormalsList("normals", [.. normalChunk.Normals])); - //meshPrim.Attributes.Add(new UsdPointsList("points", [.. vertexChunk.Vertices])); - //meshPrim.Attributes.Add(new UsdColorsList($"{nodeChunk.Name}_color", [.. colorChunk.Colors])); - //meshPrim.Attributes.Add(new UsdTexCoordsList($"{nodeChunk.Name}_UV", [.. uvChunk.UVs])); - //meshPrim.Attributes.Add(new UsdToken("subdivisionScheme", "none", true)); - //Dictionary matBindingApi = new() { ["apiSchemas"] = "[\"MaterialBindingAPI\"]" }; - //meshPrim.Properties = [new UsdProperty(matBindingApi, true)]; - - // Add GeomSubset for each submesh - //for (int j = 0; j < numberOfSubmeshes; j++) - //{ - // var submesh = meshSubsets.MeshSubsets[j]; - // var submeshName = (Path.GetFileNameWithoutExtension(submats[submesh.MatID].Name)); - // var submeshPrim = new UsdGeomSubset(CleanPathString(submeshName)); - // submeshPrim.Attributes.Add(new UsdUIntList("indices", indexChunk.Indices.Skip(submesh.FirstIndex).Take(submesh.NumIndices).ToList())); - // //submeshPrim.Attributes.Add(new UsdToken("familyType", "face", true)); - // submeshPrim.Attributes.Add(new UsdToken("elementType", "vertex", true)); - // submeshPrim.Attributes.Add(new UsdToken("familyName", "materialBind", true)); - // meshPrim.Children.Add(submeshPrim); - - // // Assign material to submesh - // var submatName = _cryData.Materials[key].SubMaterials[submesh.MatID].Name; - // submeshPrim.Properties = [new UsdProperty(matBindingApi, true)]; - // submeshPrim.Attributes.Add( - // new UsdRelativePath( - // "material:binding", - // $"/root/_materials/{GetMaterialName(key, submatName)}")); - //} - - return meshPrim; - } - private string GetMaterialName(string matKey, string submatName) - { - // material name is _mtl_ - var matfileName = Path.GetFileNameWithoutExtension(submatName); + // Ensure it starts with letter or underscore + var result = cleaned.ToString(); + if (char.IsDigit(result[0])) + result = "_" + result; - return $"{matKey}_mtl_{matfileName}".Replace(' ', '_'); - } - - /// If a prim name or value has an @, or starts with a number, it's invalid. - /// Replace @ with _, and if it starts with a digit, add an _ - private string CleanPathString(string value) - { - value = value.Replace('@', '_'); - if (char.IsDigit(value[0])) - value = "_" + value; - return value; + return result; } } diff --git a/CgfConverter/Renderers/USD/UsdSerializer.cs b/CgfConverter/Renderers/USD/UsdSerializer.cs index 94c2115f..b1d46482 100644 --- a/CgfConverter/Renderers/USD/UsdSerializer.cs +++ b/CgfConverter/Renderers/USD/UsdSerializer.cs @@ -81,9 +81,4 @@ private void SerializeObject(UsdPrim prim, StringBuilder sb, int indentLevel) sb.AppendLine("}"); } } - - private static void AppendIndent(StringBuilder sb, int indentLevel) - { - sb.Append(new string(' ', indentLevel * 4)); - } } diff --git a/CgfConverter/Services/ArgsHandler.cs b/CgfConverter/Services/ArgsHandler.cs index e25d60b6..97a6ddf7 100644 --- a/CgfConverter/Services/ArgsHandler.cs +++ b/CgfConverter/Services/ArgsHandler.cs @@ -47,6 +47,8 @@ public sealed class ArgsHandler public bool OutputGLTF { get; internal set; } /// Render glTF binary (default behavior) public bool OutputGLB { get; internal set; } + /// Render USD format files + public bool OutputUSD { get; internal set; } /// Smooth Faces public bool Smooth { get; internal set; } /// Flag used to indicate we should convert texture paths to use TIFF instead of DDS @@ -177,6 +179,10 @@ public int ProcessArgs(string[] inputArgs) case "-collada": OutputCollada = true; break; + case "-usd": + case "-usda": + OutputUSD = true; + break; case "-tif": case "-tiff": TiffTextures = true; @@ -302,6 +308,8 @@ public int ProcessArgs(string[] inputArgs) HelperMethods.Log(LogLevelEnum.Info, "Output format set to glTF (.gltf)"); if (OutputGLB) HelperMethods.Log(LogLevelEnum.Info, "Output format set to glTF Binary (.glb)"); + if (OutputUSD) + HelperMethods.Log(LogLevelEnum.Info, "Output format set to USD (.usda)"); if (AllowConflicts) HelperMethods.Log(LogLevelEnum.Info, "Allow conflicts for mtl files enabled"); @@ -371,9 +379,9 @@ public int ProcessArgs(string[] inputArgs) ExcludeMaterialNameRegexes.AddRange(ExcludeMaterialNames.Select(x => new Regex(x, RegexOptions.Compiled | RegexOptions.IgnoreCase))); ExcludeShaderNameRegexes.AddRange(ExcludeShaderNames.Select(x => new Regex(x, RegexOptions.Compiled | RegexOptions.IgnoreCase))); - // Default to Collada format - if (!OutputCollada && !OutputWavefront && !OutputGLB && !OutputGLTF) - OutputCollada = true; + // Default to USD format + if (!OutputCollada && !OutputWavefront && !OutputGLB && !OutputGLTF && !OutputUSD) + OutputUSD = true; return 0; } @@ -393,10 +401,11 @@ public static void PrintUsage() Console.WriteLine(" Defaults to current directory. Some packfile formats may accept additional options in the form of some.pack.file?key=value&key2=value2."); Console.WriteLine("-mtl/mat/material: (Optional) The material file to use."); Console.WriteLine(); - Console.WriteLine(" Export formats. By default -dae is used."); - Console.WriteLine("-dae: Export Collada format files."); + Console.WriteLine(" Export formats. By default -usd is used."); + Console.WriteLine("-usd/-usda: Export USD format files (default)."); + Console.WriteLine("-dae: Export Collada format files."); Console.WriteLine("-glb: Export glb (glTF binary) files."); - Console.WriteLine("-gltf: Export file pairs of glTF and bin files."); + Console.WriteLine("-gltf: Export file pairs of glTF and bin files."); Console.WriteLine("-obj: Export Wavefront format files (Not supported)."); Console.WriteLine(); Console.WriteLine(" Texture Options. By default the converter will look for DDS files."); diff --git a/CgfConverter/Utilities/BinaryReaderExtensions.cs b/CgfConverter/Utilities/BinaryReaderExtensions.cs index ae5b6b7f..eed8e2dc 100644 --- a/CgfConverter/Utilities/BinaryReaderExtensions.cs +++ b/CgfConverter/Utilities/BinaryReaderExtensions.cs @@ -461,4 +461,13 @@ public static void ReadInto(this BinaryReader reader, out T value) where T : public static void AlignTo(this BinaryReader reader, int unit) => reader.BaseStream.Position = (reader.BaseStream.Position + unit - 1) / unit * unit; + + public static IvoTangentFrame ReadIvoTangentFrame(this BinaryReader r) => + new() + { + Word0 = r.ReadUInt16(), + Word1 = r.ReadUInt16(), + Word2 = r.ReadUInt16(), + Word3 = r.ReadUInt16() + }; } diff --git a/CgfConverter/Utilities/Utils.cs b/CgfConverter/Utilities/Utils.cs index 37a92807..64023b60 100644 --- a/CgfConverter/Utilities/Utils.cs +++ b/CgfConverter/Utilities/Utils.cs @@ -61,21 +61,55 @@ public static void CleanNumbers(StringBuilder sb) sb.Replace("-1.000000", "-1"); } - internal static List GetNullSeparatedStrings(int numberOfNames, BinaryReader b) + /// + /// Reads null-separated strings from a BinaryReader. Prefer the bounded overload when string table size is known. + /// + public static List GetNullSeparatedStrings(int numberOfNames, BinaryReader b) { List names = []; - StringBuilder builder = new(); + List buffer = []; for (int i = 0; i < numberOfNames; i++) { - char c = b.ReadChar(); + byte c = b.ReadByte(); while (c != 0) { - builder.Append(c); - c = b.ReadChar(); + buffer.Add(c); + c = b.ReadByte(); } - names.Add(builder.ToString()); - builder.Clear(); + names.Add(Encoding.UTF8.GetString(buffer.ToArray())); + buffer.Clear(); + } + + return names; + } + + /// + /// Reads null-separated strings from a BinaryReader with a known byte limit. + /// This is the safer version - it prevents reading beyond the string table. + /// + public static List GetNullSeparatedStrings(int numberOfNames, int byteCount, BinaryReader b) + { + byte[] buffer = b.ReadBytes(byteCount); + return GetNullSeparatedStrings(numberOfNames, buffer); + } + + /// + /// Parses null-separated strings from a byte buffer. + /// + public static List GetNullSeparatedStrings(int numberOfNames, byte[] buffer) + { + List names = []; + int start = 0; + + for (int i = 0; i < numberOfNames && start < buffer.Length; i++) + { + int end = Array.IndexOf(buffer, (byte)0, start); + if (end < 0) + end = buffer.Length; + + names.Add(Encoding.UTF8.GetString(buffer, start, end - start)); + start = end + 1; } return names; diff --git a/CgfConverterIntegrationTests/IntegrationTests/ArmoredWarfare/AWIntegrationTests.cs b/CgfConverterIntegrationTests/IntegrationTests/ArmoredWarfare/AWIntegrationTests.cs index ff20650f..8b06c98b 100644 --- a/CgfConverterIntegrationTests/IntegrationTests/ArmoredWarfare/AWIntegrationTests.cs +++ b/CgfConverterIntegrationTests/IntegrationTests/ArmoredWarfare/AWIntegrationTests.cs @@ -2,9 +2,12 @@ using CgfConverter.Renderers.Collada; using CgfConverter.Renderers.Collada.Collada.Enums; using CgfConverter.Renderers.Gltf; +using CgfConverter.Renderers.USD; +using CgfConverter.Renderers.USD.Models; using CgfConverterTests.TestUtilities; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Globalization; +using System.IO; using System.Linq; using System.Threading; @@ -272,4 +275,122 @@ public void T62_Turret() var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); colladaData.GenerateDaeObject(); } + + [TestMethod] + public void Chicken_Usd() + { + // Verify skeleton, joints, and skinning for chicken model (ChunkController_829) + var args = new string[] { $@"d:\depot\armoredwarfare\objects\characters\animals\birds\chicken\chicken.chr" }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + // Generate USD + UsdRenderer usdRenderer = new(testUtils.argsHandler, cryData); + var usdDoc = usdRenderer.GenerateUsdObject(); + + // Serialize to string for inspection + var serializer = new UsdSerializer(); + using var writer = new StringWriter(); + serializer.Serialize(usdDoc, writer); + var usdOutput = writer.ToString(); + + // Verify basic structure + Assert.IsTrue(usdOutput.Contains("def SkelRoot \"Armature\""), "Should have SkelRoot"); + Assert.IsTrue(usdOutput.Contains("def Skeleton \"Skeleton\""), "Should have Skeleton"); + + // Verify skeleton data arrays + Assert.IsTrue(usdOutput.Contains("uniform token[] joints"), "Should have joints array"); + Assert.IsTrue(usdOutput.Contains("uniform matrix4d[] bindTransforms"), "Should have bindTransforms"); + Assert.IsTrue(usdOutput.Contains("uniform matrix4d[] restTransforms"), "Should have restTransforms"); + + // Verify joint hierarchy (15 joints total) + Assert.IsTrue(usdOutput.Contains("\"Bip01_\""), "Should have root joint Bip01_"); + Assert.IsTrue(usdOutput.Contains("\"Bip01_/Bip01_Pelvis\""), "Should have Pelvis joint"); + Assert.IsTrue(usdOutput.Contains("\"Bip01_/Bip01_Pelvis/Bip01_LLegThigh\""), "Should have LLegThigh joint"); + Assert.IsTrue(usdOutput.Contains("\"Bip01_/Bip01_Pelvis/Bip01_RLegThigh\""), "Should have RLegThigh joint"); + Assert.IsTrue(usdOutput.Contains("\"Bip01_/Bip01_Pelvis/Bip01_Spine1\""), "Should have Spine1 joint"); + Assert.IsTrue(usdOutput.Contains("\"Bip01_/Bip01_Pelvis/Bip01_Tail\""), "Should have Tail joint"); + Assert.IsTrue(usdOutput.Contains("\"Bip01_/Bip01_Pelvis/Bip01_Spine1/Bip01_Spine2/Bip01_Spine3/Bip01_Head\""), "Should have Head joint"); + + // Verify mesh skinning attributes + Assert.IsTrue(usdOutput.Contains("primvars:skel:geomBindTransform"), "Should have geomBindTransform"); + Assert.IsTrue(usdOutput.Contains("primvars:skel:jointIndices"), "Should have jointIndices"); + Assert.IsTrue(usdOutput.Contains("primvars:skel:jointWeights"), "Should have jointWeights"); + Assert.IsTrue(usdOutput.Contains("rel skel:skeleton"), "Should have skeleton relationship"); + Assert.IsTrue(usdOutput.Contains(""), "Should reference skeleton path"); + + // Verify mesh geometry + Assert.IsTrue(usdOutput.Contains("def Mesh \"Chicken\""), "Should have Chicken mesh"); + } + + [TestMethod] + public void Chicken_WalkAnimation_Usd() + { + // Verify animation export for chicken walk loop (ChunkController_829 animation) + // The chrparams file at chicken.chrparams defines animations that are auto-loaded + // When multiple animations exist, they're exported to separate files + var modelFile = $@"d:\depot\armoredwarfare\objects\characters\animals\birds\chicken\chicken.chr"; + var modelDir = Path.GetDirectoryName(modelFile)!; + + var args = new string[] { modelFile, "-usd", "-objectdir", objectDir }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + var cryData = new CryEngine(modelFile, testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + // Animations are loaded automatically via chrparams + Assert.IsNotNull(cryData.CafAnimations, "Should have CAF animations loaded"); + Assert.IsTrue(cryData.CafAnimations.Count >= 2, "Should have at least 2 CAF animations (walk_loop, idle01)"); + + // Verify walk_loop animation is present + var walkAnimation = cryData.CafAnimations.FirstOrDefault(a => a.Name.Contains("walk_loop")); + Assert.IsNotNull(walkAnimation, "Should have walk_loop animation"); + + // Verify animation has bone tracks (14 bones animated, Tail excluded) + Assert.AreEqual(14, walkAnimation.BoneTracks.Count, "Walk loop should have 14 bone tracks"); + + // Generate and write USD (animations go to separate files) + UsdRenderer usdRenderer = new(testUtils.argsHandler, cryData); + usdRenderer.Render(); + + // Verify animation file was created + var walkAnimFile = Path.Combine(modelDir, "chicken_anim_walk_loop.usda"); + Assert.IsTrue(File.Exists(walkAnimFile), $"Animation file should exist: {walkAnimFile}"); + + // Read the animation file to verify content + var animContent = File.ReadAllText(walkAnimFile); + + // Verify animation structure + Assert.IsTrue(animContent.Contains("def SkelAnimation"), "Should have SkelAnimation"); + Assert.IsTrue(animContent.Contains("walk_loop"), "Should have walk_loop animation name"); + + // Verify animation has timeSamples + Assert.IsTrue(animContent.Contains("translations.timeSamples"), "Should have translation timeSamples"); + Assert.IsTrue(animContent.Contains("rotations.timeSamples"), "Should have rotation timeSamples"); + Assert.IsTrue(animContent.Contains("scales.timeSamples"), "Should have scale timeSamples"); + + // Verify animation time range (31 frames, 0-30) + Assert.IsTrue(animContent.Contains("startTimeCode = 0"), "Should start at frame 0"); + Assert.IsTrue(animContent.Contains("endTimeCode = 30"), "Should end at frame 30"); + + // Verify animation joints (14 animated joints, excludes Tail) + Assert.IsTrue(animContent.Contains("\"Bip01_/Bip01_Pelvis\""), "Animation should have Pelvis joint"); + Assert.IsTrue(animContent.Contains("\"Bip01_/Bip01_Pelvis/Bip01_LLegThigh\""), "Animation should have LLegThigh joint"); + + // Verify skeleton reference + Assert.IsTrue(animContent.Contains("rel skel:animationSource"), "Should have animationSource relationship"); + + // Verify pelvis translation values are reasonable (not doubled due to previous bug) + // Frame 0 pelvis: (-0, 0.007701, 0.102881) - Z should be ~0.10, not ~0.22 + Assert.IsTrue(animContent.Contains("0.102881") || animContent.Contains("0.10288"), + "Pelvis Z translation at frame 0 should be ~0.102881 (not doubled)"); + + // Verify frame 7 pelvis Z (highest value ~0.120) + Assert.IsTrue(animContent.Contains("0.120023") || animContent.Contains("0.12002"), + "Pelvis Z translation at frame 7 should be ~0.120023"); + } } diff --git a/CgfConverterIntegrationTests/IntegrationTests/KCD2/Kcd2Tests.cs b/CgfConverterIntegrationTests/IntegrationTests/KCD2/Kcd2Tests.cs index d538ddaa..85942c57 100644 --- a/CgfConverterIntegrationTests/IntegrationTests/KCD2/Kcd2Tests.cs +++ b/CgfConverterIntegrationTests/IntegrationTests/KCD2/Kcd2Tests.cs @@ -1,12 +1,10 @@ -using CgfConverter.Renderers.Collada; -using CgfConverter; +using CgfConverter; +using CgfConverter.Renderers.Collada; +using CgfConverter.Utils; using CgfConverterTests.TestUtilities; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; using System.Globalization; using System.Threading; -using CgfConverter.Utils; -using System.Linq; namespace CgfConverterTests.IntegrationTests; @@ -15,7 +13,6 @@ namespace CgfConverterTests.IntegrationTests; public class Kcd2Tests { private readonly TestUtils testUtils = new(); - string userHome; private readonly string objectDir = @"d:\depot\kcd2"; [TestInitialize] @@ -24,7 +21,6 @@ public void Initialize() CultureInfo customCulture = (CultureInfo)Thread.CurrentThread.CurrentCulture.Clone(); customCulture.NumberFormat.NumberDecimalSeparator = "."; Thread.CurrentThread.CurrentCulture = customCulture; - userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); testUtils.GetSchemaSet(); } @@ -135,4 +131,23 @@ public void Boar_unsplit() Assert.AreEqual(13, textures.Length); Assert.AreEqual("boar_mtl_boar_hair_Diffuse", textures[0].Name); } + + [TestMethod] + public void lvl2_door_a_left_skin() + { + var args = new string[] + { + $@"{objectDir}\Objects\characters\assets\doors\lvl2_door_a_left.skin", "-dds", "-dae", "-ut", "-objectdir", objectDir + }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); + var daeObject = colladaData.DaeObject; + colladaData.GenerateDaeObject(); + testUtils.ValidateColladaXml(colladaData); + + } } diff --git a/CgfConverterIntegrationTests/IntegrationTests/MWO/MWOIntegrationTests.cs b/CgfConverterIntegrationTests/IntegrationTests/MWO/MWOIntegrationTests.cs index 20f6d982..f34beb77 100644 --- a/CgfConverterIntegrationTests/IntegrationTests/MWO/MWOIntegrationTests.cs +++ b/CgfConverterIntegrationTests/IntegrationTests/MWO/MWOIntegrationTests.cs @@ -5,6 +5,7 @@ using CgfConverter.Renderers.Collada.Collada.Enums; using CgfConverter.Renderers.Gltf; using CgfConverter.Renderers.USD; +using CgfConverter.Renderers.USD.Models; using CgfConverterIntegrationTests.Extensions; using CgfConverterTests.TestUtilities; using Extensions; @@ -13,6 +14,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Numerics; using System.Threading; using System.Xml.Linq; using System.Xml.Serialization; @@ -96,6 +98,130 @@ public void Box_Gltf() gltfData.GenerateGltfObject(); } + [TestMethod] + public void Box_Usd() + { + var args = new string[] { $@"{objectDir}\Objects\default\box.cgf", "-dds", "-dae", "-objectdir", objectDir }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem); + cryData.ProcessCryengineFiles(); + + UsdRenderer usdData = new(testUtils.argsHandler, cryData); + usdData.GenerateUsdObject(); + } + + [TestMethod] + public void Box_Usd_WithMaterials() + { + var matFile = $@"{objectDir}\Objects\default\box.mtl"; + var args = new string[] { $@"{objectDir}\Objects\default\box.cgf", "-objectdir", objectDir, "-mat", matFile }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, materialFiles: matFile, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + // Verify materials were loaded + Assert.IsTrue(cryData.Materials.Count > 0, "Materials should be loaded"); + var firstMat = cryData.Materials.First().Value; + Assert.IsTrue(firstMat.SubMaterials.Length >= 2, "Should have at least 2 submaterials"); + + // Generate USD + UsdRenderer usdRenderer = new(testUtils.argsHandler, cryData); + var usdDoc = usdRenderer.GenerateUsdObject(); + + // Serialize to string for inspection + var serializer = new UsdSerializer(); + using var writer = new StringWriter(); + serializer.Serialize(usdDoc, writer); + var usdOutput = writer.ToString(); + + // Verify material properties are in output + // Material #25: Diffuse="0.588,0.588,0.588" Opacity="1" + Assert.IsTrue(usdOutput.Contains("inputs:diffuseColor"), "Should have diffuseColor"); + Assert.IsTrue(usdOutput.Contains("inputs:opacity"), "Should have opacity"); + Assert.IsTrue(usdOutput.Contains("inputs:roughness"), "Should have roughness"); + Assert.IsTrue(usdOutput.Contains("inputs:metallic"), "Should have metallic"); + + // Verify colors use parentheses (not angle brackets) + Assert.IsTrue(usdOutput.Contains("inputs:diffuseColor = ("), "Color values should use parentheses"); + Assert.IsFalse(usdOutput.Contains("inputs:diffuseColor = <0"), "Color values should NOT use angle brackets for values"); + + // Material #26: Diffuse="0.658824,0,0" (red) Opacity="0.24" + Assert.IsTrue(usdOutput.Contains("0.658824"), "Should have red diffuse color from Material #26"); + Assert.IsTrue(usdOutput.Contains("0.24"), "Should have 0.24 opacity from Material #26"); + + // Verify material names are cleaned + Assert.IsTrue(usdOutput.Contains("Material__25"), "Material #25 should be cleaned to Material__25"); + Assert.IsTrue(usdOutput.Contains("Material__26"), "Material #26 should be cleaned to Material__26"); + + // Verify GeomSubset uses face indices (not vertex indices) + // The box has 12 triangles, so face indices should be 0-11 + Assert.IsTrue(usdOutput.Contains("elementType = \"face\""), "GeomSubset should use face element type"); + // Should NOT contain raw vertex indices like [0, 1, 2, 2, 3, 0, ...] + Assert.IsFalse(usdOutput.Contains("indices = [0, 1, 2, 2, 3, 0"), "Should not use vertex indices for face elementType"); + + // Optional: write to file for manual inspection + // usdRenderer.WriteUsdToFile(usdDoc); + } + + [TestMethod] + public void HulaGirl_Usd_WithMaterials() + { + var modelFile = $@"{objectDir}\Objects\purchasable\cockpit_standing\hulagirl\hulagirl_a.cga"; + + // Check if file exists + if (!File.Exists(modelFile)) + { + Assert.Inconclusive($"Model file not found: {modelFile}"); + return; + } + + var args = new string[] { modelFile, "-objectdir", objectDir }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + // Let materials auto-discover from MtlName chunk + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + // Verify materials were loaded + Assert.IsTrue(cryData.Materials.Count > 0, "Materials should be loaded"); + + // Check if materials have textures + var firstMat = cryData.Materials.First().Value; + if (firstMat.SubMaterials != null && firstMat.SubMaterials.Length > 0) + { + var submat = firstMat.SubMaterials[0]; + if (submat.Textures != null) + { + foreach (var texture in submat.Textures) + { + // This is where you can set a breakpoint to inspect texture.File + Console.WriteLine($"Texture Map: {texture.Map}, File: {texture.File ?? "NULL"}"); + } + } + } + + // Generate USD + UsdRenderer usdRenderer = new(testUtils.argsHandler, cryData); + var usdDoc = usdRenderer.GenerateUsdObject(); + + // Serialize to string for inspection + var serializer = new UsdSerializer(); + using var writer = new StringWriter(); + serializer.Serialize(usdDoc, writer); + var usdOutput = writer.ToString(); + + // Verify material properties are in output + Assert.IsTrue(usdOutput.Contains("inputs:diffuseColor"), "Should have diffuseColor"); + + // Optional: write to file for manual inspection + // usdRenderer.WriteUsdToFile(usdDoc); + } + [TestMethod] public void Teapot_Collada() { @@ -256,12 +382,162 @@ public void Adder_VerifyArmatureAndAnimations_Collada() var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); colladaData.GenerateDaeObject(); var daeObject = colladaData.DaeObject; + + // === Library Materials === + var materials = daeObject.Library_Materials; + Assert.AreEqual(11, materials.Material.Length); + Assert.AreEqual("adder_mtl_centre_torso", materials.Material[0].Name); + Assert.AreEqual("adder_mtl_centre_torso-material", materials.Material[0].ID); + Assert.AreEqual("#adder_mtl_centre_torso-effect", materials.Material[0].Instance_Effect.URL); + Assert.AreEqual("adder_mtl_right_torso", materials.Material[1].Name); + Assert.AreEqual("adder_mtl_left_torso", materials.Material[2].Name); + Assert.AreEqual("adder_mtl_left_arm", materials.Material[3].Name); + Assert.AreEqual("adder_mtl_right_arm", materials.Material[4].Name); + Assert.AreEqual("adder_mtl_left_leg", materials.Material[5].Name); + Assert.AreEqual("adder_mtl_right_leg", materials.Material[6].Name); + Assert.AreEqual("adder_mtl_head", materials.Material[7].Name); + + // === Library Images (no textures in this test) === + Assert.AreEqual(0, daeObject.Library_Images.Image.Length); + + // === Library Geometries === + var geometries = daeObject.Library_Geometries; + Assert.AreEqual(1, geometries.Geometry.Length); + var geometry = geometries.Geometry[0]; + Assert.AreEqual("adder-mesh", geometry.ID); + Assert.AreEqual("adder", geometry.Name); + + var mesh = geometry.Mesh; + Assert.AreEqual(4, mesh.Source.Length); // pos, norm, UV, color + + // Verify positions source + var posSource = mesh.Source.First(s => s.ID == "adder-mesh-pos"); + Assert.AreEqual(9, posSource.Float_Array.Count); // 3 vertices * 3 components + Assert.AreEqual("adder-mesh-pos-array", posSource.Float_Array.ID); + Assert.AreEqual((uint)3, posSource.Technique_Common.Accessor.Count); // 3 vertices + Assert.AreEqual((uint)3, posSource.Technique_Common.Accessor.Stride); // x, y, z + + // Verify normals source + var normSource = mesh.Source.First(s => s.ID == "adder-mesh-norm"); + Assert.AreEqual(9, normSource.Float_Array.Count); + Assert.AreEqual("adder-mesh-norm-array", normSource.Float_Array.ID); + + // Verify UV source + var uvSource = mesh.Source.First(s => s.ID == "adder-mesh-UV"); + Assert.AreEqual(6, uvSource.Float_Array.Count); // 3 UVs * 2 components + Assert.AreEqual("adder-mesh-UV-array", uvSource.Float_Array.ID); + Assert.AreEqual((uint)3, uvSource.Technique_Common.Accessor.Count); + Assert.AreEqual((uint)2, uvSource.Technique_Common.Accessor.Stride); // s, t + + // Verify triangles + Assert.AreEqual(1, mesh.Triangles.Length); + Assert.AreEqual(1, mesh.Triangles[0].Count); // 1 triangle + Assert.AreEqual("adder_mtl_centre_torso-material", mesh.Triangles[0].Material); + + // Verify vertices reference + Assert.AreEqual("adder-vertices", mesh.Vertices.ID); + Assert.AreEqual("#adder-mesh-pos", mesh.Vertices.Input[0].source); + + // === Library Controllers === + var controllers = daeObject.Library_Controllers; + Assert.AreEqual(1, controllers.Controller.Length); + var controller = controllers.Controller[0]; + Assert.AreEqual("Controller", controller.ID); + + var skin = controller.Skin; + Assert.AreEqual("#adder-mesh", skin.source); + Assert.AreEqual("1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1", skin.Bind_Shape_Matrix.Value_As_String); + + // Verify joints source + Assert.AreEqual(3, skin.Source.Length); + var jointsSource = skin.Source.First(s => s.ID == "Controller-joints"); + Assert.AreEqual(73, jointsSource.Name_Array.Count); + Assert.AreEqual("Controller-joints-array", jointsSource.Name_Array.ID); + var jointNames = jointsSource.Name_Array.Value(); + Assert.AreEqual(73, jointNames.Length); + Assert.AreEqual("Bip01", jointNames[0]); + Assert.AreEqual("Bip01_Pelvis", jointNames[1]); + Assert.AreEqual("Bip01_L_Hip", jointNames[2]); + Assert.IsTrue(jointNames.Contains("Bip01_R_Toe0Nub")); // Last joint + + // Verify bind poses source + var bindPosesSource = skin.Source.First(s => s.ID == "Controller-bind_poses"); + Assert.AreEqual(1168, bindPosesSource.Float_Array.Count); // 73 joints * 16 matrix elements + Assert.AreEqual("Controller-bind_poses-array", bindPosesSource.Float_Array.ID); + Assert.AreEqual((uint)73, bindPosesSource.Technique_Common.Accessor.Count); + Assert.AreEqual((uint)16, bindPosesSource.Technique_Common.Accessor.Stride); + + // Verify weights source + var weightsSource = skin.Source.First(s => s.ID == "Controller-weights"); + Assert.AreEqual((uint)12, weightsSource.Technique_Common.Accessor.Count); + Assert.AreEqual("Controller-weights-array", weightsSource.Float_Array.ID); + + // Verify vertex_weights + Assert.AreEqual(3, skin.Vertex_Weights.Count); + + // === Library Visual Scenes === + var visualScene = daeObject.Library_Visual_Scene.Visual_Scene[0]; + Assert.AreEqual("Scene", visualScene.ID); + + // Verify armature root node (Bip01) + var rootNode = visualScene.Node[0]; + Assert.AreEqual("Bip01", rootNode.ID); + Assert.AreEqual("Bip01", rootNode.Name); + Assert.AreEqual("Bip01", rootNode.sID); + Assert.AreEqual(ColladaNodeType.JOINT, rootNode.Type); + Assert.AreEqual("-0 1 -0 0 -1 -0 0 0 0 0 1 -0 0 0 0 1", rootNode.Matrix[0].Value_As_String); + + // Verify Bip01_Pelvis child node + var pelvisNode = rootNode.node[0]; + Assert.AreEqual("Bip01_Pelvis", pelvisNode.ID); + Assert.AreEqual("Bip01_Pelvis", pelvisNode.Name); + Assert.AreEqual(ColladaNodeType.JOINT, pelvisNode.Type); + Assert.AreEqual("-0 -0 1 -6.997413 -0 1 0 -0 -1 -0 -0 0 0 0 0 1", pelvisNode.Matrix[0].Value_As_String); + + // Verify pelvis has correct children (L_Hip, Pitch, R_Hip) + Assert.AreEqual(3, pelvisNode.node.Length); + var leftHipNode = pelvisNode.node.FirstOrDefault(n => n.ID == "Bip01_L_Hip"); + var pitchNode = pelvisNode.node.FirstOrDefault(n => n.ID == "Bip01_Pitch"); + var rightHipNode = pelvisNode.node.FirstOrDefault(n => n.ID == "Bip01_R_Hip"); + Assert.IsNotNull(leftHipNode); + Assert.IsNotNull(pitchNode); + Assert.IsNotNull(rightHipNode); + + // Verify L_Hip transform + Assert.AreEqual("-1 -0 -0 -0.971270 -0 -0.022217 0.999753 -6.995683 -0 0.999753 0.022217 -0.155460 0 0 0 1", leftHipNode.Matrix[0].Value_As_String); + + // Verify Pitch transform + Assert.AreEqual("-0 -0 1 -7.570391 -0 1 0 -0 -1 -0 -0 0 0 0 0 1", pitchNode.Matrix[0].Value_As_String); + + // Verify geometry node with controller instance + Assert.AreEqual(2, visualScene.Node.Length); + var geometryNode = visualScene.Node[1]; + Assert.AreEqual("adder", geometryNode.ID); + Assert.AreEqual("adder", geometryNode.Name); + Assert.AreEqual(ColladaNodeType.NODE, geometryNode.Type); + Assert.IsNull(geometryNode.Instance_Geometry); // No direct geometry, uses controller + Assert.AreEqual(1, geometryNode.Instance_Controller.Length); + Assert.AreEqual("#Controller", geometryNode.Instance_Controller[0].URL); + Assert.AreEqual("#Bip01", geometryNode.Instance_Controller[0].Skeleton[0].Value); + + // Verify material binding on controller instance + var bindMaterial = geometryNode.Instance_Controller[0].Bind_Material[0]; + Assert.IsNotNull(bindMaterial); + Assert.AreEqual(1, bindMaterial.Technique_Common.Instance_Material.Length); + Assert.AreEqual("adder_mtl_centre_torso-material", bindMaterial.Technique_Common.Instance_Material[0].Symbol); + Assert.AreEqual("#adder_mtl_centre_torso-material", bindMaterial.Technique_Common.Instance_Material[0].Target); + + // === Library Animations === + Assert.IsNotNull(daeObject.Library_Animations); + Assert.IsTrue(daeObject.Library_Animations.Animation.Length > 0, "Should have animations from DBA files"); + + testUtils.ValidateColladaXml(colladaData); } [TestMethod] public void Adder_VerifyArmatureAndAnimations_Gltf() { - var args = new string[] { $@"d:\depot\mwo\objects\mechs\adder\body\adder.chr", "-dds", "-dae", "-objectdir", objectDir }; + var args = new string[] { $@"d:\depot\mwo\objects\mechs\adder\body\adder.chr", "-dds", "-gltf", "-objectdir", objectDir }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); @@ -270,6 +546,82 @@ public void Adder_VerifyArmatureAndAnimations_Gltf() GltfModelRenderer gltfRenderer = new(testUtils.argsHandler, cryData, true, false); var gltfData = gltfRenderer.GenerateGltfObject(); + + // === Materials === + Assert.AreEqual(11, gltfData.Materials.Count); + Assert.AreEqual("centre_torso", gltfData.Materials[0].Name); + Assert.AreEqual("right_torso", gltfData.Materials[1].Name); + Assert.AreEqual("left_torso", gltfData.Materials[2].Name); + Assert.AreEqual("left_arm", gltfData.Materials[3].Name); + Assert.AreEqual("right_arm", gltfData.Materials[4].Name); + Assert.AreEqual("left_leg", gltfData.Materials[5].Name); + Assert.AreEqual("right_leg", gltfData.Materials[6].Name); + Assert.AreEqual("head", gltfData.Materials[7].Name); + + // === Meshes === + Assert.AreEqual(1, gltfData.Meshes.Count); + Assert.AreEqual("adder/mesh", gltfData.Meshes[0].Name); + Assert.AreEqual(1, gltfData.Meshes[0].Primitives.Count); + + // === Nodes (skeleton) === + Assert.IsTrue(gltfData.Nodes.Count >= 73, "Should have at least 73 bone nodes"); + Assert.AreEqual("Bip01", gltfData.Nodes[0].Name); + Assert.AreEqual("Bip01 Pelvis", gltfData.Nodes[1].Name); // glTF uses spaces, not underscores + + // Verify root bone rotation (Bip01 transform) - quaternion [x, y, z, w] + AssertExtensions.AreEqual([0.0f, 0.70710677f, 0.0f, 0.70710677f], gltfData.Nodes[0].Rotation, 1e-5f); + + // === Skins === + Assert.AreEqual(1, gltfData.Skins.Count); + Assert.AreEqual(73, gltfData.Skins[0].Joints.Count); + Assert.AreEqual("adder/skin", gltfData.Skins[0].Name); + + // === Animations === + Assert.IsTrue(gltfData.Animations.Count > 0, "Should have animations from DBA files"); + } + + [TestMethod] + public void Adder_VerifyArmatureAndAnimations_Usd() + { + var args = new string[] { $@"d:\depot\mwo\objects\mechs\adder\body\adder.chr", "-usd", "-objectdir", objectDir }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem); + cryData.ProcessCryengineFiles(); + + UsdRenderer usdRenderer = new(testUtils.argsHandler, cryData); + var usdDoc = usdRenderer.GenerateUsdObject(); + + // Serialize to string for inspection + var serializer = new UsdSerializer(); + using var writer = new StringWriter(); + serializer.Serialize(usdDoc, writer); + var usdOutput = writer.ToString(); + + // Verify root prim exists + Assert.IsTrue(usdOutput.Contains("def Xform \"adder\""), "Should have root prim"); + + // Verify skeleton exists + Assert.IsTrue(usdOutput.Contains("def Skeleton \"Skeleton\""), "Should have skeleton"); + + // Verify mesh exists with skin binding + Assert.IsTrue(usdOutput.Contains("def Mesh \"adder\"") || usdOutput.Contains("def Mesh \"Mesh\""), "Should have mesh"); + + // Verify skeleton joints include key bones + Assert.IsTrue(usdOutput.Contains("Bip01"), "Skeleton should include Bip01"); + Assert.IsTrue(usdOutput.Contains("Bip01_Pelvis"), "Skeleton should include Bip01_Pelvis"); + Assert.IsTrue(usdOutput.Contains("Bip01_L_Hip"), "Skeleton should include Bip01_L_Hip"); + Assert.IsTrue(usdOutput.Contains("Bip01_R_Hip"), "Skeleton should include Bip01_R_Hip"); + + // Verify materials + Assert.IsTrue(usdOutput.Contains("adder_mtl_centre_torso"), "Should have centre_torso material"); + + // Verify points (geometry) + Assert.IsTrue(usdOutput.Contains("point3f[] points"), "Should have geometry points"); + + // Verify normals + Assert.IsTrue(usdOutput.Contains("normal3f[] normals"), "Should have normals"); } [TestMethod] @@ -942,6 +1294,151 @@ public void MechFactory_CratesA_Gltf() Assert.AreEqual(2, gltfData.Materials.Count); } + [TestMethod] + public void Mechanic_Usd_WithArmature() + { + var modelFile = $@"{objectDir}\Objects\characters\mechanic\mechanic.chr"; + + // Check if file exists + if (!File.Exists(modelFile)) + { + Assert.Inconclusive($"Model file not found: {modelFile}"); + return; + } + + var args = new string[] { modelFile, "-objectdir", objectDir }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + // Process CryEngine file with skinning info + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + // Verify skinning info is present + Assert.IsNotNull(cryData.SkinningInfo, "SkinningInfo should not be null"); + Assert.IsTrue(cryData.SkinningInfo.HasSkinningInfo, "Should have skinning info"); + Assert.IsTrue(cryData.SkinningInfo.CompiledBones.Count > 0, "Should have bones"); + + Console.WriteLine($"Model has {cryData.SkinningInfo.CompiledBones.Count} bones"); + Console.WriteLine($"Root bone: {cryData.SkinningInfo.RootBone?.BoneName}"); + + // Generate USD + UsdRenderer usdRenderer = new(testUtils.argsHandler, cryData); + var usdDoc = usdRenderer.GenerateUsdObject(); + + // Serialize to string for inspection + var serializer = new UsdSerializer(); + using var writer = new StringWriter(); + serializer.Serialize(usdDoc, writer); + var usdOutput = writer.ToString(); + + // Verify skeleton structure is in output + Assert.IsTrue(usdOutput.Contains("def SkelRoot \"Armature\""), "Should have SkelRoot prim"); + Assert.IsTrue(usdOutput.Contains("def Skeleton \"Skeleton\""), "Should have Skeleton prim"); + Assert.IsTrue(usdOutput.Contains("prepend apiSchemas = [\"SkelBindingAPI\"]"), "Skeleton should have SkelBindingAPI"); + + // Verify skeleton data arrays + Assert.IsTrue(usdOutput.Contains("uniform token[] joints"), "Should have joints array"); + Assert.IsTrue(usdOutput.Contains("uniform matrix4d[] bindTransforms"), "Should have bindTransforms"); + Assert.IsTrue(usdOutput.Contains("uniform matrix4d[] restTransforms"), "Should have restTransforms"); + + // Verify mesh skinning attributes + Assert.IsTrue(usdOutput.Contains("primvars:skel:geomBindTransform"), "Should have geomBindTransform"); + Assert.IsTrue(usdOutput.Contains("primvars:skel:jointIndices"), "Should have jointIndices"); + Assert.IsTrue(usdOutput.Contains("primvars:skel:jointWeights"), "Should have jointWeights"); + Assert.IsTrue(usdOutput.Contains("rel skel:skeleton"), "Should have skeleton relationship"); + Assert.IsTrue(usdOutput.Contains(""), "Should reference skeleton path"); + + // Verify some expected bone names in joint hierarchy + Assert.IsTrue(usdOutput.Contains("Bip01"), "Should contain root bone Bip01"); + + // Test specific bone transforms against expected values + // These values are from the raw bone data and Collada comparison + + var bip01Bone = cryData.SkinningInfo.CompiledBones.FirstOrDefault(b => b.BoneName == "Bip01"); + var bip01Pelvis = cryData.SkinningInfo.CompiledBones.FirstOrDefault(b => b.BoneName == "bip_01_Pelvis"); + var bip01LThigh = cryData.SkinningInfo.CompiledBones.FirstOrDefault(b => b.BoneName == "bip_01_L_Thigh"); + + Assert.IsNotNull(bip01Bone, $"Should find Bip01 bone. Found {cryData.SkinningInfo.CompiledBones.Count} bones total."); + Assert.IsNotNull(bip01Pelvis, $"Should find bip_01_Pelvis bone"); + Assert.IsNotNull(bip01LThigh, $"Should find bip_01_L_Thigh bone"); + + // Expected values from Collada output (both bind and rest use same values) + // Bip01: Translation should be near (0, 0, 0) - root bone + // Bip01 Pelvis: Translation should be near (0, 0.950611, 0) in Z-up + // These are in LocalTransformMatrix (worldToBone / inverse bind) + + Console.WriteLine($"\nBip01 LocalTransformMatrix (worldToBone):"); + Console.WriteLine($" Translation: ({bip01Bone.LocalTransformMatrix.M14:F6}, {bip01Bone.LocalTransformMatrix.M24:F6}, {bip01Bone.LocalTransformMatrix.M34:F6})"); + + Console.WriteLine($"\nBip01 Pelvis LocalTransformMatrix (worldToBone):"); + Console.WriteLine($" Translation: ({bip01Pelvis.LocalTransformMatrix.M14:F6}, {bip01Pelvis.LocalTransformMatrix.M24:F6}, {bip01Pelvis.LocalTransformMatrix.M34:F6})"); + Console.WriteLine($" Expected: (0.000000, 0.000000, -0.950611) [inverted Z from world]"); + + Console.WriteLine($"\nBip01 Pelvis WorldTransformMatrix (boneToWorld):"); + Console.WriteLine($" Translation: ({bip01Pelvis.WorldTransformMatrix.M14:F6}, {bip01Pelvis.WorldTransformMatrix.M24:F6}, {bip01Pelvis.WorldTransformMatrix.M34:F6})"); + Console.WriteLine($" Expected: (0.000000, 0.000000, 0.950611) [Z-up position]"); + + // Verify Bip01 Pelvis world translation matches expected (from your raw data) + Assert.AreEqual(0.0, bip01Pelvis.WorldTransformMatrix.M14, 0.001, "Bip01 Pelvis X should be ~0"); + Assert.AreEqual(0.0, bip01Pelvis.WorldTransformMatrix.M24, 0.001, "Bip01 Pelvis Y should be ~0"); + Assert.AreEqual(0.950611, bip01Pelvis.WorldTransformMatrix.M34, 0.001, "Bip01 Pelvis Z should be ~0.950611"); + + // Find skeleton recursively + UsdSkeleton? skeleton = null; + foreach (var prim in usdDoc.Prims) + { + skeleton = FindSkeleton(prim); + if (skeleton is not null) break; + } + + if (skeleton == null) + { + Console.WriteLine("ERROR: No Skeleton prim found in USD output!"); + Assert.Fail("Skeleton prim not found"); + return; + } + + var restTransformsAttr = skeleton.Attributes.FirstOrDefault(a => a.Name == "restTransforms") as UsdMatrix4dArray; + if (restTransformsAttr == null) + { + Console.WriteLine("ERROR: No restTransforms attribute found!"); + Console.WriteLine($"Found {skeleton.Attributes.Count} attributes:"); + foreach (var attr in skeleton.Attributes) + { + Console.WriteLine($" - {attr.Name}"); + } + Assert.Fail("restTransforms attribute not found"); + return; + } + + var restTransforms = restTransformsAttr.Matrices; + Assert.IsTrue(restTransforms.Count >= 3, "Should have at least 3 bones"); + + // Optional: write to file for manual inspection + // usdRenderer.WriteUsdToFile(usdDoc); + } + + private void PrintMatrix(Matrix4x4 m) + { + Console.WriteLine($" ( ({m.M11:F6}, {m.M12:F6}, {m.M13:F6}, {m.M14:F6}), ({m.M21:F6}, {m.M22:F6}, {m.M23:F6}, {m.M24:F6}), ({m.M31:F6}, {m.M32:F6}, {m.M33:F6}, {m.M34:F6}), ({m.M41:F6}, {m.M42:F6}, {m.M43:F6}, {m.M44:F6}) )"); + } + + private UsdSkeleton? FindSkeleton(UsdPrim prim) + { + if (prim is UsdSkeleton skel) + return skel; + + foreach (var child in prim.Children) + { + var found = FindSkeleton(child); + if (found != null) + return found; + } + + return null; + } + [TestMethod] public void MechFactory_CratesA_Collada() { diff --git a/CgfConverterIntegrationTests/IntegrationTests/SC/StarCitizenTests.cs b/CgfConverterIntegrationTests/IntegrationTests/SC/StarCitizenTests.cs index e128428a..a5066799 100644 --- a/CgfConverterIntegrationTests/IntegrationTests/SC/StarCitizenTests.cs +++ b/CgfConverterIntegrationTests/IntegrationTests/SC/StarCitizenTests.cs @@ -1,10 +1,10 @@ using CgfConverter; using CgfConverter.CryEngineCore; -using CgfConverter.Renderers; using CgfConverter.Renderers.Collada; using CgfConverter.Renderers.Collada.Collada.Enums; using CgfConverter.Renderers.Gltf; -using CgfConverterIntegrationTests.Extensions; +using CgfConverter.Renderers.USD; +using CgfConverter.Renderers.USD.Models; using CgfConverterTests.TestUtilities; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -20,8 +20,8 @@ public class StarCitizenTests { private readonly TestUtils testUtils = new(); string userHome; - private readonly string objectDir = @"d:\depot\sc3.24\data"; - private readonly string objectDir322 = @"d:\depot\sc3.22\data"; + private readonly string objectDir = @"d:\depot\sc4.4\data"; // latest + private readonly string objectDir324 = @"d:\depot\sc3.24\data"; private readonly string objectDir41 = @"d:\depot\sc4.1\data"; [TestInitialize] @@ -37,10 +37,10 @@ public void Initialize() [TestMethod] public void AEGS_Avenger_324() { - var args = new string[] { $@"{objectDir}\objects\spaceships\ships\AEGS\Avenger\AEGS_Avenger.cga", "-dds", "-dae", "-objectdir", $"{objectDir}" }; + var args = new string[] { $@"{objectDir324}\objects\spaceships\ships\AEGS\Avenger\AEGS_Avenger.cga", "-dds", "-dae", "-objectdir", $"{objectDir324}" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); @@ -72,7 +72,7 @@ public void AEGS_Avenger_324() [TestMethod] public void AEGS_Avenger_41() { - var args = new string[] { $@"{objectDir41}\objects\spaceships\ships\AEGS\Avenger\AEGS_Avenger.cga", "-dds", "-dae", "-objectdir", $"{objectDir}" }; + var args = new string[] { $@"{objectDir41}\objects\spaceships\ships\AEGS\Avenger\AEGS_Avenger.cga", "-dds", "-dae", "-objectdir", $"{objectDir324}" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir41); @@ -104,49 +104,10 @@ public void AEGS_Avenger_41() Assert.IsTrue(noseGeo.Mesh.Source[0].Float_Array.Value_As_String.StartsWith("4.480176 -3.697465 -0.268108")); } - - [TestMethod] - public void AEGS_Avenger_322() - { - var args = new string[] { $@"{objectDir322}\objects\spaceships\ships\AEGS\Avenger\AEGS_Avenger.cga" }; - int result = testUtils.argsHandler.ProcessArgs(args); - Assert.AreEqual(0, result); - var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir322); - cryData.ProcessCryengineFiles(); - - var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); - var daeObject = colladaData.DaeObject; - colladaData.GenerateDaeObject(); - testUtils.ValidateColladaXml(colladaData); - - var noseNode = daeObject.Library_Visual_Scene.Visual_Scene[0].Node[0].node[0]; - var leftWing = daeObject.Library_Visual_Scene.Visual_Scene[0].Node[0].node[1].node[0]; - Assert.AreEqual("Nose", noseNode.ID); - Assert.AreEqual("1 -0 0 0 0 1 0 5.702999 0 0 1 -0.473000 0 0 0 0", noseNode.Matrix[0].Value_As_String); - Assert.AreEqual("#Nose-mesh", noseNode.Instance_Geometry[0].URL); - Assert.AreEqual(15, noseNode.Instance_Geometry[0].Bind_Material[0].Technique_Common.Instance_Material.Length); - Assert.AreEqual("#aegs_avenger_exterior_mtl_white_insulation_pads-material", noseNode.Instance_Geometry[0].Bind_Material[0].Technique_Common.Instance_Material[0].Target); - Assert.AreEqual("Front_LG_Door_Left", noseNode.node[28].ID); - Assert.AreEqual("1 0 0 -0.300001 0 -0.938131 -0.346280 0.512432 0 0.346280 -0.938131 -1.835138 0 0 0 0", noseNode.node[28].Matrix[0].Value_As_String); - Assert.AreEqual("Wing_Left", leftWing.Name); - Assert.AreEqual("1 0 0 -5.550000 0 1 0 -0.070000 0 0 1 -0.883000 0 0 0 0", leftWing.Matrix[0].Value_As_String); - - Assert.AreEqual(49, colladaData.DaeObject.Library_Materials.Material.Length); - Assert.AreEqual(129, colladaData.DaeObject.Library_Images.Image.Length); - - - // Geometry - var noseGeo = daeObject.Library_Geometries.Geometry[0]; - Assert.AreEqual("Nose-mesh", noseGeo.ID); - Assert.AreEqual(4, noseGeo.Mesh.Source.Length); - Assert.AreEqual(15, noseGeo.Mesh.Triangles.Length); - Assert.AreEqual(59817, noseGeo.Mesh.Source[0].Float_Array.Count); - } - [TestMethod] public void AEGS_Avenger_Gltf() { - var args = new string[] {$@"{objectDir41}\objects\spaceships\ships\aegs\Avenger\AEGS_Avenger.cga", "-objectDir", objectDir }; + var args = new string[] {$@"{objectDir41}\objects\spaceships\ships\aegs\Avenger\AEGS_Avenger.cga", "-objectDir", objectDir324 }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir41); @@ -188,10 +149,10 @@ public void AEGS_Avenger_Gltf() [TestMethod] public void AEGS_GladiusLandingGearFront_CHR() { - var args = new string[] { $@"{objectDir}\Objects\Spaceships\Ships\AEGS\LandingGear\Gladius\AEGS_Gladius_LandingGear_Front_CHR.chr" }; + var args = new string[] { $@"{objectDir324}\Objects\Spaceships\Ships\AEGS\LandingGear\Gladius\AEGS_Gladius_LandingGear_Front_CHR.chr" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); @@ -236,10 +197,10 @@ public void AEGS_Idris_Holo_viewer_cgf_41() public void AEGS_Idris_Holo_01_cga_41() { // No geometry or scenes - var args = new string[] { $@"{objectDir}\Objects\Spaceships\holoviewer_ships\AEGS_Idris_holo_01.cga" }; + var args = new string[] { $@"{objectDir324}\Objects\Spaceships\holoviewer_ships\AEGS_Idris_holo_01.cga" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, null, materialFiles: "AEGS_Idris_holo.mtl", objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, null, materialFiles: "AEGS_Idris_holo.mtl", objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); @@ -270,23 +231,6 @@ public void AEGS_Vanguard_LandingGear_Front_IvoFile() var daeObject = colladaData.DaeObject; } - [TestMethod] - public void ANVL_Arrow_322() - { - var args = new string[] { $@"{objectDir322}\objects\spaceships\ships\ANVL\Arrow\ANVL_Arrow.cga" }; - int result = testUtils.argsHandler.ProcessArgs(args); - Assert.AreEqual(0, result); - var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir322); - cryData.ProcessCryengineFiles(); - - var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); - var daeObject = colladaData.DaeObject; - colladaData.GenerateDaeObject(); - var rightWingNode = cryData.Nodes.Where(x => x.Name == "wing_right"); - var rightWingGeoNode = cryData.Models[1].NodeMap.Values.Where(x => x.Name == "wing_right").First(); - var colladaGeo = daeObject.Library_Geometries.Geometry[24].Mesh.Source[0].Float_Array.Value_As_String.Split(' '); - } - [TestMethod] public void ANVL_Arrow_Ivo() { @@ -305,10 +249,10 @@ public void ANVL_Arrow_Ivo() public void ANVL_Hurricane_Front_LandingGear_Ivo_Skin_324() { var args = new string[] { - $@"{objectDir}\Objects\Spaceships\Ships\ANVL\LandingGear\Hurricane\anvl_hurricane_landing_gear_front_SKIN.skin" }; + $@"{objectDir324}\Objects\Spaceships\Ships\ANVL\LandingGear\Hurricane\anvl_hurricane_landing_gear_front_SKIN.skin" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); var mesh = (ChunkMesh)cryData.RootNode.MeshData; Assert.AreEqual(-0.443651f, mesh.MinBound.X, TestUtils.delta); @@ -323,20 +267,6 @@ public void ANVL_Hurricane_Front_LandingGear_Ivo_Skin_324() var daeObject = colladaData.DaeObject; } - [TestMethod] - public void ANVL_Hurricane_Front_LandingGear_IvoCHR() - { - var args = new string[] { $@"{objectDir322}\Objects\Spaceships\Ships\ANVL\LandingGear\Hurricane\anvl_hurricane_landing_gear_front_chr.chr" }; - int result = testUtils.argsHandler.ProcessArgs(args); - Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir322); - cryData.ProcessCryengineFiles(); - - var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); - colladaData.GenerateDaeObject(); - var daeObject = colladaData.DaeObject; - } - [TestMethod] public void ANVL_Valkyrie_Turret_Bubble_Cga() { @@ -370,40 +300,40 @@ public void Argo_Atlas_Powersuit_41() [TestMethod] public void Avenger_LandingGear_SkinFile_324() { - var args = new string[] { $@"{objectDir}\Objects\Spaceships\Ships\AEGS\LandingGear\Avenger\AEGS_Avenger_LandingGear_Back.skin" }; + var args = new string[] { $@"{objectDir324}\Objects\Spaceships\Ships\AEGS\LandingGear\Avenger\AEGS_Avenger_LandingGear_Back.skin" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); colladaData.GenerateDaeObject(); var daeObject = colladaData.DaeObject; } - + [TestMethod] - public void Avenger_Ramp_Exterior() + public void BEHR_LaserCannon_S2_Usd() { - var args = new string[] { $@"D:\depot\SC3.22\Data\Objects\Spaceships\Ships\AEGS\Avenger\aegs_avenger_ramp_exterior.cga", "-dds", "-gltf" }; + var args = new string[] { $@"{objectDir41}\objects\spaceships\Weapons\BEHR\BEHR_LaserCannon_S2\BEHR_LaserCannon_S2.cga" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir41); cryData.ProcessCryengineFiles(); - GltfModelRenderer gltfRenderer = new(testUtils.argsHandler, cryData, true, false); - var gltfData = gltfRenderer.GenerateGltfObject(); - var geometries = gltfData.Meshes; + var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); + colladaData.GenerateDaeObject(); + var daeObject = colladaData.DaeObject; - gltfRenderer.Render(); + testUtils.ValidateColladaXml(colladaData); } [TestMethod] public void BehrRifle_324IvoCgfFile() { - var args = new string[] { $@"{objectDir}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar_stock.cgf" }; + var args = new string[] { $@"{objectDir324}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar_stock.cgf" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); @@ -416,10 +346,10 @@ public void BehrRifle_324IvoCgfFile() [TestMethod] public void BehrRifle_324IvoChrFile() { - var args = new string[] { $@"{objectDir}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar.chr" }; + var args = new string[] { $@"{objectDir324}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar.chr" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); @@ -436,8 +366,8 @@ public void BehrRifle_324IvoSkinFile() { var args = new string[] { - $@"{objectDir}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar_parts.skin", - "-dds", "-dae", "-objectdir", objectDir + $@"{objectDir324}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar_parts.skin", + "-dds", "-dae", "-objectdir", objectDir324 }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); @@ -474,11 +404,11 @@ public void BehrRifle_41IvoSkinFile() [TestMethod] public void Bmsl_Fps_APAR_Animus_Body() { - var args = new string[] { $@"{objectDir}\Objects\fps_weapons\weapons_v7\apar\launcher\animus\bmsl_fps_apar_animus_body.cga" }; + var args = new string[] { $@"{objectDir324}\Objects\fps_weapons\weapons_v7\apar\launcher\animus\bmsl_fps_apar_animus_body.cga" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); ColladaModelRenderer colladaData = new(testUtils.argsHandler, cryData); @@ -488,8 +418,8 @@ public void Bmsl_Fps_APAR_Animus_Body() [TestMethod] public void BMSL_FPS_APAR_Animus_Body_v324_Ivo() { - var args = new string[] { $@"{objectDir}\Objects\fps_weapons\weapons_v7\apar\launcher\animus\bmsl_fps_apar_animus_body.cga", "-dds", "-dae", - "-objectdir", objectDir }; + var args = new string[] { $@"{objectDir324}\Objects\fps_weapons\weapons_v7\apar\launcher\animus\bmsl_fps_apar_animus_body.cga", "-dds", "-dae", + "-objectdir", objectDir324 }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem); @@ -499,11 +429,11 @@ public void BMSL_FPS_APAR_Animus_Body_v324_Ivo() [TestMethod] public void Box_Collada_Ivo() { - var args = new string[] {$@"{objectDir}\Objects\default\box.cgf" }; + var args = new string[] {$@"{objectDir324}\Objects\default\box.cgf" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir); + var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); @@ -613,54 +543,6 @@ public void Console_Info_Banu_1_a_Collada() var daeObject = colladaData.DaeObject; } - [TestMethod] - public void Box_Collada_322() - { - var args = new string[] {$@"{objectDir322}\Objects\default\box.cgf" }; - int result = testUtils.argsHandler.ProcessArgs(args); - Assert.AreEqual(0, result); - - var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir322); - cryData.ProcessCryengineFiles(); - - var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); - colladaData.GenerateDaeObject(); - var daeObject = colladaData.DaeObject; - - // Visual Scene checks - var rootNode = daeObject.Library_Visual_Scene.Visual_Scene[0].Node[0]; - Assert.AreEqual("box", rootNode.ID); - Assert.AreEqual(ColladaNodeType.NODE, rootNode.Type); - Assert.AreEqual("1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0", rootNode.Matrix[0].Value_As_String); - var boxNode = daeObject.Library_Visual_Scene.Visual_Scene[0].Node[0].node[0]; - Assert.AreEqual("mesh_box", boxNode.ID); - Assert.AreEqual(ColladaNodeType.NODE, boxNode.Type); - Assert.AreEqual("1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0", boxNode.Matrix[0].Value_As_String); - Assert.AreEqual("#grid_grayyellow_mtl_grid_grey-material", boxNode.Instance_Geometry[0].Bind_Material[0].Technique_Common.Instance_Material[0].Target); - Assert.AreEqual("grid_grayyellow_mtl_grid_grey-material", boxNode.Instance_Geometry[0].Bind_Material[0].Technique_Common.Instance_Material[0].Symbol); - - // Geometry Checks - var geometry = daeObject.Library_Geometries.Geometry[0]; - Assert.AreEqual("mesh_box-mesh", geometry.ID); - Assert.AreEqual(1, daeObject.Library_Geometries.Geometry.Length); - var mesh = geometry.Mesh; - Assert.AreEqual(4, mesh.Source.Length); - Assert.AreEqual(1, mesh.Triangles.Length); - Assert.AreEqual(12, mesh.Triangles[0].Count); - - // Materials Checks - var mats = daeObject.Library_Materials; - Assert.AreEqual(3, mats.Material.Length); - Assert.AreEqual("grid_grayyellow_mtl_grid_grey", mats.Material[0].Name); - Assert.AreEqual("grid_grayyellow_mtl_grid_grey-material", mats.Material[0].ID); - Assert.AreEqual("#grid_grayyellow_mtl_grid_grey-effect", mats.Material[0].Instance_Effect.URL); - Assert.AreEqual("grid_grayyellow_mtl_grid_yellow", mats.Material[1].Name); - var boundMaterials = boxNode.Instance_Geometry[0].Bind_Material; - Assert.AreEqual("#grid_grayyellow_mtl_grid_grey-material", boundMaterials[0].Technique_Common.Instance_Material[0].Target); - Assert.AreEqual("grid_grayyellow_mtl_grid_grey-material", boundMaterials[0].Technique_Common.Instance_Material[0].Symbol); - Assert.AreEqual(1, boundMaterials[0].Technique_Common.Instance_Material.Length); - } - [TestMethod] public void CRUS_Spirit_Exterior() { @@ -688,7 +570,7 @@ public void DRAK_Buccaneer_Landing_Gear_Front_Skin() { var args = new string[] { $@"{objectDir41}\Objects\Spaceships\Ships\DRAK\Buccaneer\Landing_Gear\DRAK_Buccaneer_Landing_Gear_Front_Skin.skin", - "-dds", "-dae", "-objectdir", objectDir }; + "-dds", "-dae", "-objectdir", objectDir324 }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir41); @@ -709,7 +591,7 @@ public void DRAK_Buccaneer_Landing_Gear_Front_Skin() [TestMethod] public void Model_m_ccc_vanduul_helmet_01_IvoSkinFile() { - var args = new string[] { $@"{objectDir}\Objects\Characters\Human\male_v7\armor\ccc\m_ccc_vanduul_helmet_01.skin" }; + var args = new string[] { $@"{objectDir324}\Objects\Characters\Human\male_v7\armor\ccc\m_ccc_vanduul_helmet_01.skin" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); @@ -808,24 +690,6 @@ public void Med_bay_wall_bed_extender_a_Ivo() Assert.AreEqual(4, geometry.Mesh.Triangles.Length); } - [TestMethod] - public void MISC_Fury_322() - { - var args = new string[] { $@"{objectDir322}\Objects\spaceships\ships\MISC\Fury\MISC_Fury.cga" }; - int result = testUtils.argsHandler.ProcessArgs(args); - Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir322); - cryData.ProcessCryengineFiles(); - - ColladaModelRenderer colladaData = new(testUtils.argsHandler, cryData); - colladaData.GenerateDaeObject(); - - var visualScene = colladaData.DaeObject.Library_Visual_Scene; - var meshWingTopRight = visualScene.Visual_Scene[0].Node[0].node[0].node[70].node[1].node[0].node[0]; - var matrix = meshWingTopRight.Matrix[0].Value_As_String; - Assert.AreEqual("1 -0 0 -0.848649 0 1 0.000001 -1.239070 -0 -0.000001 1 0.058854 0 0 0 0", matrix); - } - [TestMethod] public void MISC_Fury_Ivo() { @@ -910,7 +774,7 @@ public void Mobiglass_Gltf() [TestMethod] public void NavyPilotFlightSuit_Ivo() { - var args = new string[] { $@"{objectDir}\Objects\Characters\Human\male_v7\armor\nvy\pilot_flightsuit\m_nvy_pilot_light_helmet_01.skin" }; + var args = new string[] { $@"{objectDir324}\Objects\Characters\Human\male_v7\armor\nvy\pilot_flightsuit\m_nvy_pilot_light_helmet_01.skin" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, materialFiles: "m_nvy_pilot_light_no_name_01_01_01", objectDir: objectDir41); @@ -968,28 +832,88 @@ public void Teapot_Ivo_Unsplit() } [TestMethod] - public void Teapot_322() + public void Teapot_Ivo_USD() { - var args = new string[] { $@"{objectDir322}\Objects\default\teapot.cgf" }; + var args = new string[] { $@"{objectDir41}\Objects\default\teapot.cgf", "-objectdir", objectDir41, "-usd" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir322); + var cryData = new CryEngine(args[0], testUtils.argsHandler.PackFileSystem, objectDir: objectDir41); cryData.ProcessCryengineFiles(); - var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); - colladaData.GenerateDaeObject(); - var daeObject = colladaData.DaeObject; + var usdRenderer = new UsdRenderer(testUtils.argsHandler, cryData); + var usdDoc = usdRenderer.GenerateUsdObject(); + + // 1. Verify USD document structure + Assert.AreEqual("root", usdDoc.Header.DefaultPrim, "Default prim should be 'root'"); + Assert.AreEqual("Z", usdDoc.Header.UpAxis, "Up axis should be Z"); + Assert.AreEqual(1, usdDoc.Header.MetersPerUnit, "MetersPerUnit should be 1"); + + // 2. Verify root prim exists + var rootPrim = usdDoc.Prims[0]; + Assert.AreEqual("root", rootPrim.Name, "Root prim should be named 'root'"); + Assert.IsTrue(rootPrim is UsdXform, "Root prim should be Xform"); + + // 3. Verify materials scope was created + var materialsScope = rootPrim.Children.FirstOrDefault(x => x.Name == "_materials"); + Assert.IsNotNull(materialsScope, "Materials scope should exist"); + Assert.IsTrue(materialsScope is UsdScope, "_materials should be a Scope"); + + // 4. Verify at least one material exists + Assert.IsTrue(materialsScope.Children.Count > 0, "Should have at least one material"); + var material = materialsScope.Children.FirstOrDefault(x => x.Name == "teapot_mtl_teapot"); + Assert.IsNotNull(material, "Should have teapot_mtl_teapot material"); + Assert.IsTrue(material is UsdMaterial, "Material should be Material type"); + + // 5. Verify geometry nodes were created (Ivo format support) + var meshNodes = rootPrim.Children.Where(x => x.Name != "_materials").ToList(); + Assert.IsTrue(meshNodes.Count > 0, "Should have at least one mesh node (Ivo format geometry)"); + + // 6. Find the teapot mesh node + var teapotXform = meshNodes.FirstOrDefault(x => x.Name == "teapot"); + Assert.IsNotNull(teapotXform, "Should have a teapot Xform node"); + Assert.IsTrue(teapotXform is UsdXform, "Teapot node should be Xform"); + + // 7. Verify the mesh exists under the Xform + var teapotMesh = teapotXform.Children.FirstOrDefault(x => x is UsdMesh); + Assert.IsNotNull(teapotMesh, "Should have a Mesh child under teapot Xform"); + + // 8. Verify mesh has required attributes + var meshAttributes = teapotMesh.Attributes; + Assert.IsTrue(meshAttributes.Any(a => a.Name == "points"), "Mesh should have points (vertices)"); + Assert.IsTrue(meshAttributes.Any(a => a.Name == "faceVertexCounts"), "Mesh should have faceVertexCounts"); + Assert.IsTrue(meshAttributes.Any(a => a.Name == "faceVertexIndices"), "Mesh should have faceVertexIndices"); + Assert.IsTrue(meshAttributes.Any(a => a.Name == "extent"), "Mesh should have extent (bounding box)"); + Assert.IsTrue(meshAttributes.Any(a => a.Name == "normals"), "Mesh should have normals"); + + // 9. Verify Ivo-specific attributes (UVs and colors from VertUV) + Assert.IsTrue(meshAttributes.Any(a => a.Name.Contains("_UV")), "Mesh should have UV coordinates from VertUV"); + Assert.IsTrue(meshAttributes.Any(a => a.Name.Contains("_color")), "Mesh should have vertex colors from VertUV"); + + // 10. Verify GeomSubset exists for material assignment + var geomSubsets = teapotMesh.Children.Where(x => x is UsdGeomSubset).ToList(); + Assert.IsTrue(geomSubsets.Count > 0, "Mesh should have at least one GeomSubset"); + + var subset = geomSubsets.FirstOrDefault(x => x.Name == "teapot_mtl_teapot"); + Assert.IsNotNull(subset, "Should have teapot_mtl_teapot GeomSubset"); + + // 11. Verify GeomSubset has proper material binding + var subsetAttributes = subset.Attributes; + Assert.IsTrue(subsetAttributes.Any(a => a.Name == "indices"), "GeomSubset should have indices"); + Assert.IsTrue(subsetAttributes.Any(a => a.Name == "elementType" && a.ToString().Contains("face")), + "GeomSubset should have elementType='face'"); + Assert.IsTrue(subsetAttributes.Any(a => a.Name == "material:binding"), + "GeomSubset should have material:binding"); } [TestMethod] public void Vgl_Armor_Medium_Helmet_324() { // Game file has wrong mtl name. It's vgl_armor_medium_helmet_01_01_01 in the game files but actual mtl file is m_vgl_armor_medium_helmet_01_01_01.mtl - var args = new string[] { $@"{objectDir}\objects\characters\human\male_v7\armor\vgl\m_vgl_armor_medium_helmet_01.cgf" }; + var args = new string[] { $@"{objectDir324}\objects\characters\human\male_v7\armor\vgl\m_vgl_armor_medium_helmet_01.cgf" }; int result = testUtils.argsHandler.ProcessArgs(args); Assert.AreEqual(0, result); - CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, null, materialFiles: "m_vgl_armor_medium_helmet_01_01_01", objectDir: objectDir); + CryEngine cryData = new(args[0], testUtils.argsHandler.PackFileSystem, null, materialFiles: "m_vgl_armor_medium_helmet_01_01_01", objectDir: objectDir324); cryData.ProcessCryengineFiles(); var colladaData = new ColladaModelRenderer(testUtils.argsHandler, cryData); @@ -1011,4 +935,96 @@ public void Vgl_Armor_Medium_Helmet_41() colladaData.GenerateDaeObject(); var daeObject = colladaData.DaeObject; } + + [TestMethod] + public void Aloprat_Skel_USD_WithCAF() + { + // Test the aloprat skeleton with #ivo CAF animation files + var skeletonPath = $@"{objectDir41}\Objects\Characters\Creatures\aloprat\aloprat_skel.chr"; + + var args = new string[] { skeletonPath, "-objectdir", objectDir41, "-usd" }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + // Load and process the skeleton + CryEngine cryData = new(skeletonPath, testUtils.argsHandler.PackFileSystem, objectDir: objectDir41); + cryData.ProcessCryengineFiles(); + + // Log diagnostic info about CAF animations + Console.WriteLine($"CafAnimations count: {cryData.CafAnimations.Count}"); + foreach (var anim in cryData.CafAnimations) + { + Console.WriteLine($" Animation: {anim.Name}, Tracks: {anim.BoneTracks.Count}"); + } + + // Generate USD output + var usdRenderer = new UsdRenderer(testUtils.argsHandler, cryData); + var usdDoc = usdRenderer.GenerateUsdObject(); + usdRenderer.Render(); + + // Basic structure checks + Assert.AreEqual("root", usdDoc.Header.DefaultPrim); + Assert.AreEqual("Z", usdDoc.Header.UpAxis); + + // Verify skeleton was created + var rootPrim = usdDoc.Prims[0]; + Assert.IsNotNull(rootPrim); + + // Verify CAF animations were loaded + Assert.IsTrue(cryData.CafAnimations.Count > 0, "Should have loaded CAF animations from chrparams wildcards"); + } + + [TestMethod] + public void Aloprat_CAF_IvoAnimation() + { + // Test loading a Star Citizen #ivo CAF animation file directly + var cafPath = $@"{objectDir41}\Animations\Characters\Creatures\aloprat\ai_aloprat_stand_walk_forward_01.caf"; + + var args = new string[] { cafPath, "-objectdir", objectDir41 }; + int result = testUtils.argsHandler.ProcessArgs(args); + Assert.AreEqual(0, result); + + // Load the CAF file + CryEngine cryData = new(cafPath, testUtils.argsHandler.PackFileSystem, objectDir: objectDir41); + cryData.ProcessCryengineFiles(); + + // Check that models were loaded + Assert.IsNotNull(cryData.Models); + Assert.IsTrue(cryData.Models.Count > 0, "Should have at least one model loaded"); + + // Check for animation-related chunks (IvoCAFData, IvoAnimInfo) + var model = cryData.Models[0]; + var chunks = model.ChunkMap.Values.ToList(); + + // Log chunk types for debugging + foreach (var chunk in chunks) + { + Console.WriteLine($"Chunk: {chunk.GetType().Name}"); + + // Debug: output IvoCAF controller details + if (chunk is CgfConverter.CryEngineCore.ChunkIvoCAF ivoCaf) + { + Console.WriteLine($" BoneHashes: {ivoCaf.BoneHashes.Length}, Controllers: {ivoCaf.Controllers.Length}"); + for (int i = 0; i < Math.Min(5, ivoCaf.Controllers.Length); i++) + { + var ctrl = ivoCaf.Controllers[i]; + Console.WriteLine($" [{i}] Hash=0x{ivoCaf.BoneHashes[i]:X08}: " + + $"Rot={ctrl.NumRotKeys}@0x{ctrl.RotDataOffset:X} (flags=0x{ctrl.RotFormatFlags:X4}), " + + $"Pos={ctrl.NumPosKeys}@0x{ctrl.PosDataOffset:X} (flags=0x{ctrl.PosFormatFlags:X4})"); + } + + // Show first position data for bones that have it + Console.WriteLine($" Position tracks: {ivoCaf.Positions.Count}"); + int posCount = 0; + foreach (var (hash, positions) in ivoCaf.Positions) + { + if (positions.Count > 0) + { + Console.WriteLine($" 0x{hash:X08}: first pos = {positions[0]}"); + if (++posCount >= 5) break; + } + } + } + } + } } diff --git a/CgfConverterIntegrationTests/ManualTests/ManualRenderTests.cs b/CgfConverterIntegrationTests/ManualTests/ManualRenderTests.cs new file mode 100644 index 00000000..2dcd0de6 --- /dev/null +++ b/CgfConverterIntegrationTests/ManualTests/ManualRenderTests.cs @@ -0,0 +1,408 @@ +using CgfConverter; +using CgfConverter.Renderers.Collada; +using CgfConverter.Renderers.Gltf; +using CgfConverter.Renderers.USD; +using CgfConverter.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; + +namespace CgfConverterTests.ManualTests; + +/// +/// Manual render tests that write output files to the source file's directory. +/// Run these from Test Explorer to quickly iterate on renderer changes. +/// +/// These tests are NOT for CI - they require game assets and write to disk. +/// Use TestCategory "manual" to exclude from automated runs. +/// +[TestClass] +[TestCategory("manual")] +public class ManualRenderTests +{ + private readonly ArgsHandler argsHandler = new(); + private readonly string armedWarfareObjectDir = @"d:\depot\armoredwarfare"; + private readonly string kcd2ObjectDir = @"d:\depot\kcd2"; + private readonly string mwoObjectDir = @"d:\depot\mwo"; + private readonly string sc41ObjectDir = @"d:\depot\sc4.1\data"; + private readonly string sc44ObjectDir = @"d:\depot\sc4.4\data"; + private readonly string archeageObjectDir = @"d:\depot\archeage"; + + [TestInitialize] + public void Initialize() + { + CultureInfo customCulture = (CultureInfo)Thread.CurrentThread.CurrentCulture.Clone(); + customCulture.NumberFormat.NumberDecimalSeparator = "."; + Thread.CurrentThread.CurrentCulture = customCulture; + + // Enable debug logging for manual tests + HelperMethods.LogLevel = LogLevelEnum.Debug; + } + + #region ArcheAge tests + + [TestMethod] + public void Basket_Mix_Ani_USD() // has 2 material libraries + { + RenderToUsd($@"{archeageObjectDir}\game\objects\env\01_nuia\001_housing\01_tools\basket_mix_ani.cga", archeageObjectDir); + } + + [TestMethod] + public void Basket_Mix_Ani_Collada() // has 2 material libraries + { + RenderToCollada($@"{archeageObjectDir}\game\objects\env\01_nuia\001_housing\01_tools\basket_mix_ani.cga", archeageObjectDir); + } + + [TestMethod] + public void ArcheAge_Chicken_USD() // 0x801 compiled bones format + { + RenderToUsd($@"{archeageObjectDir}\game\objects\characters\animals\chicken\chicken.chr", archeageObjectDir); + } + + [TestMethod] + public void ArcheAge_Chicken_Gltf() // 0x801 compiled bones format + { + RenderToGltf($@"{archeageObjectDir}\game\objects\characters\animals\chicken\chicken.chr", archeageObjectDir); + } + + #endregion + + #region Armored Warfare Test Files + + [TestMethod] + public void ArmoredWarfare_Chicken_USD() + { + RenderToUsd($@"{armedWarfareObjectDir}\Objects\characters\animals\birds\chicken\chicken.chr", armedWarfareObjectDir); + } + + [TestMethod] + public void ArmoredWarfare_Chicken_Gltf() + { + RenderToGltf($@"{armedWarfareObjectDir}\Objects\characters\animals\birds\chicken\chicken.chr", armedWarfareObjectDir); + } + + #endregion + + #region KCD2 Test Files + + [TestMethod] + public void Kcd2_Boar_Usd() + { + RenderToUsd($@"{kcd2ObjectDir}\Objects\characters\animals\boar\boar.chr", kcd2ObjectDir); + } + + [TestMethod] + public void Kcd2_Skeleton_Pig_USD() + { + RenderToUsd($@"{kcd2ObjectDir}\Objects\characters\animals\boar\skeleton_pig_01.chr", kcd2ObjectDir); + } + + [TestMethod] + public void Kcd2_Boar_Gltf() + { + RenderToGltf($@"{kcd2ObjectDir}\Objects\characters\animals\boar\boar.chr", kcd2ObjectDir); + } + + [TestMethod] + public void Kcd2_Skeleton_Pig_Gltf() + { + RenderToGltf($@"{kcd2ObjectDir}\Objects\characters\animals\boar\skeleton_pig_01.chr", kcd2ObjectDir); + } + + #endregion + + #region MWO Test Files + + [TestMethod] + public void _50Cal_Necklace_USD() + { + RenderToUsd($@"{mwoObjectDir}\Objects\purchasable\cockpit_hanging\50calnecklace\50calnecklace_a.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Box_USD() + { + RenderToUsd($@"{mwoObjectDir}\Objects\default\box.cgf", mwoObjectDir); + } + + [TestMethod] + public void MWO_Box_Collada() + { + RenderToCollada($@"{mwoObjectDir}\Objects\default\box.cgf", mwoObjectDir); + } + + [TestMethod] + public void MWO_Box_Gltf() + { + RenderToGltf($@"{mwoObjectDir}\Objects\default\box.cgf", mwoObjectDir); + } + + [TestMethod] + public void MWO_AdderCockpit_USD() + { + RenderToUsd($@"{mwoObjectDir}\objects\mechs\adder\cockpit_standard\adder_a_cockpit_standard.cga", mwoObjectDir); + } + + [TestMethod] + public void MWO_AdderChr_USD() + { + RenderToUsd($@"{mwoObjectDir}\objects\mechs\adder\body\adder.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_AdderChr_Collada() + { + RenderToCollada($@"{mwoObjectDir}\objects\mechs\adder\body\adder.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_AdderChr_Gltf() + { + RenderToGltf($@"{mwoObjectDir}\objects\mechs\adder\body\adder.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Mechanic_Collada() + { + RenderToCollada($@"{mwoObjectDir}\objects\characters\mechanic\mechanic.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Mechanic_USD() + { + RenderToUsd($@"{mwoObjectDir}\objects\characters\mechanic\mechanic.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Mechanic_Gtlf() + { + RenderToGltf($@"{mwoObjectDir}\objects\characters\mechanic\mechanic.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Pilot_Gtlf() + { + RenderToGltf($@"{mwoObjectDir}\objects\characters\pilot\pilot.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Pilot_Diagnostic() + { + var inputFile = $@"{mwoObjectDir}\objects\characters\pilot\pilot.chr"; + var args = new string[] { inputFile, "-gltf", "-objectdir", mwoObjectDir }; + argsHandler.ProcessArgs(args); + + var cryData = new CryEngine(inputFile, argsHandler.PackFileSystem, objectDir: mwoObjectDir); + cryData.ProcessCryengineFiles(); + + var rootNode = cryData.RootNode; + var meshData = rootNode?.MeshData; + var geomInfo = meshData?.GeometryInfo; + var skinningInfo = cryData.SkinningInfo; + + System.Console.WriteLine($"=== PILOT.CHR DIAGNOSTIC ==="); + System.Console.WriteLine($"RootNode: {rootNode?.Name}"); + + if (geomInfo != null) + { + System.Console.WriteLine($"\n--- Geometry Info ---"); + System.Console.WriteLine($"Vertices count: {geomInfo.Vertices?.Data.Length ?? 0}"); + System.Console.WriteLine($"Indices count: {geomInfo.Indices?.Data.Length ?? 0}"); + System.Console.WriteLine($"Normals count: {geomInfo.Normals?.Data.Length ?? 0}"); + System.Console.WriteLine($"UVs count: {geomInfo.UVs?.Data.Length ?? 0}"); + + var subsets = geomInfo.GeometrySubsets; + System.Console.WriteLine($"\n--- Geometry Subsets ({subsets?.Count ?? 0}) ---"); + int totalSubsetVerts = 0; + if (subsets != null) + { + foreach (var (subset, idx) in subsets.Select((s, i) => (s, i))) + { + System.Console.WriteLine($" Subset {idx}: FirstVertex={subset.FirstVertex}, NumVertices={subset.NumVertices}, FirstIndex={subset.FirstIndex}, NumIndices={subset.NumIndices}"); + totalSubsetVerts += subset.NumVertices; + } + } + System.Console.WriteLine($"Sum of NumVertices: {totalSubsetVerts}"); + } + + if (skinningInfo != null) + { + System.Console.WriteLine($"\n--- Skinning Info ---"); + System.Console.WriteLine($"HasSkinningInfo: {skinningInfo.HasSkinningInfo}"); + System.Console.WriteLine($"BoneMappings count: {skinningInfo.BoneMappings?.Count ?? 0}"); + System.Console.WriteLine($"IntVertices count: {skinningInfo.IntVertices?.Count ?? 0}"); + System.Console.WriteLine($"Ext2IntMap count: {skinningInfo.Ext2IntMap?.Count ?? 0}"); + System.Console.WriteLine($"HasIntToExtMapping: {skinningInfo.HasIntToExtMapping}"); + System.Console.WriteLine($"CompiledBones count: {skinningInfo.CompiledBones?.Count ?? 0}"); + } + + // Check for index out of bounds + if (geomInfo?.Indices != null && geomInfo?.Vertices != null) + { + var maxIndex = geomInfo.Indices.Data.Max(); + var vertCount = geomInfo.Vertices.Data.Length; + System.Console.WriteLine($"\n--- Index Bounds Check ---"); + System.Console.WriteLine($"Max index value: {maxIndex}"); + System.Console.WriteLine($"Vertex count: {vertCount}"); + System.Console.WriteLine($"Index within bounds: {maxIndex < vertCount}"); + } + } + + [TestMethod] + public void MWO_Pilot_Usd() + { + RenderToUsd($@"{mwoObjectDir}\objects\characters\pilot\pilot.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Turret_Collada() + { + RenderToCollada($@"{mwoObjectDir}\objects\gamemodes\turret\turret_a.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Turret_USD() + { + RenderToUsd($@"{mwoObjectDir}\objects\gamemodes\turret\turret_a.chr", mwoObjectDir); + } + + [TestMethod] + public void MWO_Turret_Gltf() + { + RenderToGltf($@"{mwoObjectDir}\objects\gamemodes\turret\turret_a.chr", mwoObjectDir); + } + + #endregion + + #region SC test files + + [TestMethod] + public void SC41_Avenger_USD() + { + RenderToUsd($@"{sc41ObjectDir}\Objects\Spaceships\Ships\AEGS\Avenger\AEGS_Avenger.cga", sc41ObjectDir); + } + + [TestMethod] + public void SC41_Avenger_Gltf() + { + RenderToGltf($@"{sc41ObjectDir}\Objects\Spaceships\Ships\AEGS\Avenger\AEGS_Avenger.cga", sc41ObjectDir); + } + + [TestMethod] + public void Aloprat_USD() + { + RenderToUsd($@"{sc41ObjectDir}\Objects\Characters\Creatures\aloprat\aloprat_skel.chr", sc41ObjectDir); + } + + [TestMethod] + public void Aloprat_Skin_USD() + { + RenderToUsd($@"{sc41ObjectDir}\Objects\Characters\Creatures\aloprat\aloprat.skin", sc41ObjectDir); + } + + [TestMethod] + public void BEHR_LaserCannon_S2_Usd() + { + RenderToUsd($@"{sc41ObjectDir}\objects\spaceships\Weapons\BEHR\BEHR_LaserCannon_S2\BEHR_LaserCannon_S2.cga", sc41ObjectDir); + } + + [TestMethod] + public void BEHR_LaserCannon_S2_gltf() + { + RenderToGltf($@"{sc41ObjectDir}\objects\spaceships\Weapons\BEHR\BEHR_LaserCannon_S2\BEHR_LaserCannon_S2.cga", sc41ObjectDir); + } + + [TestMethod] + public void BEHR_LaserCannon_S2_Collada() + { + RenderToCollada($@"{sc41ObjectDir}\objects\spaceships\Weapons\BEHR\BEHR_LaserCannon_S2\BEHR_LaserCannon_S2.cga", sc41ObjectDir); + } + + [TestMethod] + public void brfl_fps_behr_p4ar_chr_Usd() + { + RenderToUsd($@"{sc41ObjectDir}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar.chr", sc41ObjectDir); + } + + [TestMethod] + public void GLSN_Shiv_Door_Ramp_Cga_USD() + { + RenderToUsd($@"{sc41ObjectDir}\objects\spaceships\ships\GLSN\shiv\Maelstrom\GLSN_Shiv_Door_Ramp.cga", sc41ObjectDir); + } + + [TestMethod] + public void mgzn_s04_behr_40gb_01_spin_01_USD() + { + RenderToUsd($@"{sc41ObjectDir}\objects\fps_weapons\attachments\magazines\behr\mgzn_s04_behr_40gb_01.cga", sc41ObjectDir); + } + + [TestMethod] + public void Teapot_USD() + { + RenderToUsd($@"{sc41ObjectDir}\objects\default\teapot.cgf", sc41ObjectDir); + } + + [TestMethod] + public void AEGS_Avenger_LandingGear_Back_USD() + { + RenderToUsd($@"{sc41ObjectDir}\Objects\Spaceships\Ships\AEGS\LandingGear\Avenger\AEGS_Avenger_LandingGear_Back_CHR.chr", sc41ObjectDir); + } + + [TestMethod] + public void AEGS_Avenger_LandingGear_Back_Gltf() + { + RenderToGltf($@"{sc41ObjectDir}\Objects\Spaceships\Ships\AEGS\LandingGear\Avenger\AEGS_Avenger_LandingGear_Back_CHR.chr", sc41ObjectDir); + } + #endregion + + #region Helper Methods + + private void RenderToUsd(string inputFile, string objectDir) + { + var args = new string[] { inputFile, "-usd", "-objectdir", objectDir }; + argsHandler.ProcessArgs(args); + + var cryData = new CryEngine(inputFile, argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + var renderer = new UsdRenderer(argsHandler, cryData); + renderer.Render(); + + var outputPath = Path.ChangeExtension(inputFile, ".usda"); + Assert.IsTrue(File.Exists(outputPath), $"Output file not created: {outputPath}"); + } + + private void RenderToCollada(string inputFile, string objectDir) + { + var args = new string[] { inputFile, "-dae", "-objectdir", objectDir }; + argsHandler.ProcessArgs(args); + + var cryData = new CryEngine(inputFile, argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + var renderer = new ColladaModelRenderer(argsHandler, cryData); + renderer.Render(); + + var outputPath = Path.ChangeExtension(inputFile, ".dae"); + Assert.IsTrue(File.Exists(outputPath), $"Output file not created: {outputPath}"); + } + + private void RenderToGltf(string inputFile, string objectDir) + { + var args = new string[] { inputFile, "-gltf", "-objectdir", objectDir }; + argsHandler.ProcessArgs(args); + + var cryData = new CryEngine(inputFile, argsHandler.PackFileSystem, objectDir: objectDir); + cryData.ProcessCryengineFiles(); + + var renderer = new GltfModelRenderer(argsHandler, cryData, writeText: true, writeBinary: false); + renderer.Render(); + + var outputPath = Path.ChangeExtension(inputFile, ".gltf"); + Assert.IsTrue(File.Exists(outputPath), $"Output file not created: {outputPath}"); + } + + #endregion +} diff --git a/CgfConverterIntegrationTests/TestData/hulagirl__gold_a.usda b/CgfConverterIntegrationTests/TestData/hulagirl__gold_a.usda new file mode 100644 index 00000000..930d73bd --- /dev/null +++ b/CgfConverterIntegrationTests/TestData/hulagirl__gold_a.usda @@ -0,0 +1,110 @@ +#usda 1.0 +( + defaultPrim = "root" + doc = "Generated by CgfConverter" + metersPerUnit = 1 + upAxis = "Z" +) + +def Xform "root" +{ + def Xform "hulagirl_a" + { + matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 0) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + def Xform "HulaGirl_UpperBody" + { + matrix4d xformOp:transform = ( (0.9955745, -0.0019677, -0.0939548, 0), (-0.0172511, 0.9789642, -0.2033015, 0), (0.0923785, 0.2040226, 0.9745979, 0), (0.0001013, 0, 0.0640777, 0) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + def Mesh "HulaGirl_UpperBody" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + + float3[] extent = [(-0.0174944, -0.0126749, -0.0194372), (0.0325437, 0.0126749, 0.0514650)] + int[] faceVertexCounts = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] + int[] faceVertexIndices = [0, 1, 2, 3, 4, 0, 5, 0, 4, 4, 6, 5, 7, 5, 6, 6, 8, 7, 9, 8, 6, 6, 10, 9, 10, 6, 11, 11, 12, 10, 13, 12, 11, 6, 4, 14, 14, 11, 6, 4, 15, 16, 16, 17, 4, 4, 17, 18, 18, 14, 4, 19, 14, 18, 11, 14, 19, 19, 20, 11, 11, 20, 13, 13, 20, 21, 21, 22, 13, 22, 21, 23, 23, 24, 22, 24, 23, 25, 25, 26, 24, 27, 26, 25, 25, 28, 27, 28, 25, 29, 29, 30, 28, 30, 29, 31, 31, 32, 30, 33, 34, 35, 35, 34, 31, 31, 29, 35, 35, 29, 25, 25, 23, 35, 35, 23, 21, 21, 36, 35, 36, 21, 20, 20, 19, 36, 36, 19, 37, 18, 37, 19, 37, 18, 38, 38, 18, 17, 17, 39, 38, 35, 36, 33, 37, 33, 36, 33, 37, 34, 38, 40, 37, 41, 42, 43, 43, 44, 41, 41, 44, 28, 28, 30, 41, 45, 46, 47, 48, 49, 46, 46, 49, 50, 50, 47, 46, 50, 51, 47, 51, 50, 52, 53, 54, 55, 56, 55, 54, 55, 56, 57, 57, 56, 58, 58, 59, 57, 59, 58, 60, 60, 61, 59, 61, 60, 62, 62, 63, 61, 63, 62, 64, 64, 52, 63, 52, 64, 65, 52, 65, 51, 51, 65, 64, 64, 47, 51, 66, 47, 64, 47, 66, 45, 45, 66, 67, 67, 66, 68, 57, 69, 55, 70, 55, 69, 55, 70, 53, 69, 71, 70, 62, 72, 73, 72, 62, 60, 73, 64, 62, 64, 73, 66, 73, 68, 66, 68, 73, 72, 68, 72, 74, 60, 74, 72, 75, 74, 60, 60, 58, 75, 76, 75, 58, 58, 56, 76, 54, 76, 56, 76, 54, 77, 77, 54, 53, 53, 78, 77, 78, 53, 70, 70, 79, 78, 80, 79, 70, 81, 79, 80, 71, 81, 80, 80, 82, 71, 71, 82, 70, 70, 82, 80, 83, 77, 78, 83, 78, 84, 84, 78, 79, 79, 85, 84, 85, 79, 81, 81, 86, 85, 85, 86, 87, 87, 84, 85, 88, 84, 87, 89, 84, 88, 88, 90, 89, 90, 88, 91, 92, 91, 88, 92, 93, 91, 84, 89, 83, 87, 92, 88, 94, 76, 95, 77, 95, 76, 77, 96, 95, 83, 96, 77, 97, 96, 83, 83, 98, 97, 98, 83, 89, 99, 98, 89, 89, 90, 99, 90, 100, 99, 91, 100, 90, 101, 100, 91, 91, 93, 101, 102, 101, 93, 93, 45, 102, 45, 93, 92, 92, 48, 45, 46, 45, 48, 99, 100, 101, 99, 101, 102, 103, 104, 94, 96, 103, 94, 94, 95, 96, 105, 103, 96, 105, 96, 97, 97, 106, 105, 67, 102, 45, 107, 102, 67, 102, 107, 99, 108, 99, 107, 98, 99, 108, 98, 108, 109, 98, 109, 97, 109, 110, 97, 110, 106, 97, 110, 111, 106, 105, 106, 111, 111, 112, 105, 112, 103, 105, 113, 103, 112, 103, 113, 104, 75, 104, 113, 94, 104, 75, 75, 76, 94, 107, 109, 108, 111, 110, 109, 111, 109, 107, 111, 107, 67, 67, 112, 111, 67, 68, 112, 112, 68, 114, 74, 114, 68, 114, 74, 113, 113, 74, 75, 112, 114, 113, 115, 116, 117, 118, 119, 120, 120, 121, 118, 121, 120, 122, 122, 123, 121, 123, 122, 124, 124, 125, 123, 125, 124, 126, 126, 127, 125, 127, 126, 128, 128, 117, 127, 117, 128, 115, 115, 128, 129, 115, 129, 130, 131, 130, 129, 129, 132, 131, 132, 129, 133, 133, 134, 132, 134, 133, 135, 119, 136, 137, 137, 120, 119, 122, 120, 137, 137, 138, 122, 124, 122, 138, 138, 139, 124, 124, 139, 135, 135, 126, 124, 128, 126, 135, 135, 133, 128, 128, 133, 129, 135, 140, 134, 140, 135, 139, 139, 141, 140, 141, 139, 138, 138, 142, 141, 142, 138, 137, 137, 143, 142, 143, 137, 136, 136, 144, 143, 145, 143, 144, 144, 146, 145, 147, 145, 146, 146, 148, 147, 142, 143, 145, 145, 149, 142, 141, 142, 149, 145, 147, 150, 150, 149, 145, 151, 149, 150, 149, 151, 141, 141, 151, 152, 152, 140, 141, 134, 140, 152, 152, 153, 134, 132, 134, 153, 153, 154, 132, 131, 132, 154, 154, 155, 131, 155, 154, 156, 156, 157, 155, 158, 156, 154, 154, 153, 158, 159, 158, 153, 153, 152, 159, 159, 152, 151, 151, 160, 159, 150, 160, 151, 161, 162, 163, 164, 163, 162, 162, 165, 164, 165, 166, 164, 164, 166, 167, 163, 164, 167, 167, 168, 163, 169, 163, 168, 168, 170, 169, 163, 169, 161, 171, 172, 173, 173, 174, 171, 174, 173, 175, 175, 176, 174, 176, 175, 177, 177, 178, 176, 178, 177, 179, 179, 180, 178, 180, 179, 181, 181, 182, 180, 183, 182, 181, 181, 184, 183, 185, 183, 184, 184, 186, 185, 187, 185, 186, 186, 188, 187, 189, 187, 188, 188, 190, 189, 190, 191, 192, 192, 189, 190, 189, 192, 193, 193, 194, 189, 187, 189, 194, 194, 195, 187, 185, 187, 195, 195, 196, 185, 183, 185, 196, 196, 197, 183, 182, 183, 197, 197, 198, 182, 180, 182, 198, 198, 199, 180, 178, 180, 199, 199, 200, 178, 176, 178, 200, 200, 201, 176, 174, 176, 201, 201, 202, 174, 171, 174, 202, 202, 203, 171, 204, 205, 206, 206, 207, 204, 208, 207, 206, 206, 209, 208, 208, 209, 210, 210, 211, 208, 212, 211, 210, 210, 213, 212, 212, 213, 214, 214, 215, 212, 212, 215, 216, 217, 204, 207, 207, 218, 217, 218, 207, 208, 208, 219, 218, 219, 208, 211, 211, 220, 219, 220, 211, 212, 212, 221, 220, 216, 221, 212, 222, 221, 216, 216, 223, 222, 224, 222, 223, 225, 222, 224, 224, 226, 225, 227, 225, 226, 226, 228, 227, 227, 228, 229, 230, 229, 228, 223, 231, 224, 232, 217, 218, 218, 233, 232, 233, 218, 219, 220, 221, 222, 222, 225, 220, 219, 220, 225, 225, 227, 219, 219, 227, 233, 229, 233, 227, 232, 233, 229, 229, 234, 232, 234, 235, 232, 217, 232, 235, 235, 236, 217, 204, 217, 236, 236, 237, 204, 205, 204, 237, 237, 238, 205, 228, 239, 230, 239, 228, 226, 226, 240, 239, 240, 226, 224, 224, 241, 240, 241, 224, 231, 231, 242, 241, 229, 230, 243, 243, 234, 229, 234, 243, 244, 244, 245, 234, 234, 245, 235, 245, 246, 235, 236, 235, 246, 246, 247, 236, 237, 236, 247, 247, 248, 237, 238, 237, 248, 248, 249, 238, 250, 251, 252, 253, 254, 255, 255, 250, 253, 250, 255, 256, 256, 251, 250, 257, 251, 256, 252, 251, 257, 257, 258, 252, 258, 257, 259, 259, 260, 258, 261, 262, 263, 264, 261, 263, 263, 265, 264, 265, 260, 264, 260, 265, 266, 266, 258, 260, 258, 266, 267, 267, 252, 258, 268, 252, 267, 252, 268, 250, 250, 268, 269, 269, 253, 250, 253, 269, 270, 270, 271, 253, 253, 271, 272, 272, 254, 253, 254, 272, 273, 273, 274, 254, 262, 275, 276, 276, 263, 262, 276, 265, 263, 265, 276, 266, 267, 277, 278, 278, 279, 267, 279, 268, 267, 268, 279, 269, 269, 279, 270, 270, 279, 278, 278, 280, 270, 255, 254, 274, 274, 281, 255, 256, 255, 281, 281, 282, 256, 256, 282, 257, 257, 282, 283, 283, 259, 257, 284, 259, 283, 260, 259, 284, 284, 264, 260, 261, 264, 284, 284, 285, 261, 262, 261, 285, 285, 286, 262, 287, 286, 285, 285, 288, 287, 287, 288, 289, 290, 289, 288, 289, 291, 287, 288, 292, 290, 282, 290, 292, 292, 283, 282, 283, 292, 284, 285, 284, 292, 292, 288, 285, 293, 294, 295, 296, 297, 293, 298, 293, 299, 300, 301, 298, 302, 296, 303, 293, 303, 296, 304, 303, 293, 303, 304, 302, 305, 302, 304, 304, 306, 305, 306, 307, 305, 295, 308, 309, 309, 310, 295, 295, 310, 293, 311, 293, 310, 293, 311, 304, 304, 311, 312, 312, 306, 304, 306, 312, 313, 313, 307, 306, 307, 313, 314, 314, 315, 307, 316, 315, 314, 314, 317, 316, 316, 317, 318, 319, 318, 317, 318, 319, 320, 320, 321, 318, 321, 320, 322, 322, 323, 321, 318, 324, 316, 313, 312, 325, 325, 326, 313, 314, 313, 326, 326, 327, 314, 314, 327, 328, 328, 317, 314, 317, 328, 319, 329, 319, 328, 328, 330, 329, 331, 330, 328, 328, 327, 331, 332, 331, 327, 327, 326, 332, 333, 332, 326, 326, 325, 333, 333, 325, 334, 334, 325, 312, 312, 311, 334, 310, 334, 311, 334, 310, 335, 336, 335, 310, 310, 309, 336, 335, 337, 334, 334, 337, 333, 338, 339, 340, 338, 340, 341, 342, 343, 340, 340, 339, 342, 344, 342, 339, 339, 345, 344, 346, 345, 339, 339, 347, 346, 347, 339, 338, 338, 348, 347, 348, 338, 349, 349, 338, 341, 349, 341, 350, 351, 352, 346, 346, 347, 351, 353, 351, 347, 347, 348, 353, 353, 348, 354, 349, 354, 348, 354, 349, 355, 356, 357, 344, 344, 345, 356, 358, 356, 345, 345, 346, 358, 359, 358, 346, 346, 352, 359, 360, 359, 352, 352, 361, 360, 361, 352, 351, 351, 362, 361, 362, 351, 363, 353, 363, 351, 364, 363, 353, 364, 353, 365, 354, 365, 353, 366, 365, 354, 354, 367, 366, 355, 367, 354, 367, 355, 368, 363, 369, 362, 370, 364, 365, 368, 371, 367, 366, 367, 371, 371, 372, 366, 373, 374, 375, 375, 376, 373, 376, 375, 377, 377, 378, 376, 378, 377, 379, 380, 381, 373, 374, 373, 381, 381, 382, 374, 374, 382, 383, 379, 384, 385, 379, 377, 384, 384, 377, 386, 375, 386, 377, 387, 386, 375, 387, 375, 374, 388, 387, 374, 374, 389, 388, 389, 374, 383, 383, 390, 389, 390, 383, 391, 391, 392, 390, 386, 393, 384, 394, 380, 395, 373, 395, 380, 396, 395, 373, 373, 376, 396, 397, 396, 376, 376, 378, 397, 397, 378, 398, 379, 398, 378, 398, 379, 399, 385, 399, 379, 399, 385, 400, 400, 401, 399, 402, 399, 401, 401, 403, 402, 402, 403, 404, 404, 405, 402, 406, 405, 404, 407, 405, 406, 398, 407, 397, 405, 407, 398, 398, 402, 405, 399, 402, 398, 404, 408, 406, 406, 409, 407, 407, 409, 397, 410, 397, 409, 396, 397, 410, 396, 410, 411, 395, 396, 411, 412, 395, 411, 411, 413, 412, 409, 414, 410, 415, 416, 417, 417, 394, 415, 418, 415, 394, 394, 395, 418] + point3f[] points = [(-0.0003535, 0.0053572, 0.0135448), (-0.0004016, 0.0057463, 0.0132984), (-0.0021475, 0.0070081, 0.0142598), (-0.0021475, 0.0070081, 0.0142598), (-0.0023243, 0.0066126, 0.0156680), (-0.0009971, 0.0048770, 0.0130947), (-0.0044631, 0.0062980, 0.0154734), (-0.0012681, 0.0051288, 0.0126201), (-0.0045695, 0.0063144, 0.0140454), (-0.0067971, 0.0075092, 0.0142949), (-0.0064558, 0.0074616, 0.0154080), (-0.0042122, 0.0091964, 0.0160422), (-0.0064465, 0.0090853, 0.0160260), (-0.0066858, 0.0104192, 0.0153568), (-0.0019654, 0.0094535, 0.0158213), (-0.0021475, 0.0070081, 0.0142598), (0.0006502, 0.0070297, 0.0128032), (0.0011685, 0.0071743, 0.0132363), (0.0012708, 0.0094789, 0.0131626), (-0.0027407, 0.0115333, 0.0153124), (-0.0047717, 0.0111795, 0.0154384), (-0.0048663, 0.0113733, 0.0147251), (-0.0066556, 0.0106677, 0.0145360), (-0.0048606, 0.0109800, 0.0140940), (-0.0065243, 0.0099807, 0.0137316), (-0.0043609, 0.0088411, 0.0142321), (-0.0061812, 0.0083852, 0.0137446), (-0.0067971, 0.0075092, 0.0142949), (-0.0045695, 0.0063144, 0.0140454), (-0.0021423, 0.0092295, 0.0144265), (-0.0021475, 0.0070081, 0.0142598), (0.0007520, 0.0093316, 0.0127255), (0.0006502, 0.0070297, 0.0128032), (0.0002366, 0.0115091, 0.0123547), (0.0000241, 0.0111435, 0.0121810), (-0.0028212, 0.0113464, 0.0140743), (-0.0028306, 0.0117371, 0.0146512), (0.0005450, 0.0112898, 0.0126156), (0.0007520, 0.0093316, 0.0127255), (0.0006502, 0.0070297, 0.0128032), (0.0000241, 0.0111435, 0.0121810), (-0.0004016, 0.0057463, 0.0132984), (-0.0003535, 0.0053572, 0.0135448), (-0.0009971, 0.0048770, 0.0130947), (-0.0012681, 0.0051288, 0.0126201), (-0.0032643, 0.0024015, 0.0348020), (-0.0042269, 0.0007486, 0.0357456), (-0.0048651, 0.0004606, 0.0382002), (-0.0021414, 0.0002122, 0.0337395), (-0.0029901, -0.0009884, 0.0350314), (-0.0044476, -0.0020837, 0.0377500), (-0.0056137, -0.0013247, 0.0377810), (-0.0054360, -0.0024477, 0.0411568), (0.0039572, 0.0026211, 0.0404271), (0.0035449, 0.0033887, 0.0413310), (0.0045462, 0.0020096, 0.0425591), (0.0033215, 0.0036595, 0.0439503), (0.0041529, 0.0012678, 0.0458676), (-0.0003026, 0.0048557, 0.0445203), (-0.0003026, 0.0023183, 0.0477978), (-0.0039267, 0.0036595, 0.0439503), (-0.0047581, 0.0012678, 0.0458676), (-0.0051514, 0.0020095, 0.0425591), (-0.0054826, -0.0018923, 0.0432405), (-0.0050945, 0.0004416, 0.0405927), (-0.0060225, -0.0016042, 0.0403546), (-0.0043735, 0.0031350, 0.0375835), (-0.0031075, 0.0042833, 0.0368711), (-0.0035304, 0.0036366, 0.0394568), (0.0048774, -0.0018922, 0.0432405), (0.0044893, 0.0004416, 0.0405927), (0.0048308, -0.0024477, 0.0411568), (-0.0041501, 0.0033887, 0.0413310), (-0.0045624, 0.0026211, 0.0404271), (-0.0025286, 0.0044874, 0.0416052), (-0.0003026, 0.0053197, 0.0418847), (0.0019234, 0.0044874, 0.0416052), (0.0029252, 0.0036366, 0.0394568), (0.0037683, 0.0031350, 0.0375835), (0.0042599, 0.0004606, 0.0382002), (0.0050085, -0.0013248, 0.0377810), (0.0038424, -0.0020837, 0.0377500), (0.0054173, -0.0016042, 0.0403546), (0.0025023, 0.0042833, 0.0368711), (0.0026600, 0.0024015, 0.0348020), (0.0036216, 0.0007486, 0.0357456), (0.0023849, -0.0009884, 0.0350314), (0.0015362, 0.0002122, 0.0337395), (0.0007375, 0.0028250, 0.0333366), (0.0018950, 0.0039203, 0.0349305), (0.0007513, 0.0044663, 0.0336480), (-0.0003026, 0.0028336, 0.0332193), (-0.0003026, 0.0010005, 0.0327494), (-0.0013428, 0.0028250, 0.0333366), (0.0007936, 0.0044685, 0.0398061), (0.0015430, 0.0041502, 0.0396526), (0.0011861, 0.0049632, 0.0380796), (0.0009428, 0.0052232, 0.0372894), (0.0013051, 0.0047061, 0.0358439), (-0.0003026, 0.0051634, 0.0351827), (-0.0003026, 0.0047336, 0.0335722), (-0.0013566, 0.0044663, 0.0336480), (-0.0025002, 0.0039203, 0.0349305), (-0.0003026, 0.0054508, 0.0387188), (-0.0003026, 0.0051886, 0.0399269), (-0.0003026, 0.0064961, 0.0378644), (-0.0003026, 0.0062834, 0.0371652), (-0.0019103, 0.0047061, 0.0358439), (-0.0003026, 0.0052773, 0.0357438), (-0.0003026, 0.0056463, 0.0361613), (-0.0003026, 0.0055210, 0.0369720), (-0.0015480, 0.0052231, 0.0372894), (-0.0017912, 0.0049632, 0.0380796), (-0.0013988, 0.0044685, 0.0398061), (-0.0021483, 0.0041502, 0.0396526), (-0.0091264, -0.0018447, 0.0202246), (-0.0079634, -0.0021566, 0.0210943), (-0.0076500, -0.0063599, 0.0233610), (-0.0079634, -0.0021566, 0.0210943), (-0.0091264, -0.0018447, 0.0202246), (-0.0095437, 0.0016223, 0.0215514), (-0.0087336, 0.0014513, 0.0228067), (-0.0103347, 0.0009487, 0.0247378), (-0.0091756, 0.0000917, 0.0263160), (-0.0110456, -0.0017043, 0.0264093), (-0.0094652, -0.0025370, 0.0280952), (-0.0115057, -0.0044257, 0.0254041), (-0.0085052, -0.0056869, 0.0270392), (-0.0112914, -0.0050105, 0.0223109), (-0.0158329, 0.0047911, 0.0143485), (-0.0131355, 0.0051859, 0.0142487), (-0.0130810, 0.0059848, 0.0136518), (-0.0153982, 0.0068109, 0.0130898), (-0.0174944, 0.0058374, 0.0156849), (-0.0172768, 0.0078509, 0.0141262), (-0.0167759, 0.0066917, 0.0177606), (-0.0131355, 0.0051859, 0.0142487), (-0.0125365, 0.0058220, 0.0158809), (-0.0133908, 0.0062783, 0.0174609), (-0.0154325, 0.0066213, 0.0185707), (-0.0166978, 0.0082270, 0.0166039), (-0.0153044, 0.0077123, 0.0178308), (-0.0133533, 0.0068152, 0.0170484), (-0.0126544, 0.0061798, 0.0153052), (-0.0130810, 0.0059848, 0.0136518), (-0.0120393, 0.0064585, 0.0149218), (-0.0123105, 0.0066960, 0.0132886), (-0.0067971, 0.0075092, 0.0142949), (-0.0061812, 0.0083852, 0.0137446), (-0.0125756, 0.0069374, 0.0165156), (-0.0064558, 0.0074616, 0.0154080), (-0.0139650, 0.0083421, 0.0173271), (-0.0150020, 0.0096261, 0.0160685), (-0.0152510, 0.0096045, 0.0135404), (-0.0141375, 0.0082166, 0.0125244), (-0.0123105, 0.0066960, 0.0132886), (-0.0065243, 0.0099807, 0.0137316), (-0.0061812, 0.0083852, 0.0137446), (-0.0066556, 0.0106677, 0.0145360), (-0.0066858, 0.0104192, 0.0153568), (-0.0064465, 0.0090853, 0.0160260), (0.0041120, 0.0041921, 0.0408156), (0.0018522, 0.0054224, 0.0425319), (0.0034662, 0.0055937, 0.0435835), (-0.0003418, 0.0048622, 0.0467342), (-0.0014192, 0.0046533, 0.0454314), (-0.0014234, 0.0032800, 0.0470038), (0.0006933, 0.0030044, 0.0471165), (0.0037677, 0.0035462, 0.0446239), (0.0057030, 0.0026362, 0.0409506), (0.0054359, 0.0023125, 0.0426428), (-0.0002026, -0.0090132, 0.0043990), (-0.0002026, -0.0126749, -0.0194372), (0.0093830, -0.0086730, -0.0194371), (0.0073601, -0.0064730, 0.0043990), (0.0140593, 0.0004313, -0.0194371), (0.0105159, -0.0003221, 0.0043990), (0.0113166, 0.0067204, -0.0194372), (0.0078219, 0.0049419, 0.0043990), (0.0065213, 0.0109431, -0.0194372), (0.0045014, 0.0077795, 0.0043990), (-0.0002026, 0.0126749, -0.0194372), (-0.0002026, 0.0090224, 0.0043990), (-0.0049099, 0.0077795, 0.0043990), (-0.0069298, 0.0109431, -0.0194372), (-0.0082304, 0.0049419, 0.0043990), (-0.0117252, 0.0067204, -0.0194372), (-0.0112140, -0.0007252, 0.0043990), (-0.0144644, 0.0004313, -0.0194372), (-0.0077678, -0.0067088, 0.0043990), (-0.0097881, -0.0086730, -0.0194372), (-0.0002026, -0.0126749, -0.0194372), (-0.0002026, -0.0090132, 0.0043990), (-0.0002026, -0.0060682, 0.0087761), (-0.0064052, -0.0046269, 0.0087761), (-0.0089116, -0.0004208, 0.0087761), (-0.0069506, 0.0036673, 0.0087761), (-0.0037146, 0.0046470, 0.0087761), (-0.0002026, 0.0060609, 0.0087761), (0.0033094, 0.0046470, 0.0087761), (0.0064343, 0.0037086, 0.0087761), (0.0081195, -0.0001313, 0.0087761), (0.0061233, -0.0044431, 0.0087761), (-0.0002026, -0.0060682, 0.0087761), (0.0161034, -0.0032975, 0.0187562), (0.0253328, -0.0002268, 0.0211332), (0.0251452, 0.0001721, 0.0218094), (0.0160292, -0.0028060, 0.0199096), (0.0159184, -0.0013425, 0.0205197), (0.0246998, 0.0019138, 0.0219711), (0.0243392, 0.0031640, 0.0212943), (0.0158024, -0.0000141, 0.0196324), (0.0157661, 0.0006092, 0.0178611), (0.0242991, 0.0032088, 0.0199063), (0.0247325, 0.0016837, 0.0197047), (0.0162118, -0.0010013, 0.0166925), (0.0145598, -0.0011721, 0.0166635), (0.0151163, -0.0035822, 0.0186808), (0.0154128, -0.0030757, 0.0200736), (0.0153324, -0.0015504, 0.0205246), (0.0149784, -0.0003143, 0.0196871), (0.0146966, 0.0002755, 0.0178284), (0.0136504, 0.0000733, 0.0183356), (0.0131316, -0.0013429, 0.0173930), (0.0087628, 0.0010091, 0.0229833), (0.0142458, -0.0004146, 0.0199606), (0.0094945, 0.0001246, 0.0258505), (0.0148096, -0.0016564, 0.0208001), (0.0098668, -0.0025637, 0.0272529), (0.0095648, -0.0051968, 0.0259355), (0.0080798, -0.0057024, 0.0271133), (0.0082914, -0.0027777, 0.0215828), (0.0140592, -0.0038429, 0.0193824), (0.0147184, -0.0034681, 0.0203991), (0.0087805, -0.0053699, 0.0230324), (0.0133708, -0.0034852, 0.0176596), (0.0147753, -0.0031345, 0.0171353), (0.0161792, -0.0027839, 0.0170876), (0.0252685, -0.0000525, 0.0203592), (0.0084769, -0.0028291, 0.0286073), (0.0082152, 0.0002824, 0.0272518), (0.0074424, 0.0017594, 0.0235910), (0.0073577, -0.0028841, 0.0219845), (0.0073453, -0.0058688, 0.0235236), (0.0073577, -0.0028841, 0.0219845), (0.0082914, -0.0027777, 0.0215828), (0.0131316, -0.0013429, 0.0173930), (0.0145598, -0.0011721, 0.0166635), (0.0162118, -0.0010013, 0.0166925), (0.0247325, 0.0016837, 0.0197047), (0.0065851, -0.0059424, 0.0437769), (0.0049697, -0.0087722, 0.0441236), (0.0040615, -0.0081866, 0.0390420), (0.0065249, -0.0002522, 0.0428843), (0.0046721, 0.0023705, 0.0464545), (0.0051154, -0.0044969, 0.0487068), (0.0046497, -0.0074087, 0.0480841), (-0.0003026, -0.0109051, 0.0441892), (-0.0003026, -0.0099736, 0.0390868), (-0.0055749, -0.0087722, 0.0441236), (-0.0046668, -0.0081866, 0.0390420), (-0.0067970, -0.0038035, 0.0382398), (-0.0043459, -0.0014559, 0.0373860), (-0.0086622, -0.0054573, 0.0354709), (-0.0066919, -0.0061270, 0.0387763), (-0.0084582, -0.0089542, 0.0299170), (-0.0003026, -0.0111345, 0.0272537), (0.0078529, -0.0089542, 0.0299170), (0.0060866, -0.0061270, 0.0387763), (0.0061918, -0.0038035, 0.0382398), (0.0037406, -0.0014558, 0.0373860), (0.0045854, 0.0019465, 0.0424951), (0.0033259, 0.0036883, 0.0439425), (-0.0003026, 0.0044918, 0.0456209), (-0.0009581, 0.0033895, 0.0490776), (-0.0003026, -0.0014558, 0.0373860), (-0.0003026, -0.0077191, 0.0278350), (-0.0003026, -0.0111345, 0.0272537), (-0.0003026, -0.0077191, 0.0278350), (0.0080569, -0.0054573, 0.0354709), (-0.0003026, -0.0014558, 0.0373860), (-0.0013756, -0.0041985, 0.0514650), (-0.0008857, -0.0084212, 0.0500747), (-0.0052877, -0.0074086, 0.0482048), (-0.0071903, -0.0059424, 0.0437769), (-0.0068005, -0.0002848, 0.0428877), (-0.0052201, 0.0019638, 0.0426405), (-0.0039987, 0.0037681, 0.0441276), (-0.0052187, 0.0022915, 0.0469067), (-0.0009581, 0.0033895, 0.0490776), (-0.0013756, -0.0041985, 0.0514650), (-0.0003026, 0.0044918, 0.0456209), (-0.0056517, -0.0044969, 0.0489327), (0.0274260, 0.0054053, 0.0204944), (0.0286014, 0.0066585, 0.0178505), (0.0278080, 0.0066655, 0.0173903), (0.0307428, 0.0055796, 0.0172862), (0.0277399, 0.0047130, 0.0192345), (0.0286014, 0.0066585, 0.0178505), (0.0277399, 0.0047130, 0.0192345), (0.0277399, 0.0047130, 0.0192345), (0.0286284, 0.0060646, 0.0175314), (0.0317484, 0.0036320, 0.0176628), (0.0312796, 0.0058051, 0.0176623), (0.0323926, 0.0038576, 0.0181888), (0.0318994, 0.0016117, 0.0177315), (0.0325437, 0.0018372, 0.0182575), (0.0322780, 0.0015423, 0.0179930), (0.0278984, 0.0060233, 0.0170112), (0.0256022, 0.0041406, 0.0193301), (0.0254946, 0.0047157, 0.0209230), (0.0290092, 0.0031712, 0.0213469), (0.0292326, 0.0009109, 0.0215323), (0.0292348, 0.0004984, 0.0209698), (0.0290626, 0.0006599, 0.0201598), (0.0318994, 0.0016117, 0.0177315), (0.0317484, 0.0036320, 0.0176628), (0.0284995, 0.0028638, 0.0196751), (0.0277399, 0.0047130, 0.0192345), (0.0256022, 0.0041406, 0.0193301), (0.0278984, 0.0060233, 0.0170112), (0.0286284, 0.0060646, 0.0175314), (0.0278080, 0.0066655, 0.0173903), (0.0286014, 0.0066585, 0.0178505), (0.0307428, 0.0055796, 0.0172862), (0.0272553, 0.0003992, 0.0218539), (0.0272506, -0.0000234, 0.0212402), (0.0270727, 0.0001271, 0.0205315), (0.0264691, 0.0022100, 0.0198917), (0.0242991, 0.0032088, 0.0199063), (0.0247325, 0.0016837, 0.0197047), (0.0252685, -0.0000525, 0.0203592), (0.0253328, -0.0002268, 0.0211332), (0.0251452, 0.0001721, 0.0218094), (0.0268881, 0.0025152, 0.0218178), (0.0243392, 0.0031640, 0.0212943), (0.0242991, 0.0032088, 0.0199063), (0.0246998, 0.0019138, 0.0219711), (0.0035213, -0.0079627, 0.0260449), (0.0055596, -0.0052844, 0.0170877), (0.0073453, -0.0058688, 0.0235236), (0.0080798, -0.0057024, 0.0271133), (0.0064057, -0.0011273, 0.0165219), (0.0073577, -0.0028841, 0.0219845), (0.0062349, -0.0003588, 0.0115705), (0.0049309, -0.0034947, 0.0113411), (-0.0003026, -0.0045608, 0.0116206), (-0.0003026, -0.0064203, 0.0168781), (-0.0003026, -0.0084831, 0.0261002), (0.0022805, -0.0031583, 0.0312524), (0.0084769, -0.0028291, 0.0286073), (-0.0061648, -0.0052930, 0.0170877), (-0.0055361, -0.0034948, 0.0113411), (-0.0041265, -0.0079627, 0.0260449), (-0.0003026, -0.0046832, 0.0318230), (0.0019071, -0.0030773, 0.0350970), (0.0065006, -0.0048298, 0.0068683), (0.0088051, -0.0004416, 0.0071684), (-0.0003027, -0.0066376, 0.0069718), (-0.0071058, -0.0048298, 0.0068683), (-0.0094103, -0.0004416, 0.0071684), (-0.0068402, -0.0003588, 0.0115705), (-0.0070109, -0.0011273, 0.0164245), (-0.0076500, -0.0063599, 0.0233610), (-0.0085052, -0.0056869, 0.0270392), (-0.0028857, -0.0030592, 0.0313668), (-0.0025123, -0.0030773, 0.0350970), (-0.0003026, -0.0042158, 0.0353506), (0.0028109, -0.0046660, 0.0377293), (-0.0079634, -0.0021566, 0.0210943), (-0.0094652, -0.0025370, 0.0280952), (-0.0003027, -0.0055612, 0.0375481), (-0.0034161, -0.0046660, 0.0377293), (-0.0003026, 0.0002898, 0.0293961), (0.0017958, -0.0003821, 0.0298212), (0.0054760, 0.0033499, 0.0240229), (-0.0003026, 0.0045142, 0.0235313), (0.0073346, 0.0057095, 0.0195501), (-0.0003026, 0.0061805, 0.0192499), (0.0051068, 0.0028255, 0.0157621), (-0.0021414, 0.0002122, 0.0337395), (-0.0003026, 0.0010005, 0.0327494), (0.0015362, 0.0002122, 0.0337395), (0.0023849, -0.0009884, 0.0350314), (0.0064057, -0.0011273, 0.0165219), (0.0062349, -0.0003588, 0.0115705), (0.0074424, 0.0017594, 0.0235910), (0.0082152, 0.0002824, 0.0272518), (0.0084769, -0.0028291, 0.0286073), (0.0022805, -0.0031583, 0.0312524), (0.0019071, -0.0030773, 0.0350970), (0.0038424, -0.0020837, 0.0377500), (0.0028109, -0.0046660, 0.0377293), (0.0073577, -0.0028841, 0.0219845), (-0.0029901, -0.0009884, 0.0350314), (-0.0024010, -0.0003821, 0.0298212), (-0.0060812, 0.0033499, 0.0240780), (-0.0079398, 0.0057094, 0.0195501), (-0.0003027, 0.0047850, 0.0159657), (0.0051379, 0.0028496, 0.0115774), (0.0088051, -0.0004416, 0.0071684), (0.0067386, 0.0040122, 0.0071615), (-0.0003026, 0.0047712, 0.0113704), (-0.0003026, 0.0066962, 0.0071684), (-0.0073438, 0.0040122, 0.0071615), (-0.0057431, 0.0028496, 0.0115774), (-0.0068402, -0.0003588, 0.0115705), (-0.0057120, 0.0028255, 0.0157621), (-0.0094103, -0.0004416, 0.0071684), (-0.0070109, -0.0011273, 0.0164245), (-0.0087336, 0.0014513, 0.0228067), (-0.0091756, 0.0000917, 0.0263160), (-0.0028857, -0.0030592, 0.0313668), (-0.0094652, -0.0025370, 0.0280952), (-0.0079634, -0.0021566, 0.0210943), (-0.0025123, -0.0030773, 0.0350970), (-0.0034161, -0.0046660, 0.0377293), (-0.0044476, -0.0020837, 0.0377500), (-0.0028857, -0.0030592, 0.0313668)] + color3f[] primvars:HulaGirl_UpperBody_color = [(0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216)] ( + interpolation = "vertex" + ) + texCoord2f[] primvars:HulaGirl_UpperBody_UV = [(0.8427134, 0.0737356), (0.8414416, 0.0781143), (0.8213814, 0.0802436), (0.8213814, 0.0802436), (0.8144776, 0.0653412), (0.8460211, 0.0666132), (0.8142235, 0.0444824), (0.8490734, 0.0622889), (0.8254163, 0.0383775), (0.8177848, 0.0269302), (0.8061632, 0.0288854), (0.7887856, 0.0477895), (0.7895486, 0.0279478), (0.7712331, 0.0279478), (0.7885305, 0.0673763), (0.8213814, 0.0802436), (0.8057934, 0.1162169), (0.8015046, 0.1121469), (0.7816627, 0.1108751), (0.7666388, 0.0636258), (0.7670344, 0.0454999), (0.7604210, 0.0454996), (0.7629188, 0.0269933), (0.7538074, 0.0454997), (0.7546359, 0.0268013), (0.7374011, 0.0485526), (0.7348577, 0.0296797), (0.7201348, 0.0321922), (0.7150164, 0.0454998), (0.7389276, 0.0686483), (0.7193407, 0.0676305), (0.7396907, 0.1085858), (0.7190862, 0.1073139), (0.7613130, 0.1065504), (0.7583740, 0.1109290), (0.7554464, 0.0654062), (0.7606094, 0.0646054), (0.7652692, 0.1065502), (0.7803912, 0.1162171), (0.8057934, 0.1162169), (0.7583740, 0.1109290), (0.7053500, 0.0826390), (0.7022966, 0.0854370), (0.6961919, 0.0800949), (0.6974639, 0.0750077), (0.2780165, 0.7425255), (0.3178560, 0.7427143), (0.3447932, 0.7885327), (0.2778282, 0.6918606), (0.3235205, 0.7058330), (0.3801645, 0.7537912), (0.3708499, 0.7775815), (0.4121370, 0.8315820), (0.1255817, 0.8415893), (0.1396168, 0.8600297), (0.1073930, 0.8770229), (0.1494353, 0.9035826), (0.1140642, 0.9436108), (0.2206805, 0.9094987), (0.2204286, 0.9798630), (0.2919258, 0.9037715), (0.3271706, 0.9441143), (0.3339684, 0.8774008), (0.4091159, 0.8748831), (0.3564995, 0.8349809), (0.3897307, 0.8172324), (0.2990378, 0.7929386), (0.2656181, 0.7801619), (0.2891251, 0.8288446), (0.0322453, 0.8741279), (0.0851128, 0.8343514), (0.0294761, 0.8307008), (0.3018702, 0.8603445), (0.3160310, 0.8420298), (0.2658065, 0.8658831), (0.2206805, 0.8675821), (0.1756174, 0.8657572), (0.1522676, 0.8284669), (0.1425748, 0.7926238), (0.0969453, 0.7880295), (0.0708890, 0.7768264), (0.0617002, 0.7530363), (0.0518821, 0.8166032), (0.1760579, 0.7799733), (0.1638480, 0.7423369), (0.1241343, 0.7422110), (0.1185960, 0.7053291), (0.1644146, 0.6916088), (0.2048199, 0.7174136), (0.1830440, 0.7547355), (0.2059529, 0.7386863), (0.2209323, 0.7146440), (0.2211839, 0.6825459), (0.2370442, 0.7174135), (0.1938688, 0.8439810), (0.1781349, 0.8346031), (0.1892120, 0.8080436), (0.1957574, 0.7938198), (0.1899665, 0.7688962), (0.2208483, 0.7630222), (0.2209323, 0.7434694), (0.2357853, 0.7386864), (0.2586950, 0.7548612), (0.2207648, 0.8187428), (0.2206805, 0.8444213), (0.2207642, 0.8046449), (0.2206808, 0.7961693), (0.2516461, 0.7691483), (0.2207645, 0.7693997), (0.2207645, 0.7754419), (0.2207645, 0.7871063), (0.2458553, 0.7940714), (0.2524009, 0.8081695), (0.2475551, 0.8440440), (0.2634783, 0.8346032), (0.6382644, 0.5058909), (0.6422993, 0.5142369), (0.6155866, 0.5517441), (0.4623280, 0.5047916), (0.4645877, 0.4900512), (0.5009184, 0.4953194), (0.4992271, 0.5107160), (0.5264633, 0.5170607), (0.5247911, 0.5359389), (0.5490993, 0.5359537), (0.5478888, 0.5544106), (0.5725427, 0.5407828), (0.5788477, 0.5641336), (0.6067564, 0.5357319), (0.6084320, 0.4248767), (0.6290193, 0.4213610), (0.6296856, 0.4120183), (0.6124063, 0.4164367), (0.5893375, 0.4275212), (0.5908379, 0.4201926), (0.5711707, 0.4308253), (0.4921298, 0.4206413), (0.5108876, 0.4294945), (0.5312734, 0.4335816), (0.5538702, 0.4353139), (0.5706306, 0.4239103), (0.5539512, 0.4258352), (0.5322723, 0.4239627), (0.5118266, 0.4195300), (0.4932317, 0.4111593), (0.5128837, 0.4067774), (0.4960143, 0.4005506), (0.5175077, 0.3454611), (0.5057580, 0.3444792), (0.5328113, 0.4113928), (0.5346494, 0.3464605), (0.5541953, 0.4128925), (0.5702489, 0.4108562), (0.5922540, 0.4080935), (0.6102643, 0.4056647), (0.6263666, 0.4001299), (0.6031935, 0.3398913), (0.6205933, 0.3434855), (0.5834421, 0.3399731), (0.5677096, 0.3406978), (0.5504057, 0.3436555), (0.1749215, 0.6435530), (0.1377891, 0.6591762), (0.1504727, 0.6344172), (0.0787879, 0.6291391), (0.0705630, 0.6482831), (0.0526712, 0.6292239), (0.0779318, 0.6036398), (0.1486924, 0.6109236), (0.1929811, 0.6205234), (0.1766445, 0.6035286), (0.1772511, 0.2955979), (0.0412926, 0.1579787), (0.1028601, 0.1067726), (0.2122747, 0.2564243), (0.1745922, 0.0600854), (0.2552892, 0.2258691), (0.2286823, 0.0377587), (0.2853240, 0.2144719), (0.2810755, 0.0235335), (0.3123107, 0.2077586), (0.3380480, 0.0172990), (0.3438305, 0.2039162), (0.3755178, 0.2056985), (0.3955573, 0.0200199), (0.4029467, 0.2109032), (0.4483923, 0.0309122), (0.4334843, 0.2201746), (0.5034868, 0.0499223), (0.4783114, 0.2481033), (0.5781839, 0.0919303), (0.6431950, 0.1390309), (0.5157436, 0.2851191), (0.4889862, 0.3024122), (0.4607107, 0.2710645), (0.4216521, 0.2460335), (0.3955572, 0.2388008), (0.3721501, 0.2347729), (0.3448834, 0.2333775), (0.3175423, 0.2365953), (0.2946159, 0.2421788), (0.2687517, 0.2511356), (0.2312258, 0.2783762), (0.2050868, 0.3112327), (0.0877430, 0.4243659), (0.1176375, 0.3327072), (0.1277982, 0.3351398), (0.1036751, 0.4290723), (0.1239514, 0.4304310), (0.1408428, 0.3376071), (0.1512563, 0.3394100), (0.1427278, 0.4283472), (0.1634254, 0.4242339), (0.1621734, 0.3400543), (0.1780020, 0.3389754), (0.1830980, 0.4185886), (0.1846132, 0.4274079), (0.0854713, 0.4348744), (0.1023436, 0.4389846), (0.1236679, 0.4387696), (0.1426146, 0.4378294), (0.1652721, 0.4336821), (0.1663807, 0.4443089), (0.1875627, 0.4380697), (0.1716533, 0.5187129), (0.1434169, 0.4476149), (0.1432400, 0.5291610), (0.1237702, 0.4489098), (0.1174369, 0.5354041), (0.0915124, 0.5291039), (0.0842672, 0.5494788), (0.1997699, 0.5018063), (0.0827495, 0.4443468), (0.1007018, 0.4490691), (0.0653244, 0.5118770), (0.0661563, 0.4383898), (0.0693278, 0.4280854), (0.0734925, 0.4197922), (0.1013931, 0.3276660), (0.1136488, 0.5562523), (0.1434366, 0.5515243), (0.1799063, 0.5385773), (0.2047258, 0.5084508), (0.0555452, 0.5266749), (0.0335025, 0.5041050), (0.0384342, 0.4935818), (0.0499491, 0.4309022), (0.0536698, 0.4215840), (0.0580383, 0.4107938), (0.0847988, 0.3233194), (0.5021027, 0.8208235), (0.5263729, 0.8200557), (0.5272590, 0.8567268), (0.4718093, 0.8200555), (0.4630104, 0.7693896), (0.5080076, 0.7822039), (0.5259600, 0.7914158), (0.5655241, 0.8158630), (0.5672959, 0.8547190), (0.6036717, 0.8101941), (0.6061518, 0.8495225), (0.6562272, 0.8520026), (0.6870528, 0.8629863), (0.6427634, 0.8759186), (0.6315439, 0.8485776), (0.6213280, 0.9163098), (0.5727284, 0.9530990), (0.5196414, 0.9231009), (0.5013944, 0.8580260), (0.4771830, 0.8623959), (0.4475391, 0.8757415), (0.4465942, 0.8223587), (0.4359649, 0.7720469), (0.4439960, 0.7217346), (0.4741124, 0.7210264), (0.7158695, 0.8903863), (0.6363864, 0.9689252), (0.5727284, 0.9530990), (0.5113147, 0.9768379), (0.4933631, 0.8854849), (0.4225012, 0.9076295), (0.5372977, 0.7380331), (0.5651597, 0.7578674), (0.6024905, 0.7774794), (0.6278825, 0.8105484), (0.6603022, 0.8103712), (0.6856353, 0.8136192), (0.6973269, 0.7729917), (0.6701639, 0.7618310), (0.6824218, 0.7253202), (0.6084310, 0.7303125), (0.7114711, 0.7400778), (0.6223316, 0.7674407), (0.8107193, 0.2067941), (0.8392094, 0.1989084), (0.8425160, 0.2062855), (0.8046603, 0.1535805), (0.8195892, 0.1925268), (0.8392094, 0.1989084), (0.8195892, 0.1925268), (0.8195892, 0.1925268), (0.8381917, 0.1953474), (0.7791763, 0.1530404), (0.7997805, 0.1592253), (0.7796846, 0.1594796), (0.7602087, 0.1564587), (0.7618781, 0.1622781), (0.7575542, 0.1622781), (0.8453144, 0.2108643), (0.8206394, 0.2340123), (0.8097011, 0.2276530), (0.7842628, 0.2037416), (0.7649865, 0.2075643), (0.7593229, 0.2070380), (0.7534279, 0.2067877), (0.7519298, 0.1632956), (0.7333880, 0.1625322), (0.7341508, 0.2024698), (0.7143098, 0.2042507), (0.7104938, 0.2268898), (0.6919240, 0.1978909), (0.6995561, 0.1897509), (0.6903980, 0.1928032), (0.6965031, 0.1872072), (0.7125291, 0.1645672), (0.7651848, 0.2253636), (0.7588257, 0.2253638), (0.7524661, 0.2253638), (0.7328797, 0.2228198), (0.7168534, 0.2388458), (0.7308444, 0.2398635), (0.7505930, 0.2419526), (0.7575542, 0.2424072), (0.7664573, 0.2431701), (0.7842639, 0.2233289), (0.8000349, 0.2418981), (0.8127529, 0.2452052), (0.7845173, 0.2434243), (0.8233177, 0.4785692), (0.8382795, 0.3868133), (0.8541182, 0.4500254), (0.8651235, 0.4946083), (0.8711519, 0.3741369), (0.8837051, 0.4277110), (0.8659465, 0.3242719), (0.8315850, 0.3281353), (0.7795126, 0.3309850), (0.7795131, 0.3857771), (0.7795131, 0.4798177), (0.8093699, 0.5397264), (0.8786411, 0.5203386), (0.7159942, 0.3858891), (0.7276997, 0.3281353), (0.7359669, 0.4806773), (0.7797722, 0.5368121), (0.8029583, 0.5711382), (0.8383213, 0.2830580), (0.8780517, 0.2836931), (0.7795126, 0.2859076), (0.7209644, 0.2830579), (0.6832116, 0.2827037), (0.6923490, 0.3252614), (0.6841760, 0.3722882), (0.7081342, 0.4500901), (0.6960103, 0.4957270), (0.7499147, 0.5398559), (0.7563266, 0.5712677), (0.7797722, 0.5737290), (0.8082688, 0.5995060), (0.6785477, 0.4237542), (0.6776764, 0.5176300), (0.7797722, 0.5993761), (0.7511455, 0.5995060), (0.8571644, 0.8764452), (0.8356625, 0.8818212), (0.8041853, 0.8159538), (0.8569055, 0.8083116), (0.7892885, 0.7739028), (0.8566464, 0.7668611), (0.7969737, 0.7351108), (0.8740036, 0.9162117), (0.8574239, 0.9081807), (0.8405844, 0.9164708), (0.8289263, 0.9294243), (0.7540883, 0.7406470), (0.7551417, 0.6942906), (0.7681754, 0.8194513), (0.7685640, 0.8527409), (0.7613097, 0.8770932), (0.8192110, 0.8984661), (0.8180459, 0.9296830), (0.8185638, 0.9631025), (0.8058693, 0.9594759), (0.7178371, 0.8029999), (0.8856615, 0.9291651), (0.8787316, 0.8815621), (0.9096255, 0.8153061), (0.9265857, 0.7729443), (0.8566464, 0.7353845), (0.7967620, 0.6950027), (0.7374933, 0.6572283), (0.7918800, 0.6511884), (0.8563871, 0.6931569), (0.8561280, 0.6506702), (0.9206350, 0.6504109), (0.9218720, 0.6949865), (0.9700664, 0.6965576), (0.9199406, 0.7333755), (0.9864366, 0.6528890), (0.9728364, 0.7366964), (0.9490304, 0.8167424), (0.9482816, 0.8517044), (0.8988888, 0.8980775), (0.9606280, 0.8707891), (0.9835396, 0.7938519), (0.9000543, 0.9291651), (0.9084595, 0.9589573), (0.8957649, 0.9625845), (0.8988888, 0.8980775)] ( + interpolation = "vertex" + ) + normal3f[] normals = [(0.15722176, -0.77924919, 0.60667276), (-0.07822289, 0.53348446, -0.84218484), (0.06674977, 0.07452704, -0.99498242), (0.06674977, 0.07452704, -0.99498242), (-0.08426932, -0.04307675, 0.99551141), (-0.47783077, -0.85918307, -0.18298115), (0.11858578, -0.20108867, 0.97236860), (-0.47783077, -0.85918307, -0.18298115), (0.07539062, 0.06731875, -0.99487907), (-0.50361586, -0.85585797, 0.11780535), (-0.50361586, -0.85585797, 0.11780535), (-0.14080870, 0.25182021, 0.95747554), (-0.02858569, 0.44410965, 0.89551628), (-0.37794426, 0.88989115, 0.25544411), (0.11858578, -0.20108867, 0.97236860), (0.06674977, 0.07452704, -0.99498242), (0.20920719, -0.97497022, 0.07526781), (0.57127911, 0.00088073, 0.82075530), (0.64005893, 0.02454317, -0.76793361), (0.27334866, 0.92909092, 0.24913937), (-0.36401451, 0.88546258, 0.28887585), (-0.37794426, 0.88989115, 0.25544411), (-0.37794426, 0.88989115, 0.25544411), (0.00187327, -0.06402867, -0.99794626), (-0.28273031, 0.70616126, -0.64915305), (0.00187327, -0.06402867, -0.99794626), (0.24782464, 0.04540037, -0.96774048), (-0.50361586, -0.85585797, 0.11780535), (0.07539062, 0.06731875, -0.99487907), (0.10895329, -0.12906033, -0.98563302), (0.06674977, 0.07452704, -0.99498242), (0.63642180, 0.02398824, -0.77096802), (0.20920719, -0.97497022, 0.07526781), (0.32759091, 0.87742627, 0.35043857), (0.63642180, 0.02398824, -0.77096802), (0.00187327, -0.06402867, -0.99794626), (0.32759091, 0.87742627, 0.35043857), (0.63642180, 0.02398824, -0.77096802), (0.63642180, 0.02398824, -0.77096802), (0.20920719, -0.97497022, 0.07526781), (0.63642180, 0.02398824, -0.77096802), (-0.07822289, 0.53348446, -0.84218484), (0.15722176, -0.77924919, 0.60667276), (-0.47783077, -0.85918307, -0.18298115), (-0.47783077, -0.85918307, -0.18298115), (-0.69413227, -0.00668782, -0.71981627), (-0.69413227, -0.00668782, -0.71981627), (-0.95860487, -0.11087712, -0.26226491), (-0.69413227, -0.00668782, -0.71981627), (-0.70490688, -0.22540957, -0.67253006), (-0.53386754, -0.81035984, -0.24145851), (-0.53386754, -0.81035984, -0.24145851), (-0.53386754, -0.81035984, -0.24145851), (0.76856339, 0.61604810, -0.17261210), (0.76856339, 0.61604810, -0.17261210), (0.80597514, 0.59190124, 0.00753940), (0.80597514, 0.59190124, 0.00753940), (0.86093616, 0.46488914, 0.20655966), (0.41160205, 0.72063214, 0.55791837), (0.41160205, 0.72063214, 0.55791837), (-0.55610931, 0.83022094, -0.03841221), (-0.98404658, 0.10818899, 0.14123528), (-0.80596703, 0.59191233, 0.00753410), (-0.98404658, 0.10818899, 0.14123528), (-0.66420650, 0.21810752, 0.71502358), (-0.71445709, -0.67431140, -0.18669508), (-0.64260846, 0.76142812, 0.08533125), (-0.64260846, 0.76142812, 0.08533125), (-0.64260846, 0.76142812, 0.08533125), (0.98404747, 0.10819355, 0.14122556), (0.66420418, 0.21810536, 0.71502638), (0.71444088, -0.67432606, -0.18670419), (-0.55610931, 0.83022094, -0.03841221), (-0.97161156, 0.23556001, -0.02196225), (-0.55610931, 0.83022094, -0.03841221), (0.36334053, 0.91754609, 0.16153219), (0.55610681, 0.83022267, -0.03841023), (0.76856339, 0.61604810, -0.17261210), (0.72717845, 0.53451586, -0.43070185), (0.95860302, -0.11088081, -0.26227000), (0.71444088, -0.67432606, -0.18670419), (0.53386015, -0.81036371, -0.24146186), (0.71444088, -0.67432606, -0.18670419), (0.54031527, 0.83287311, 0.11992335), (0.69418043, -0.00701179, -0.71976680), (0.70491338, -0.22542119, -0.67251933), (0.70491338, -0.22542119, -0.67251933), (0.70491338, -0.22542119, -0.67251933), (0.11061868, 0.24680664, -0.96273041), (0.76507699, 0.11383442, -0.63379711), (0.11629102, 0.18136592, -0.97651559), (0.11061868, 0.24680664, -0.96273041), (0.11061868, 0.24680664, -0.96273041), (-0.11061735, 0.24680674, -0.96273053), (0.42568082, 0.83964294, 0.33733580), (0.31216359, 0.92410654, -0.22041060), (0.54031527, 0.83287311, 0.11992335), (0.46696916, 0.86323804, -0.19172849), (0.46696916, 0.86323804, -0.19172849), (0.48597485, 0.87175232, -0.06225889), (0.11629102, 0.18136592, -0.97651559), (-0.11165194, 0.18435673, -0.97649693), (-0.61377966, 0.36790091, -0.69851506), (0.42062336, 0.84233886, 0.33695865), (0.54388267, 0.83728576, -0.05607131), (0.71675032, 0.69727445, 0.00878204), (0.29240435, 0.23488069, -0.92700088), (-0.46698961, 0.86323333, -0.19169961), (0.29380441, 0.71613604, -0.63310981), (-0.19367677, 0.96954501, 0.14990541), (-0.19367677, 0.96954501, 0.14990541), (-0.19367677, 0.96954501, 0.14990541), (-0.36936694, 0.85708725, 0.35912302), (-0.42567801, 0.83964717, 0.33732873), (-0.31217358, 0.92410332, -0.22040999), (0.61976510, 0.34446174, -0.70515043), (0.61976510, 0.34446174, -0.70515043), (-0.36745921, -0.92623544, 0.08403278), (0.61976510, 0.34446174, -0.70515043), (0.61976510, 0.34446174, -0.70515043), (0.61976510, 0.34446174, -0.70515043), (-0.63342619, 0.74507499, 0.20888825), (-0.08241468, 0.97899270, 0.18649659), (-0.63342619, 0.74507499, 0.20888825), (-0.49859825, 0.55377126, 0.66688603), (-0.47293675, 0.52901030, 0.70461249), (-0.75209916, -0.11267601, 0.64934641), (-0.56339473, -0.41183612, 0.71622425), (-0.44608286, -0.88454783, 0.13632727), (-0.69985318, -0.26316479, -0.66404045), (0.05832086, -0.60007918, -0.79781169), (0.41316363, -0.91065639, 0.00080646), (-0.60854125, -0.44358355, -0.65795976), (-0.60854125, -0.44358355, -0.65795976), (-0.60854125, -0.44358355, -0.65795976), (-0.96111375, 0.19566092, 0.19487688), (0.05832086, -0.60007918, -0.79781169), (0.84257960, 0.51748973, 0.14921059), (0.88022840, 0.24899693, 0.40397811), (0.48344895, 0.45177206, 0.74978596), (-0.33934632, 0.66563654, 0.66465920), (0.51059949, 0.50119758, 0.69863361), (0.84257960, 0.51748973, 0.14921059), (0.84257960, 0.51748973, 0.14921059), (0.41316363, -0.91065639, 0.00080646), (0.41316363, -0.91065639, 0.00080646), (0.60696751, -0.76588625, -0.21215221), (-0.50361586, -0.85585797, 0.11780535), (0.24782464, 0.04540037, -0.96774048), (0.13933663, -0.93446887, 0.32764778), (-0.50361586, -0.85585797, 0.11780535), (-0.04495765, 0.68052882, 0.73134065), (-0.33934632, 0.66563654, 0.66465920), (0.01838322, 0.60017693, -0.79965591), (0.01838322, 0.60017693, -0.79965591), (0.60696751, -0.76588625, -0.21215221), (-0.28273031, 0.70616126, -0.64915305), (0.24782464, 0.04540037, -0.96774048), (-0.37794426, 0.88989115, 0.25544411), (-0.37794426, 0.88989115, 0.25544411), (-0.02858569, 0.44410965, 0.89551628), (0.69468015, 0.69395930, -0.18931407), (-0.14228198, 0.98815697, 0.05745886), (0.51431084, 0.44747978, 0.73160511), (-0.02799490, 0.18650547, 0.98205495), (-0.68487811, 0.54971719, 0.47828114), (-0.02799490, 0.18650547, 0.98205495), (-0.02799490, 0.18650547, 0.98205495), (0.51431084, 0.44747978, 0.73160511), (0.74369097, 0.62511045, 0.23698211), (0.74369097, 0.62511045, 0.23698211), (0.20845553, -0.81145030, 0.54598033), (0.38144678, -0.91367316, 0.14035526), (0.88342708, -0.45376408, 0.11685286), (0.82714403, -0.38294080, 0.41132587), (0.90663397, 0.39537951, 0.14727443), (0.82429153, 0.36175665, 0.43551749), (0.65327883, 0.74187911, 0.15113553), (0.27052557, 0.90083331, 0.33958086), (0.24667580, 0.95772803, 0.14801350), (0.29653162, 0.73657012, 0.60789245), (0.25270250, 0.95638126, 0.14654756), (0.21378334, 0.80908066, 0.54743493), (-0.20308864, 0.76916009, 0.60592705), (-0.24655913, 0.95775896, 0.14800769), (-0.60082883, 0.70306301, 0.38040370), (-0.65328008, 0.74187797, 0.15113582), (-0.81199467, 0.42748836, 0.39738926), (-0.90740186, 0.39521822, 0.14291376), (-0.77285045, -0.44511274, 0.45230150), (-0.88389063, -0.45400411, 0.11232752), (0.38144678, -0.91367316, 0.14035526), (0.20845553, -0.81145030, 0.54598033), (0.20845553, -0.81145030, 0.54598033), (-0.77218670, -0.46013770, 0.43817902), (-0.84013665, 0.40299237, 0.36299780), (-0.23023890, 0.76050234, 0.60714585), (-0.31631237, 0.78570306, 0.53161728), (0.29653162, 0.73657012, 0.60789245), (0.29653162, 0.73657012, 0.60789245), (0.27052557, 0.90083331, 0.33958086), (0.82429153, 0.36175665, 0.43551749), (0.82714403, -0.38294080, 0.41132587), (0.20845553, -0.81145030, 0.54598033), (0.22679523, -0.89062703, 0.39414117), (0.06135494, -0.85218954, 0.51962334), (0.07649174, -0.82149130, 0.56506717), (0.37565932, -0.33224037, 0.86515677), (-0.18293454, 0.53496605, 0.82483095), (0.08872079, -0.06953967, 0.99362606), (-0.79646873, 0.60318637, 0.04246875), (-0.30517405, 0.89633429, 0.32164180), (-0.29037395, 0.89852804, 0.32913554), (-0.63901502, 0.70868748, -0.29903466), (0.19011311, -0.29438740, -0.93658578), (0.03954102, -0.21693054, -0.97538584), (-0.44216734, 0.58731091, -0.67790395), (0.12363925, -0.94551855, -0.30117744), (0.10608303, -0.27675208, 0.95506781), (-0.13692355, 0.52844894, 0.83785051), (-0.29037395, 0.89852804, 0.32913554), (-0.29037395, 0.89852804, 0.32913554), (-0.44216734, 0.58731091, -0.67790395), (-0.41846475, 0.60451484, -0.67782658), (0.33227336, 0.92182475, 0.19958310), (-0.02090647, 0.95541674, 0.29451939), (0.33227336, 0.92182475, 0.19958310), (0.64483184, 0.60690463, 0.46460578), (0.58321470, 0.43774250, 0.68428206), (0.64938271, -0.39853096, 0.64766890), (0.64938271, -0.39853096, 0.64766890), (-0.29090726, -0.50820190, -0.81061929), (0.08327195, -0.96857393, -0.23437184), (0.74739355, -0.17921671, 0.63975316), (-0.29090726, -0.50820190, -0.81061929), (0.12363925, -0.94551855, -0.30117744), (0.12363925, -0.94551855, -0.30117744), (0.02472171, -0.23109157, -0.97261781), (0.11796332, -0.96663624, -0.22737399), (0.58321470, 0.43774250, 0.68428206), (0.64193517, 0.74846989, 0.16646877), (0.64193517, 0.74846989, 0.16646877), (-0.29090726, -0.50820190, -0.81061929), (-0.14362098, -0.45307216, -0.87982869), (-0.29090726, -0.50820190, -0.81061929), (-0.29090726, -0.50820190, -0.81061929), (-0.41846475, 0.60451484, -0.67782658), (-0.44216734, 0.58731091, -0.67790395), (0.03954102, -0.21693054, -0.97538584), (0.19011311, -0.29438740, -0.93658578), (0.99746978, -0.00060985, -0.07108802), (0.36766738, -0.91409105, -0.17104991), (0.69192380, -0.63635081, 0.34102634), (0.99746978, -0.00060985, -0.07108802), (0.44699663, 0.30469689, 0.84104323), (0.44699663, 0.30469689, 0.84104323), (0.91575235, -0.21857326, 0.33708042), (0.37217435, -0.91307199, -0.16669039), (0.37217435, -0.91307199, -0.16669039), (-0.37181529, -0.91231072, -0.17158780), (-0.69192117, -0.63635457, 0.34102458), (-0.84865105, 0.08222811, 0.52252251), (-0.37260997, 0.69579935, -0.61402351), (-0.25641096, 0.81366742, -0.52172655), (-0.69192117, -0.63635457, 0.34102458), (-0.25641096, 0.81366742, -0.52172655), (0.22801135, -0.96900648, 0.09506371), (0.69192380, -0.63635081, 0.34102634), (0.69192380, -0.63635081, 0.34102634), (0.84864819, 0.08222024, 0.52252835), (0.37260860, 0.69579941, -0.61402428), (0.70091289, 0.53473270, -0.47199777), (0.75495476, 0.64487737, -0.11906447), (0.33342683, 0.89653456, 0.29163703), (0.44699663, 0.30469689, 0.84104323), (00, 0.83623087, -0.54837734), (-0.25641096, 0.81366742, -0.52172655), (0.22801135, -0.96900648, 0.09506371), (-0.25641096, 0.81366742, -0.52172655), (0.37260860, 0.69579941, -0.61402428), (00, 0.83623087, -0.54837734), (0.38791803, 0.25712729, 0.88510168), (0.36796427, -0.25199583, 0.89504206), (-0.85918421, -0.46406621, 0.21551071), (-0.99305892, -0.06723332, -0.09650695), (-0.99700636, 0.06128517, -0.04714131), (-0.75658315, 0.48332676, -0.44042826), (-0.81428945, 0.57828271, -0.05021532), (-0.49251351, 0.28573087, 0.82206339), (0.44699663, 0.30469689, 0.84104323), (0.38791803, 0.25712729, 0.88510168), (0.33342683, 0.89653456, 0.29163703), (-0.50389940, 0.27638131, 0.81835115), (0.06269005, 0.39427885, 0.91685003), (-0.19398117, 0.91704416, 0.34842673), (-0.81548184, 0.52460808, -0.24449031), (-0.52583814, 0.68334728, -0.50648838), (0.01674655, -0.22521462, -0.97416520), (-0.19398117, 0.91704416, 0.34842673), (0.01674655, -0.22521462, -0.97416520), (0.01674655, -0.22521462, -0.97416520), (0.36828953, -0.81220621, -0.45241988), (-0.63416642, -0.07357017, -0.76968837), (0.48799142, 0.47655663, 0.73127151), (0.48799142, 0.47655663, 0.73127151), (0.58130574, 0.18651263, -0.79202050), (0.58130574, 0.18651263, -0.79202050), (0.58130574, 0.18651263, -0.79202050), (-0.77907991, 0.23370643, -0.58173501), (0.01674655, -0.22521462, -0.97416520), (0.06269005, 0.39427885, 0.91685003), (0.06269005, 0.39427885, 0.91685003), (0.63040644, -0.62482345, 0.46063346), (0.63040644, -0.62482345, 0.46063346), (-0.03231066, -0.22254212, -0.97438747), (0.58130574, 0.18651263, -0.79202050), (-0.63416642, -0.07357017, -0.76968837), (-0.03231066, -0.22254212, -0.97438747), (0.01674655, -0.22521462, -0.97416520), (0.01674655, -0.22521462, -0.97416520), (-0.77907991, 0.23370643, -0.58173501), (0.36828953, -0.81220621, -0.45241988), (-0.81548184, 0.52460808, -0.24449031), (-0.19398117, 0.91704416, 0.34842673), (-0.52583814, 0.68334728, -0.50648838), (0.07649174, -0.82149130, 0.56506717), (0.07649174, -0.82149130, 0.56506717), (-0.09160894, -0.31652451, -0.94415039), (-0.03231066, -0.22254212, -0.97438747), (-0.63901502, 0.70868748, -0.29903466), (0.19011311, -0.29438740, -0.93658578), (0.11796332, -0.96663624, -0.22737399), (0.06135494, -0.85218954, 0.51962334), (0.07649174, -0.82149130, 0.56506717), (0.08872079, -0.06953967, 0.99362606), (-0.79646873, 0.60318637, 0.04246875), (-0.63901502, 0.70868748, -0.29903466), (0.08872079, -0.06953967, 0.99362606), (0.18532588, -0.69984144, 0.68983775), (0.90774822, -0.36197788, -0.21204954), (-0.14362098, -0.45307216, -0.87982869), (0.64938271, -0.39853096, 0.64766890), (0.79804945, 0.15006033, -0.58360845), (-0.29090726, -0.50820190, -0.81061929), (0.83344203, -0.37619954, 0.40478158), (0.83344203, -0.37619954, 0.40478158), (0.20342052, -0.89393973, 0.39936408), (0.19101317, -0.92541230, -0.32730088), (0.12856516, -0.96778554, -0.21647634), (0.18532588, -0.69984144, 0.68983775), (0.58321470, 0.43774250, 0.68428206), (-0.97561556, -0.20888387, -0.06739184), (-0.80015409, -0.36742410, 0.47408104), (-0.20510793, -0.69548941, 0.68864000), (0.51832485, -0.85245121, 0.06830898), (0.51832485, -0.85245121, 0.06830898), (0.83344203, -0.37619954, 0.40478158), (0.77299029, -0.43738469, 0.45954379), (0.20342052, -0.89393973, 0.39936408), (-0.79482996, -0.44558087, 0.41195005), (-0.80015409, -0.36742410, 0.47408104), (-0.80015409, -0.36742410, 0.47408104), (-0.79196781, 0.16157813, -0.58879477), (-0.36745921, -0.92623544, 0.08403278), (-0.56339473, -0.41183612, 0.71622425), (-0.20510793, -0.69548941, 0.68864000), (-0.46541286, -0.87742287, 0.11627491), (0.46541476, -0.87742269, 0.11626858), (0.32452869, -0.75638866, -0.56794113), (0.61976510, 0.34446174, -0.70515043), (-0.47293675, 0.52901030, 0.70461249), (0.26570058, -0.82219142, -0.50339276), (-0.25852430, -0.78521556, -0.56267357), (0.13329986, 0.79273123, 0.59481776), (0.16117379, 0.78018230, 0.60443228), (0.64193517, 0.74846989, 0.16646877), (0.04317913, 0.93103796, 0.36235857), (0.79804945, 0.15006033, -0.58360845), (0.07243589, 0.77256066, -0.63079548), (0.79804945, 0.15006033, -0.58360845), (-0.69413227, -0.00668782, -0.71981627), (0.11061868, 0.24680664, -0.96273041), (0.70491338, -0.22542119, -0.67251933), (0.70491338, -0.22542119, -0.67251933), (0.79804945, 0.15006033, -0.58360845), (0.83344203, -0.37619954, 0.40478158), (0.64193517, 0.74846989, 0.16646877), (0.64193517, 0.74846989, 0.16646877), (0.58321470, 0.43774250, 0.68428206), (0.18532588, -0.69984144, 0.68983775), (0.51832485, -0.85245121, 0.06830898), (0.53386015, -0.81036371, -0.24146186), (0.32452869, -0.75638866, -0.56794113), (-0.29090726, -0.50820190, -0.81061929), (-0.70490688, -0.22540957, -0.67253006), (-0.28325278, 0.71106189, 0.64355159), (-0.63342619, 0.74507499, 0.20888825), (-0.79196781, 0.16157813, -0.58879477), (0.30363861, 0.87691456, -0.37259135), (0.30114654, 0.88960612, 0.34338251), (0.77299029, -0.43738469, 0.45954379), (0.32778889, 0.85890651, 0.39348939), (0.32778889, 0.85890651, 0.39348939), (0.32778889, 0.85890651, 0.39348939), (-0.30114600, 0.88960636, 0.34338233), (-0.30114600, 0.88960636, 0.34338233), (-0.80015409, -0.36742410, 0.47408104), (-0.79196781, 0.16157813, -0.58879477), (-0.80015409, -0.36742410, 0.47408104), (-0.79196781, 0.16157813, -0.58879477), (-0.63342619, 0.74507499, 0.20888825), (-0.63342619, 0.74507499, 0.20888825), (-0.20510793, -0.69548941, 0.68864000), (-0.47293675, 0.52901030, 0.70461249), (0.61976510, 0.34446174, -0.70515043), (-0.46541286, -0.87742287, 0.11627491), (-0.25852430, -0.78521556, -0.56267357), (-0.53386754, -0.81035984, -0.24145851), (-0.20510793, -0.69548941, 0.68864000)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + def GeomSubset "hulagirl_a_mtl_hulagirl_a" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + int[] indices = [0, 1, 2, 3, 4, 0, 5, 0, 4, 4, 6, 5, 7, 5, 6, 6, 8, 7, 9, 8, 6, 6, 10, 9, 10, 6, 11, 11, 12, 10, 13, 12, 11, 6, 4, 14, 14, 11, 6, 4, 15, 16, 16, 17, 4, 4, 17, 18, 18, 14, 4, 19, 14, 18, 11, 14, 19, 19, 20, 11, 11, 20, 13, 13, 20, 21, 21, 22, 13, 22, 21, 23, 23, 24, 22, 24, 23, 25, 25, 26, 24, 27, 26, 25, 25, 28, 27, 28, 25, 29, 29, 30, 28, 30, 29, 31, 31, 32, 30, 33, 34, 35, 35, 34, 31, 31, 29, 35, 35, 29, 25, 25, 23, 35, 35, 23, 21, 21, 36, 35, 36, 21, 20, 20, 19, 36, 36, 19, 37, 18, 37, 19, 37, 18, 38, 38, 18, 17, 17, 39, 38, 35, 36, 33, 37, 33, 36, 33, 37, 34, 38, 40, 37, 41, 42, 43, 43, 44, 41, 41, 44, 28, 28, 30, 41, 45, 46, 47, 48, 49, 46, 46, 49, 50, 50, 47, 46, 50, 51, 47, 51, 50, 52, 53, 54, 55, 56, 55, 54, 55, 56, 57, 57, 56, 58, 58, 59, 57, 59, 58, 60, 60, 61, 59, 61, 60, 62, 62, 63, 61, 63, 62, 64, 64, 52, 63, 52, 64, 65, 52, 65, 51, 51, 65, 64, 64, 47, 51, 66, 47, 64, 47, 66, 45, 45, 66, 67, 67, 66, 68, 57, 69, 55, 70, 55, 69, 55, 70, 53, 69, 71, 70, 62, 72, 73, 72, 62, 60, 73, 64, 62, 64, 73, 66, 73, 68, 66, 68, 73, 72, 68, 72, 74, 60, 74, 72, 75, 74, 60, 60, 58, 75, 76, 75, 58, 58, 56, 76, 54, 76, 56, 76, 54, 77, 77, 54, 53, 53, 78, 77, 78, 53, 70, 70, 79, 78, 80, 79, 70, 81, 79, 80, 71, 81, 80, 80, 82, 71, 71, 82, 70, 70, 82, 80, 83, 77, 78, 83, 78, 84, 84, 78, 79, 79, 85, 84, 85, 79, 81, 81, 86, 85, 85, 86, 87, 87, 84, 85, 88, 84, 87, 89, 84, 88, 88, 90, 89, 90, 88, 91, 92, 91, 88, 92, 93, 91, 84, 89, 83, 87, 92, 88, 94, 76, 95, 77, 95, 76, 77, 96, 95, 83, 96, 77, 97, 96, 83, 83, 98, 97, 98, 83, 89, 99, 98, 89, 89, 90, 99, 90, 100, 99, 91, 100, 90, 101, 100, 91, 91, 93, 101, 102, 101, 93, 93, 45, 102, 45, 93, 92, 92, 48, 45, 46, 45, 48, 99, 100, 101, 99, 101, 102, 103, 104, 94, 96, 103, 94, 94, 95, 96, 105, 103, 96, 105, 96, 97, 97, 106, 105, 67, 102, 45, 107, 102, 67, 102, 107, 99, 108, 99, 107, 98, 99, 108, 98, 108, 109, 98, 109, 97, 109, 110, 97, 110, 106, 97, 110, 111, 106, 105, 106, 111, 111, 112, 105, 112, 103, 105, 113, 103, 112, 103, 113, 104, 75, 104, 113, 94, 104, 75, 75, 76, 94, 107, 109, 108, 111, 110, 109, 111, 109, 107, 111, 107, 67, 67, 112, 111, 67, 68, 112, 112, 68, 114, 74, 114, 68, 114, 74, 113, 113, 74, 75, 112, 114, 113, 115, 116, 117, 118, 119, 120, 120, 121, 118, 121, 120, 122, 122, 123, 121, 123, 122, 124, 124, 125, 123, 125, 124, 126, 126, 127, 125, 127, 126, 128, 128, 117, 127, 117, 128, 115, 115, 128, 129, 115, 129, 130, 131, 130, 129, 129, 132, 131, 132, 129, 133, 133, 134, 132, 134, 133, 135, 119, 136, 137, 137, 120, 119, 122, 120, 137, 137, 138, 122, 124, 122, 138, 138, 139, 124, 124, 139, 135, 135, 126, 124, 128, 126, 135, 135, 133, 128, 128, 133, 129, 135, 140, 134, 140, 135, 139, 139, 141, 140, 141, 139, 138, 138, 142, 141, 142, 138, 137, 137, 143, 142, 143, 137, 136, 136, 144, 143, 145, 143, 144, 144, 146, 145, 147, 145, 146, 146, 148, 147, 142, 143, 145, 145, 149, 142, 141, 142, 149, 145, 147, 150, 150, 149, 145, 151, 149, 150, 149, 151, 141, 141, 151, 152, 152, 140, 141, 134, 140, 152, 152, 153, 134, 132, 134, 153, 153, 154, 132, 131, 132, 154, 154, 155, 131, 155, 154, 156, 156, 157, 155, 158, 156, 154, 154, 153, 158, 159, 158, 153, 153, 152, 159, 159, 152, 151, 151, 160, 159, 150, 160, 151, 161, 162, 163, 164, 163, 162, 162, 165, 164, 165, 166, 164, 164, 166, 167, 163, 164, 167, 167, 168, 163, 169, 163, 168, 168, 170, 169, 163, 169, 161, 171, 172, 173, 173, 174, 171, 174, 173, 175, 175, 176, 174, 176, 175, 177, 177, 178, 176, 178, 177, 179, 179, 180, 178, 180, 179, 181, 181, 182, 180, 183, 182, 181, 181, 184, 183, 185, 183, 184, 184, 186, 185, 187, 185, 186, 186, 188, 187, 189, 187, 188, 188, 190, 189, 190, 191, 192, 192, 189, 190, 189, 192, 193, 193, 194, 189, 187, 189, 194, 194, 195, 187, 185, 187, 195, 195, 196, 185, 183, 185, 196, 196, 197, 183, 182, 183, 197, 197, 198, 182, 180, 182, 198, 198, 199, 180, 178, 180, 199, 199, 200, 178, 176, 178, 200, 200, 201, 176, 174, 176, 201, 201, 202, 174, 171, 174, 202, 202, 203, 171, 204, 205, 206, 206, 207, 204, 208, 207, 206, 206, 209, 208, 208, 209, 210, 210, 211, 208, 212, 211, 210, 210, 213, 212, 212, 213, 214, 214, 215, 212, 212, 215, 216, 217, 204, 207, 207, 218, 217, 218, 207, 208, 208, 219, 218, 219, 208, 211, 211, 220, 219, 220, 211, 212, 212, 221, 220, 216, 221, 212, 222, 221, 216, 216, 223, 222, 224, 222, 223, 225, 222, 224, 224, 226, 225, 227, 225, 226, 226, 228, 227, 227, 228, 229, 230, 229, 228, 223, 231, 224, 232, 217, 218, 218, 233, 232, 233, 218, 219, 220, 221, 222, 222, 225, 220, 219, 220, 225, 225, 227, 219, 219, 227, 233, 229, 233, 227, 232, 233, 229, 229, 234, 232, 234, 235, 232, 217, 232, 235, 235, 236, 217, 204, 217, 236, 236, 237, 204, 205, 204, 237, 237, 238, 205, 228, 239, 230, 239, 228, 226, 226, 240, 239, 240, 226, 224, 224, 241, 240, 241, 224, 231, 231, 242, 241, 229, 230, 243, 243, 234, 229, 234, 243, 244, 244, 245, 234, 234, 245, 235, 245, 246, 235, 236, 235, 246, 246, 247, 236, 237, 236, 247, 247, 248, 237, 238, 237, 248, 248, 249, 238, 250, 251, 252, 253, 254, 255, 255, 250, 253, 250, 255, 256, 256, 251, 250, 257, 251, 256, 252, 251, 257, 257, 258, 252, 258, 257, 259, 259, 260, 258, 261, 262, 263, 264, 261, 263, 263, 265, 264, 265, 260, 264, 260, 265, 266, 266, 258, 260, 258, 266, 267, 267, 252, 258, 268, 252, 267, 252, 268, 250, 250, 268, 269, 269, 253, 250, 253, 269, 270, 270, 271, 253, 253, 271, 272, 272, 254, 253, 254, 272, 273, 273, 274, 254, 262, 275, 276, 276, 263, 262, 276, 265, 263, 265, 276, 266, 267, 277, 278, 278, 279, 267, 279, 268, 267, 268, 279, 269, 269, 279, 270, 270, 279, 278, 278, 280, 270, 255, 254, 274, 274, 281, 255, 256, 255, 281, 281, 282, 256, 256, 282, 257, 257, 282, 283, 283, 259, 257, 284, 259, 283, 260, 259, 284, 284, 264, 260, 261, 264, 284, 284, 285, 261, 262, 261, 285, 285, 286, 262, 287, 286, 285, 285, 288, 287, 287, 288, 289, 290, 289, 288, 289, 291, 287, 288, 292, 290, 282, 290, 292, 292, 283, 282, 283, 292, 284, 285, 284, 292, 292, 288, 285, 293, 294, 295, 296, 297, 293, 298, 293, 299, 300, 301, 298, 302, 296, 303, 293, 303, 296, 304, 303, 293, 303, 304, 302, 305, 302, 304, 304, 306, 305, 306, 307, 305, 295, 308, 309, 309, 310, 295, 295, 310, 293, 311, 293, 310, 293, 311, 304, 304, 311, 312, 312, 306, 304, 306, 312, 313, 313, 307, 306, 307, 313, 314, 314, 315, 307, 316, 315, 314, 314, 317, 316, 316, 317, 318, 319, 318, 317, 318, 319, 320, 320, 321, 318, 321, 320, 322, 322, 323, 321, 318, 324, 316, 313, 312, 325, 325, 326, 313, 314, 313, 326, 326, 327, 314, 314, 327, 328, 328, 317, 314, 317, 328, 319, 329, 319, 328, 328, 330, 329, 331, 330, 328, 328, 327, 331, 332, 331, 327, 327, 326, 332, 333, 332, 326, 326, 325, 333, 333, 325, 334, 334, 325, 312, 312, 311, 334, 310, 334, 311, 334, 310, 335, 336, 335, 310, 310, 309, 336, 335, 337, 334, 334, 337, 333, 338, 339, 340, 338, 340, 341, 342, 343, 340, 340, 339, 342, 344, 342, 339, 339, 345, 344, 346, 345, 339, 339, 347, 346, 347, 339, 338, 338, 348, 347, 348, 338, 349, 349, 338, 341, 349, 341, 350, 351, 352, 346, 346, 347, 351, 353, 351, 347, 347, 348, 353, 353, 348, 354, 349, 354, 348, 354, 349, 355, 356, 357, 344, 344, 345, 356, 358, 356, 345, 345, 346, 358, 359, 358, 346, 346, 352, 359, 360, 359, 352, 352, 361, 360, 361, 352, 351, 351, 362, 361, 362, 351, 363, 353, 363, 351, 364, 363, 353, 364, 353, 365, 354, 365, 353, 366, 365, 354, 354, 367, 366, 355, 367, 354, 367, 355, 368, 363, 369, 362, 370, 364, 365, 368, 371, 367, 366, 367, 371, 371, 372, 366, 373, 374, 375, 375, 376, 373, 376, 375, 377, 377, 378, 376, 378, 377, 379, 380, 381, 373, 374, 373, 381, 381, 382, 374, 374, 382, 383, 379, 384, 385, 379, 377, 384, 384, 377, 386, 375, 386, 377, 387, 386, 375, 387, 375, 374, 388, 387, 374, 374, 389, 388, 389, 374, 383, 383, 390, 389, 390, 383, 391, 391, 392, 390, 386, 393, 384, 394, 380, 395, 373, 395, 380, 396, 395, 373, 373, 376, 396, 397, 396, 376, 376, 378, 397, 397, 378, 398, 379, 398, 378, 398, 379, 399, 385, 399, 379, 399, 385, 400, 400, 401, 399, 402, 399, 401, 401, 403, 402, 402, 403, 404, 404, 405, 402, 406, 405, 404, 407, 405, 406, 398, 407, 397, 405, 407, 398, 398, 402, 405, 399, 402, 398, 404, 408, 406, 406, 409, 407, 407, 409, 397, 410, 397, 409, 396, 397, 410, 396, 410, 411, 395, 396, 411, 412, 395, 411, 411, 413, 412, 409, 414, 410, 415, 416, 417, 417, 394, 415, 418, 415, 394, 394, 395, 418] + uniform token elementType = "face" + uniform token familyName = "materialBind" + rel material:binding = + } + } + } + def Xform "HulaGirl_LowerBody" + { + matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 0) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + def Mesh "HulaGirl_LowerBody" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + + float3[] extent = [(-0.0193278, -0.0180276, 0.0000956), (0.0188266, 0.0201269, 0.0527202)] + int[] faceVertexCounts = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] + int[] faceVertexIndices = [0, 1, 2, 3, 4, 5, 5, 6, 3, 7, 3, 6, 6, 8, 7, 7, 8, 0, 0, 9, 7, 2, 9, 0, 9, 2, 10, 10, 11, 9, 9, 11, 7, 3, 7, 11, 11, 12, 3, 4, 3, 12, 12, 13, 4, 14, 4, 13, 15, 14, 13, 14, 15, 16, 16, 17, 14, 4, 14, 17, 17, 5, 4, 13, 18, 15, 19, 20, 21, 21, 22, 19, 23, 22, 21, 24, 25, 23, 23, 26, 24, 21, 26, 23, 26, 21, 27, 27, 21, 28, 28, 29, 27, 30, 27, 29, 31, 27, 30, 30, 32, 31, 31, 32, 33, 34, 33, 32, 32, 35, 34, 36, 35, 32, 32, 30, 36, 36, 30, 37, 29, 37, 30, 33, 38, 31, 26, 31, 38, 27, 31, 26, 38, 24, 26, 37, 39, 36, 40, 41, 42, 43, 44, 45, 45, 46, 43, 47, 43, 46, 46, 48, 47, 49, 47, 48, 48, 50, 49, 51, 49, 50, 50, 52, 51, 42, 52, 50, 50, 53, 42, 42, 53, 40, 40, 53, 54, 54, 55, 40, 55, 54, 56, 56, 57, 55, 57, 56, 58, 58, 59, 57, 53, 50, 48, 48, 54, 53, 54, 48, 46, 46, 56, 54, 56, 46, 45, 45, 58, 56, 60, 61, 62, 63, 64, 62, 65, 66, 67, 63, 67, 66, 66, 68, 63, 64, 63, 68, 68, 69, 64, 62, 64, 69, 69, 70, 62, 60, 62, 70, 70, 71, 60, 61, 60, 71, 71, 72, 61, 73, 67, 63, 63, 62, 73, 74, 73, 62, 74, 62, 61, 61, 75, 74, 75, 61, 72, 72, 76, 75, 74, 75, 76, 76, 77, 74, 78, 74, 77, 77, 79, 78, 80, 78, 79, 79, 81, 80, 82, 80, 81, 81, 83, 82, 73, 82, 83, 83, 65, 73, 67, 73, 65, 74, 78, 80, 80, 73, 74, 80, 82, 73, 84, 85, 86, 86, 87, 84, 87, 86, 88, 88, 89, 87, 89, 88, 90, 90, 91, 89, 91, 90, 92, 92, 93, 91, 93, 92, 94, 94, 95, 93, 95, 94, 96, 96, 97, 95, 98, 95, 97, 97, 99, 98, 95, 98, 93, 93, 98, 100, 100, 91, 93, 91, 100, 101, 101, 89, 91, 89, 101, 87, 87, 101, 102, 102, 84, 87, 103, 104, 105, 105, 106, 103, 106, 105, 107, 107, 108, 106, 108, 107, 109, 109, 110, 108, 111, 112, 113, 113, 114, 111, 114, 113, 115, 115, 116, 114, 116, 115, 117, 117, 118, 116, 118, 117, 119, 119, 120, 118, 120, 119, 121, 121, 122, 120, 122, 121, 123, 123, 124, 122, 124, 123, 125, 125, 126, 124, 126, 125, 127, 127, 128, 126, 128, 127, 104, 104, 103, 128] + point3f[] points = [(-0.0048427, -0.0028369, 0.0122185), (-0.0030211, -0.0029197, 0.0122185), (-0.0029521, -0.0028990, 0.0163032), (-0.0083098, 0.0075818, 0.0140780), (-0.0047219, 0.0072610, 0.0141125), (-0.0045632, 0.0076059, 0.0122151), (-0.0082994, 0.0079682, 0.0122151), (-0.0101520, 0.0048667, 0.0140953), (-0.0103867, 0.0051289, 0.0122151), (-0.0050083, -0.0027679, 0.0159616), (-0.0031143, -0.0024367, 0.0179901), (-0.0046598, -0.0014103, 0.0179901), (-0.0040871, 0.0007441, 0.0179901), (-0.0023199, -0.0004694, 0.0179901), (-0.0014721, -0.0016294, 0.0156994), (-0.0029521, -0.0028990, 0.0163032), (-0.0030211, -0.0029197, 0.0122185), (-0.0016101, -0.0018398, 0.0122185), (-0.0031143, -0.0024367, 0.0179901), (-0.0023199, -0.0004694, 0.0179901), (-0.0040871, 0.0007441, 0.0179901), (-0.0041147, 0.0027692, 0.0329731), (-0.0011996, 0.0003508, 0.0329731), (-0.0014445, 0.0012168, 0.0355156), (-0.0038388, 0.0038904, 0.0527202), (-0.0005475, 0.0009132, 0.0524270), (-0.0039767, 0.0039077, 0.0349360), (-0.0068712, 0.0003508, 0.0329731), (-0.0040871, 0.0007441, 0.0179901), (-0.0046598, -0.0014103, 0.0179901), (-0.0041147, -0.0025195, 0.0329731), (-0.0064262, 0.0012168, 0.0355156), (-0.0039767, -0.0013810, 0.0357364), (-0.0038388, -0.0021124, 0.0527202), (-0.0005475, 0.0009132, 0.0524270), (-0.0014445, 0.0012168, 0.0355156), (-0.0011996, 0.0003508, 0.0329731), (-0.0031143, -0.0024367, 0.0179901), (-0.0072093, 0.0011892, 0.0527202), (-0.0023199, -0.0004694, 0.0179901), (0.0034578, 0.0007441, 0.0179591), (0.0016914, -0.0004694, 0.0179617), (0.0005737, 0.0003508, 0.0329731), (0.0032128, -0.0021124, 0.0527202), (-0.0000818, 0.0009132, 0.0524270), (0.0008186, 0.0012168, 0.0355156), (0.0033474, -0.0013810, 0.0357364), (0.0065833, 0.0011891, 0.0527202), (0.0058003, 0.0012168, 0.0355156), (0.0032128, 0.0038904, 0.0527202), (0.0033473, 0.0039077, 0.0349360), (-0.0000818, 0.0009132, 0.0524270), (0.0008186, 0.0012168, 0.0355156), (0.0034888, 0.0027692, 0.0329731), (0.0062418, 0.0003508, 0.0329731), (0.0040330, -0.0014103, 0.0179609), (0.0034888, -0.0025195, 0.0329731), (0.0024884, -0.0024367, 0.0179626), (0.0005737, 0.0003508, 0.0329731), (0.0016914, -0.0004694, 0.0179617), (-0.0170523, -0.0034523, 0.0123462), (-0.0125486, -0.0112496, 0.0123462), (-0.0170506, 0.0055521, 0.0123462), (-0.0047526, 0.0178513, 0.0123462), (-0.0125498, 0.0133476, 0.0123462), (0.0137157, 0.0150138, 0.0103487), (0.0048595, 0.0201269, 0.0103487), (0.0042518, 0.0178496, 0.0123462), (-0.0053616, 0.0201239, 0.0103487), (-0.0142148, 0.0150159, 0.0103487), (-0.0193278, 0.0061598, 0.0103487), (-0.0193249, -0.0040613, 0.0103487), (-0.0142169, -0.0129145, 0.0103487), (0.0120474, 0.0133489, 0.0123462), (0.0042514, -0.0157520, 0.0123462), (-0.0047530, -0.0157503, 0.0123462), (-0.0053608, -0.0180276, 0.0103487), (0.0048603, -0.0180247, 0.0103487), (0.0120486, -0.0112483, 0.0123462), (0.0137135, -0.0129166, 0.0103487), (0.0165493, -0.0034528, 0.0123462), (0.0188266, -0.0040605, 0.0103487), (0.0165510, 0.0055516, 0.0123462), (0.0188237, 0.0061606, 0.0103487), (0.0023228, -0.0028990, 0.0162756), (0.0023952, -0.0029197, 0.0122185), (0.0042133, -0.0028369, 0.0122185), (0.0043823, -0.0027679, 0.0159306), (0.0097607, 0.0051289, 0.0122151), (0.0095227, 0.0048667, 0.0140953), (0.0076735, 0.0079682, 0.0122151), (0.0076839, 0.0075818, 0.0140780), (0.0039339, 0.0076059, 0.0122151), (0.0040960, 0.0072610, 0.0141125), (0.0009842, -0.0018399, 0.0122185), (0.0008462, -0.0016294, 0.0156718), (0.0023952, -0.0029197, 0.0122185), (0.0023228, -0.0028990, 0.0162756), (0.0016914, -0.0004694, 0.0179617), (0.0024884, -0.0024367, 0.0179626), (0.0034578, 0.0007441, 0.0179591), (0.0040330, -0.0014103, 0.0179609), (0.0024884, -0.0024367, 0.0179626), (-0.0193249, -0.0040613, 0.0103487), (-0.0193249, -0.0040613, 0.0000956), (-0.0142169, -0.0129145, 0.0000956), (-0.0142169, -0.0129145, 0.0103487), (-0.0053608, -0.0180276, 0.0000956), (-0.0053608, -0.0180276, 0.0103487), (0.0048603, -0.0180247, 0.0000956), (0.0048603, -0.0180247, 0.0103487), (0.0048603, -0.0180247, 0.0103487), (0.0048603, -0.0180247, 0.0000956), (0.0137135, -0.0129166, 0.0000956), (0.0137135, -0.0129166, 0.0103487), (0.0188266, -0.0040605, 0.0000956), (0.0188266, -0.0040605, 0.0103487), (0.0188237, 0.0061606, 0.0000956), (0.0188237, 0.0061606, 0.0103487), (0.0137157, 0.0150138, 0.0000956), (0.0137157, 0.0150138, 0.0103487), (0.0048595, 0.0201269, 0.0000956), (0.0048595, 0.0201269, 0.0103487), (-0.0053616, 0.0201239, 0.0000956), (-0.0053616, 0.0201239, 0.0103487), (-0.0142148, 0.0150159, 0.0000956), (-0.0142148, 0.0150159, 0.0103487), (-0.0193278, 0.0061598, 0.0000956), (-0.0193278, 0.0061598, 0.0103487)] + color3f[] primvars:HulaGirl_LowerBody_color = [(0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216), (0.0039216, 0.0039216, 0.0039216)] ( + interpolation = "vertex" + ) + texCoord2f[] primvars:HulaGirl_LowerBody_UV = [(0.4215636, 0.6430903), (0.4283217, 0.6639974), (0.4030493, 0.6703741), (0.3618152, 0.5431943), (0.3295307, 0.5483283), (0.3176361, 0.5334645), (0.3626933, 0.5235826), (0.3926632, 0.5544800), (0.4029754, 0.5371604), (0.4019758, 0.6491259), (0.3853458, 0.6692438), (0.3764974, 0.6446978), (0.3541492, 0.6332128), (0.3345056, 0.6464699), (0.3109937, 0.6524755), (0.3120357, 0.6645058), (0.2969718, 0.6619312), (0.2974496, 0.6468932), (0.3352712, 0.6646534), (0.6755742, 0.6001200), (0.6755742, 0.6168710), (0.5640114, 0.6168708), (0.5640114, 0.5943242), (0.5467531, 0.5943242), (0.4820342, 0.6168709), (0.4820342, 0.5943242), (0.5467532, 0.6168709), (0.5640114, 0.6412008), (0.6755742, 0.6168710), (0.6755746, 0.6444812), (0.5640114, 0.6673146), (0.5467530, 0.6412010), (0.5467530, 0.6673146), (0.4820346, 0.6673146), (0.4820341, 0.6903709), (0.5467532, 0.6902435), (0.5640114, 0.6902434), (0.6755742, 0.6673146), (0.4820341, 0.6412010), (0.6755742, 0.6846386), (0.6755742, 0.6673146), (0.6755742, 0.6846386), (0.5640114, 0.6902434), (0.4820342, 0.6168709), (0.4820342, 0.5943242), (0.5467531, 0.5943242), (0.5467532, 0.6168709), (0.4820341, 0.6412010), (0.5467530, 0.6412010), (0.4820346, 0.6673146), (0.5467530, 0.6673146), (0.4820341, 0.6903709), (0.5467532, 0.6902435), (0.5640114, 0.6673146), (0.5640114, 0.6412008), (0.6755746, 0.6444812), (0.5640114, 0.6168708), (0.6755742, 0.6168710), (0.5640114, 0.5943242), (0.6755742, 0.6001200), (0.3476928, 0.2852973), (0.3954274, 0.2981829), (0.2999567, 0.2981832), (0.2522222, 0.3810607), (0.2651073, 0.3333253), (0.2935143, 0.4747748), (0.2536864, 0.4352393), (0.2651078, 0.4287962), (0.2393361, 0.3810608), (0.2536864, 0.3265902), (0.2935143, 0.2870545), (0.3476928, 0.2724115), (0.4018705, 0.2870545), (0.2999567, 0.4636464), (0.4431639, 0.3810607), (0.4302776, 0.3333250), (0.4416995, 0.3265898), (0.4560495, 0.3810608), (0.4302776, 0.4287962), (0.4416995, 0.4352390), (0.3954274, 0.4636461), (0.4018711, 0.4747748), (0.3476923, 0.4765319), (0.3476928, 0.4894177), (0.4030493, 0.6703741), (0.4283217, 0.6639974), (0.4215636, 0.6430903), (0.4019758, 0.6491259), (0.4029754, 0.5371604), (0.3926632, 0.5544800), (0.3626933, 0.5235826), (0.3618152, 0.5431943), (0.3176361, 0.5334645), (0.3295307, 0.5483283), (0.2974496, 0.6468932), (0.3109937, 0.6524755), (0.2969718, 0.6619312), (0.3120357, 0.6645058), (0.3345056, 0.6464699), (0.3352712, 0.6646534), (0.3541492, 0.6332128), (0.3764974, 0.6446978), (0.3853458, 0.6692438), (0.9702790, 0.1747702), (0.9260076, 0.1747707), (0.9260076, 0.1304993), (0.9702790, 0.1304991), (0.9260073, 0.0862275), (0.9702790, 0.0862274), (0.9260076, 0.0422007), (0.9702790, 0.0422005), (0.9702790, 0.5727251), (0.9260073, 0.5727254), (0.9260073, 0.5286981), (0.9702790, 0.5286979), (0.9260073, 0.4844268), (0.9702790, 0.4844267), (0.9260073, 0.4401553), (0.9702790, 0.4401554), (0.9260076, 0.3958837), (0.9702790, 0.3958836), (0.9260076, 0.3516123), (0.9702790, 0.3516122), (0.9260073, 0.3075852), (0.9702790, 0.3075851), (0.9260076, 0.2633139), (0.9702790, 0.2633138), (0.9260076, 0.2192867), (0.9702788, 0.2187975)] ( + interpolation = "vertex" + ) + normal3f[] normals = [(-0.06617371, -0.99768823, 0.01546251), (0.64938933, -0.76042271, -0.00711547), (0.91066945, -0.36771855, 0.18831913), (-0.53803635, 0.14301342, 0.83070081), (0.34634605, 0.50437856, 0.79097819), (0.08576077, 0.97895896, 0.18516035), (-0.82203001, 0.55847204, 0.11124522), (-0.35620591, 0.24742785, 0.90105307), (-0.79253137, 0.58261782, 0.18013979), (-0.46329045, -0.69765997, 0.54647267), (0.70096159, -0.71190935, 0.04286921), (-0.95232219, 0.25313312, -0.17031144), (-0.65604448, 0.74776024, -0.10227470), (0.63592583, 0.76654130, -0.08951301), (0.91845340, 0.36354327, 0.15581888), (0.91066945, -0.36771855, 0.18831913), (0.64938933, -0.76042271, -0.00711547), (0.94092989, 0.33368841, -0.05746998), (0.70096159, -0.71190935, 0.04286921), (0.63592583, 0.76654130, -0.08951301), (-0.65604448, 0.74776024, -0.10227470), (-0.61451101, 0.70042038, -0.36302528), (0.62601578, 0.75459576, -0.19669610), (0.66959399, 0.74239570, -0.02218792), (-0.62536836, 0.78030944, 0.00560805), (0.66959399, 0.74239570, -0.02218792), (-0.62536836, 0.78030944, 0.00560805), (-0.74622184, 0.65904945, -0.09384326), (-0.65604448, 0.74776024, -0.10227470), (-0.95232219, 0.25313312, -0.17031144), (0.67927295, -0.68988204, 0.25030160), (-0.74260950, 0.66892439, -0.03272860), (0.67449796, -0.73733729, -0.03722967), (0.67449796, -0.73733729, -0.03722967), (0.66959399, 0.74239570, -0.02218792), (0.66959399, 0.74239570, -0.02218792), (0.62601578, 0.75459576, -0.19669610), (0.70096159, -0.71190935, 0.04286921), (-0.62536836, 0.78030944, 0.00560805), (0.63592583, 0.76654130, -0.08951301), (0.95213664, 0.25410563, -0.16989994), (-0.56420690, 0.82105011, -0.08687438), (-0.57280415, 0.69047552, -0.44174513), (-0.71457559, -0.69864422, -0.03574604), (-0.73132575, 0.68150526, -0.02670213), (-0.73132575, 0.68150526, -0.02670213), (-0.71457559, -0.69864422, -0.03574604), (0.62515575, 0.78002572, -0.02720188), (0.73834664, 0.67439246, 0.00623699), (0.73834664, 0.67439246, 0.00623699), (0.73834664, 0.67439246, 0.00623699), (-0.73132575, 0.68150526, -0.02670213), (-0.73132575, 0.68150526, -0.02670213), (0.73641860, 0.60691196, -0.29890686), (0.65348977, 0.74390036, -0.13986832), (0.95213664, 0.25410563, -0.16989994), (-0.67008501, -0.68056482, 0.29634005), (-0.92574877, -0.37502325, -0.04844246), (-0.57280415, 0.69047552, -0.44174513), (-0.56420690, 0.82105011, -0.08687438), (-0.66014200, -0.00018586, 0.75114071), (-0.32981160, -0.57125431, 0.75159341), (-0.65940619, 0.00012257, 0.75178671), (-0.33020356, 0.57168293, 0.75109524), (-0.57124752, 0.32980990, 0.75159931), (0.32981214, 0.57125282, 0.75159431), (-0.00018848, 0.66014665, 0.75113660), (0.00012222, 0.65940547, 0.75178736), (-0.32956189, 0.57119477, 0.75174820), (-0.57125092, 0.32981035, 0.75159651), (-0.66014200, -0.00018586, 0.75114071), (-0.66014200, -0.00018586, 0.75114071), (-0.32981011, -0.57124275, 0.75160283), (0.32981151, 0.57124805, 0.75159818), (0.33019745, -0.57167786, 0.75110173), (-0.00012222, -0.65939713, 0.75179470), (0.00018902, -0.66013956, 0.75114280), (0.32956123, -0.57119381, 0.75174922), (0.57125235, -0.32980904, 0.75159603), (0.57124287, -0.32980788, 0.75160372), (0.65939677, -0.00012457, 0.75179499), (0.66014647, 0.00018971, 0.75113672), (0.57168275, 0.33020440, 0.75109500), (0.57119179, 0.32955909, 0.75175166), (-0.66970074, -0.53293878, 0.51718175), (-0.60754770, -0.79391748, 0.02409779), (0.04549600, -0.99894702, 0.00590407), (0.10367914, -0.96179157, 0.25339189), (0.79232681, 0.58246130, 0.18154038), (0.35434812, 0.24572994, 0.90224946), (0.79232681, 0.58246130, 0.18154038), (0.35434812, 0.24572994, 0.90224946), (-0.09441006, 0.97468573, 0.20266820), (-0.03739655, 0.51066911, 0.85896355), (-0.94585156, 0.29541537, 0.13451545), (-0.91874754, 0.36306199, 0.15520550), (-0.60754770, -0.79391748, 0.02409779), (-0.66970074, -0.53293878, 0.51718175), (-0.56420690, 0.82105011, -0.08687438), (-0.92574877, -0.37502325, -0.04844246), (0.95213664, 0.25410563, -0.16989994), (0.95213664, 0.25410563, -0.16989994), (-0.92574877, -0.37502325, -0.04844246), (-0.66014200, -0.00018586, 0.75114071), (-0.99999988, -0.00028155, -00), (-0.50000334, -0.86602336, -00), (-0.32981011, -0.57124275, 0.75160283), (0.00028633, -0.99999988, 00), (0.00018902, -0.66013956, 0.75114280), (0.49975237, -0.86616826, 00), (0.32956123, -0.57119381, 0.75174922), (0.32956123, -0.57119381, 0.75174922), (0.49975237, -0.86616826, 00), (0.86602491, -0.50000066, 00), (0.57124287, -0.32980788, 0.75160372), (0.99999988, 0.00028738, 00), (0.66014647, 0.00018971, 0.75113672), (0.86616886, 0.49975127, 00), (0.57119179, 0.32955909, 0.75175166), (0.49999902, 0.86602592, 00), (0.32981214, 0.57125282, 0.75159431), (-0.00028551, 0.99999988, 00), (-0.00018848, 0.66014665, 0.75113660), (-0.49975252, 0.86616814, 00), (-0.32956189, 0.57119477, 0.75174820), (-0.86602634, 0.49999821, 00), (-0.57125092, 0.32981035, 0.75159651), (-0.99999988, -0.00028155, -00), (-0.66014200, -0.00018586, 0.75114071)] ( + interpolation = "faceVarying" + ) + uniform token subdivisionScheme = "none" + def GeomSubset "hulagirl_a_mtl_hulagirl_a" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + int[] indices = [0, 1, 2, 3, 4, 5, 5, 6, 3, 7, 3, 6, 6, 8, 7, 7, 8, 0, 0, 9, 7, 2, 9, 0, 9, 2, 10, 10, 11, 9, 9, 11, 7, 3, 7, 11, 11, 12, 3, 4, 3, 12, 12, 13, 4, 14, 4, 13, 15, 14, 13, 14, 15, 16, 16, 17, 14, 4, 14, 17, 17, 5, 4, 13, 18, 15, 19, 20, 21, 21, 22, 19, 23, 22, 21, 24, 25, 23, 23, 26, 24, 21, 26, 23, 26, 21, 27, 27, 21, 28, 28, 29, 27, 30, 27, 29, 31, 27, 30, 30, 32, 31, 31, 32, 33, 34, 33, 32, 32, 35, 34, 36, 35, 32, 32, 30, 36, 36, 30, 37, 29, 37, 30, 33, 38, 31, 26, 31, 38, 27, 31, 26, 38, 24, 26, 37, 39, 36, 40, 41, 42, 43, 44, 45, 45, 46, 43, 47, 43, 46, 46, 48, 47, 49, 47, 48, 48, 50, 49, 51, 49, 50, 50, 52, 51, 42, 52, 50, 50, 53, 42, 42, 53, 40, 40, 53, 54, 54, 55, 40, 55, 54, 56, 56, 57, 55, 57, 56, 58, 58, 59, 57, 53, 50, 48, 48, 54, 53, 54, 48, 46, 46, 56, 54, 56, 46, 45, 45, 58, 56, 60, 61, 62, 63, 64, 62, 65, 66, 67, 63, 67, 66, 66, 68, 63, 64, 63, 68, 68, 69, 64, 62, 64, 69, 69, 70, 62, 60, 62, 70, 70, 71, 60, 61, 60, 71, 71, 72, 61, 73, 67, 63, 63, 62, 73, 74, 73, 62, 74, 62, 61, 61, 75, 74, 75, 61, 72, 72, 76, 75, 74, 75, 76, 76, 77, 74, 78, 74, 77, 77, 79, 78, 80, 78, 79, 79, 81, 80, 82, 80, 81, 81, 83, 82, 73, 82, 83, 83, 65, 73, 67, 73, 65, 74, 78, 80, 80, 73, 74, 80, 82, 73, 84, 85, 86, 86, 87, 84, 87, 86, 88, 88, 89, 87, 89, 88, 90, 90, 91, 89, 91, 90, 92, 92, 93, 91, 93, 92, 94, 94, 95, 93, 95, 94, 96, 96, 97, 95, 98, 95, 97, 97, 99, 98, 95, 98, 93, 93, 98, 100, 100, 91, 93, 91, 100, 101, 101, 89, 91, 89, 101, 87, 87, 101, 102, 102, 84, 87, 103, 104, 105, 105, 106, 103, 106, 105, 107, 107, 108, 106, 108, 107, 109, 109, 110, 108, 111, 112, 113, 113, 114, 111, 114, 113, 115, 115, 116, 114, 116, 115, 117, 117, 118, 116, 118, 117, 119, 119, 120, 118, 120, 119, 121, 121, 122, 120, 122, 121, 123, 123, 124, 122, 124, 123, 125, 125, 126, 124, 126, 125, 127, 127, 128, 126, 128, 127, 104, 104, 103, 128] + uniform token elementType = "face" + uniform token familyName = "materialBind" + rel material:binding = + } + } + } + } + def Scope "_materials" + { + def Material "hulagirl_a_mtl_hulagirl_a" + { + token outputs:surface.connect = + def Shader "Principled_BSDF" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:clearcoat = 0 + float inputs:clearcoatRoughness = 0.03 + token outputs:surface + color3f inputs:diffuseColor.connect = + } + def Shader "hula_girl_dif" + { + uniform token info:id = "UsdUVTexture" + token inputs:wrapS = "repeat" + token inputs:wrapT = "repeat" + asset inputs:file = @d:/depot/mwo/Objects/characters/hula_girl/hula_girl_dif.dds@ + token inputs:sourceColorSpace = "sRGB" + float3 outputs:rgb + } + } + } +} diff --git a/CgfConverterIntegrationTests/TestData/hulagirl_gold.usda b/CgfConverterIntegrationTests/TestData/hulagirl_gold.usda new file mode 100644 index 00000000..4017462a --- /dev/null +++ b/CgfConverterIntegrationTests/TestData/hulagirl_gold.usda @@ -0,0 +1,125 @@ +#usda 1.0 +( + defaultPrim = "root" + doc = "Blender v4.1.1" + metersPerUnit = 1 + upAxis = "Z" +) + +def Xform "root" ( + customData = { + dictionary Blender = { + bool generated = 1 + } + } +) +{ + def Xform "hulagirl_a" + { + matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + + def Xform "HulaGirl_LowerBody" + { + matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + + def Mesh "HulaGirl_LowerBody" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-0.019328, -0.018028, 0.000096), (0.018827, 0.020127, 0.05272)] + int[] faceVertexCounts = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] + int[] faceVertexIndices = [0, 1, 2, 3, 4, 5, 5, 6, 3, 7, 3, 6, 6, 8, 7, 7, 8, 0, 0, 9, 7, 2, 9, 0, 9, 2, 10, 10, 11, 9, 9, 11, 7, 3, 7, 11, 11, 12, 3, 4, 3, 12, 12, 13, 4, 14, 4, 13, 15, 14, 13, 14, 15, 16, 16, 17, 14, 4, 14, 17, 17, 5, 4, 13, 18, 15, 19, 20, 21, 21, 22, 19, 23, 22, 21, 24, 25, 23, 23, 26, 24, 21, 26, 23, 26, 21, 27, 27, 21, 28, 28, 29, 27, 30, 27, 29, 31, 27, 30, 30, 32, 31, 31, 32, 33, 34, 33, 32, 32, 35, 34, 36, 35, 32, 32, 30, 36, 36, 30, 37, 29, 37, 30, 33, 38, 31, 26, 31, 38, 27, 31, 26, 38, 24, 26, 37, 39, 36, 40, 41, 42, 43, 44, 45, 45, 46, 43, 47, 43, 46, 46, 48, 47, 49, 47, 48, 48, 50, 49, 51, 49, 50, 50, 52, 51, 42, 52, 50, 50, 53, 42, 42, 53, 40, 40, 53, 54, 54, 55, 40, 55, 54, 56, 56, 57, 55, 57, 56, 58, 58, 59, 57, 53, 50, 48, 48, 54, 53, 54, 48, 46, 46, 56, 54, 56, 46, 45, 45, 58, 56, 60, 61, 62, 63, 64, 62, 65, 66, 67, 63, 67, 66, 66, 68, 63, 64, 63, 68, 68, 69, 64, 62, 64, 69, 69, 70, 62, 60, 62, 70, 70, 71, 60, 61, 60, 71, 71, 72, 61, 73, 67, 63, 63, 62, 73, 74, 73, 62, 74, 62, 61, 61, 75, 74, 75, 61, 72, 72, 76, 75, 74, 75, 76, 76, 77, 74, 78, 74, 77, 77, 79, 78, 80, 78, 79, 79, 81, 80, 82, 80, 81, 81, 83, 82, 73, 82, 83, 83, 65, 73, 67, 73, 65, 74, 78, 80, 80, 73, 74, 80, 82, 73, 84, 85, 86, 86, 87, 84, 87, 86, 88, 88, 89, 87, 89, 88, 90, 90, 91, 89, 91, 90, 92, 92, 93, 91, 93, 92, 94, 94, 95, 93, 95, 94, 96, 96, 97, 95, 98, 95, 97, 97, 99, 98, 95, 98, 93, 93, 98, 100, 100, 91, 93, 91, 100, 101, 101, 89, 91, 89, 101, 87, 87, 101, 102, 102, 84, 87, 103, 104, 105, 105, 106, 103, 106, 105, 107, 107, 108, 106, 108, 107, 109, 109, 110, 108, 111, 112, 113, 113, 114, 111, 114, 113, 115, 115, 116, 114, 116, 115, 117, 117, 118, 116, 118, 117, 119, 119, 120, 118, 120, 119, 121, 121, 122, 120, 122, 121, 123, 123, 124, 122, 124, 123, 125, 125, 126, 124, 126, 125, 127, 127, 128, 126, 128, 127, 104, 104, 103, 128] + rel material:binding = + normal3f[] normals = [(-0.06618896, -0.9976875, 0.015451182), (0.649431, -0.7603868, -0.0071604173), (0.9106648, -0.3677383, 0.18830374), (-0.53811306, 0.14295375, 0.8306615), (0.3463649, 0.5044155, 0.7909465), (0.08575063, 0.9789568, 0.1851767), (0.08575063, 0.9789568, 0.1851767), (-0.8220232, 0.5584798, 0.11125694), (-0.53811306, 0.14295375, 0.8306615), (-0.35616475, 0.24743813, 0.90106666), (-0.53811306, 0.14295375, 0.8306615), (-0.8220232, 0.5584798, 0.11125694), (-0.8220232, 0.5584798, 0.11125694), (-0.7925265, 0.58263534, 0.18010542), (-0.35616475, 0.24743813, 0.90106666), (-0.35616475, 0.24743813, 0.90106666), (-0.7925265, 0.58263534, 0.18010542), (-0.06618896, -0.9976875, 0.015451182), (-0.06618896, -0.9976875, 0.015451182), (-0.46332753, -0.6976641, 0.546436), (-0.35616475, 0.24743813, 0.90106666), (0.9106648, -0.3677383, 0.18830374), (-0.46332753, -0.6976641, 0.546436), (-0.06618896, -0.9976875, 0.015451182), (-0.46332753, -0.6976641, 0.546436), (0.9106648, -0.3677383, 0.18830374), (0.70094746, -0.71191794, 0.042959236), (0.70094746, -0.71191794, 0.042959236), (-0.9523264, 0.25313592, -0.17028368), (-0.46332753, -0.6976641, 0.546436), (-0.46332753, -0.6976641, 0.546436), (-0.9523264, 0.25313592, -0.17028368), (-0.35616475, 0.24743813, 0.90106666), (-0.53811306, 0.14295375, 0.8306615), (-0.35616475, 0.24743813, 0.90106666), (-0.9523264, 0.25313592, -0.17028368), (-0.9523264, 0.25313592, -0.17028368), (-0.6560547, 0.74775225, -0.10226861), (-0.53811306, 0.14295375, 0.8306615), (0.3463649, 0.5044155, 0.7909465), (-0.53811306, 0.14295375, 0.8306615), (-0.6560547, 0.74775225, -0.10226861), (-0.6560547, 0.74775225, -0.10226861), (0.63590676, 0.7665552, -0.089531034), (0.3463649, 0.5044155, 0.7909465), (0.9184378, 0.36357477, 0.15583807), (0.3463649, 0.5044155, 0.7909465), (0.63590676, 0.7665552, -0.089531034), (0.9106394, -0.36777484, 0.18835515), (0.9184378, 0.36357477, 0.15583807), (0.63590676, 0.7665552, -0.089531034), (0.9184378, 0.36357477, 0.15583807), (0.9106394, -0.36777484, 0.18835515), (0.64938885, -0.7604233, -0.0071172705), (0.64938885, -0.7604233, -0.0071172705), (0.94091856, 0.3337267, -0.057434175), (0.9184378, 0.36357477, 0.15583807), (0.3463649, 0.5044155, 0.7909465), (0.9184378, 0.36357477, 0.15583807), (0.94091856, 0.3337267, -0.057434175), (0.94091856, 0.3337267, -0.057434175), (0.08575063, 0.9789568, 0.1851767), (0.3463649, 0.5044155, 0.7909465), (0.63590676, 0.7665552, -0.089531034), (0.70097905, -0.71189404, 0.042840473), (0.9106394, -0.36777484, 0.18835515), (0.635916, 0.76654935, -0.089514755), (-0.65601563, 0.74777657, -0.10234131), (-0.6145149, 0.7004037, -0.3630512), (-0.6145149, 0.7004037, -0.3630512), (0.6260159, 0.7545948, -0.19669951), (0.635916, 0.76654935, -0.089514755), (0.669604, 0.74238634, -0.022203624), (0.6260159, 0.7545948, -0.19669951), (-0.6145149, 0.7004037, -0.3630512), (-0.62540585, 0.78027934, 0.0056270435), (0.6695914, 0.7423987, -0.022171017), (0.669604, 0.74238634, -0.022203624), (0.669604, 0.74238634, -0.022203624), (-0.6253795, 0.7803003, 0.005644817), (-0.62540585, 0.78027934, 0.0056270435), (-0.6145149, 0.7004037, -0.3630512), (-0.6253795, 0.7803003, 0.005644817), (0.669604, 0.74238634, -0.022203624), (-0.6253795, 0.7803003, 0.005644817), (-0.6145149, 0.7004037, -0.3630512), (-0.74618316, 0.6590953, -0.093830556), (-0.74618316, 0.6590953, -0.093830556), (-0.6145149, 0.7004037, -0.3630512), (-0.6560433, 0.74775654, -0.10231005), (-0.6560433, 0.74775654, -0.10231005), (-0.9523215, 0.25315243, -0.1702865), (-0.74618316, 0.6590953, -0.093830556), (0.6793312, -0.6898458, 0.25024354), (-0.74618316, 0.6590953, -0.093830556), (-0.9523215, 0.25315243, -0.1702865), (-0.74258775, 0.6689495, -0.032711983), (-0.74618316, 0.6590953, -0.093830556), (0.6793312, -0.6898458, 0.25024354), (0.6793312, -0.6898458, 0.25024354), (0.6745458, -0.7372925, -0.037251472), (-0.74258775, 0.6689495, -0.032711983), (-0.74258775, 0.6689495, -0.032711983), (0.6745458, -0.7372925, -0.037251472), (0.6745265, -0.73731244, -0.03720762), (0.6696419, 0.7423529, -0.022179257), (0.6745265, -0.73731244, -0.03720762), (0.6745458, -0.7372925, -0.037251472), (0.6745458, -0.7372925, -0.037251472), (0.66959834, 0.74239314, -0.022145202), (0.6696419, 0.7423529, -0.022179257), (0.6260548, 0.7545734, -0.19665788), (0.66959834, 0.74239314, -0.022145202), (0.6745458, -0.7372925, -0.037251472), (0.6745458, -0.7372925, -0.037251472), (0.6793312, -0.6898458, 0.25024354), (0.6260548, 0.7545734, -0.19665788), (0.6260548, 0.7545734, -0.19665788), (0.6793312, -0.6898458, 0.25024354), (0.70095384, -0.71191823, 0.042847253), (-0.9523215, 0.25315243, -0.1702865), (0.70095384, -0.71191823, 0.042847253), (0.6793312, -0.6898458, 0.25024354), (0.6745265, -0.73731244, -0.03720762), (-0.6253596, 0.7803165, 0.005627813), (-0.74258775, 0.6689495, -0.032711983), (-0.6253795, 0.7803003, 0.005644817), (-0.74258775, 0.6689495, -0.032711983), (-0.6253596, 0.7803165, 0.005627813), (-0.74618316, 0.6590953, -0.093830556), (-0.74258775, 0.6689495, -0.032711983), (-0.6253795, 0.7803003, 0.005644817), (-0.6253907, 0.78029126, 0.0056426623), (-0.6253907, 0.78029126, 0.0056426623), (-0.6253907, 0.78029126, 0.0056426623), (0.70095384, -0.71191823, 0.042847253), (0.63597095, 0.76650953, -0.08946625), (0.6260548, 0.7545734, -0.19665788), (0.9521509, 0.2541046, -0.1698224), (-0.56418896, 0.8210629, -0.086870916), (-0.57283044, 0.6904751, -0.44171193), (-0.71459067, -0.6986281, -0.035761036), (-0.73132265, 0.68151003, -0.026670644), (-0.7312955, 0.6815375, -0.026715547), (-0.7312955, 0.6815375, -0.026715547), (-0.7145964, -0.69862366, -0.035735663), (-0.71459067, -0.6986281, -0.035761036), (0.6251233, 0.7800519, -0.02720297), (-0.71459067, -0.6986281, -0.035761036), (-0.7145964, -0.69862366, -0.035735663), (-0.7145964, -0.69862366, -0.035735663), (0.7383423, 0.6743976, 0.006209828), (0.6251233, 0.7800519, -0.02720297), (0.7383115, 0.67443126, 0.006216705), (0.6251233, 0.7800519, -0.02720297), (0.7383423, 0.6743976, 0.006209828), (0.7383473, 0.67439187, 0.0062459344), (0.7383473, 0.67439187, 0.0062459344), (0.7383473, 0.67439187, 0.0062459344), (-0.7313185, 0.6815136, -0.026691247), (0.7383598, 0.6743781, 0.006248914), (0.7383144, 0.674428, 0.00621441), (0.7383144, 0.674428, 0.00621441), (-0.73133177, 0.6814974, -0.026740944), (-0.7313185, 0.6815136, -0.026691247), (-0.57283044, 0.6904751, -0.44171193), (-0.73133177, 0.6814974, -0.026740944), (0.7383144, 0.674428, 0.00621441), (0.7383144, 0.674428, 0.00621441), (0.7364504, 0.6068992, -0.2988547), (-0.57283044, 0.6904751, -0.44171193), (-0.57283044, 0.6904751, -0.44171193), (0.7364504, 0.6068992, -0.2988547), (0.9521509, 0.2541046, -0.1698224), (0.9521509, 0.2541046, -0.1698224), (0.7364504, 0.6068992, -0.2988547), (0.653448, 0.74394125, -0.13984707), (0.653448, 0.74394125, -0.13984707), (0.9521358, 0.25411916, -0.16988441), (0.9521509, 0.2541046, -0.1698224), (0.9521358, 0.25411916, -0.16988441), (0.653448, 0.74394125, -0.13984707), (-0.6700908, -0.68052864, 0.2964104), (-0.6700908, -0.68052864, 0.2964104), (-0.925749, -0.37502086, -0.048458017), (0.9521358, 0.25411916, -0.16988441), (-0.925749, -0.37502086, -0.048458017), (-0.6700908, -0.68052864, 0.2964104), (-0.5728559, 0.6904174, -0.44176906), (-0.5728559, 0.6904174, -0.44176906), (-0.56420964, 0.8210523, -0.08683666), (-0.925749, -0.37502086, -0.048458017), (0.7364504, 0.6068992, -0.2988547), (0.7383144, 0.674428, 0.00621441), (0.7383423, 0.6743976, 0.006209828), (0.7383423, 0.6743976, 0.006209828), (0.653448, 0.74394125, -0.13984707), (0.7364504, 0.6068992, -0.2988547), (0.653448, 0.74394125, -0.13984707), (0.7383423, 0.6743976, 0.006209828), (-0.7145964, -0.69862366, -0.035735663), (-0.7145964, -0.69862366, -0.035735663), (-0.6700908, -0.68052864, 0.2964104), (0.653448, 0.74394125, -0.13984707), (-0.6700908, -0.68052864, 0.2964104), (-0.7145964, -0.69862366, -0.035735663), (-0.7312955, 0.6815375, -0.026715547), (-0.7312955, 0.6815375, -0.026715547), (-0.5728559, 0.6904174, -0.44176906), (-0.6700908, -0.68052864, 0.2964104), (-0.6601439, -0.00017026067, 0.75113916), (-0.3298332, -0.5712124, 0.7516159), (-0.6594119, 0.00010206704, 0.7517818), (-0.33018646, 0.57166904, 0.7511135), (-0.57124734, 0.32976824, 0.7516179), (-0.6594119, 0.00010206704, 0.7517818), (0.3298557, 0.5712099, 0.75160784), (-0.00014435995, 0.6601277, 0.75115335), (0.0001381971, 0.65936506, 0.75182295), (-0.33018646, 0.57166904, 0.7511135), (0.0001381971, 0.65936506, 0.75182295), (-0.00014435995, 0.6601277, 0.75115335), (-0.00014435995, 0.6601277, 0.75115335), (-0.32952824, 0.57120377, 0.7517563), (-0.33018646, 0.57166904, 0.7511135), (-0.57124734, 0.32976824, 0.7516179), (-0.33018646, 0.57166904, 0.7511135), (-0.32952824, 0.57120377, 0.7517563), (-0.32952824, 0.57120377, 0.7517563), (-0.5712262, 0.329823, 0.7516098), (-0.57124734, 0.32976824, 0.7516179), (-0.6594119, 0.00010206704, 0.7517818), (-0.57124734, 0.32976824, 0.7516179), (-0.5712262, 0.329823, 0.7516098), (-0.5712262, 0.329823, 0.7516098), (-0.6601304, -0.00014600158, 0.751151), (-0.6594119, 0.00010206704, 0.7517818), (-0.66005725, -0.00015802257, 0.7512153), (-0.65935796, 0.00007310524, 0.75182915), (-0.66005725, -0.0001580221, 0.7512153), (-0.6599954, -0.00019385923, 0.7512697), (-0.6599954, -0.00019385923, 0.7512697), (-0.6599954, -0.00019385923, 0.7512697), (-0.3298332, -0.5712124, 0.7516159), (-0.6601439, -0.00017026067, 0.75113916), (-0.6601523, -0.00022777915, 0.7511318), (-0.6601523, -0.00022777915, 0.7511318), (-0.32982242, -0.57121795, 0.75161636), (-0.3298332, -0.5712124, 0.7516159), (0.32979992, 0.57123464, 0.7516135), (0.0001381971, 0.65936506, 0.75182295), (-0.33018646, 0.57166904, 0.7511135), (-0.33018646, 0.57166904, 0.7511135), (-0.6594119, 0.00010206704, 0.7517818), (0.32979992, 0.57123464, 0.7516135), (0.33013573, -0.5716702, 0.7511349), (0.32979992, 0.57123464, 0.7516135), (-0.6594119, 0.00010206704, 0.7517818), (0.33013573, -0.5716702, 0.7511349), (-0.6594119, 0.00010206704, 0.7517818), (-0.3298332, -0.5712124, 0.7516159), (-0.3298332, -0.5712124, 0.7516159), (-0.00009124837, -0.6593746, 0.7518145), (0.33013573, -0.5716702, 0.7511349), (-0.00009124837, -0.6593746, 0.7518145), (-0.3298332, -0.5712124, 0.7516159), (-0.32982242, -0.57121795, 0.75161636), (-0.32982242, -0.57121795, 0.75161636), (0.00015143912, -0.6601333, 0.75114846), (-0.00009124837, -0.6593746, 0.7518145), (0.33013573, -0.5716702, 0.7511349), (-0.00009124837, -0.6593746, 0.7518145), (0.00015143912, -0.6601333, 0.75114846), (0.00015143912, -0.6601333, 0.75114846), (0.32953334, -0.57120556, 0.7517525), (0.33013573, -0.5716702, 0.7511349), (0.57122785, -0.3298177, 0.75161105), (0.33013573, -0.5716702, 0.7511349), (0.32953334, -0.57120556, 0.7517525), (0.32953334, -0.57120556, 0.7517525), (0.5712037, -0.32983828, 0.75162023), (0.57122785, -0.3298177, 0.75161105), (0.6593764, -0.00016347319, 0.75181293), (0.57122785, -0.3298177, 0.75161105), (0.5712037, -0.32983828, 0.75162023), (0.5712037, -0.32983828, 0.75162023), (0.660136, 0.00017361075, 0.7511461), (0.6593764, -0.00016347319, 0.75181293), (0.5716855, 0.3301522, 0.7511159), (0.6593764, -0.00016347319, 0.75181293), (0.660136, 0.00017361075, 0.7511461), (0.660136, 0.00017361075, 0.7511461), (0.5712132, 0.32949954, 0.7517616), (0.5716855, 0.3301522, 0.7511159), (0.32979992, 0.57123464, 0.7516135), (0.5716855, 0.3301522, 0.7511159), (0.5712132, 0.32949954, 0.7517616), (0.5712132, 0.32949954, 0.7517616), (0.3298557, 0.5712099, 0.75160784), (0.32979992, 0.57123464, 0.7516135), (0.0001381971, 0.65936506, 0.75182295), (0.32979992, 0.57123464, 0.7516135), (0.3298557, 0.5712099, 0.75160784), (0.33013573, -0.5716702, 0.7511349), (0.57122785, -0.3298177, 0.75161105), (0.6593764, -0.00016347319, 0.75181293), (0.6593764, -0.00016347319, 0.75181293), (0.32979992, 0.57123464, 0.7516135), (0.33013573, -0.5716702, 0.7511349), (0.6593764, -0.00016347319, 0.75181293), (0.5716855, 0.3301522, 0.7511159), (0.32979992, 0.57123464, 0.7516135), (-0.66969764, -0.53296226, 0.51716185), (-0.60757226, -0.793898, 0.024123387), (0.045536064, -0.9989454, 0.0058870604), (0.045536064, -0.9989454, 0.0058870604), (0.10372288, -0.9617923, 0.25337145), (-0.66969764, -0.53296226, 0.51716185), (0.10372288, -0.9617923, 0.25337145), (0.045536064, -0.9989454, 0.0058870604), (0.7923036, 0.5824933, 0.18153916), (0.7923036, 0.5824933, 0.18153916), (0.35430247, 0.24578388, 0.9022528), (0.10372288, -0.9617923, 0.25337145), (0.35430247, 0.24578388, 0.9022528), (0.7923036, 0.5824933, 0.18153916), (0.7923376, 0.58245724, 0.18150683), (0.7923376, 0.58245724, 0.18150683), (0.35434097, 0.2457282, 0.90225273), (0.35430247, 0.24578388, 0.9022528), (0.35434097, 0.2457282, 0.90225273), (0.7923376, 0.58245724, 0.18150683), (-0.094390824, 0.97468805, 0.20266613), (-0.094390824, 0.97468805, 0.20266613), (-0.03741524, 0.51064223, 0.85897875), (0.35434097, 0.2457282, 0.90225273), (-0.03741524, 0.51064223, 0.85897875), (-0.094390824, 0.97468805, 0.20266613), (-0.9458379, 0.2954784, 0.13447364), (-0.9458379, 0.2954784, 0.13447364), (-0.9187307, 0.36310804, 0.15519801), (-0.03741524, 0.51064223, 0.85897875), (-0.9187307, 0.36310804, 0.15519801), (-0.9458379, 0.2954784, 0.13447364), (-0.6075679, -0.7939026, 0.024086157), (-0.6075679, -0.7939026, 0.024086157), (-0.6697179, -0.532935, 0.51716363), (-0.9187307, 0.36310804, 0.15519801), (-0.5641864, 0.8210637, -0.086880326), (-0.9187307, 0.36310804, 0.15519801), (-0.6697179, -0.532935, 0.51716363), (-0.6697179, -0.532935, 0.51716363), (-0.92575264, -0.3750183, -0.04840833), (-0.5641864, 0.8210637, -0.086880326), (-0.9187307, 0.36310804, 0.15519801), (-0.5641864, 0.8210637, -0.086880326), (-0.03741524, 0.51064223, 0.85897875), (-0.03741524, 0.51064223, 0.85897875), (-0.5641864, 0.8210637, -0.086880326), (0.9521449, 0.25405377, -0.16993204), (0.9521449, 0.25405377, -0.16993204), (0.35434097, 0.2457282, 0.90225273), (-0.03741524, 0.51064223, 0.85897875), (0.35434097, 0.2457282, 0.90225273), (0.9521449, 0.25405377, -0.16993204), (0.9521471, 0.25408185, -0.16987677), (0.9521471, 0.25408185, -0.16987677), (0.35430247, 0.24578388, 0.9022528), (0.35434097, 0.2457282, 0.90225273), (0.35430247, 0.24578388, 0.9022528), (0.9521471, 0.25408185, -0.16987677), (0.10372288, -0.9617923, 0.25337145), (0.10372288, -0.9617923, 0.25337145), (0.9521471, 0.25408185, -0.16987677), (-0.92575413, -0.37500733, -0.048465863), (-0.92575413, -0.37500733, -0.048465863), (-0.66969764, -0.53296226, 0.51716185), (0.10372288, -0.9617923, 0.25337145), (-0.6601021, -0.00016912818, 0.7511759), (-0.9999999, -0.00032445788, -2.2599284e-8), (-0.5000411, -0.8660016, 0), (-0.5000411, -0.8660016, 0), (-0.32977062, -0.5712578, 0.75160885), (-0.6601021, -0.00016912818, 0.7511759), (-0.32977062, -0.5712578, 0.75160885), (-0.5000411, -0.8660016, 0), (0.00024941564, -0.99999994, 0), (0.00024941564, -0.99999994, 0), (0.0002117455, -0.66013837, 0.751144), (-0.32977062, -0.5712578, 0.75160885), (0.0002117455, -0.66013837, 0.751144), (0.00024941564, -0.99999994, 0), (0.49974224, -0.86617416, 0.000023974237), (0.49974224, -0.86617416, 0.000023974237), (0.32958013, -0.57117134, 0.75175804), (0.0002117455, -0.66013837, 0.751144), (0.32954955, -0.57115597, 0.7517832), (0.49971682, -0.8661888, 0), (0.86601406, -0.5000195, 0), (0.86601406, -0.5000195, 0), (0.57120293, -0.32979712, 0.75163895), (0.32954955, -0.57115597, 0.7517832), (0.57120293, -0.32979712, 0.75163895), (0.86601406, -0.5000195, 0), (1, 0.00023381412, 0), (1, 0.00023381412, 0), (0.660129, 0.00016576052, 0.7511522), (0.57120293, -0.32979712, 0.75163895), (0.660129, 0.00016576052, 0.7511522), (1, 0.00023381412, 0), (0.86618733, 0.4997194, 0), (0.86618733, 0.4997194, 0), (0.5711479, 0.32958996, 0.7517716), (0.660129, 0.00016576052, 0.7511522), (0.5711479, 0.32958996, 0.7517716), (0.86618733, 0.4997194, 0), (0.50004697, 0.86599827, 0), (0.50004697, 0.86599827, 0), (0.32978845, 0.57121474, 0.75163376), (0.5711479, 0.32958996, 0.7517716), (0.32978845, 0.57121474, 0.75163376), (0.50004697, 0.86599827, 0), (-0.00023402274, 0.9999999, 0), (-0.00023402274, 0.9999999, 0), (-0.00016579032, 0.660129, 0.7511522), (0.32978845, 0.57121474, 0.75163376), (-0.00016579032, 0.660129, 0.7511522), (-0.00023402274, 0.9999999, 0), (-0.4997194, 0.86618733, 0), (-0.4997194, 0.86618733, 0), (-0.32958984, 0.57114786, 0.7517717), (-0.00016579032, 0.660129, 0.7511522), (-0.32958984, 0.57114786, 0.7517717), (-0.4997194, 0.86618733, 0), (-0.86599827, 0.5000469, 0), (-0.86599827, 0.5000469, 0), (-0.5712147, 0.32978845, 0.75163376), (-0.32958984, 0.57114786, 0.7517717), (-0.5712147, 0.32978845, 0.75163376), (-0.86599827, 0.5000469, 0), (-1, -0.00023399293, 0), (-1, -0.00023399293, 0), (-0.6601297, -0.00016535819, 0.7511515), (-0.5712147, 0.32978845, 0.75163376), (-0.6601297, -0.00016535819, 0.7511515), (-1, -0.00023399293, 0), (-0.9999999, -0.00032445788, -2.2599284e-8), (-0.9999999, -0.00032445788, -2.2599284e-8), (-0.6601021, -0.00016912818, 0.7511759), (-0.6601297, -0.00016535819, 0.7511515)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(-0.004843, -0.002837, 0.012219), (-0.003021, -0.0029199999, 0.012219), (-0.002952, -0.002899, 0.016303), (-0.00831, 0.007582, 0.014078), (-0.004722, 0.007261, 0.014113), (-0.004563, 0.007606, 0.012215), (-0.008299, 0.007968, 0.012215), (-0.010152, 0.004867, 0.014095), (-0.010387, 0.005129, 0.012215), (-0.005008, -0.002768, 0.015962), (-0.003114, -0.002437, 0.01799), (-0.00466, -0.00141, 0.01799), (-0.004087, 0.000744, 0.01799), (-0.00232, -0.000469, 0.01799), (-0.001472, -0.001629, 0.015699), (-0.002952, -0.002899, 0.016303), (-0.003021, -0.0029199999, 0.012219), (-0.00161, -0.00184, 0.012219), (-0.003114, -0.002437, 0.01799), (-0.00232, -0.000469, 0.01799), (-0.004087, 0.000744, 0.01799), (-0.004115, 0.002769, 0.032973), (-0.0011999999, 0.000351, 0.032973), (-0.001445, 0.001217, 0.035516), (-0.003839, 0.00389, 0.05272), (-0.000548, 0.000913, 0.052427), (-0.003977, 0.003908, 0.034936), (-0.006871, 0.000351, 0.032973), (-0.004087, 0.000744, 0.01799), (-0.00466, -0.00141, 0.01799), (-0.004115, -0.002519, 0.032973), (-0.006426, 0.001217, 0.035516), (-0.003977, -0.001381, 0.035736), (-0.003839, -0.002112, 0.05272), (-0.000548, 0.000913, 0.052427), (-0.001445, 0.001217, 0.035516), (-0.0011999999, 0.000351, 0.032973), (-0.003114, -0.002437, 0.01799), (-0.007209, 0.001189, 0.05272), (-0.00232, -0.000469, 0.01799), (0.003458, 0.000744, 0.017959), (0.001691, -0.000469, 0.017962), (0.000574, 0.000351, 0.032973), (0.003213, -0.002112, 0.05272), (-0.000082, 0.000913, 0.052427), (0.000819, 0.001217, 0.035516), (0.003347, -0.001381, 0.035736), (0.006583, 0.001189, 0.05272), (0.0058, 0.001217, 0.035516), (0.003213, 0.00389, 0.05272), (0.003347, 0.003908, 0.034936), (-0.000082, 0.000913, 0.052427), (0.000819, 0.001217, 0.035516), (0.003489, 0.002769, 0.032973), (0.006242, 0.000351, 0.032973), (0.004033, -0.00141, 0.017961), (0.003489, -0.002519, 0.032973), (0.002488, -0.002437, 0.017963), (0.000574, 0.000351, 0.032973), (0.001691, -0.000469, 0.017962), (-0.017052, -0.003452, 0.012346), (-0.012549, -0.0112499995, 0.012346), (-0.017051, 0.005552, 0.012346), (-0.004753, 0.017851, 0.012346), (-0.01255, 0.013348, 0.012346), (0.013716, 0.015014, 0.010349), (0.00486, 0.020127, 0.010349), (0.004252, 0.01785, 0.012346), (-0.005362, 0.020124, 0.010349), (-0.014215, 0.015016, 0.010349), (-0.019328, 0.00616, 0.010349), (-0.019325, -0.004061, 0.010349), (-0.0142169995, -0.012914, 0.010349), (0.012047, 0.013349, 0.012346), (0.004251, -0.015752, 0.012346), (-0.004753, -0.01575, 0.012346), (-0.005361, -0.018028, 0.010349), (0.00486, -0.018025, 0.010349), (0.012049, -0.011248, 0.012346), (0.013714, -0.012917, 0.010349), (0.016549, -0.003453, 0.012346), (0.018827, -0.004061, 0.010349), (0.016551, 0.005552, 0.012346), (0.018824, 0.006161, 0.010349), (0.002323, -0.002899, 0.016276), (0.002395, -0.0029199999, 0.012219), (0.004213, -0.002837, 0.012219), (0.004382, -0.002768, 0.015931), (0.009761, 0.005129, 0.012215), (0.009523, 0.004867, 0.014095), (0.0076739998, 0.007968, 0.012215), (0.007684, 0.007582, 0.014078), (0.003934, 0.007606, 0.012215), (0.004096, 0.007261, 0.014113), (0.000984, -0.00184, 0.012219), (0.000846, -0.001629, 0.015672), (0.002395, -0.0029199999, 0.012219), (0.002323, -0.002899, 0.016276), (0.001691, -0.000469, 0.017962), (0.002488, -0.002437, 0.017963), (0.003458, 0.000744, 0.017959), (0.004033, -0.00141, 0.017961), (0.002488, -0.002437, 0.017963), (-0.019325, -0.004061, 0.010349), (-0.019325, -0.004061, 0.000096), (-0.0142169995, -0.012914, 0.000096), (-0.0142169995, -0.012914, 0.010349), (-0.005361, -0.018028, 0.000096), (-0.005361, -0.018028, 0.010349), (0.00486, -0.018025, 0.000096), (0.00486, -0.018025, 0.010349), (0.00486, -0.018025, 0.010349), (0.00486, -0.018025, 0.000096), (0.013714, -0.012917, 0.000096), (0.013714, -0.012917, 0.010349), (0.018827, -0.004061, 0.000096), (0.018827, -0.004061, 0.010349), (0.018824, 0.006161, 0.000096), (0.018824, 0.006161, 0.010349), (0.013716, 0.015014, 0.000096), (0.013716, 0.015014, 0.010349), (0.00486, 0.020127, 0.000096), (0.00486, 0.020127, 0.010349), (-0.005362, 0.020124, 0.000096), (-0.005362, 0.020124, 0.010349), (-0.014215, 0.015016, 0.000096), (-0.014215, 0.015016, 0.010349), (-0.019328, 0.00616, 0.000096), (-0.019328, 0.00616, 0.010349)] + color3f[] primvars:HulaGirl_LowerBody_mesh_color = [(1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1)] ( + interpolation = "faceVarying" + ) + texCoord2f[] primvars:HulaGirl_LowerBody_mesh_UV = [(0.421564, 0.64309), (0.428322, 0.663997), (0.403049, 0.670374), (0.361815, 0.543194), (0.329531, 0.548328), (0.317636, 0.533464), (0.317636, 0.533464), (0.362693, 0.523583), (0.361815, 0.543194), (0.392663, 0.55448), (0.361815, 0.543194), (0.362693, 0.523583), (0.362693, 0.523583), (0.402975, 0.53716), (0.392663, 0.55448), (0.392663, 0.55448), (0.402975, 0.53716), (0.421564, 0.64309), (0.421564, 0.64309), (0.401976, 0.649126), (0.392663, 0.55448), (0.403049, 0.670374), (0.401976, 0.649126), (0.421564, 0.64309), (0.401976, 0.649126), (0.403049, 0.670374), (0.385346, 0.669244), (0.385346, 0.669244), (0.376497, 0.644698), (0.401976, 0.649126), (0.401976, 0.649126), (0.376497, 0.644698), (0.392663, 0.55448), (0.361815, 0.543194), (0.392663, 0.55448), (0.376497, 0.644698), (0.376497, 0.644698), (0.354149, 0.633213), (0.361815, 0.543194), (0.329531, 0.548328), (0.361815, 0.543194), (0.354149, 0.633213), (0.354149, 0.633213), (0.334506, 0.64647), (0.329531, 0.548328), (0.310994, 0.652476), (0.329531, 0.548328), (0.334506, 0.64647), (0.312036, 0.664506), (0.310994, 0.652476), (0.334506, 0.64647), (0.310994, 0.652476), (0.312036, 0.664506), (0.296972, 0.661931), (0.296972, 0.661931), (0.29745, 0.646893), (0.310994, 0.652476), (0.329531, 0.548328), (0.310994, 0.652476), (0.29745, 0.646893), (0.29745, 0.646893), (0.317636, 0.533464), (0.329531, 0.548328), (0.334506, 0.64647), (0.335271, 0.664653), (0.312036, 0.664506), (0.675574, 0.60012), (0.675574, 0.616871), (0.564011, 0.616871), (0.564011, 0.616871), (0.564011, 0.594324), (0.675574, 0.60012), (0.546753, 0.594324), (0.564011, 0.594324), (0.564011, 0.616871), (0.482034, 0.616871), (0.482034, 0.594324), (0.546753, 0.594324), (0.546753, 0.594324), (0.546753, 0.616871), (0.482034, 0.616871), (0.564011, 0.616871), (0.546753, 0.616871), (0.546753, 0.594324), (0.546753, 0.616871), (0.564011, 0.616871), (0.564011, 0.641201), (0.564011, 0.641201), (0.564011, 0.616871), (0.675574, 0.616871), (0.675574, 0.616871), (0.675575, 0.644481), (0.564011, 0.641201), (0.564011, 0.667315), (0.564011, 0.641201), (0.675575, 0.644481), (0.546753, 0.641201), (0.564011, 0.641201), (0.564011, 0.667315), (0.564011, 0.667315), (0.546753, 0.667315), (0.546753, 0.641201), (0.546753, 0.641201), (0.546753, 0.667315), (0.482035, 0.667315), (0.482034, 0.690371), (0.482035, 0.667315), (0.546753, 0.667315), (0.546753, 0.667315), (0.546753, 0.690244), (0.482034, 0.690371), (0.564011, 0.690243), (0.546753, 0.690244), (0.546753, 0.667315), (0.546753, 0.667315), (0.564011, 0.667315), (0.564011, 0.690243), (0.564011, 0.690243), (0.564011, 0.667315), (0.675574, 0.667315), (0.675575, 0.644481), (0.675574, 0.667315), (0.564011, 0.667315), (0.482035, 0.667315), (0.482034, 0.641201), (0.546753, 0.641201), (0.546753, 0.616871), (0.546753, 0.641201), (0.482034, 0.641201), (0.564011, 0.641201), (0.546753, 0.641201), (0.546753, 0.616871), (0.482034, 0.641201), (0.482034, 0.616871), (0.546753, 0.616871), (0.675574, 0.667315), (0.675574, 0.684639), (0.564011, 0.690243), (0.675574, 0.667315), (0.675574, 0.684639), (0.564011, 0.690243), (0.482034, 0.616871), (0.482034, 0.594324), (0.546753, 0.594324), (0.546753, 0.594324), (0.546753, 0.616871), (0.482034, 0.616871), (0.482034, 0.641201), (0.482034, 0.616871), (0.546753, 0.616871), (0.546753, 0.616871), (0.546753, 0.641201), (0.482034, 0.641201), (0.482035, 0.667315), (0.482034, 0.641201), (0.546753, 0.641201), (0.546753, 0.641201), (0.546753, 0.667315), (0.482035, 0.667315), (0.482034, 0.690371), (0.482035, 0.667315), (0.546753, 0.667315), (0.546753, 0.667315), (0.546753, 0.690244), (0.482034, 0.690371), (0.564011, 0.690243), (0.546753, 0.690244), (0.546753, 0.667315), (0.546753, 0.667315), (0.564011, 0.667315), (0.564011, 0.690243), (0.564011, 0.690243), (0.564011, 0.667315), (0.675574, 0.667315), (0.675574, 0.667315), (0.564011, 0.667315), (0.564011, 0.641201), (0.564011, 0.641201), (0.675575, 0.644481), (0.675574, 0.667315), (0.675575, 0.644481), (0.564011, 0.641201), (0.564011, 0.616871), (0.564011, 0.616871), (0.675574, 0.616871), (0.675575, 0.644481), (0.675574, 0.616871), (0.564011, 0.616871), (0.564011, 0.594324), (0.564011, 0.594324), (0.675574, 0.60012), (0.675574, 0.616871), (0.564011, 0.667315), (0.546753, 0.667315), (0.546753, 0.641201), (0.546753, 0.641201), (0.564011, 0.641201), (0.564011, 0.667315), (0.564011, 0.641201), (0.546753, 0.641201), (0.546753, 0.616871), (0.546753, 0.616871), (0.564011, 0.616871), (0.564011, 0.641201), (0.564011, 0.616871), (0.546753, 0.616871), (0.546753, 0.594324), (0.546753, 0.594324), (0.564011, 0.594324), (0.564011, 0.616871), (0.347693, 0.285297), (0.395427, 0.298183), (0.299957, 0.298183), (0.252222, 0.381061), (0.265107, 0.333325), (0.299957, 0.298183), (0.293514, 0.474775), (0.253686, 0.435239), (0.265108, 0.428796), (0.252222, 0.381061), (0.265108, 0.428796), (0.253686, 0.435239), (0.253686, 0.435239), (0.239336, 0.381061), (0.252222, 0.381061), (0.265107, 0.333325), (0.252222, 0.381061), (0.239336, 0.381061), (0.239336, 0.381061), (0.253686, 0.32659), (0.265107, 0.333325), (0.299957, 0.298183), (0.265107, 0.333325), (0.253686, 0.32659), (0.253686, 0.32659), (0.293514, 0.287054), (0.299957, 0.298183), (0.347693, 0.285297), (0.299957, 0.298183), (0.293514, 0.287054), (0.293514, 0.287054), (0.347693, 0.272411), (0.347693, 0.285297), (0.395427, 0.298183), (0.347693, 0.285297), (0.347693, 0.272411), (0.347693, 0.272411), (0.401871, 0.287055), (0.395427, 0.298183), (0.299957, 0.463646), (0.265108, 0.428796), (0.252222, 0.381061), (0.252222, 0.381061), (0.299957, 0.298183), (0.299957, 0.463646), (0.443164, 0.381061), (0.299957, 0.463646), (0.299957, 0.298183), (0.443164, 0.381061), (0.299957, 0.298183), (0.395427, 0.298183), (0.395427, 0.298183), (0.430278, 0.333325), (0.443164, 0.381061), (0.430278, 0.333325), (0.395427, 0.298183), (0.401871, 0.287055), (0.401871, 0.287055), (0.4417, 0.32659), (0.430278, 0.333325), (0.443164, 0.381061), (0.430278, 0.333325), (0.4417, 0.32659), (0.4417, 0.32659), (0.456049, 0.381061), (0.443164, 0.381061), (0.430278, 0.428796), (0.443164, 0.381061), (0.456049, 0.381061), (0.456049, 0.381061), (0.4417, 0.435239), (0.430278, 0.428796), (0.395427, 0.463646), (0.430278, 0.428796), (0.4417, 0.435239), (0.4417, 0.435239), (0.401871, 0.474775), (0.395427, 0.463646), (0.347692, 0.476532), (0.395427, 0.463646), (0.401871, 0.474775), (0.401871, 0.474775), (0.347693, 0.489418), (0.347692, 0.476532), (0.299957, 0.463646), (0.347692, 0.476532), (0.347693, 0.489418), (0.347693, 0.489418), (0.293514, 0.474775), (0.299957, 0.463646), (0.265108, 0.428796), (0.299957, 0.463646), (0.293514, 0.474775), (0.443164, 0.381061), (0.430278, 0.428796), (0.395427, 0.463646), (0.395427, 0.463646), (0.299957, 0.463646), (0.443164, 0.381061), (0.395427, 0.463646), (0.347692, 0.476532), (0.299957, 0.463646), (0.403049, 0.670374), (0.428322, 0.663997), (0.421564, 0.64309), (0.421564, 0.64309), (0.401976, 0.649126), (0.403049, 0.670374), (0.401976, 0.649126), (0.421564, 0.64309), (0.402975, 0.53716), (0.402975, 0.53716), (0.392663, 0.55448), (0.401976, 0.649126), (0.392663, 0.55448), (0.402975, 0.53716), (0.362693, 0.523583), (0.362693, 0.523583), (0.361815, 0.543194), (0.392663, 0.55448), (0.361815, 0.543194), (0.362693, 0.523583), (0.317636, 0.533464), (0.317636, 0.533464), (0.329531, 0.548328), (0.361815, 0.543194), (0.329531, 0.548328), (0.317636, 0.533464), (0.29745, 0.646893), (0.29745, 0.646893), (0.310994, 0.652476), (0.329531, 0.548328), (0.310994, 0.652476), (0.29745, 0.646893), (0.296972, 0.661931), (0.296972, 0.661931), (0.312036, 0.664506), (0.310994, 0.652476), (0.334506, 0.64647), (0.310994, 0.652476), (0.312036, 0.664506), (0.312036, 0.664506), (0.335271, 0.664653), (0.334506, 0.64647), (0.310994, 0.652476), (0.334506, 0.64647), (0.329531, 0.548328), (0.329531, 0.548328), (0.334506, 0.64647), (0.354149, 0.633213), (0.354149, 0.633213), (0.361815, 0.543194), (0.329531, 0.548328), (0.361815, 0.543194), (0.354149, 0.633213), (0.376497, 0.644698), (0.376497, 0.644698), (0.392663, 0.55448), (0.361815, 0.543194), (0.392663, 0.55448), (0.376497, 0.644698), (0.401976, 0.649126), (0.401976, 0.649126), (0.376497, 0.644698), (0.385346, 0.669244), (0.385346, 0.669244), (0.403049, 0.670374), (0.401976, 0.649126), (0.970279, 0.17477), (0.926008, 0.174771), (0.926008, 0.130499), (0.926008, 0.130499), (0.970279, 0.130499), (0.970279, 0.17477), (0.970279, 0.130499), (0.926008, 0.130499), (0.926007, 0.086227), (0.926007, 0.086227), (0.970279, 0.086227), (0.970279, 0.130499), (0.970279, 0.086227), (0.926007, 0.086227), (0.926008, 0.042201), (0.926008, 0.042201), (0.970279, 0.042201), (0.970279, 0.086227), (0.970279, 0.572725), (0.926007, 0.572725), (0.926007, 0.528698), (0.926007, 0.528698), (0.970279, 0.528698), (0.970279, 0.572725), (0.970279, 0.528698), (0.926007, 0.528698), (0.926007, 0.484427), (0.926007, 0.484427), (0.970279, 0.484427), (0.970279, 0.528698), (0.970279, 0.484427), (0.926007, 0.484427), (0.926007, 0.440155), (0.926007, 0.440155), (0.970279, 0.440155), (0.970279, 0.484427), (0.970279, 0.440155), (0.926007, 0.440155), (0.926008, 0.395884), (0.926008, 0.395884), (0.970279, 0.395884), (0.970279, 0.440155), (0.970279, 0.395884), (0.926008, 0.395884), (0.926008, 0.351612), (0.926008, 0.351612), (0.970279, 0.351612), (0.970279, 0.395884), (0.970279, 0.351612), (0.926008, 0.351612), (0.926007, 0.307585), (0.926007, 0.307585), (0.970279, 0.307585), (0.970279, 0.351612), (0.970279, 0.307585), (0.926007, 0.307585), (0.926008, 0.263314), (0.926008, 0.263314), (0.970279, 0.263314), (0.970279, 0.307585), (0.970279, 0.263314), (0.926008, 0.263314), (0.926008, 0.219287), (0.926008, 0.219287), (0.970279, 0.218798), (0.970279, 0.263314), (0.970279, 0.218798), (0.926008, 0.219287), (0.926008, 0.174771), (0.926008, 0.174771), (0.970279, 0.17477), (0.970279, 0.218798)] ( + interpolation = "faceVarying" + ) + bool[] primvars:sharp_face = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ( + interpolation = "uniform" + ) + uniform token subdivisionScheme = "none" + } + } + + def Xform "HulaGirl_UpperBody" + { + matrix4d xformOp:transform = ( (0.9955740571022034, -0.001967928372323513, -0.09395463019609451, 0), (-0.01725092902779579, 0.9789638519287109, -0.20330139994621277, 0), (0.09237835556268692, 0.2040226012468338, 0.9745979905128479, 0), (0.00010099999781232327, 0, 0.06407800316810608, 1) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + + def Mesh "HulaGirl_UpperBody" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + uniform bool doubleSided = 1 + float3[] extent = [(-0.017494, -0.012675, -0.019437), (0.032544, 0.012675, 0.051465)] + int[] faceVertexCounts = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] + int[] faceVertexIndices = [0, 1, 2, 3, 4, 0, 5, 0, 4, 4, 6, 5, 7, 5, 6, 6, 8, 7, 9, 8, 6, 6, 10, 9, 10, 6, 11, 11, 12, 10, 13, 12, 11, 6, 4, 14, 14, 11, 6, 4, 15, 16, 16, 17, 4, 4, 17, 18, 18, 14, 4, 19, 14, 18, 11, 14, 19, 19, 20, 11, 11, 20, 13, 13, 20, 21, 21, 22, 13, 22, 21, 23, 23, 24, 22, 24, 23, 25, 25, 26, 24, 27, 26, 25, 25, 28, 27, 28, 25, 29, 29, 30, 28, 30, 29, 31, 31, 32, 30, 33, 34, 35, 35, 34, 31, 31, 29, 35, 35, 29, 25, 25, 23, 35, 35, 23, 21, 21, 36, 35, 36, 21, 20, 20, 19, 36, 36, 19, 37, 18, 37, 19, 37, 18, 38, 38, 18, 17, 17, 39, 38, 35, 36, 33, 37, 33, 36, 33, 37, 34, 38, 40, 37, 41, 42, 43, 43, 44, 41, 41, 44, 28, 28, 30, 41, 45, 46, 47, 48, 49, 46, 46, 49, 50, 50, 47, 46, 50, 51, 47, 51, 50, 52, 53, 54, 55, 56, 55, 54, 55, 56, 57, 57, 56, 58, 58, 59, 57, 59, 58, 60, 60, 61, 59, 61, 60, 62, 62, 63, 61, 63, 62, 64, 64, 52, 63, 52, 64, 65, 52, 65, 51, 51, 65, 64, 64, 47, 51, 66, 47, 64, 47, 66, 45, 45, 66, 67, 67, 66, 68, 57, 69, 55, 70, 55, 69, 55, 70, 53, 69, 71, 70, 62, 72, 73, 72, 62, 60, 73, 64, 62, 64, 73, 66, 73, 68, 66, 68, 73, 72, 68, 72, 74, 60, 74, 72, 75, 74, 60, 60, 58, 75, 76, 75, 58, 58, 56, 76, 54, 76, 56, 76, 54, 77, 77, 54, 53, 53, 78, 77, 78, 53, 70, 70, 79, 78, 80, 79, 70, 81, 79, 80, 71, 81, 80, 80, 82, 71, 71, 82, 70, 70, 82, 80, 83, 77, 78, 83, 78, 84, 84, 78, 79, 79, 85, 84, 85, 79, 81, 81, 86, 85, 85, 86, 87, 87, 84, 85, 88, 84, 87, 89, 84, 88, 88, 90, 89, 90, 88, 91, 92, 91, 88, 92, 93, 91, 84, 89, 83, 87, 92, 88, 94, 76, 95, 77, 95, 76, 77, 96, 95, 83, 96, 77, 97, 96, 83, 83, 98, 97, 98, 83, 89, 99, 98, 89, 89, 90, 99, 90, 100, 99, 91, 100, 90, 101, 100, 91, 91, 93, 101, 102, 101, 93, 93, 45, 102, 45, 93, 92, 92, 48, 45, 46, 45, 48, 99, 100, 101, 99, 101, 102, 103, 104, 94, 96, 103, 94, 94, 95, 96, 105, 103, 96, 105, 96, 97, 97, 106, 105, 67, 102, 45, 107, 102, 67, 102, 107, 99, 108, 99, 107, 98, 99, 108, 98, 108, 109, 98, 109, 97, 109, 110, 97, 110, 106, 97, 110, 111, 106, 105, 106, 111, 111, 112, 105, 112, 103, 105, 113, 103, 112, 103, 113, 104, 75, 104, 113, 94, 104, 75, 75, 76, 94, 107, 109, 108, 111, 110, 109, 111, 109, 107, 111, 107, 67, 67, 112, 111, 67, 68, 112, 112, 68, 114, 74, 114, 68, 114, 74, 113, 113, 74, 75, 112, 114, 113, 115, 116, 117, 118, 119, 120, 120, 121, 118, 121, 120, 122, 122, 123, 121, 123, 122, 124, 124, 125, 123, 125, 124, 126, 126, 127, 125, 127, 126, 128, 128, 117, 127, 117, 128, 115, 115, 128, 129, 115, 129, 130, 131, 130, 129, 129, 132, 131, 132, 129, 133, 133, 134, 132, 134, 133, 135, 119, 136, 137, 137, 120, 119, 122, 120, 137, 137, 138, 122, 124, 122, 138, 138, 139, 124, 124, 139, 135, 135, 126, 124, 128, 126, 135, 135, 133, 128, 128, 133, 129, 135, 140, 134, 140, 135, 139, 139, 141, 140, 141, 139, 138, 138, 142, 141, 142, 138, 137, 137, 143, 142, 143, 137, 136, 136, 144, 143, 145, 143, 144, 144, 146, 145, 147, 145, 146, 146, 148, 147, 142, 143, 145, 145, 149, 142, 141, 142, 149, 145, 147, 150, 150, 149, 145, 151, 149, 150, 149, 151, 141, 141, 151, 152, 152, 140, 141, 134, 140, 152, 152, 153, 134, 132, 134, 153, 153, 154, 132, 131, 132, 154, 154, 155, 131, 155, 154, 156, 156, 157, 155, 158, 156, 154, 154, 153, 158, 159, 158, 153, 153, 152, 159, 159, 152, 151, 151, 160, 159, 150, 160, 151, 161, 162, 163, 164, 163, 162, 162, 165, 164, 165, 166, 164, 164, 166, 167, 163, 164, 167, 167, 168, 163, 169, 163, 168, 168, 170, 169, 163, 169, 161, 171, 172, 173, 173, 174, 171, 174, 173, 175, 175, 176, 174, 176, 175, 177, 177, 178, 176, 178, 177, 179, 179, 180, 178, 180, 179, 181, 181, 182, 180, 183, 182, 181, 181, 184, 183, 185, 183, 184, 184, 186, 185, 187, 185, 186, 186, 188, 187, 189, 187, 188, 188, 190, 189, 190, 191, 192, 192, 189, 190, 189, 192, 193, 193, 194, 189, 187, 189, 194, 194, 195, 187, 185, 187, 195, 195, 196, 185, 183, 185, 196, 196, 197, 183, 182, 183, 197, 197, 198, 182, 180, 182, 198, 198, 199, 180, 178, 180, 199, 199, 200, 178, 176, 178, 200, 200, 201, 176, 174, 176, 201, 201, 202, 174, 171, 174, 202, 202, 203, 171, 204, 205, 206, 206, 207, 204, 208, 207, 206, 206, 209, 208, 208, 209, 210, 210, 211, 208, 212, 211, 210, 210, 213, 212, 212, 213, 214, 214, 215, 212, 212, 215, 216, 217, 204, 207, 207, 218, 217, 218, 207, 208, 208, 219, 218, 219, 208, 211, 211, 220, 219, 220, 211, 212, 212, 221, 220, 216, 221, 212, 222, 221, 216, 216, 223, 222, 224, 222, 223, 225, 222, 224, 224, 226, 225, 227, 225, 226, 226, 228, 227, 227, 228, 229, 230, 229, 228, 223, 231, 224, 232, 217, 218, 218, 233, 232, 233, 218, 219, 220, 221, 222, 222, 225, 220, 219, 220, 225, 225, 227, 219, 219, 227, 233, 229, 233, 227, 232, 233, 229, 229, 234, 232, 234, 235, 232, 217, 232, 235, 235, 236, 217, 204, 217, 236, 236, 237, 204, 205, 204, 237, 237, 238, 205, 228, 239, 230, 239, 228, 226, 226, 240, 239, 240, 226, 224, 224, 241, 240, 241, 224, 231, 231, 242, 241, 229, 230, 243, 243, 234, 229, 234, 243, 244, 244, 245, 234, 234, 245, 235, 245, 246, 235, 236, 235, 246, 246, 247, 236, 237, 236, 247, 247, 248, 237, 238, 237, 248, 248, 249, 238, 250, 251, 252, 253, 254, 255, 255, 250, 253, 250, 255, 256, 256, 251, 250, 257, 251, 256, 252, 251, 257, 257, 258, 252, 258, 257, 259, 259, 260, 258, 261, 262, 263, 264, 261, 263, 263, 265, 264, 265, 260, 264, 260, 265, 266, 266, 258, 260, 258, 266, 267, 267, 252, 258, 268, 252, 267, 252, 268, 250, 250, 268, 269, 269, 253, 250, 253, 269, 270, 270, 271, 253, 253, 271, 272, 272, 254, 253, 254, 272, 273, 273, 274, 254, 262, 275, 276, 276, 263, 262, 276, 265, 263, 265, 276, 266, 267, 277, 278, 278, 279, 267, 279, 268, 267, 268, 279, 269, 269, 279, 270, 270, 279, 278, 278, 280, 270, 255, 254, 274, 274, 281, 255, 256, 255, 281, 281, 282, 256, 256, 282, 257, 257, 282, 283, 283, 259, 257, 284, 259, 283, 260, 259, 284, 284, 264, 260, 261, 264, 284, 284, 285, 261, 262, 261, 285, 285, 286, 262, 287, 286, 285, 285, 288, 287, 287, 288, 289, 290, 289, 288, 289, 291, 287, 288, 292, 290, 282, 290, 292, 292, 283, 282, 283, 292, 284, 285, 284, 292, 292, 288, 285, 293, 294, 295, 296, 297, 293, 298, 293, 299, 300, 301, 298, 302, 296, 303, 293, 303, 296, 304, 303, 293, 303, 304, 302, 305, 302, 304, 304, 306, 305, 306, 307, 305, 295, 308, 309, 309, 310, 295, 295, 310, 293, 311, 293, 310, 293, 311, 304, 304, 311, 312, 312, 306, 304, 306, 312, 313, 313, 307, 306, 307, 313, 314, 314, 315, 307, 316, 315, 314, 314, 317, 316, 316, 317, 318, 319, 318, 317, 318, 319, 320, 320, 321, 318, 321, 320, 322, 322, 323, 321, 318, 324, 316, 313, 312, 325, 325, 326, 313, 314, 313, 326, 326, 327, 314, 314, 327, 328, 328, 317, 314, 317, 328, 319, 329, 319, 328, 328, 330, 329, 331, 330, 328, 328, 327, 331, 332, 331, 327, 327, 326, 332, 333, 332, 326, 326, 325, 333, 333, 325, 334, 334, 325, 312, 312, 311, 334, 310, 334, 311, 334, 310, 335, 336, 335, 310, 310, 309, 336, 335, 337, 334, 334, 337, 333, 338, 339, 340, 338, 340, 341, 342, 343, 340, 340, 339, 342, 344, 342, 339, 339, 345, 344, 346, 345, 339, 339, 347, 346, 347, 339, 338, 338, 348, 347, 348, 338, 349, 349, 338, 341, 349, 341, 350, 351, 352, 346, 346, 347, 351, 353, 351, 347, 347, 348, 353, 353, 348, 354, 349, 354, 348, 354, 349, 355, 356, 357, 344, 344, 345, 356, 358, 356, 345, 345, 346, 358, 359, 358, 346, 346, 352, 359, 360, 359, 352, 352, 361, 360, 361, 352, 351, 351, 362, 361, 362, 351, 363, 353, 363, 351, 364, 363, 353, 364, 353, 365, 354, 365, 353, 366, 365, 354, 354, 367, 366, 355, 367, 354, 367, 355, 368, 363, 369, 362, 370, 364, 365, 368, 371, 367, 366, 367, 371, 371, 372, 366, 373, 374, 375, 375, 376, 373, 376, 375, 377, 377, 378, 376, 378, 377, 379, 380, 381, 373, 374, 373, 381, 381, 382, 374, 374, 382, 383, 379, 384, 385, 379, 377, 384, 384, 377, 386, 375, 386, 377, 387, 386, 375, 387, 375, 374, 388, 387, 374, 374, 389, 388, 389, 374, 383, 383, 390, 389, 390, 383, 391, 391, 392, 390, 386, 393, 384, 394, 380, 395, 373, 395, 380, 396, 395, 373, 373, 376, 396, 397, 396, 376, 376, 378, 397, 397, 378, 398, 379, 398, 378, 398, 379, 399, 385, 399, 379, 399, 385, 400, 400, 401, 399, 402, 399, 401, 401, 403, 402, 402, 403, 404, 404, 405, 402, 406, 405, 404, 407, 405, 406, 398, 407, 397, 405, 407, 398, 398, 402, 405, 399, 402, 398, 404, 408, 406, 406, 409, 407, 407, 409, 397, 410, 397, 409, 396, 397, 410, 396, 410, 411, 395, 396, 411, 412, 395, 411, 411, 413, 412, 409, 414, 410, 415, 416, 417, 417, 394, 415, 418, 415, 394, 394, 395, 418] + rel material:binding = + normal3f[] normals = [(0.157242, -0.7792969, 0.6066065), (-0.07824846, 0.5335353, -0.8421504), (0.066799894, 0.07442972, -0.9949864), (0.066816166, 0.074540876, -0.994977), (-0.08424765, -0.043077126, 0.9955133), (0.15725875, -0.77922523, 0.60669416), (-0.47785795, -0.85916454, -0.1829974), (0.15725875, -0.77922523, 0.60669416), (-0.08424765, -0.043077126, 0.9955133), (-0.08424765, -0.043077126, 0.9955133), (0.11861768, -0.20106822, 0.97236896), (-0.47785795, -0.85916454, -0.1829974), (-0.47783226, -0.8591819, -0.18298277), (-0.47785795, -0.85916454, -0.1829974), (0.11861768, -0.20106822, 0.97236896), (0.11861768, -0.20106822, 0.97236896), (0.075378835, 0.06720851, -0.9948874), (-0.47783226, -0.8591819, -0.18298277), (-0.5036119, -0.85586137, 0.11779801), (0.075378835, 0.06720851, -0.9948874), (0.11861768, -0.20106822, 0.97236896), (0.11861768, -0.20106822, 0.97236896), (-0.50364184, -0.8558456, 0.11778585), (-0.5036119, -0.85586137, 0.11779801), (-0.50364184, -0.8558456, 0.11778585), (0.11861768, -0.20106822, 0.97236896), (-0.14082026, 0.25178352, 0.9574835), (-0.14082026, 0.25178352, 0.9574835), (-0.028590197, 0.44407013, 0.8955357), (-0.50364184, -0.8558456, 0.11778585), (-0.3778982, 0.8899293, 0.25537956), (-0.028590197, 0.44407013, 0.8955357), (-0.14082026, 0.25178352, 0.9574835), (0.11861768, -0.20106822, 0.97236896), (-0.08424765, -0.043077126, 0.9955133), (0.11860046, -0.20103574, 0.97237784), (0.11860046, -0.20103574, 0.97237784), (-0.14082026, 0.25178352, 0.9574835), (0.11861768, -0.20106822, 0.97236896), (-0.08424765, -0.043077126, 0.9955133), (0.06680271, 0.07450263, -0.9949808), (0.20921169, -0.9749693, 0.07526838), (0.20921169, -0.9749693, 0.07526838), (0.57128817, 0.0009213686, 0.8207489), (-0.08424765, -0.043077126, 0.9955133), (-0.08424765, -0.043077126, 0.9955133), (0.57128817, 0.0009213686, 0.8207489), (0.64004254, 0.024520874, -0.76794815), (0.64004254, 0.024520874, -0.76794815), (0.11860046, -0.20103574, 0.97237784), (-0.08424765, -0.043077126, 0.9955133), (0.2734034, 0.9290668, 0.24916928), (0.11860046, -0.20103574, 0.97237784), (0.64004254, 0.024520874, -0.76794815), (-0.14082026, 0.25178352, 0.9574835), (0.11860046, -0.20103574, 0.97237784), (0.2734034, 0.9290668, 0.24916928), (0.2734034, 0.9290668, 0.24916928), (-0.3640303, 0.8854531, 0.28888524), (-0.14082026, 0.25178352, 0.9574835), (-0.14082026, 0.25178352, 0.9574835), (-0.3640303, 0.8854531, 0.28888524), (-0.3778982, 0.8899293, 0.25537956), (-0.3778982, 0.8899293, 0.25537956), (-0.3640303, 0.8854531, 0.28888524), (-0.37795097, 0.88989055, 0.25543675), (-0.3779398, 0.88989115, 0.25545087), (-0.37793976, 0.88989115, 0.25545084), (-0.37793973, 0.88989115, 0.25545081), (-0.37792274, 0.8898994, 0.25544775), (-0.37795097, 0.88989055, 0.25543675), (0.0018957183, -0.06406659, -0.99794376), (0.0018957183, -0.06406659, -0.99794376), (-0.28273812, 0.7061593, -0.64915174), (-0.37792274, 0.8898994, 0.25544775), (-0.28273812, 0.7061593, -0.64915174), (0.0018957183, -0.06406659, -0.99794376), (0.0018929765, -0.06401375, -0.99794716), (0.0018929765, -0.06401375, -0.99794716), (0.24783258, 0.0453873, -0.9677391), (-0.28273812, 0.7061593, -0.64915174), (-0.50364363, -0.8558369, 0.11784009), (0.24783258, 0.0453873, -0.9677391), (0.0018929765, -0.06401375, -0.99794716), (0.0018929765, -0.06401375, -0.99794716), (0.07536206, 0.0673563, -0.99487877), (-0.50364363, -0.8558369, 0.11784009), (0.07536206, 0.0673563, -0.99487877), (0.0018929765, -0.06401375, -0.99794716), (0.1089189, -0.12906995, -0.9856356), (0.1089189, -0.12906995, -0.9856356), (0.06670452, 0.07454735, -0.994984), (0.07536206, 0.0673563, -0.99487877), (0.06670452, 0.07454735, -0.994984), (0.1089189, -0.12906995, -0.9856356), (0.6364041, 0.023999795, -0.7709824), (0.6364041, 0.023999795, -0.7709824), (0.20916046, -0.97497916, 0.07528261), (0.06670452, 0.07454735, -0.994984), (0.32762647, 0.8774109, 0.35044402), (0.6364191, 0.02396974, -0.77097094), (0.001933597, -0.06406513, -0.99794376), (0.001933597, -0.06406513, -0.99794376), (0.6364191, 0.02396974, -0.77097094), (0.6364041, 0.023999795, -0.7709824), (0.6364041, 0.023999795, -0.7709824), (0.1089189, -0.12906995, -0.9856356), (0.001933597, -0.06406513, -0.99794376), (0.001933597, -0.06406513, -0.99794376), (0.1089189, -0.12906995, -0.9856356), (0.0018929765, -0.06401375, -0.99794716), (0.0020227311, -0.064064115, -0.99794364), (0.0020227437, -0.06406406, -0.9979437), (0.0020227372, -0.06406409, -0.99794364), (0.001933597, -0.06406513, -0.99794376), (0.0018957183, -0.06406659, -0.99794376), (-0.37795097, 0.88989055, 0.25543675), (-0.37795097, 0.88989055, 0.25543675), (0.3275993, 0.877427, 0.35042915), (0.001933597, -0.06406513, -0.99794376), (0.3275993, 0.877427, 0.35042915), (-0.37795097, 0.88989055, 0.25543675), (-0.3640303, 0.8854531, 0.28888524), (-0.3640303, 0.8854531, 0.28888524), (0.2734034, 0.9290668, 0.24916928), (0.3275993, 0.877427, 0.35042915), (0.3275993, 0.877427, 0.35042915), (0.2734034, 0.9290668, 0.24916928), (0.636406, 0.024075836, -0.77097857), (0.64004254, 0.024520874, -0.76794815), (0.636406, 0.024075836, -0.77097857), (0.2734034, 0.9290668, 0.24916928), (0.636406, 0.024075836, -0.77097857), (0.64004254, 0.024520874, -0.76794815), (0.6364207, 0.023995288, -0.7709688), (0.6364207, 0.023995288, -0.7709688), (0.64004254, 0.024520874, -0.76794815), (0.57128817, 0.0009213686, 0.8207489), (0.57128817, 0.0009213686, 0.8207489), (0.20919383, -0.97497016, 0.07530688), (0.6364207, 0.023995288, -0.7709688), (0.001933597, -0.06406513, -0.99794376), (0.3275993, 0.877427, 0.35042915), (0.32762647, 0.8774109, 0.35044402), (0.636406, 0.024075836, -0.77097857), (0.32762647, 0.8774109, 0.35044402), (0.3275993, 0.877427, 0.35042915), (0.32762647, 0.8774109, 0.35044402), (0.636406, 0.024075836, -0.77097857), (0.6364191, 0.02396974, -0.77097094), (0.6364636, 0.023991896, -0.7709335), (0.6364636, 0.02399184, -0.7709335), (0.6364636, 0.023991847, -0.7709335), (-0.0782249, 0.5334784, -0.84218866), (0.15726951, -0.77924746, 0.6066627), (-0.4778665, -0.8591593, -0.18299988), (-0.4778665, -0.8591593, -0.18299988), (-0.4778597, -0.8591788, -0.18292597), (-0.0782249, 0.5334784, -0.84218866), (-0.0782249, 0.5334784, -0.84218866), (-0.4778597, -0.8591788, -0.18292597), (0.07536206, 0.0673563, -0.99487877), (0.07536206, 0.0673563, -0.99487877), (0.06670452, 0.07454735, -0.994984), (-0.0782249, 0.5334784, -0.84218866), (-0.69415474, -0.006649509, -0.7197951), (-0.69437766, -0.00856078, -0.7195598), (-0.9586047, -0.11083432, -0.26228386), (-0.69414693, -0.006745085, -0.7198016), (-0.7049082, -0.22542153, -0.6725247), (-0.69437766, -0.00856078, -0.7195598), (-0.69437766, -0.00856078, -0.7195598), (-0.7049082, -0.22542153, -0.6725247), (-0.5338426, -0.8103731, -0.24146953), (-0.5338426, -0.8103731, -0.24146953), (-0.9586047, -0.11083432, -0.26228386), (-0.69437766, -0.00856078, -0.7195598), (-0.5338426, -0.8103731, -0.24146953), (-0.53390944, -0.81033707, -0.24144284), (-0.9586047, -0.11083432, -0.26228386), (-0.5339094, -0.81034815, -0.24140549), (-0.5339094, -0.81034815, -0.24140549), (-0.5339094, -0.81034815, -0.24140549), (0.768598, 0.6160157, -0.17257385), (0.76854223, 0.6160832, -0.17258093), (0.80600977, 0.5918546, 0.0075004697), (0.8059582, 0.59192437, 0.0075411275), (0.80600977, 0.5918546, 0.0075004697), (0.76854223, 0.6160832, -0.17258093), (0.80600977, 0.5918546, 0.0075004697), (0.8059582, 0.59192437, 0.0075411275), (0.8609263, 0.46489862, 0.20657972), (0.8609263, 0.46489862, 0.20657972), (0.8059582, 0.59192437, 0.0075411275), (0.41155142, 0.7206405, 0.5579451), (0.41155142, 0.7206405, 0.5579451), (0.41159004, 0.7206366, 0.55792165), (0.8609263, 0.46489862, 0.20657972), (0.41159004, 0.7206366, 0.55792165), (0.41155142, 0.7206405, 0.5579451), (-0.55611825, 0.83021533, -0.038406864), (-0.55611825, 0.83021533, -0.038406864), (-0.98404616, 0.10822181, 0.14121312), (0.41159004, 0.7206366, 0.55792165), (-0.98404616, 0.10822181, 0.14121312), (-0.55611825, 0.83021533, -0.038406864), (-0.8059955, 0.5918734, 0.0075543635), (-0.8059955, 0.5918734, 0.0075543635), (-0.9840551, 0.10818402, 0.14117974), (-0.98404616, 0.10822181, 0.14121312), (-0.9840551, 0.10818402, 0.14117974), (-0.8059955, 0.5918734, 0.0075543635), (-0.6641832, 0.21807992, 0.7150537), (-0.6641832, 0.21807992, 0.7150537), (-0.5338679, -0.8103624, -0.2414493), (-0.9840551, 0.10818402, 0.14117974), (-0.5338679, -0.8103624, -0.2414493), (-0.6641832, 0.21807992, 0.7150537), (-0.7144404, -0.67432225, -0.18672007), (-0.5338679, -0.8103624, -0.2414493), (-0.7144404, -0.67432225, -0.18672007), (-0.53390944, -0.81033707, -0.24144284), (-0.53390944, -0.81033707, -0.24144284), (-0.7144404, -0.67432225, -0.18672007), (-0.6641832, 0.21807992, 0.7150537), (-0.6641832, 0.21807992, 0.7150537), (-0.9586047, -0.11083432, -0.26228386), (-0.53390944, -0.81033707, -0.24144284), (-0.64256865, 0.7614622, 0.08532807), (-0.9586047, -0.11083432, -0.26228386), (-0.6641832, 0.21807992, 0.7150537), (-0.9586047, -0.11083432, -0.26228386), (-0.64256865, 0.7614622, 0.08532807), (-0.69415474, -0.006649509, -0.7197951), (-0.69415474, -0.006649509, -0.7197951), (-0.64256865, 0.7614622, 0.08532807), (-0.6426084, 0.76143265, 0.08529284), (-0.6428431, 0.7612382, 0.08525893), (-0.6428431, 0.7612382, 0.08525893), (-0.6428431, 0.7612382, 0.08525893), (0.8609263, 0.46489862, 0.20657972), (0.9840491, 0.10819808, 0.1412114), (0.80600977, 0.5918546, 0.0075004697), (0.6641785, 0.21809483, 0.7150536), (0.80600977, 0.5918546, 0.0075004697), (0.9840491, 0.10819808, 0.1412114), (0.80600977, 0.5918546, 0.0075004697), (0.6641785, 0.21809483, 0.7150536), (0.768598, 0.6160157, -0.17257385), (0.9840491, 0.10819808, 0.1412114), (0.7144342, -0.67434525, -0.18666083), (0.6641785, 0.21809483, 0.7150536), (-0.8059955, 0.5918734, 0.0075543635), (-0.5561636, 0.8301842, -0.038420983), (-0.9716005, 0.23560475, -0.021975815), (-0.5561636, 0.8301842, -0.038420983), (-0.8059955, 0.5918734, 0.0075543635), (-0.55611825, 0.83021533, -0.038406864), (-0.9716005, 0.23560475, -0.021975815), (-0.6641832, 0.21807992, 0.7150537), (-0.8059955, 0.5918734, 0.0075543635), (-0.6641832, 0.21807992, 0.7150537), (-0.9716005, 0.23560475, -0.021975815), (-0.64256865, 0.7614622, 0.08532807), (-0.9716005, 0.23560475, -0.021975815), (-0.642597, 0.7614399, 0.08531302), (-0.64256865, 0.7614622, 0.08532807), (-0.642597, 0.7614399, 0.08531302), (-0.9716005, 0.23560475, -0.021975815), (-0.5561636, 0.8301842, -0.038420983), (-0.642597, 0.7614399, 0.08531302), (-0.5561636, 0.8301842, -0.038420983), (-0.5560609, 0.83025295, -0.038422935), (-0.5559622, 0.8303265, -0.038262285), (-0.5559622, 0.8303265, -0.038262285), (-0.5559622, 0.8303265, -0.038262285), (0.3633271, 0.91756195, 0.16147278), (-0.5560609, 0.83025295, -0.038422935), (-0.55611825, 0.83021533, -0.038406864), (-0.55611825, 0.83021533, -0.038406864), (0.41155142, 0.7206405, 0.5579451), (0.3633271, 0.91756195, 0.16147278), (0.5560866, 0.83023554, -0.038428828), (0.3633271, 0.91756195, 0.16147278), (0.41155142, 0.7206405, 0.5579451), (0.41155142, 0.7206405, 0.5579451), (0.8059582, 0.59192437, 0.0075411275), (0.5560866, 0.83023554, -0.038428828), (0.76854223, 0.6160832, -0.17258093), (0.5560866, 0.83023554, -0.038428828), (0.8059582, 0.59192437, 0.0075411275), (0.5560866, 0.83023554, -0.038428828), (0.76854223, 0.6160832, -0.17258093), (0.7685537, 0.61607176, -0.17257087), (0.7685693, 0.61603826, -0.1726211), (0.7685693, 0.61603826, -0.1726211), (0.76856923, 0.61603826, -0.17262112), (0.768598, 0.6160157, -0.17257385), (0.7271925, 0.5345071, -0.43068928), (0.7685537, 0.61607176, -0.17257087), (0.7271925, 0.5345071, -0.43068928), (0.768598, 0.6160157, -0.17257385), (0.6641785, 0.21809483, 0.7150536), (0.6641785, 0.21809483, 0.7150536), (0.9586103, -0.11083169, -0.26226455), (0.7271925, 0.5345071, -0.43068928), (0.71443135, -0.6743395, -0.18669297), (0.9586103, -0.11083169, -0.26226455), (0.6641785, 0.21809483, 0.7150536), (0.5338493, -0.81037354, -0.24145335), (0.9586103, -0.11083169, -0.26226455), (0.71443135, -0.6743395, -0.18669297), (0.71442413, -0.6743503, -0.18668106), (0.5338493, -0.81037354, -0.24145335), (0.71443135, -0.6743395, -0.18669297), (0.71443367, -0.674333, -0.18670744), (0.71443367, -0.674333, -0.18670738), (0.71443367, -0.67433304, -0.18670739), (0.7144342, -0.67434525, -0.18666083), (0.714474, -0.67429066, -0.18670604), (0.6641785, 0.21809483, 0.7150536), (0.6641785, 0.21809483, 0.7150536), (0.714474, -0.67429066, -0.18670604), (0.71443135, -0.6743395, -0.18669297), (0.5403399, 0.832863, 0.11988364), (0.7685537, 0.61607176, -0.17257087), (0.7271925, 0.5345071, -0.43068928), (0.5403399, 0.832863, 0.11988364), (0.7271925, 0.5345071, -0.43068928), (0.6942019, -0.0069688037, -0.71974665), (0.6942019, -0.0069688037, -0.71974665), (0.7271925, 0.5345071, -0.43068928), (0.9586103, -0.11083169, -0.26226455), (0.9586103, -0.11083169, -0.26226455), (0.7049351, -0.22541295, -0.67249936), (0.6942019, -0.0069688037, -0.71974665), (0.7049351, -0.22541295, -0.67249936), (0.9586103, -0.11083169, -0.26226455), (0.5338493, -0.81037354, -0.24145335), (0.5338493, -0.81037354, -0.24145335), (0.7049356, -0.22544765, -0.6724873), (0.7049351, -0.22541295, -0.67249936), (0.7050025, -0.22525027, -0.67248327), (0.7050025, -0.22525027, -0.67248327), (0.7050025, -0.22525027, -0.67248327), (0.70489246, -0.22537096, -0.6725581), (0.6942019, -0.0069688037, -0.71974665), (0.7049351, -0.22541295, -0.67249936), (0.110649236, 0.24677202, -0.9627359), (0.6942019, -0.0069688037, -0.71974665), (0.70489246, -0.22537096, -0.6725581), (0.7651011, 0.11383755, -0.6337677), (0.6942019, -0.0069688037, -0.71974665), (0.110649236, 0.24677202, -0.9627359), (0.110649236, 0.24677202, -0.9627359), (0.11626847, 0.18134674, -0.9765219), (0.7651011, 0.11383755, -0.6337677), (0.11626847, 0.18134674, -0.9765219), (0.110649236, 0.24677202, -0.9627359), (0.11056765, 0.2467939, -0.9627396), (0.11078751, 0.24678044, -0.96271783), (0.11078752, 0.24678048, -0.96271783), (0.11078751, 0.24678048, -0.96271783), (0.11064513, 0.24682562, -0.9627227), (-0.11066179, 0.2467564, -0.96273845), (0.11056765, 0.2467939, -0.9627396), (0.6942019, -0.0069688037, -0.71974665), (0.7651011, 0.11383755, -0.6337677), (0.5403399, 0.832863, 0.11988364), (0.70489246, -0.22537096, -0.6725581), (0.11063683, 0.24677359, -0.96273685), (0.110649236, 0.24677202, -0.9627359), (0.42566916, 0.839666, 0.33729315), (0.5560866, 0.83023554, -0.038428828), (0.312179, 0.9241119, -0.22036695), (0.7685537, 0.61607176, -0.17257087), (0.312179, 0.9241119, -0.22036695), (0.5560866, 0.83023554, -0.038428828), (0.7685537, 0.61607176, -0.17257087), (0.5403089, 0.8328718, 0.119961575), (0.312179, 0.9241119, -0.22036695), (0.5403399, 0.832863, 0.11988364), (0.5403089, 0.8328718, 0.119961575), (0.7685537, 0.61607176, -0.17257087), (0.4669993, 0.8632243, -0.19171676), (0.5403089, 0.8328718, 0.119961575), (0.5403399, 0.832863, 0.11988364), (0.5403399, 0.832863, 0.11988364), (0.4669533, 0.86324215, -0.19174862), (0.4669993, 0.8632243, -0.19171676), (0.4669533, 0.86324215, -0.19174862), (0.5403399, 0.832863, 0.11988364), (0.7651011, 0.11383755, -0.6337677), (0.48595667, 0.8717658, -0.062214904), (0.4669533, 0.86324215, -0.19174862), (0.7651011, 0.11383755, -0.6337677), (0.7651011, 0.11383755, -0.6337677), (0.11626847, 0.18134674, -0.9765219), (0.48595667, 0.8717658, -0.062214904), (0.11626847, 0.18134674, -0.9765219), (0.11625941, 0.1813635, -0.9765199), (0.48595667, 0.8717658, -0.062214904), (0.11056765, 0.2467939, -0.9627396), (0.11625941, 0.1813635, -0.9765199), (0.11626847, 0.18134674, -0.9765219), (-0.11160407, 0.18433559, -0.9765065), (0.11625941, 0.1813635, -0.9765199), (0.11056765, 0.2467939, -0.9627396), (0.11056765, 0.2467939, -0.9627396), (-0.11066179, 0.2467564, -0.96273845), (-0.11160407, 0.18433559, -0.9765065), (-0.61378354, 0.36791992, -0.69850177), (-0.11160407, 0.18433559, -0.9765065), (-0.11066179, 0.2467564, -0.96273845), (-0.11066179, 0.2467564, -0.96273845), (-0.69415474, -0.006649509, -0.7197951), (-0.61378354, 0.36791992, -0.69850177), (-0.69415474, -0.006649509, -0.7197951), (-0.11066179, 0.2467564, -0.96273845), (0.11064513, 0.24682562, -0.9627227), (0.11064513, 0.24682562, -0.9627227), (-0.6941027, -0.006645724, -0.7198453), (-0.69415474, -0.006649509, -0.7197951), (-0.69419783, -0.006618217, -0.7197538), (-0.69419783, -0.006618217, -0.7197538), (-0.69419783, -0.006618217, -0.7197538), (0.48595667, 0.8717658, -0.062214904), (0.11625941, 0.1813635, -0.9765199), (-0.11160407, 0.18433559, -0.9765065), (0.48595667, 0.8717658, -0.062214904), (-0.11160407, 0.18433559, -0.9765065), (-0.61378354, 0.36791992, -0.69850177), (0.42059723, 0.8423402, 0.33698815), (0.5439144, 0.83726376, -0.05609347), (0.42566916, 0.839666, 0.33729315), (0.5403089, 0.8328718, 0.119961575), (0.42059723, 0.8423402, 0.33698815), (0.42566916, 0.839666, 0.33729315), (0.42566916, 0.839666, 0.33729315), (0.312179, 0.9241119, -0.22036695), (0.5403089, 0.8328718, 0.119961575), (0.7167675, 0.6972564, 0.008829098), (0.42059723, 0.8423402, 0.33698815), (0.5403089, 0.8328718, 0.119961575), (0.7167675, 0.6972564, 0.008829098), (0.5403089, 0.8328718, 0.119961575), (0.4669993, 0.8632243, -0.19171676), (0.4669993, 0.8632243, -0.19171676), (0.2923779, 0.2348066, -0.92702806), (0.7167675, 0.6972564, 0.008829098), (-0.6426084, 0.76143265, 0.08529284), (-0.61378354, 0.36791992, -0.69850177), (-0.69415474, -0.006649509, -0.7197951), (-0.46699604, 0.86322635, -0.19171529), (-0.61378354, 0.36791992, -0.69850177), (-0.6426084, 0.76143265, 0.08529284), (-0.61378354, 0.36791992, -0.69850177), (-0.46699604, 0.86322635, -0.19171529), (0.48595667, 0.8717658, -0.062214904), (0.29377455, 0.7161683, -0.6330872), (0.48595667, 0.8717658, -0.062214904), (-0.46699604, 0.86322635, -0.19171529), (0.4669533, 0.86324215, -0.19174862), (0.48595667, 0.8717658, -0.062214904), (0.29377455, 0.7161683, -0.6330872), (0.4669533, 0.86324215, -0.19174862), (0.29377455, 0.7161683, -0.6330872), (-0.19367632, 0.96954256, 0.14992213), (0.4669533, 0.86324215, -0.19174862), (-0.19367632, 0.96954256, 0.14992213), (0.4669993, 0.8632243, -0.19171676), (-0.19367632, 0.96954256, 0.14992213), (-0.19367282, 0.9695508, 0.14987352), (0.4669993, 0.8632243, -0.19171676), (-0.19367282, 0.9695508, 0.14987352), (0.2923779, 0.2348066, -0.92702806), (0.4669993, 0.8632243, -0.19171676), (-0.19367282, 0.9695508, 0.14987352), (-0.19367883, 0.96954787, 0.149885), (0.2923779, 0.2348066, -0.92702806), (0.7167675, 0.6972564, 0.008829098), (0.2923779, 0.2348066, -0.92702806), (-0.19367883, 0.96954787, 0.149885), (-0.19367883, 0.96954787, 0.149885), (-0.36942077, 0.8570695, 0.3591103), (0.7167675, 0.6972564, 0.008829098), (-0.36942077, 0.8570695, 0.3591103), (0.42059723, 0.8423402, 0.33698815), (0.7167675, 0.6972564, 0.008829098), (-0.42567742, 0.83966225, 0.3372923), (0.42059723, 0.8423402, 0.33698815), (-0.36942077, 0.8570695, 0.3591103), (0.42059723, 0.8423402, 0.33698815), (-0.42567742, 0.83966225, 0.3372923), (0.5439144, 0.83726376, -0.05609347), (0.3633271, 0.91756195, 0.16147278), (0.5439144, 0.83726376, -0.05609347), (-0.42567742, 0.83966225, 0.3372923), (0.42566916, 0.839666, 0.33729315), (0.5439144, 0.83726376, -0.05609347), (0.3633271, 0.91756195, 0.16147278), (0.3633271, 0.91756195, 0.16147278), (0.5560866, 0.83023554, -0.038428828), (0.42566916, 0.839666, 0.33729315), (-0.46699604, 0.86322635, -0.19171529), (-0.19367632, 0.96954256, 0.14992213), (0.29377455, 0.7161683, -0.6330872), (-0.19382036, 0.96954685, 0.14970832), (-0.19382037, 0.96954685, 0.14970835), (-0.19382037, 0.96954685, 0.14970833), (-0.19367883, 0.96954787, 0.149885), (-0.19367632, 0.96954256, 0.14992213), (-0.46699604, 0.86322635, -0.19171529), (-0.19367883, 0.96954787, 0.149885), (-0.46699604, 0.86322635, -0.19171529), (-0.6426084, 0.76143265, 0.08529284), (-0.6426084, 0.76143265, 0.08529284), (-0.36942077, 0.8570695, 0.3591103), (-0.19367883, 0.96954787, 0.149885), (-0.6426084, 0.76143265, 0.08529284), (-0.642597, 0.7614399, 0.08531302), (-0.36942077, 0.8570695, 0.3591103), (-0.36942077, 0.8570695, 0.3591103), (-0.642597, 0.7614399, 0.08531302), (-0.31220415, 0.9241057, -0.2203566), (-0.5560609, 0.83025295, -0.038422935), (-0.31220415, 0.9241057, -0.2203566), (-0.642597, 0.7614399, 0.08531302), (-0.31220415, 0.9241057, -0.2203566), (-0.5560609, 0.83025295, -0.038422935), (-0.42567742, 0.83966225, 0.3372923), (-0.42567742, 0.83966225, 0.3372923), (-0.5560609, 0.83025295, -0.038422935), (0.3633271, 0.91756195, 0.16147278), (-0.36942077, 0.8570695, 0.3591103), (-0.31220415, 0.9241057, -0.2203566), (-0.42567742, 0.83966225, 0.3372923), (0.61973417, 0.34446985, -0.7051738), (0.6197666, 0.34448764, -0.70513666), (-0.3674645, -0.9262328, 0.08403818), (0.6194834, 0.34447485, -0.70539165), (0.6194834, 0.34447485, -0.70539165), (0.6194834, 0.34447485, -0.70539165), (0.6197359, 0.3444615, -0.70517635), (-0.6334046, 0.74508166, 0.2089302), (0.6198144, 0.34446687, -0.70510477), (-0.6334046, 0.74508166, 0.2089302), (0.6197359, 0.3444615, -0.70517635), (-0.082387045, 0.97898674, 0.18654087), (-0.082387045, 0.97898674, 0.18654087), (-0.6334475, 0.74505097, 0.20890959), (-0.6334046, 0.74508166, 0.2089302), (-0.6334475, 0.74505097, 0.20890959), (-0.082387045, 0.97898674, 0.18654087), (-0.49859303, 0.55374545, 0.66691136), (-0.49859303, 0.55374545, 0.66691136), (-0.47293413, 0.5290102, 0.70461434), (-0.6334475, 0.74505097, 0.20890959), (-0.47293413, 0.5290102, 0.70461434), (-0.49859303, 0.55374545, 0.66691136), (-0.7521359, -0.1126757, 0.6493041), (-0.7521359, -0.1126757, 0.6493041), (-0.5634085, -0.4118312, 0.71621627), (-0.47293413, 0.5290102, 0.70461434), (-0.5634085, -0.4118312, 0.71621627), (-0.7521359, -0.1126757, 0.6493041), (-0.44609088, -0.8845432, 0.1363309), (-0.44609088, -0.8845432, 0.1363309), (-0.3674645, -0.9262328, 0.08403818), (-0.5634085, -0.4118312, 0.71621627), (-0.3674645, -0.9262328, 0.08403818), (-0.44609088, -0.8845432, 0.1363309), (0.61973417, 0.34446985, -0.7051738), (0.61973417, 0.34446985, -0.7051738), (-0.44609088, -0.8845432, 0.1363309), (-0.69982135, -0.26319766, -0.664061), (0.61973417, 0.34446985, -0.7051738), (-0.69982135, -0.26319766, -0.664061), (0.05832257, -0.60009074, -0.797803), (0.41316542, -0.9106556, 0.00075416826), (0.05832257, -0.60009074, -0.797803), (-0.69982135, -0.26319766, -0.664061), (-0.69982135, -0.26319766, -0.664061), (-0.6085454, -0.44357532, -0.65796155), (0.41316542, -0.9106556, 0.00075416826), (-0.6085454, -0.44357532, -0.65796155), (-0.69982135, -0.26319766, -0.664061), (-0.6085215, -0.44356483, -0.65799063), (-0.60839564, -0.4437699, -0.6579688), (-0.60839564, -0.4437699, -0.6579688), (-0.60839564, -0.4437699, -0.6579688), (-0.60856193, -0.4435518, -0.65796214), (-0.6085215, -0.44356483, -0.65799063), (-0.9611042, 0.19567043, 0.19491407), (0.61978304, 0.3444804, -0.7051257), (0.05833882, -0.6000179, -0.79785657), (0.8425878, 0.5174817, 0.14919297), (0.8425878, 0.5174817, 0.14919297), (0.6197359, 0.3444615, -0.70517635), (0.61978304, 0.3444804, -0.7051257), (-0.082387045, 0.97898674, 0.18654087), (0.6197359, 0.3444615, -0.70517635), (0.8425878, 0.5174817, 0.14919297), (0.8425878, 0.5174817, 0.14919297), (0.8802011, 0.2490178, 0.40402502), (-0.082387045, 0.97898674, 0.18654087), (-0.49859303, 0.55374545, 0.66691136), (-0.082387045, 0.97898674, 0.18654087), (0.8802011, 0.2490178, 0.40402502), (0.8802011, 0.2490178, 0.40402502), (0.4834528, 0.45173544, 0.74980575), (-0.49859303, 0.55374545, 0.66691136), (-0.49859303, 0.55374545, 0.66691136), (0.4834528, 0.45173544, 0.74980575), (-0.9611042, 0.19567043, 0.19491407), (-0.9611042, 0.19567043, 0.19491407), (-0.7521359, -0.1126757, 0.6493041), (-0.49859303, 0.55374545, 0.66691136), (-0.44609088, -0.8845432, 0.1363309), (-0.7521359, -0.1126757, 0.6493041), (-0.9611042, 0.19567043, 0.19491407), (-0.9611042, 0.19567043, 0.19491407), (-0.6085215, -0.44356483, -0.65799063), (-0.44609088, -0.8845432, 0.1363309), (-0.44609088, -0.8845432, 0.1363309), (-0.6085215, -0.44356483, -0.65799063), (-0.69982135, -0.26319766, -0.664061), (-0.9611042, 0.19567043, 0.19491407), (-0.33934775, 0.66564155, 0.66465366), (-0.60856193, -0.4435518, -0.65796214), (-0.33934775, 0.66564155, 0.66465366), (-0.9611042, 0.19567043, 0.19491407), (0.4834528, 0.45173544, 0.74980575), (0.4834528, 0.45173544, 0.74980575), (0.51062346, 0.5012442, 0.6985828), (-0.33934775, 0.66564155, 0.66465366), (0.51062346, 0.5012442, 0.6985828), (0.4834528, 0.45173544, 0.74980575), (0.8802011, 0.2490178, 0.40402502), (0.8802011, 0.2490178, 0.40402502), (0.8425495, 0.5175488, 0.14917631), (0.51062346, 0.5012442, 0.6985828), (0.8425495, 0.5175488, 0.14917631), (0.8802011, 0.2490178, 0.40402502), (0.8425878, 0.5174817, 0.14919297), (0.8426023, 0.51744294, 0.14924572), (0.8426023, 0.517443, 0.14924581), (0.8426023, 0.5174429, 0.14924568), (0.84260523, 0.51746505, 0.14915237), (0.8425878, 0.5174817, 0.14919297), (0.05833882, -0.6000179, -0.79785657), (0.05833882, -0.6000179, -0.79785657), (0.41315767, -0.91065925, 0.0008060988), (0.84260523, 0.51746505, 0.14915237), (0.41315505, -0.9106603, 0.0008198954), (0.84260523, 0.51746505, 0.14915237), (0.41315767, -0.91065925, 0.0008060988), (0.41315767, -0.91065925, 0.0008060988), (0.60691774, -0.76592153, -0.21216781), (0.41315505, -0.9106603, 0.0008198954), (-0.5036361, -0.8558487, 0.1177862), (0.41315505, -0.9106603, 0.0008198954), (0.60691774, -0.76592153, -0.21216781), (0.60691774, -0.76592153, -0.21216781), (0.24781407, 0.04544411, -0.96774125), (-0.5036361, -0.8558487, 0.1177862), (0.8425495, 0.5175488, 0.14917631), (0.84260523, 0.51746505, 0.14915237), (0.41315505, -0.9106603, 0.0008198954), (0.41315505, -0.9106603, 0.0008198954), (0.13934837, -0.93447584, 0.32762313), (0.8425495, 0.5175488, 0.14917631), (0.51062346, 0.5012442, 0.6985828), (0.8425495, 0.5175488, 0.14917631), (0.13934837, -0.93447584, 0.32762313), (0.41315505, -0.9106603, 0.0008198954), (-0.5036361, -0.8558487, 0.1177862), (-0.5036613, -0.85583484, 0.11777988), (-0.5036613, -0.85583484, 0.11777988), (0.13934837, -0.93447584, 0.32762313), (0.41315505, -0.9106603, 0.0008198954), (-0.044968337, 0.68049896, 0.7313678), (0.13934837, -0.93447584, 0.32762313), (-0.5036613, -0.85583484, 0.11777988), (0.13934837, -0.93447584, 0.32762313), (-0.044968337, 0.68049896, 0.7313678), (0.51062346, 0.5012442, 0.6985828), (0.51062346, 0.5012442, 0.6985828), (-0.044968337, 0.68049896, 0.7313678), (-0.339352, 0.6656672, 0.66462594), (-0.339352, 0.6656672, 0.66462594), (-0.33934775, 0.66564155, 0.66465366), (0.51062346, 0.5012442, 0.6985828), (-0.60856193, -0.4435518, -0.65796214), (-0.33934775, 0.66564155, 0.66465366), (-0.339352, 0.6656672, 0.66462594), (-0.339352, 0.6656672, 0.66462594), (0.018353254, 0.60013396, -0.7996889), (-0.60856193, -0.4435518, -0.65796214), (-0.6085454, -0.44357532, -0.65796155), (-0.60856193, -0.4435518, -0.65796214), (0.018353254, 0.60013396, -0.7996889), (0.018353254, 0.60013396, -0.7996889), (0.018424481, 0.6001916, -0.799644), (-0.6085454, -0.44357532, -0.65796155), (0.41316542, -0.9106556, 0.00075416826), (-0.6085454, -0.44357532, -0.65796155), (0.018424481, 0.6001916, -0.799644), (0.018424481, 0.6001916, -0.799644), (0.60696554, -0.76587844, -0.21218625), (0.41316542, -0.9106556, 0.00075416826), (0.60696554, -0.76587844, -0.21218625), (0.018424481, 0.6001916, -0.799644), (-0.28273648, 0.7061675, -0.6491437), (-0.28273648, 0.7061675, -0.6491437), (0.24777283, 0.045392077, -0.9677542), (0.60696554, -0.76587844, -0.21218625), (-0.3779652, 0.8898799, 0.25545263), (-0.28273648, 0.7061675, -0.6491437), (0.018424481, 0.6001916, -0.799644), (0.018424481, 0.6001916, -0.799644), (0.018353254, 0.60013396, -0.7996889), (-0.3779652, 0.8898799, 0.25545263), (-0.37795776, 0.8898746, 0.25548247), (-0.3779652, 0.8898799, 0.25545263), (0.018353254, 0.60013396, -0.7996889), (0.018353254, 0.60013396, -0.7996889), (-0.339352, 0.6656672, 0.66462594), (-0.37795776, 0.8898746, 0.25548247), (-0.37795776, 0.8898746, 0.25548247), (-0.339352, 0.6656672, 0.66462594), (-0.044968337, 0.68049896, 0.7313678), (-0.044968337, 0.68049896, 0.7313678), (-0.028566748, 0.44407523, 0.89553404), (-0.37795776, 0.8898746, 0.25548247), (-0.5036613, -0.85583484, 0.11777988), (-0.028566748, 0.44407523, 0.89553404), (-0.044968337, 0.68049896, 0.7313678), (0.6946393, 0.69399416, -0.18933627), (-0.1422621, 0.9881628, 0.057408255), (0.5143163, 0.44744173, 0.7316247), (-0.028000206, 0.18646924, 0.9820616), (0.5143163, 0.44744173, 0.7316247), (-0.1422621, 0.9881628, 0.057408255), (-0.1422621, 0.9881628, 0.057408255), (-0.6848715, 0.5497341, 0.47827134), (-0.028000206, 0.18646924, 0.9820616), (-0.6848715, 0.5497341, 0.47827134), (-0.027967036, 0.18650429, 0.9820561), (-0.028000206, 0.18646924, 0.9820616), (-0.027653903, 0.18650436, 0.98206484), (-0.027653903, 0.18650436, 0.98206484), (-0.027653903, 0.18650436, 0.98206484), (0.5143163, 0.44744173, 0.7316247), (-0.028000206, 0.18646924, 0.9820616), (-0.028040823, 0.18653442, 0.98204815), (-0.028040823, 0.18653442, 0.98204815), (0.51433235, 0.4474904, 0.73158365), (0.5143163, 0.44744173, 0.7316247), (0.7436924, 0.6251131, 0.23697081), (0.5143163, 0.44744173, 0.7316247), (0.51433235, 0.4474904, 0.73158365), (0.51433235, 0.4474904, 0.73158365), (0.7437326, 0.6250412, 0.23703419), (0.7436924, 0.6251131, 0.23697081), (0.5143163, 0.44744173, 0.7316247), (0.7436924, 0.6251131, 0.23697081), (0.6946393, 0.69399416, -0.18933627), (0.20847389, -0.81146806, 0.5459472), (0.3814434, -0.91367227, 0.14037037), (0.8834162, -0.4537797, 0.116874605), (0.8834162, -0.4537797, 0.116874605), (0.8271425, -0.38295388, 0.41131705), (0.20847389, -0.81146806, 0.5459472), (0.8271425, -0.38295388, 0.41131705), (0.8834162, -0.4537797, 0.116874605), (0.90665096, 0.39534387, 0.14726603), (0.90665096, 0.39534387, 0.14726603), (0.8242951, 0.36171523, 0.43554538), (0.8271425, -0.38295388, 0.41131705), (0.8242951, 0.36171523, 0.43554538), (0.90665096, 0.39534387, 0.14726603), (0.65330017, 0.74186087, 0.15113315), (0.65330017, 0.74186087, 0.15113315), (0.2705118, 0.9008539, 0.3395375), (0.8242951, 0.36171523, 0.43554538), (0.2705118, 0.9008539, 0.3395375), (0.65330017, 0.74186087, 0.15113315), (0.24670777, 0.9577197, 0.14801429), (0.24670777, 0.9577197, 0.14801429), (0.2965589, 0.7365758, 0.6078724), (0.2705118, 0.9008539, 0.3395375), (0.2965589, 0.7365758, 0.6078724), (0.24670777, 0.9577197, 0.14801429), (0.2526781, 0.95638615, 0.14655867), (0.2526781, 0.95638615, 0.14655867), (0.21373242, 0.80908906, 0.54744256), (0.2965589, 0.7365758, 0.6078724), (-0.20308314, 0.7691847, 0.6058978), (0.21373242, 0.80908906, 0.54744256), (0.2526781, 0.95638615, 0.14655867), (0.2526781, 0.95638615, 0.14655867), (-0.24660149, 0.9577472, 0.14801309), (-0.20308314, 0.7691847, 0.6058978), (-0.60085964, 0.7030473, 0.3803842), (-0.20308314, 0.7691847, 0.6058978), (-0.24660149, 0.9577472, 0.14801309), (-0.24660149, 0.9577472, 0.14801309), (-0.65329534, 0.7418663, 0.1511285), (-0.60085964, 0.7030473, 0.3803842), (-0.81200945, 0.42747158, 0.39737713), (-0.60085964, 0.7030473, 0.3803842), (-0.65329534, 0.7418663, 0.1511285), (-0.65329534, 0.7418663, 0.1511285), (-0.9074054, 0.3952075, 0.14292155), (-0.81200945, 0.42747158, 0.39737713), (-0.77282476, -0.44515246, 0.45230633), (-0.81200945, 0.42747158, 0.39737713), (-0.9074054, 0.3952075, 0.14292155), (-0.9074054, 0.3952075, 0.14292155), (-0.8838715, -0.45404485, 0.11231343), (-0.77282476, -0.44515246, 0.45230633), (-0.8838715, -0.45404485, 0.11231343), (0.38147512, -0.91366863, 0.14030829), (0.20847473, -0.81142515, 0.54601055), (0.20847473, -0.81142515, 0.54601055), (-0.77282476, -0.44515246, 0.45230633), (-0.8838715, -0.45404485, 0.11231343), (-0.77282476, -0.44515246, 0.45230633), (0.20847473, -0.81142515, 0.54601055), (0.20844491, -0.8114526, 0.545981), (0.20844491, -0.8114526, 0.545981), (-0.7721504, -0.46016902, 0.43821034), (-0.77282476, -0.44515246, 0.45230633), (-0.81200945, 0.42747158, 0.39737713), (-0.77282476, -0.44515246, 0.45230633), (-0.7721504, -0.46016902, 0.43821034), (-0.7721504, -0.46016902, 0.43821034), (-0.84014225, 0.40294847, 0.36303398), (-0.81200945, 0.42747158, 0.39737713), (-0.60085964, 0.7030473, 0.3803842), (-0.81200945, 0.42747158, 0.39737713), (-0.84014225, 0.40294847, 0.36303398), (-0.84014225, 0.40294847, 0.36303398), (-0.23023985, 0.76052225, 0.6071209), (-0.60085964, 0.7030473, 0.3803842), (-0.20308314, 0.7691847, 0.6058978), (-0.60085964, 0.7030473, 0.3803842), (-0.23023985, 0.76052225, 0.6071209), (-0.23023985, 0.76052225, 0.6071209), (-0.31628945, 0.78570205, 0.5316325), (-0.20308314, 0.7691847, 0.6058978), (0.21373242, 0.80908906, 0.54744256), (-0.20308314, 0.7691847, 0.6058978), (-0.31628945, 0.78570205, 0.5316325), (-0.31628945, 0.78570205, 0.5316325), (0.29651254, 0.73658496, 0.607884), (0.21373242, 0.80908906, 0.54744256), (0.2965589, 0.7365758, 0.6078724), (0.21373242, 0.80908906, 0.54744256), (0.29651254, 0.73658496, 0.607884), (0.29653764, 0.736521, 0.6079492), (0.29653764, 0.736521, 0.6079492), (0.29653764, 0.736521, 0.6079492), (0.2705118, 0.9008539, 0.3395375), (0.2965589, 0.7365758, 0.6078724), (0.2965711, 0.7365912, 0.60784787), (0.2965711, 0.7365912, 0.60784787), (0.27057883, 0.90082264, 0.33956704), (0.2705118, 0.9008539, 0.3395375), (0.8242951, 0.36171523, 0.43554538), (0.2705118, 0.9008539, 0.3395375), (0.27057883, 0.90082264, 0.33956704), (0.27057883, 0.90082264, 0.33956704), (0.8243024, 0.36173987, 0.4355113), (0.8242951, 0.36171523, 0.43554538), (0.8271425, -0.38295388, 0.41131705), (0.8242951, 0.36171523, 0.43554538), (0.8243024, 0.36173987, 0.4355113), (0.8243024, 0.36173987, 0.4355113), (0.82714343, -0.3829637, 0.4113058), (0.8271425, -0.38295388, 0.41131705), (0.20847389, -0.81146806, 0.5459472), (0.8271425, -0.38295388, 0.41131705), (0.82714343, -0.3829637, 0.4113058), (0.82714343, -0.3829637, 0.4113058), (0.20844388, -0.8114561, 0.5459763), (0.20847389, -0.81146806, 0.5459472), (0.22677898, -0.89065003, 0.3940987), (0.061383836, -0.8521862, 0.5196253), (0.076510705, -0.8214875, 0.5650703), (0.076510705, -0.8214875, 0.5650703), (0.37567335, -0.33227548, 0.8651373), (0.22677898, -0.89065003, 0.3940987), (-0.18289302, 0.5349467, 0.8248529), (0.37567335, -0.33227548, 0.8651373), (0.076510705, -0.8214875, 0.5650703), (0.076510705, -0.8214875, 0.5650703), (0.08871531, -0.06950816, 0.9936288), (-0.18289302, 0.5349467, 0.8248529), (-0.18289302, 0.5349467, 0.8248529), (0.08871531, -0.06950816, 0.9936288), (-0.7965106, 0.6031311, 0.042470876), (-0.7965106, 0.6031311, 0.042470876), (-0.30518728, 0.8963103, 0.32169628), (-0.18289302, 0.5349467, 0.8248529), (-0.29036555, 0.89852816, 0.3291427), (-0.30518728, 0.8963103, 0.32169628), (-0.7965106, 0.6031311, 0.042470876), (-0.7965106, 0.6031311, 0.042470876), (-0.63904834, 0.70867145, -0.29900163), (-0.29036555, 0.89852816, 0.3291427), (-0.29036555, 0.89852816, 0.3291427), (-0.63904834, 0.70867145, -0.29900163), (0.19010122, -0.2943905, -0.9365873), (0.19010122, -0.2943905, -0.9365873), (0.039598376, -0.21694374, -0.97538066), (-0.29036555, 0.89852816, 0.3291427), (-0.29036555, 0.89852816, 0.3291427), (0.039598376, -0.21694374, -0.97538066), (-0.44212314, 0.58732146, -0.6779237), (0.123656295, -0.9455194, -0.30116832), (0.22677898, -0.89065003, 0.3940987), (0.37567335, -0.33227548, 0.8651373), (0.37567335, -0.33227548, 0.8651373), (0.10603699, -0.2767344, 0.9550781), (0.123656295, -0.9455194, -0.30116832), (0.10603699, -0.2767344, 0.9550781), (0.37567335, -0.33227548, 0.8651373), (-0.18289302, 0.5349467, 0.8248529), (-0.18289302, 0.5349467, 0.8248529), (-0.13691033, 0.5284271, 0.83786654), (0.10603699, -0.2767344, 0.9550781), (-0.13691033, 0.5284271, 0.83786654), (-0.18289302, 0.5349467, 0.8248529), (-0.30518728, 0.8963103, 0.32169628), (-0.30518728, 0.8963103, 0.32169628), (-0.29033685, 0.8985303, 0.32916242), (-0.13691033, 0.5284271, 0.83786654), (-0.29033685, 0.8985303, 0.32916242), (-0.30518728, 0.8963103, 0.32169628), (-0.29036555, 0.89852816, 0.3291427), (-0.29046446, 0.8985296, 0.32905158), (-0.29046443, 0.8985295, 0.32905155), (-0.29046443, 0.8985296, 0.32905155), (-0.44212314, 0.58732146, -0.6779237), (-0.290348, 0.89852357, 0.32917085), (-0.29036555, 0.89852816, 0.3291427), (-0.44210836, 0.5873271, -0.67792857), (-0.290348, 0.89852357, 0.32917085), (-0.44212314, 0.58732146, -0.6779237), (-0.44212314, 0.58732146, -0.6779237), (-0.41850454, 0.6044942, -0.6778205), (-0.44210836, 0.5873271, -0.67792857), (0.33226866, 0.9218161, 0.19963121), (-0.44210836, 0.5873271, -0.67792857), (-0.41850454, 0.6044942, -0.6778205), (-0.02087222, 0.9554028, 0.29456714), (-0.44210836, 0.5873271, -0.67792857), (0.33226866, 0.9218161, 0.19963121), (0.33226866, 0.9218161, 0.19963121), (0.3322605, 0.92182016, 0.19962616), (-0.02087222, 0.9554028, 0.29456714), (0.64483225, 0.60689175, 0.4646221), (-0.02087222, 0.9554028, 0.29456714), (0.3322605, 0.92182016, 0.19962616), (0.3322605, 0.92182016, 0.19962616), (0.58324, 0.43775025, 0.68425554), (0.64483225, 0.60689175, 0.4646221), (0.64483225, 0.60689175, 0.4646221), (0.58324, 0.43775025, 0.68425554), (0.64936507, -0.39853966, 0.6476813), (0.6493937, -0.3985724, 0.6476325), (0.64936507, -0.39853966, 0.6476813), (0.58324, 0.43775025, 0.68425554), (-0.41850454, 0.6044942, -0.6778205), (-0.29089987, -0.50821406, -0.81061447), (0.33226866, 0.9218161, 0.19963121), (0.08329323, -0.9685821, -0.23433119), (0.123656295, -0.9455194, -0.30116832), (0.10603699, -0.2767344, 0.9550781), (0.10603699, -0.2767344, 0.9550781), (0.7473663, -0.17918652, 0.6397935), (0.08329323, -0.9685821, -0.23433119), (0.7473663, -0.17918652, 0.6397935), (0.10603699, -0.2767344, 0.9550781), (-0.13691033, 0.5284271, 0.83786654), (-0.29033685, 0.8985303, 0.32916242), (-0.290348, 0.89852357, 0.32917085), (-0.44210836, 0.5873271, -0.67792857), (-0.44210836, 0.5873271, -0.67792857), (-0.02087222, 0.9554028, 0.29456714), (-0.29033685, 0.8985303, 0.32916242), (-0.13691033, 0.5284271, 0.83786654), (-0.29033685, 0.8985303, 0.32916242), (-0.02087222, 0.9554028, 0.29456714), (-0.02087222, 0.9554028, 0.29456714), (0.64483225, 0.60689175, 0.4646221), (-0.13691033, 0.5284271, 0.83786654), (-0.13691033, 0.5284271, 0.83786654), (0.64483225, 0.60689175, 0.4646221), (0.7473663, -0.17918652, 0.6397935), (0.64936507, -0.39853966, 0.6476813), (0.7473663, -0.17918652, 0.6397935), (0.64483225, 0.60689175, 0.4646221), (0.08329323, -0.9685821, -0.23433119), (0.7473663, -0.17918652, 0.6397935), (0.64936507, -0.39853966, 0.6476813), (0.64936507, -0.39853966, 0.6476813), (-0.29090402, -0.5081665, -0.81064284), (0.08329323, -0.9685821, -0.23433119), (-0.29090402, -0.5081665, -0.81064284), (0.12365647, -0.94552463, -0.30115142), (0.08329323, -0.9685821, -0.23433119), (0.123656295, -0.9455194, -0.30116832), (0.08329323, -0.9685821, -0.23433119), (0.12365647, -0.94552463, -0.30115142), (0.1236062, -0.9455415, -0.3011192), (0.12360625, -0.94554144, -0.30111918), (0.12360621, -0.94554144, -0.30111918), (0.22677898, -0.89065003, 0.3940987), (0.123656295, -0.9455194, -0.30116832), (0.123660624, -0.9455321, -0.3011264), (0.123660624, -0.9455321, -0.3011264), (0.024726212, -0.23111674, -0.9726118), (0.22677898, -0.89065003, 0.3940987), (0.061383836, -0.8521862, 0.5196253), (0.22677898, -0.89065003, 0.3940987), (0.024726212, -0.23111674, -0.9726118), (0.024726212, -0.23111674, -0.9726118), (0.1179426, -0.9666351, -0.22738968), (0.061383836, -0.8521862, 0.5196253), (0.58324, 0.43775025, 0.68425554), (0.5832208, 0.43775365, 0.6842699), (0.6493937, -0.3985724, 0.6476325), (0.5832208, 0.43775365, 0.6842699), (0.58324, 0.43775025, 0.68425554), (0.3322605, 0.92182016, 0.19962616), (0.3322605, 0.92182016, 0.19962616), (0.6419383, 0.7484561, 0.16651884), (0.5832208, 0.43775365, 0.6842699), (0.6419383, 0.7484561, 0.16651884), (0.3322605, 0.92182016, 0.19962616), (0.33226866, 0.9218161, 0.19963121), (0.33226866, 0.9218161, 0.19963121), (0.64191127, 0.748488, 0.1664806), (0.6419383, 0.7484561, 0.16651884), (0.64191127, 0.748488, 0.1664806), (0.33226866, 0.9218161, 0.19963121), (-0.29089987, -0.50821406, -0.81061447), (-0.29089987, -0.50821406, -0.81061447), (-0.29090706, -0.50823367, -0.8105996), (0.64191127, 0.748488, 0.1664806), (0.64936507, -0.39853966, 0.6476813), (0.6493937, -0.3985724, 0.6476325), (-0.14367713, -0.45303375, -0.8798392), (-0.14367713, -0.45303375, -0.8798392), (-0.29090402, -0.5081665, -0.81064284), (0.64936507, -0.39853966, 0.6476813), (-0.29090402, -0.5081665, -0.81064284), (-0.14367713, -0.45303375, -0.8798392), (-0.29087174, -0.5081899, -0.8106396), (-0.29093975, -0.50819516, -0.8106119), (-0.29093978, -0.50819516, -0.81061196), (-0.29093975, -0.50819516, -0.81061196), (-0.29090402, -0.5081665, -0.81064284), (-0.29095995, -0.50819373, -0.81060565), (0.12365647, -0.94552463, -0.30115142), (-0.29095995, -0.50819373, -0.81060565), (-0.41851187, 0.6045278, -0.6777862), (0.12365647, -0.94552463, -0.30115142), (0.123660624, -0.9455321, -0.3011264), (0.12365647, -0.94552463, -0.30115142), (-0.41851187, 0.6045278, -0.6777862), (-0.41851187, 0.6045278, -0.6777862), (-0.44220254, 0.58729166, -0.67789775), (0.123660624, -0.9455321, -0.3011264), (0.024726212, -0.23111674, -0.9726118), (0.123660624, -0.9455321, -0.3011264), (-0.44220254, 0.58729166, -0.67789775), (-0.44220254, 0.58729166, -0.67789775), (0.03958904, -0.21694064, -0.97538173), (0.024726212, -0.23111674, -0.9726118), (0.1179426, -0.9666351, -0.22738968), (0.024726212, -0.23111674, -0.9726118), (0.03958904, -0.21694064, -0.97538173), (0.03958904, -0.21694064, -0.97538173), (0.19016212, -0.2943683, -0.9365819), (0.1179426, -0.9666351, -0.22738968), (0.99747336, -0.0006284993, -0.07103865), (0.36767468, -0.9140909, -0.17103589), (0.6919224, -0.6363529, 0.34102553), (0.99747, -0.0006003827, -0.07108529), (0.44694594, 0.30468678, 0.841074), (0.44698295, 0.30470106, 0.84104896), (0.44698295, 0.30470106, 0.84104896), (0.99747336, -0.0006284993, -0.07103865), (0.99747, -0.0006003827, -0.07108529), (0.99747336, -0.0006284993, -0.07103865), (0.44698295, 0.30470106, 0.84104896), (0.9157564, -0.21853916, 0.33709192), (0.9157564, -0.21853916, 0.33709192), (0.36767468, -0.9140909, -0.17103589), (0.99747336, -0.0006284993, -0.07103865), (0.37214172, -0.9130876, -0.16667774), (0.36767468, -0.9140909, -0.17103589), (0.9157564, -0.21853916, 0.33709192), (0.6919224, -0.6363529, 0.34102553), (0.36767468, -0.9140909, -0.17103589), (0.37214172, -0.9130876, -0.16667774), (0.37214172, -0.9130876, -0.16667774), (0.37215254, -0.9130813, -0.16668856), (0.6919224, -0.6363529, 0.34102553), (0.37215254, -0.9130813, -0.16668856), (0.37214172, -0.9130876, -0.16667774), (-0.37185887, -0.9122941, -0.17158179), (-0.37185887, -0.9122941, -0.17158179), (-0.69192785, -0.63636386, 0.34099388), (0.37215254, -0.9130813, -0.16668856), (-0.84862113, 0.082286805, 0.5225621), (-0.37263507, 0.6958134, -0.6139924), (-0.25643647, 0.8136529, -0.52173674), (-0.691942, -0.63633084, 0.34102705), (-0.84862113, 0.082286805, 0.5225621), (-0.25643647, 0.8136529, -0.52173674), (-0.25643647, 0.8136529, -0.52173674), (-0.2564211, 0.8136694, -0.5217187), (-0.691942, -0.63633084, 0.34102705), (-0.2564211, 0.8136694, -0.5217187), (-0.69192785, -0.63636386, 0.34099388), (-0.691942, -0.63633084, 0.34102705), (-0.69192785, -0.63636386, 0.34099388), (-0.2564211, 0.8136694, -0.5217187), (0.22801322, -0.9690002, 0.09512451), (0.22801322, -0.9690002, 0.09512451), (0.37215254, -0.9130813, -0.16668856), (-0.69192785, -0.63636386, 0.34099388), (0.37215254, -0.9130813, -0.16668856), (0.22801322, -0.9690002, 0.09512451), (0.691961, -0.6363389, 0.34097332), (0.691961, -0.6363389, 0.34097332), (0.6919224, -0.6363529, 0.34102553), (0.37215254, -0.9130813, -0.16668856), (0.69187057, -0.6364182, 0.34100875), (0.69187057, -0.6364182, 0.34100875), (0.69187057, -0.6364182, 0.34100875), (0.6919224, -0.6363529, 0.34102553), (0.6919403, -0.6363182, 0.34105387), (0.99747336, -0.0006284993, -0.07103865), (0.99747336, -0.0006284993, -0.07103865), (0.6919403, -0.6363182, 0.34105387), (0.84862536, 0.08220887, 0.5225673), (0.84862536, 0.08220887, 0.5225673), (0.99747, -0.0006003827, -0.07108529), (0.99747336, -0.0006284993, -0.07103865), (0.99747, -0.0006003827, -0.07108529), (0.84862536, 0.08220887, 0.5225673), (0.3726347, 0.69581234, -0.6139939), (0.3726347, 0.69581234, -0.6139939), (0.7009114, 0.5347544, -0.4719754), (0.99747, -0.0006003827, -0.07108529), (0.99747, -0.0006003827, -0.07108529), (0.7009114, 0.5347544, -0.4719754), (0.75496453, 0.64487225, -0.119031355), (0.75496453, 0.64487225, -0.119031355), (0.44694594, 0.30468678, 0.841074), (0.99747, -0.0006003827, -0.07108529), (0.44694594, 0.30468678, 0.841074), (0.75496453, 0.64487225, -0.119031355), (0.33341703, 0.89652824, 0.2916678), (0.33341703, 0.89652824, 0.2916678), (0.44696993, 0.30465028, 0.8410742), (0.44694594, 0.30468678, 0.841074), (-0.37263507, 0.6958134, -0.6139924), (0, 0.83624244, -0.54836), (-0.2564064, 0.81366426, -0.5217339), (-0.2564064, 0.81366426, -0.5217339), (-0.25643647, 0.8136529, -0.52173674), (-0.37263507, 0.6958134, -0.6139924), (-0.25642106, 0.81366235, -0.52172965), (-0.25642106, 0.81366235, -0.52172965), (-0.25642106, 0.81366235, -0.52172965), (-0.2564211, 0.8136694, -0.5217187), (-0.25642896, 0.81369114, -0.5216809), (0.22801322, -0.9690002, 0.09512451), (0.69190603, -0.636346, 0.3410716), (0.2280032, -0.96900916, 0.0950562), (-0.2564323, 0.81368816, -0.52168405), (-0.2564323, 0.81368816, -0.52168405), (0.37258512, 0.695779, -0.61406183), (0.69190603, -0.636346, 0.3410716), (0.37258512, 0.695779, -0.61406183), (0.6919403, -0.6363182, 0.34105387), (0.69190603, -0.636346, 0.3410716), (0.6919403, -0.6363182, 0.34105387), (0.37258512, 0.695779, -0.61406183), (0.84862536, 0.08220887, 0.5225673), (0.84862536, 0.08220887, 0.5225673), (0.37258512, 0.695779, -0.61406183), (0.3726347, 0.69581234, -0.6139939), (0.3726347, 0.69581234, -0.6139939), (0.37258512, 0.695779, -0.61406183), (-0.2564323, 0.81368816, -0.52168405), (-0.2564323, 0.81368816, -0.52168405), (0, 0.8362424, -0.54835993), (0.3726347, 0.69581234, -0.6139939), (0.44715178, 0.30475157, 0.8409411), (0.44715178, 0.30475157, 0.8409411), (0.44715178, 0.30475157, 0.8409411), (0.44699383, 0.30469492, 0.84104556), (0.38790148, 0.2570852, 0.8851212), (0.44698295, 0.30470106, 0.84104896), (0.9157564, -0.21853916, 0.33709192), (0.44698295, 0.30470106, 0.84104896), (0.38790148, 0.2570852, 0.8851212), (0.38790148, 0.2570852, 0.8851212), (0.36799264, -0.25202248, 0.8950229), (0.9157564, -0.21853916, 0.33709192), (0.9157564, -0.21853916, 0.33709192), (0.36799264, -0.25202248, 0.8950229), (0.37214172, -0.9130876, -0.16667774), (0.37214172, -0.9130876, -0.16667774), (0.36799264, -0.25202248, 0.8950229), (-0.8591592, -0.46411148, 0.21551356), (-0.8591592, -0.46411148, 0.21551356), (-0.37185887, -0.9122941, -0.17158179), (0.37214172, -0.9130876, -0.16667774), (-0.99305797, -0.06728896, -0.09647803), (-0.37185887, -0.9122941, -0.17158179), (-0.8591592, -0.46411148, 0.21551356), (-0.69192785, -0.63636386, 0.34099388), (-0.37185887, -0.9122941, -0.17158179), (-0.99305797, -0.06728896, -0.09647803), (-0.99305797, -0.06728896, -0.09647803), (-0.691942, -0.63633084, 0.34102705), (-0.69192785, -0.63636386, 0.34099388), (-0.84862113, 0.082286805, 0.5225621), (-0.691942, -0.63633084, 0.34102705), (-0.99305797, -0.06728896, -0.09647803), (-0.99305797, -0.06728896, -0.09647803), (-0.9970009, 0.06135328, -0.047169074), (-0.84862113, 0.082286805, 0.5225621), (-0.37263507, 0.6958134, -0.6139924), (-0.84862113, 0.082286805, 0.5225621), (-0.9970009, 0.06135328, -0.047169074), (-0.9970009, 0.06135328, -0.047169074), (-0.75661004, 0.4833315, -0.44037697), (-0.37263507, 0.6958134, -0.6139924), (-0.81427246, 0.57830656, -0.050217755), (-0.75661004, 0.4833315, -0.44037697), (-0.9970009, 0.06135328, -0.047169074), (-0.9970009, 0.06135328, -0.047169074), (-0.49254018, 0.28570718, 0.8220557), (-0.81427246, 0.57830656, -0.050217755), (-0.81427246, 0.57830656, -0.050217755), (-0.49254018, 0.28570718, 0.8220557), (0.44699022, 0.30466473, 0.8410584), (0.3879177, 0.2571383, 0.8850988), (0.44699022, 0.30466473, 0.8410584), (-0.49254018, 0.28570718, 0.8220557), (0.44699022, 0.30466473, 0.8410584), (0.33346564, 0.8965218, 0.29163224), (-0.81427246, 0.57830656, -0.050217755), (-0.49254018, 0.28570718, 0.8220557), (-0.5039123, 0.27639785, 0.81833774), (0.3879177, 0.2571383, 0.8850988), (0.36799264, -0.25202248, 0.8950229), (0.3879177, 0.2571383, 0.8850988), (-0.5039123, 0.27639785, 0.81833774), (-0.5039123, 0.27639785, 0.81833774), (-0.8591592, -0.46411148, 0.21551356), (0.36799264, -0.25202248, 0.8950229), (-0.8591592, -0.46411148, 0.21551356), (-0.5039123, 0.27639785, 0.81833774), (-0.99305797, -0.06728896, -0.09647803), (-0.9970009, 0.06135328, -0.047169074), (-0.99305797, -0.06728896, -0.09647803), (-0.5039123, 0.27639785, 0.81833774), (-0.5039123, 0.27639785, 0.81833774), (-0.49254018, 0.28570718, 0.8220557), (-0.9970009, 0.06135328, -0.047169074), (0.06277138, 0.39427918, 0.91684437), (-0.1939954, 0.9170577, 0.34838343), (-0.8154485, 0.5246636, -0.24448255), (-0.52588075, 0.6833436, -0.50644964), (0.016720425, -0.22519729, -0.9741697), (0.06267691, 0.39425203, 0.9168626), (-0.1940413, 0.91703665, 0.3484133), (0.06273908, 0.39428803, 0.9168428), (0.016740479, -0.22515708, -0.9741787), (0.01678326, -0.22520271, -0.97416747), (0.36830592, -0.81222475, -0.4523736), (-0.19396967, 0.91703457, 0.34845847), (-0.63416135, -0.073517025, -0.76969784), (-0.52588075, 0.6833436, -0.50644964), (0.48794526, 0.4765733, 0.7312916), (0.06267691, 0.39425203, 0.9168626), (0.48794526, 0.4765733, 0.7312916), (-0.52588075, 0.6833436, -0.50644964), (0.48793632, 0.47658524, 0.7312898), (0.48794526, 0.4765733, 0.7312916), (0.06267691, 0.39425203, 0.9168626), (0.48794526, 0.4765733, 0.7312916), (0.48793632, 0.47658524, 0.7312898), (-0.63416135, -0.073517025, -0.76969784), (0.58131486, 0.18648957, -0.7920195), (-0.63416135, -0.073517025, -0.76969784), (0.48793632, 0.47658524, 0.7312898), (0.48793632, 0.47658524, 0.7312898), (0.5812991, 0.18644053, -0.7920426), (0.58131486, 0.18648957, -0.7920195), (0.58125204, 0.18649524, -0.7920641), (0.5812521, 0.18649517, -0.7920641), (0.5812471, 0.18651012, -0.79206425), (-0.8154485, 0.5246636, -0.24448255), (-0.77907157, 0.23366879, -0.5817613), (0.016777623, -0.2252155, -0.97416455), (0.016777623, -0.2252155, -0.97416455), (0.06273493, 0.39431557, 0.91683125), (-0.8154485, 0.5246636, -0.24448255), (-0.8154485, 0.5246636, -0.24448255), (0.06273493, 0.39431557, 0.91683125), (0.06277138, 0.39427918, 0.91684437), (0.062870964, 0.39457527, 0.9167102), (0.062870964, 0.39457527, 0.9167102), (0.062870964, 0.39457527, 0.9167102), (0.06267691, 0.39425203, 0.9168626), (0.06270385, 0.39427453, 0.9168511), (0.48793632, 0.47658524, 0.7312898), (0.48793632, 0.47658524, 0.7312898), (0.06270385, 0.39427453, 0.9168511), (0.6303764, -0.6248422, 0.4606493), (0.6303764, -0.6248422, 0.4606493), (0.5812991, 0.18644053, -0.7920426), (0.48793632, 0.47658524, 0.7312898), (0.5812991, 0.18644053, -0.7920426), (0.6303764, -0.6248422, 0.4606493), (0.6304163, -0.62483823, 0.46060017), (0.6304163, -0.62483823, 0.46060017), (0.58127344, 0.18654476, -0.7920367), (0.5812991, 0.18644053, -0.7920426), (0.58127344, 0.18654476, -0.7920367), (0.6304163, -0.62483823, 0.46060017), (-0.032301784, -0.22250974, -0.97439516), (-0.032301784, -0.22250974, -0.97439516), (0.581261, 0.18653238, -0.7920488), (0.58127344, 0.18654476, -0.7920367), (-0.6341444, -0.07359891, -0.7697039), (0.581261, 0.18653238, -0.7920488), (-0.032301784, -0.22250974, -0.97439516), (-0.032301784, -0.22250974, -0.97439516), (-0.03233166, -0.22255403, -0.9743841), (-0.6341444, -0.07359891, -0.7697039), (-0.6341444, -0.07359891, -0.7697039), (-0.03233166, -0.22255403, -0.9743841), (0.016763777, -0.22526541, -0.97415316), (0.01673434, -0.2252672, -0.9741533), (0.016763777, -0.22526541, -0.97415316), (-0.03233166, -0.22255403, -0.9743841), (0.016763777, -0.22526541, -0.97415316), (0.01673434, -0.2252672, -0.9741533), (-0.77903676, 0.23377198, -0.58176666), (-0.77903676, 0.23377198, -0.58176666), (0.368276, -0.8122213, -0.452404), (0.016763777, -0.22526541, -0.97415316), (0.368276, -0.8122213, -0.452404), (-0.77903676, 0.23377198, -0.58176666), (-0.8154878, 0.52461547, -0.24445483), (-0.8154878, 0.52461547, -0.24445483), (-0.19391526, 0.91702795, 0.34850618), (0.368276, -0.8122213, -0.452404), (0.016763777, -0.22526541, -0.97415316), (-0.52590525, 0.6833035, -0.506478), (-0.6341444, -0.07359891, -0.7697039), (0.6304163, -0.62483823, 0.46060017), (0.6303764, -0.6248422, 0.4606493), (0.0765219, -0.8215164, 0.5650267), (0.0765219, -0.8215164, 0.5650267), (0.07648867, -0.8214919, 0.56506693), (0.6304163, -0.62483823, 0.46060017), (-0.032301784, -0.22250974, -0.97439516), (0.6304163, -0.62483823, 0.46060017), (0.07648867, -0.8214919, 0.56506693), (0.07648867, -0.8214919, 0.56506693), (-0.091628, -0.31650868, -0.944154), (-0.032301784, -0.22250974, -0.97439516), (-0.032301784, -0.22250974, -0.97439516), (-0.091628, -0.31650868, -0.944154), (-0.032291096, -0.22255254, -0.97438574), (-0.032396115, -0.22268671, -0.97435164), (-0.032396115, -0.22268671, -0.97435164), (-0.032396115, -0.22268671, -0.97435164), (-0.03233166, -0.22255403, -0.9743841), (-0.032291096, -0.22255254, -0.97438574), (0.01673434, -0.2252672, -0.9741533), (-0.6390211, 0.7086826, -0.29903334), (0.01673434, -0.2252672, -0.9741533), (-0.032291096, -0.22255254, -0.97438574), (-0.032291096, -0.22255254, -0.97438574), (0.190105, -0.29436842, -0.9365935), (-0.6390211, 0.7086826, -0.29903334), (0.11801356, -0.9666301, -0.22737408), (0.190105, -0.29436842, -0.9365935), (-0.032291096, -0.22255254, -0.97438574), (-0.032291096, -0.22255254, -0.97438574), (-0.091628, -0.31650868, -0.944154), (0.11801356, -0.9666301, -0.22737408), (0.061382916, -0.8522138, 0.51958036), (0.11801356, -0.9666301, -0.22737408), (-0.091628, -0.31650868, -0.944154), (-0.091628, -0.31650868, -0.944154), (0.07648867, -0.8214919, 0.56506693), (0.061382916, -0.8522138, 0.51958036), (0.076480135, -0.821513, 0.5650373), (0.061382916, -0.8522138, 0.51958036), (0.07648867, -0.8214919, 0.56506693), (0.07647615, -0.82156205, 0.5649665), (0.07647615, -0.82156205, 0.5649665), (0.07647615, -0.82156205, 0.5649665), (0.07656914, -0.8215069, 0.56503415), (0.0765219, -0.8215164, 0.5650267), (0.08872197, -0.06950701, 0.9936283), (0.08872197, -0.06950701, 0.9936283), (0.0765219, -0.8215164, 0.5650267), (0.6303764, -0.6248422, 0.4606493), (0.6303764, -0.6248422, 0.4606493), (0.06270385, 0.39427453, 0.9168511), (0.08872197, -0.06950701, 0.9936283), (0.06273493, 0.39431557, 0.91683125), (0.08872197, -0.06950701, 0.9936283), (0.06270385, 0.39427453, 0.9168511), (0.08872197, -0.06950701, 0.9936283), (0.06273493, 0.39431557, 0.91683125), (-0.79649854, 0.6031494, 0.04243575), (-0.63904905, 0.7086735, -0.29899517), (-0.79649854, 0.6031494, 0.04243575), (0.06273493, 0.39431557, 0.91683125), (0.06273493, 0.39431557, 0.91683125), (0.016777623, -0.2252155, -0.97416455), (-0.63904905, 0.7086735, -0.29899517), (-0.79649854, 0.6031494, 0.04243575), (0.085285656, -0.07043783, 0.99386364), (0.08872197, -0.06950701, 0.9936283), (0.08872197, -0.06950701, 0.9936283), (0.085285656, -0.07043783, 0.99386364), (0.07656914, -0.8215069, 0.56503415), (0.1852775, -0.6998343, 0.6898581), (0.90775615, -0.36198196, -0.21200903), (-0.1435819, -0.45309237, -0.87982464), (0.1852775, -0.6998343, 0.6898581), (-0.1435819, -0.45309237, -0.87982464), (0.6493744, -0.39852497, 0.64768094), (0.798023, 0.15005928, -0.5836451), (-0.29089662, -0.5081445, -0.81065917), (-0.1435819, -0.45309237, -0.87982464), (-0.1435819, -0.45309237, -0.87982464), (0.90775615, -0.36198196, -0.21200903), (0.798023, 0.15005928, -0.5836451), (0.8334307, -0.3761986, 0.40480605), (0.798023, 0.15005928, -0.5836451), (0.90775615, -0.36198196, -0.21200903), (0.90775615, -0.36198196, -0.21200903), (0.8334262, -0.37615934, 0.40485156), (0.8334307, -0.3761986, 0.40480605), (0.2034271, -0.8939488, 0.39934087), (0.8334262, -0.37615934, 0.40485156), (0.90775615, -0.36198196, -0.21200903), (0.90775615, -0.36198196, -0.21200903), (0.1909559, -0.9254263, -0.32729477), (0.2034271, -0.8939488, 0.39934087), (0.1909559, -0.9254263, -0.32729477), (0.90775615, -0.36198196, -0.21200903), (0.1852775, -0.6998343, 0.6898581), (0.1852775, -0.6998343, 0.6898581), (0.12854032, -0.9677891, -0.21647531), (0.1909559, -0.9254263, -0.32729477), (0.12854032, -0.9677891, -0.21647531), (0.1852775, -0.6998343, 0.6898581), (0.1853395, -0.69985574, 0.68981975), (0.1853395, -0.69985574, 0.68981975), (0.1852775, -0.6998343, 0.6898581), (0.6493744, -0.39852497, 0.64768094), (0.1853395, -0.69985574, 0.68981975), (0.6493744, -0.39852497, 0.64768094), (0.58323884, 0.43774182, 0.68426204), (-0.9756173, -0.2088786, -0.06738478), (-0.80014133, -0.36739057, 0.4741288), (0.2034271, -0.8939488, 0.39934087), (0.2034271, -0.8939488, 0.39934087), (0.1909559, -0.9254263, -0.32729477), (-0.9756173, -0.2088786, -0.06738478), (-0.20512436, -0.6954645, 0.68866044), (-0.9756173, -0.2088786, -0.06738478), (0.1909559, -0.9254263, -0.32729477), (0.1909559, -0.9254263, -0.32729477), (0.12854032, -0.9677891, -0.21647531), (-0.20512436, -0.6954645, 0.68866044), (-0.20512436, -0.6954645, 0.68866044), (0.12854032, -0.9677891, -0.21647531), (0.51832545, -0.8524516, 0.06830135), (0.1853395, -0.69985574, 0.68981975), (0.51832545, -0.8524516, 0.06830135), (0.12854032, -0.9677891, -0.21647531), (0.51832545, -0.8524516, 0.06830135), (0.1853395, -0.69985574, 0.68981975), (0.5183015, -0.85246825, 0.068275824), (0.83342314, -0.3762217, 0.4048002), (0.77302474, -0.43731013, 0.4595571), (0.83342314, -0.37622142, 0.40480047), (0.83346534, -0.3761281, 0.4048001), (0.83346534, -0.3761281, 0.4048001), (0.83346534, -0.3761281, 0.4048001), (0.2034014, -0.89394164, 0.3993697), (0.83346665, -0.37618446, 0.40474522), (0.8334262, -0.37615934, 0.40485156), (0.8334262, -0.37615934, 0.40485156), (0.2034271, -0.8939488, 0.39934087), (0.2034014, -0.89394164, 0.3993697), (-0.7948148, -0.44563046, 0.41192585), (0.2034014, -0.89394164, 0.3993697), (0.2034271, -0.8939488, 0.39934087), (0.2034271, -0.8939488, 0.39934087), (-0.80014133, -0.36739057, 0.4741288), (-0.7948148, -0.44563046, 0.41192585), (-0.80015606, -0.3674387, 0.4740665), (-0.7948148, -0.44563046, 0.41192585), (-0.80014133, -0.36739057, 0.4741288), (-0.80018187, -0.36734828, 0.47409305), (-0.80018187, -0.36734828, 0.47409305), (-0.80018187, -0.36734828, 0.47409305), (-0.80014473, -0.36740866, 0.474109), (-0.80014133, -0.36739057, 0.4741288), (-0.9756173, -0.2088786, -0.06738478), (-0.9756173, -0.2088786, -0.06738478), (-0.7919577, 0.1616041, -0.5888016), (-0.80014473, -0.36740866, 0.474109), (-0.7919577, 0.1616041, -0.5888016), (-0.9756173, -0.2088786, -0.06738478), (-0.3674934, -0.92622733, 0.08397349), (-0.20512436, -0.6954645, 0.68866044), (-0.3674934, -0.92622733, 0.08397349), (-0.9756173, -0.2088786, -0.06738478), (-0.5633809, -0.41189295, 0.71620256), (-0.3674934, -0.92622733, 0.08397349), (-0.20512436, -0.6954645, 0.68866044), (-0.5633809, -0.41189295, 0.71620256), (-0.20512436, -0.6954645, 0.68866044), (-0.20515859, -0.69550484, 0.6886094), (0.51832545, -0.8524516, 0.06830135), (-0.20515859, -0.69550484, 0.6886094), (-0.20512436, -0.6954645, 0.68866044), (-0.4653967, -0.87743706, 0.11623255), (-0.20515859, -0.69550484, 0.6886094), (0.51832545, -0.8524516, 0.06830135), (0.51832545, -0.8524516, 0.06830135), (0.46544683, -0.87740195, 0.11629775), (-0.4653967, -0.87743706, 0.11623255), (0.5183015, -0.85246825, 0.068275824), (0.46544683, -0.87740195, 0.11629775), (0.51832545, -0.8524516, 0.06830135), (0.46544683, -0.87740195, 0.11629775), (0.5183015, -0.85246825, 0.068275824), (0.3245302, -0.7563859, -0.5679441), (-0.3674934, -0.92622733, 0.08397349), (0.6197587, 0.344458, -0.70515805), (-0.7919577, 0.1616041, -0.5888016), (-0.47295403, 0.52899927, 0.7046092), (-0.5633809, -0.41189295, 0.71620256), (-0.20515859, -0.69550484, 0.6886094), (0.3245302, -0.7563859, -0.5679441), (0.2656464, -0.82220376, -0.5034014), (0.46544683, -0.87740195, 0.11629775), (-0.4653967, -0.87743706, 0.11623255), (0.46544683, -0.87740195, 0.11629775), (0.2656464, -0.82220376, -0.5034014), (0.2656464, -0.82220376, -0.5034014), (-0.25867322, -0.78535616, -0.5624088), (-0.4653967, -0.87743706, 0.11623255), (0.13326499, 0.79274666, 0.5948051), (0.16119686, 0.78020054, 0.6044028), (0.6419169, 0.74849796, 0.16641349), (0.6419169, 0.74849796, 0.16641349), (0.043162018, 0.93103635, 0.36236498), (0.13326499, 0.79274666, 0.5948051), (0.043162018, 0.93103635, 0.36236498), (0.6419169, 0.74849796, 0.16641349), (0.7980621, 0.15000147, -0.5836065), (0.7980621, 0.15000147, -0.5836065), (0.07244417, 0.77255595, -0.6308004), (0.043162018, 0.93103635, 0.36236498), (0.07244417, 0.77255595, -0.6308004), (0.7980621, 0.15000147, -0.5836065), (0.798042, 0.15004085, -0.58362377), (-0.69410133, -0.00665465, -0.7198466), (0.110598475, 0.24685025, -0.9627217), (0.13326499, 0.79274666, 0.5948051), (0.16119686, 0.78020054, 0.6044028), (0.13326499, 0.79274666, 0.5948051), (0.110598475, 0.24685025, -0.9627217), (0.110598475, 0.24685025, -0.9627217), (0.70492077, -0.22539629, -0.67252004), (0.16119686, 0.78020054, 0.6044028), (0.16119686, 0.78020054, 0.6044028), (0.70492077, -0.22539629, -0.67252004), (0.704908, -0.22545819, -0.6725127), (0.798042, 0.15004085, -0.58362377), (0.79804367, 0.15005334, -0.58361846), (0.83344465, -0.3762327, 0.4047457), (0.7980287, 0.15006979, -0.5836345), (0.7980287, 0.15006979, -0.5836345), (0.7980287, 0.15006979, -0.5836345), (0.7980499, 0.15009022, -0.5836004), (0.7980621, 0.15000147, -0.5836065), (0.641906, 0.7484939, 0.16647375), (0.6419169, 0.74849796, 0.16641349), (0.641906, 0.7484939, 0.16647375), (0.7980621, 0.15000147, -0.5836065), (0.64190006, 0.74849445, 0.16649428), (0.64190006, 0.74849445, 0.16649428), (0.6419, 0.74849445, 0.16649427), (0.64196503, 0.7484577, 0.1664094), (0.6419169, 0.74849796, 0.16641349), (0.16119686, 0.78020054, 0.6044028), (0.5831758, 0.4377292, 0.6843239), (0.64196503, 0.7484577, 0.1664094), (0.16119686, 0.78020054, 0.6044028), (0.16119686, 0.78020054, 0.6044028), (0.18530317, -0.6998447, 0.6898407), (0.5831758, 0.4377292, 0.6843239), (0.18530317, -0.6998447, 0.6898407), (0.16119686, 0.78020054, 0.6044028), (0.704908, -0.22545819, -0.6725127), (0.704908, -0.22545819, -0.6725127), (0.51830053, -0.85246384, 0.06833787), (0.18530317, -0.6998447, 0.6898407), (0.51830053, -0.85246384, 0.06833787), (0.704908, -0.22545819, -0.6725127), (0.53384185, -0.81037605, -0.24146129), (0.53384185, -0.81037605, -0.24146129), (0.32447892, -0.75640714, -0.5679452), (0.51830053, -0.85246384, 0.06833787), (0.641906, 0.7484939, 0.16647375), (-0.2908848, -0.50827825, -0.8105796), (0.7980499, 0.15009022, -0.5836004), (-0.7048761, -0.22541907, -0.67255926), (-0.69410133, -0.00665465, -0.7198466), (-0.28328103, 0.71106356, 0.6435374), (0.13326499, 0.79274666, 0.5948051), (-0.28328103, 0.71106356, 0.6435374), (-0.69410133, -0.00665465, -0.7198466), (-0.63340735, 0.7450885, 0.20889777), (-0.28328103, 0.71106356, 0.6435374), (0.13326499, 0.79274666, 0.5948051), (0.13326499, 0.79274666, 0.5948051), (0.043162018, 0.93103635, 0.36236498), (-0.63340735, 0.7450885, 0.20889777), (-0.79197097, 0.16154058, -0.58880097), (-0.63340735, 0.7450885, 0.20889777), (0.043162018, 0.93103635, 0.36236498), (0.043162018, 0.93103635, 0.36236498), (0.07244417, 0.77255595, -0.6308004), (-0.79197097, 0.16154058, -0.58880097), (-0.79197097, 0.16154058, -0.58880097), (0.07244417, 0.77255595, -0.6308004), (0.3036125, 0.87692034, -0.37259912), (0.798042, 0.15004085, -0.58362377), (0.3036125, 0.87692034, -0.37259912), (0.07244417, 0.77255595, -0.6308004), (0.3036125, 0.87692034, -0.37259912), (0.798042, 0.15004085, -0.58362377), (0.30115902, 0.8895909, 0.3434111), (0.83344465, -0.3762327, 0.4047457), (0.30115902, 0.8895909, 0.3434111), (0.798042, 0.15004085, -0.58362377), (0.30115902, 0.8895909, 0.3434111), (0.83344465, -0.3762327, 0.4047457), (0.77299833, -0.43744934, 0.45946884), (0.77299833, -0.43744934, 0.45946884), (0.32782787, 0.8588934, 0.3934856), (0.30115902, 0.8895909, 0.3434111), (0.32775572, 0.85891515, 0.39349842), (0.30115902, 0.8895909, 0.3434111), (0.32782787, 0.8588934, 0.3934856), (0.32771134, 0.8589353, 0.39349133), (0.32771134, 0.8589353, 0.39349133), (0.32771134, 0.8589353, 0.39349133), (0.32775572, 0.85891515, 0.39349842), (0.32782575, 0.8588897, 0.39349583), (-0.30116418, 0.8895973, 0.34338996), (-0.30116418, 0.8895973, 0.34338996), (-0.30118677, 0.8895893, 0.34339106), (0.32775572, 0.85891515, 0.39349842), (-0.8001463, -0.367474, 0.47405565), (-0.30118677, 0.8895893, 0.34339106), (-0.30116418, 0.8895973, 0.34338996), (-0.7919663, 0.16154069, -0.58880717), (-0.30118677, 0.8895893, 0.34339106), (-0.8001463, -0.367474, 0.47405565), (0.3036125, 0.87692034, -0.37259912), (-0.7919663, 0.16154069, -0.58880717), (-0.79197097, 0.16154058, -0.58880097), (-0.30118677, 0.8895893, 0.34339106), (-0.7919663, 0.16154069, -0.58880717), (0.3036125, 0.87692034, -0.37259912), (0.3036125, 0.87692034, -0.37259912), (0.32775572, 0.85891515, 0.39349842), (-0.30118677, 0.8895893, 0.34339106), (0.30115902, 0.8895909, 0.3434111), (0.32775572, 0.85891515, 0.39349842), (0.3036125, 0.87692034, -0.37259912), (-0.30116418, 0.8895973, 0.34338996), (-0.8001561, -0.3674751, 0.47403845), (-0.8001463, -0.367474, 0.47405565), (-0.8001463, -0.367474, 0.47405565), (-0.7919611, 0.16156271, -0.58880824), (-0.7919663, 0.16154069, -0.58880717), (-0.7919069, 0.1616559, -0.5888556), (-0.7919069, 0.1616559, -0.5888556), (-0.7919069, 0.1616559, -0.5888556), (-0.63339484, 0.74509007, 0.20893025), (-0.79197097, 0.16154058, -0.58880097), (-0.791988, 0.16153088, -0.5887809), (-0.63340735, 0.7450885, 0.20889777), (-0.79197097, 0.16154058, -0.58880097), (-0.63339484, 0.74509007, 0.20893025), (-0.63339096, 0.7451302, 0.2087985), (-0.63339096, 0.7451302, 0.2087985), (-0.63339096, 0.7451302, 0.2087985), (-0.28328103, 0.71106356, 0.6435374), (-0.63340735, 0.7450885, 0.20889777), (-0.6334404, 0.74506575, 0.2088787), (-0.20514818, -0.69547856, 0.68863916), (-0.28328103, 0.71106356, 0.6435374), (-0.6334404, 0.74506575, 0.2088787), (-0.6334404, 0.74506575, 0.2088787), (-0.47291854, 0.52901495, 0.70462126), (-0.20514818, -0.69547856, 0.68863916), (-0.791988, 0.16153088, -0.5887809), (0.6197366, 0.3445027, -0.70515555), (-0.63339484, 0.74509007, 0.20893025), (-0.4653762, -0.8774481, 0.1162322), (-0.2584987, -0.78525305, -0.5626331), (-0.5338653, -0.81037825, -0.24140206), (-0.5338653, -0.81037825, -0.24140206), (-0.7048761, -0.22541907, -0.67255926), (-0.4653762, -0.8774481, 0.1162322), (-0.20507011, -0.6955182, 0.68862236), (-0.4653762, -0.8774481, 0.1162322), (-0.7048761, -0.22541907, -0.67255926), (-0.7048761, -0.22541907, -0.67255926), (-0.28328103, 0.71106356, 0.6435374), (-0.20507011, -0.6955182, 0.68862236)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(-0.000354, 0.005357, 0.013545), (-0.000402, 0.005746, 0.013298), (-0.002147, 0.007008, 0.01426), (-0.002147, 0.007008, 0.01426), (-0.002324, 0.006613, 0.015668), (-0.000997, 0.004877, 0.013095), (-0.0044629998, 0.006298, 0.015473), (-0.001268, 0.005129, 0.01262), (-0.00457, 0.006314, 0.014045), (-0.006797, 0.007509, 0.014295), (-0.006456, 0.007462, 0.015408), (-0.004212, 0.009196, 0.016042), (-0.006446, 0.009085, 0.016026), (-0.006686, 0.010419, 0.015357), (-0.001965, 0.009453, 0.015821), (-0.002147, 0.007008, 0.01426), (0.00065, 0.00703, 0.012803), (0.001169, 0.007174, 0.013236), (0.001271, 0.009479, 0.013163), (-0.002741, 0.011533, 0.015312), (-0.004772, 0.01118, 0.015438), (-0.004866, 0.011373, 0.014725), (-0.006656, 0.010668, 0.014536), (-0.004861, 0.01098, 0.014094), (-0.006524, 0.009981, 0.013732), (-0.004361, 0.008841, 0.014232), (-0.006181, 0.008385, 0.013745), (-0.006797, 0.007509, 0.014295), (-0.00457, 0.006314, 0.014045), (-0.002142, 0.009229, 0.014427), (-0.002147, 0.007008, 0.01426), (0.000752, 0.009332, 0.012726), (0.00065, 0.00703, 0.012803), (0.000237, 0.011509, 0.012355), (0.000024, 0.011143, 0.012181), (-0.002821, 0.011346, 0.014074), (-0.002831, 0.011737, 0.014651), (0.000545, 0.01129, 0.012616), (0.000752, 0.009332, 0.012726), (0.00065, 0.00703, 0.012803), (0.000024, 0.011143, 0.012181), (-0.000402, 0.005746, 0.013298), (-0.000354, 0.005357, 0.013545), (-0.000997, 0.004877, 0.013095), (-0.001268, 0.005129, 0.01262), (-0.003264, 0.002401, 0.034802), (-0.004227, 0.000749, 0.035746), (-0.004865, 0.000461, 0.0382), (-0.002141, 0.000212, 0.033739), (-0.00299, -0.000988, 0.035031), (-0.004448, -0.002084, 0.03775), (-0.005614, -0.001325, 0.037781), (-0.005436, -0.002448, 0.041157), (0.003957, 0.002621, 0.040427), (0.003545, 0.003389, 0.041331), (0.004546, 0.00201, 0.042559), (0.003321, 0.003659, 0.04395), (0.004153, 0.001268, 0.045868), (-0.000303, 0.004856, 0.044519998), (-0.000303, 0.002318, 0.047798), (-0.003927, 0.003659, 0.04395), (-0.004758, 0.001268, 0.045868), (-0.005151, 0.00201, 0.042559), (-0.005483, -0.001892, 0.04324), (-0.005094, 0.000442, 0.040593), (-0.006023, -0.001604, 0.040355), (-0.004373, 0.003135, 0.037583), (-0.003108, 0.004283, 0.036871), (-0.00353, 0.003637, 0.039457), (0.004877, -0.001892, 0.04324), (0.004489, 0.000442, 0.040593), (0.0048309998, -0.002448, 0.041157), (-0.00415, 0.003389, 0.041331), (-0.004562, 0.002621, 0.040427), (-0.002529, 0.004487, 0.041605), (-0.000303, 0.00532, 0.041885), (0.001923, 0.004487, 0.041605), (0.002925, 0.003637, 0.039457), (0.003768, 0.003135, 0.037583), (0.00426, 0.000461, 0.0382), (0.005009, -0.001325, 0.037781), (0.003842, -0.002084, 0.03775), (0.005417, -0.001604, 0.040355), (0.002502, 0.004283, 0.036871), (0.00266, 0.002401, 0.034802), (0.003622, 0.000749, 0.035746), (0.002385, -0.000988, 0.035031), (0.001536, 0.000212, 0.033739), (0.000738, 0.002825, 0.033337), (0.001895, 0.00392, 0.034931), (0.000751, 0.004466, 0.033648), (-0.000303, 0.002834, 0.033219), (-0.000303, 0.001, 0.032749), (-0.001343, 0.002825, 0.033337), (0.000794, 0.004468, 0.039806), (0.001543, 0.00415, 0.039653), (0.001186, 0.004963, 0.03808), (0.000943, 0.005223, 0.037289), (0.001305, 0.004706, 0.035844), (-0.000303, 0.005163, 0.035183), (-0.000303, 0.004734, 0.033572), (-0.001357, 0.004466, 0.033648), (-0.0025, 0.00392, 0.034931), (-0.000303, 0.005451, 0.038719), (-0.000303, 0.005189, 0.039927), (-0.000303, 0.006496, 0.037864), (-0.000303, 0.006283, 0.037165), (-0.00191, 0.004706, 0.035844), (-0.000303, 0.005277, 0.035744), (-0.000303, 0.005646, 0.036160998), (-0.000303, 0.005521, 0.036972), (-0.001548, 0.005223, 0.037289), (-0.001791, 0.004963, 0.03808), (-0.001399, 0.004468, 0.039806), (-0.002148, 0.00415, 0.039653), (-0.009126, -0.001845, 0.020225), (-0.007963, -0.002157, 0.021094), (-0.00765, -0.00636, 0.023361), (-0.007963, -0.002157, 0.021094), (-0.009126, -0.001845, 0.020225), (-0.009544, 0.001622, 0.021551), (-0.008734, 0.001451, 0.022807), (-0.010335, 0.000949, 0.024738), (-0.009176, 0.000092, 0.026316), (-0.011046, -0.001704, 0.026409), (-0.009465, -0.002537, 0.028095), (-0.011506, -0.004426, 0.025404), (-0.008505, -0.0056869998, 0.027039), (-0.011291, -0.005011, 0.022311), (-0.015833, 0.004791, 0.014349), (-0.013135, 0.005186, 0.014249), (-0.013081, 0.005985, 0.013652), (-0.015398, 0.006811, 0.01309), (-0.017494, 0.005837, 0.015685), (-0.017277, 0.007851, 0.014126), (-0.016776, 0.006692, 0.017761), (-0.013135, 0.005186, 0.014249), (-0.012537, 0.005822, 0.015881), (-0.013391, 0.006278, 0.017461), (-0.015432, 0.006621, 0.018571), (-0.016698, 0.008227, 0.016604), (-0.015304, 0.007712, 0.017831), (-0.013353, 0.006815, 0.017048), (-0.012654, 0.00618, 0.015305), (-0.013081, 0.005985, 0.013652), (-0.012039, 0.006459, 0.014922), (-0.012311, 0.0066959998, 0.013289), (-0.006797, 0.007509, 0.014295), (-0.006181, 0.008385, 0.013745), (-0.012576, 0.006937, 0.016516), (-0.006456, 0.007462, 0.015408), (-0.013965, 0.008342, 0.017327), (-0.015002, 0.009626, 0.016069), (-0.015251, 0.009604, 0.01354), (-0.014138, 0.008217, 0.012524), (-0.012311, 0.0066959998, 0.013289), (-0.006524, 0.009981, 0.013732), (-0.006181, 0.008385, 0.013745), (-0.006656, 0.010668, 0.014536), (-0.006686, 0.010419, 0.015357), (-0.006446, 0.009085, 0.016026), (0.004112, 0.004192, 0.040816), (0.001852, 0.005422, 0.042532), (0.003466, 0.0055939998, 0.043583), (-0.00034199998, 0.004862, 0.046734), (-0.001419, 0.004653, 0.045431), (-0.001423, 0.00328, 0.047004), (0.000693, 0.003004, 0.047116), (0.003768, 0.003546, 0.044624), (0.005703, 0.002636, 0.040951), (0.005436, 0.002313, 0.042643), (-0.000203, -0.009013, 0.004399), (-0.000203, -0.012675, -0.019437), (0.009383, -0.008673, -0.019437), (0.00736, -0.006473, 0.004399), (0.014059, 0.000431, -0.019437), (0.010516, -0.000322, 0.004399), (0.011317, 0.00672, -0.019437), (0.007822, 0.004942, 0.004399), (0.006521, 0.010943, -0.019437), (0.004501, 0.00778, 0.004399), (-0.000203, 0.012675, -0.019437), (-0.000203, 0.009022, 0.004399), (-0.00491, 0.00778, 0.004399), (-0.00693, 0.010943, -0.019437), (-0.00823, 0.004942, 0.004399), (-0.011725, 0.00672, -0.019437), (-0.011214, -0.000725, 0.004399), (-0.014464, 0.000431, -0.019437), (-0.007768, -0.006709, 0.004399), (-0.009788, -0.008673, -0.019437), (-0.000203, -0.012675, -0.019437), (-0.000203, -0.009013, 0.004399), (-0.000203, -0.006068, 0.008776), (-0.006405, -0.004627, 0.008776), (-0.008912, -0.000421, 0.008776), (-0.006951, 0.003667, 0.008776), (-0.0037149999, 0.0046469998, 0.008776), (-0.000203, 0.006061, 0.008776), (0.003309, 0.0046469998, 0.008776), (0.006434, 0.003709, 0.008776), (0.008119, -0.000131, 0.008776), (0.006123, -0.004443, 0.008776), (-0.000203, -0.006068, 0.008776), (0.016103, -0.003298, 0.018756), (0.025333, -0.000227, 0.021133), (0.025145, 0.000172, 0.021809), (0.016029, -0.002806, 0.01991), (0.015918, -0.001342, 0.02052), (0.024699999, 0.001914, 0.021971), (0.024339, 0.0031639999, 0.021294), (0.015802, -0.000014, 0.019632), (0.015766, 0.000609, 0.017861), (0.024299, 0.003209, 0.019906), (0.024733, 0.001684, 0.019705), (0.016212, -0.001001, 0.016693), (0.01456, -0.001172, 0.016664), (0.015116, -0.003582, 0.018680999), (0.015413, -0.003076, 0.020074), (0.015332, -0.00155, 0.020525), (0.014978, -0.000314, 0.019687), (0.014697, 0.000275, 0.017828), (0.01365, 0.000073, 0.018336), (0.013132, -0.001343, 0.017393), (0.008763, 0.001009, 0.022983), (0.014246, -0.000415, 0.019961), (0.009495, 0.000125, 0.02585), (0.01481, -0.001656, 0.0208), (0.009867, -0.002564, 0.027253), (0.009565, -0.0051969998, 0.025936), (0.00808, -0.005702, 0.027113), (0.008291, -0.002778, 0.021583), (0.014059, -0.003843, 0.019382), (0.014718, -0.003468, 0.020399), (0.00878, -0.00537, 0.023032), (0.013371, -0.003485, 0.01766), (0.014775, -0.003135, 0.017135), (0.016179, -0.002784, 0.017088), (0.025268, -0.000053, 0.020359), (0.008477, -0.002829, 0.028607), (0.008215, 0.000282, 0.027252), (0.007442, 0.001759, 0.023591), (0.007358, -0.002884, 0.021985), (0.007345, -0.0058689998, 0.023524), (0.007358, -0.002884, 0.021985), (0.008291, -0.002778, 0.021583), (0.013132, -0.001343, 0.017393), (0.01456, -0.001172, 0.016664), (0.016212, -0.001001, 0.016693), (0.024733, 0.001684, 0.019705), (0.006585, -0.005942, 0.043777), (0.00497, -0.008772, 0.044124), (0.004061, -0.008187, 0.039042), (0.006525, -0.000252, 0.042884), (0.004672, 0.00237, 0.046454), (0.005115, -0.004497, 0.048707), (0.00465, -0.007409, 0.048084), (-0.000303, -0.010905, 0.044189), (-0.000303, -0.009974, 0.039087), (-0.005575, -0.008772, 0.044124), (-0.004667, -0.008187, 0.039042), (-0.006797, -0.003804, 0.03824), (-0.004346, -0.001456, 0.037386), (-0.008662, -0.005457, 0.035471), (-0.006692, -0.006127, 0.038776), (-0.008458, -0.008954, 0.029917), (-0.000303, -0.011135, 0.027254), (0.007853, -0.008954, 0.029917), (0.006087, -0.006127, 0.038776), (0.006192, -0.003803, 0.03824), (0.003741, -0.001456, 0.037386), (0.004585, 0.001947, 0.042495), (0.003326, 0.003688, 0.043942), (-0.000303, 0.004492, 0.045621), (-0.000958, 0.00339, 0.049078), (-0.000303, -0.001456, 0.037386), (-0.000303, -0.007719, 0.027835), (-0.000303, -0.011135, 0.027254), (-0.000303, -0.007719, 0.027835), (0.008057, -0.005457, 0.035471), (-0.000303, -0.001456, 0.037386), (-0.001376, -0.004199, 0.051465), (-0.000886, -0.008421, 0.050075), (-0.005288, -0.007409, 0.048205), (-0.00719, -0.005942, 0.043777), (-0.006801, -0.000285, 0.042888), (-0.00522, 0.001964, 0.04264), (-0.003999, 0.003768, 0.044128), (-0.005219, 0.002292, 0.046907), (-0.000958, 0.00339, 0.049078), (-0.001376, -0.004199, 0.051465), (-0.000303, 0.004492, 0.045621), (-0.005652, -0.004497, 0.048933), (0.027426, 0.005405, 0.020494), (0.028601, 0.006658, 0.01785), (0.027808, 0.0066649998, 0.01739), (0.030743, 0.00558, 0.017286), (0.02774, 0.004713, 0.019235), (0.028601, 0.006658, 0.01785), (0.02774, 0.004713, 0.019235), (0.02774, 0.004713, 0.019235), (0.028628, 0.006065, 0.017531), (0.031748, 0.003632, 0.017663), (0.03128, 0.005805, 0.017662), (0.032393, 0.003858, 0.018189), (0.031899, 0.001612, 0.017732), (0.032544, 0.001837, 0.018257), (0.032278, 0.001542, 0.017993), (0.027898, 0.006023, 0.017011), (0.025602, 0.004141, 0.01933), (0.025495, 0.004716, 0.020923), (0.029009, 0.003171, 0.021347), (0.029233, 0.000911, 0.021532), (0.029235, 0.000498, 0.02097), (0.029063, 0.00066, 0.02016), (0.031899, 0.001612, 0.017732), (0.031748, 0.003632, 0.017663), (0.0285, 0.002864, 0.019675), (0.02774, 0.004713, 0.019235), (0.025602, 0.004141, 0.01933), (0.027898, 0.006023, 0.017011), (0.028628, 0.006065, 0.017531), (0.027808, 0.0066649998, 0.01739), (0.028601, 0.006658, 0.01785), (0.030743, 0.00558, 0.017286), (0.027255, 0.000399, 0.021854), (0.027251, -0.000023, 0.02124), (0.027073, 0.000127, 0.020531), (0.026469, 0.00221, 0.019892), (0.024299, 0.003209, 0.019906), (0.024733, 0.001684, 0.019705), (0.025268, -0.000053, 0.020359), (0.025333, -0.000227, 0.021133), (0.025145, 0.000172, 0.021809), (0.026888, 0.002515, 0.021818), (0.024339, 0.0031639999, 0.021294), (0.024299, 0.003209, 0.019906), (0.024699999, 0.001914, 0.021971), (0.003521, -0.007963, 0.026045), (0.00556, -0.005284, 0.017088), (0.007345, -0.0058689998, 0.023524), (0.00808, -0.005702, 0.027113), (0.006406, -0.001127, 0.016522), (0.007358, -0.002884, 0.021985), (0.006235, -0.000359, 0.01157), (0.004931, -0.003495, 0.011341), (-0.000303, -0.004561, 0.011621), (-0.000303, -0.00642, 0.016878), (-0.000303, -0.008483, 0.0261), (0.002281, -0.003158, 0.031252), (0.008477, -0.002829, 0.028607), (-0.006165, -0.005293, 0.017088), (-0.005536, -0.003495, 0.011341), (-0.004127, -0.007963, 0.026045), (-0.000303, -0.004683, 0.031822998), (0.001907, -0.003077, 0.035097), (0.006501, -0.00483, 0.006868), (0.008805, -0.000442, 0.007168), (-0.000303, -0.006638, 0.006972), (-0.007106, -0.00483, 0.006868), (-0.00941, -0.000442, 0.007168), (-0.00684, -0.000359, 0.01157), (-0.007011, -0.001127, 0.016424), (-0.00765, -0.00636, 0.023361), (-0.008505, -0.0056869998, 0.027039), (-0.002886, -0.003059, 0.031367), (-0.002512, -0.003077, 0.035097), (-0.000303, -0.004216, 0.035351), (0.002811, -0.004666, 0.037729), (-0.007963, -0.002157, 0.021094), (-0.009465, -0.002537, 0.028095), (-0.000303, -0.005561, 0.037547998), (-0.003416, -0.004666, 0.037729), (-0.000303, 0.00029, 0.029396), (0.0017959999, -0.000382, 0.029821), (0.005476, 0.00335, 0.024023), (-0.000303, 0.004514, 0.023531), (0.007335, 0.005709, 0.01955), (-0.000303, 0.00618, 0.01925), (0.005107, 0.002825, 0.015762), (-0.002141, 0.000212, 0.033739), (-0.000303, 0.001, 0.032749), (0.001536, 0.000212, 0.033739), (0.002385, -0.000988, 0.035031), (0.006406, -0.001127, 0.016522), (0.006235, -0.000359, 0.01157), (0.007442, 0.001759, 0.023591), (0.008215, 0.000282, 0.027252), (0.008477, -0.002829, 0.028607), (0.002281, -0.003158, 0.031252), (0.001907, -0.003077, 0.035097), (0.003842, -0.002084, 0.03775), (0.002811, -0.004666, 0.037729), (0.007358, -0.002884, 0.021985), (-0.00299, -0.000988, 0.035031), (-0.002401, -0.000382, 0.029821), (-0.006081, 0.00335, 0.024078), (-0.00794, 0.005709, 0.01955), (-0.000303, 0.004785, 0.015966), (0.005138, 0.00285, 0.011577), (0.008805, -0.000442, 0.007168), (0.006739, 0.004012, 0.007162), (-0.000303, 0.004771, 0.01137), (-0.000303, 0.0066959998, 0.007168), (-0.007344, 0.004012, 0.007162), (-0.005743, 0.00285, 0.011577), (-0.00684, -0.000359, 0.01157), (-0.005712, 0.002825, 0.015762), (-0.00941, -0.000442, 0.007168), (-0.007011, -0.001127, 0.016424), (-0.008734, 0.001451, 0.022807), (-0.009176, 0.000092, 0.026316), (-0.002886, -0.003059, 0.031367), (-0.009465, -0.002537, 0.028095), (-0.007963, -0.002157, 0.021094), (-0.002512, -0.003077, 0.035097), (-0.003416, -0.004666, 0.037729), (-0.004448, -0.002084, 0.03775), (-0.002886, -0.003059, 0.031367)] + color3f[] primvars:HulaGirl_UpperBody_mesh_color = [(1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1)] ( + interpolation = "faceVarying" + ) + texCoord2f[] primvars:HulaGirl_UpperBody_mesh_UV = [(0.842713, 0.073736), (0.841442, 0.078114), (0.821381, 0.080244), (0.821381, 0.080244), (0.814478, 0.065341), (0.842713, 0.073736), (0.846021, 0.066612996), (0.842713, 0.073736), (0.814478, 0.065341), (0.814478, 0.065341), (0.814224, 0.044482), (0.846021, 0.066612996), (0.849073, 0.062289), (0.846021, 0.066612996), (0.814224, 0.044482), (0.814224, 0.044482), (0.82541597, 0.038377), (0.849073, 0.062289), (0.817785, 0.02693), (0.82541597, 0.038377), (0.814224, 0.044482), (0.814224, 0.044482), (0.806163, 0.028885), (0.817785, 0.02693), (0.806163, 0.028885), (0.814224, 0.044482), (0.788786, 0.047789), (0.788786, 0.047789), (0.789549, 0.027948), (0.806163, 0.028885), (0.771233, 0.027948), (0.789549, 0.027948), (0.788786, 0.047789), (0.814224, 0.044482), (0.814478, 0.065341), (0.78853, 0.067376), (0.78853, 0.067376), (0.788786, 0.047789), (0.814224, 0.044482), (0.814478, 0.065341), (0.821381, 0.080244), (0.805793, 0.116217), (0.805793, 0.116217), (0.80150497, 0.112147), (0.814478, 0.065341), (0.814478, 0.065341), (0.80150497, 0.112147), (0.781663, 0.110875), (0.781663, 0.110875), (0.78853, 0.067376), (0.814478, 0.065341), (0.766639, 0.063626), (0.78853, 0.067376), (0.781663, 0.110875), (0.788786, 0.047789), (0.78853, 0.067376), (0.766639, 0.063626), (0.766639, 0.063626), (0.767034, 0.0455), (0.788786, 0.047789), (0.788786, 0.047789), (0.767034, 0.0455), (0.771233, 0.027948), (0.771233, 0.027948), (0.767034, 0.0455), (0.760421, 0.0455), (0.760421, 0.0455), (0.762919, 0.026992999), (0.771233, 0.027948), (0.762919, 0.026992999), (0.760421, 0.0455), (0.753807, 0.0455), (0.753807, 0.0455), (0.754636, 0.026801), (0.762919, 0.026992999), (0.754636, 0.026801), (0.753807, 0.0455), (0.737401, 0.048553), (0.737401, 0.048553), (0.734858, 0.02968), (0.754636, 0.026801), (0.720135, 0.032192), (0.734858, 0.02968), (0.737401, 0.048553), (0.737401, 0.048553), (0.715016, 0.0455), (0.720135, 0.032192), (0.715016, 0.0455), (0.737401, 0.048553), (0.738928, 0.068648), (0.738928, 0.068648), (0.719341, 0.067631), (0.715016, 0.0455), (0.719341, 0.067631), (0.738928, 0.068648), (0.739691, 0.108586), (0.739691, 0.108586), (0.719086, 0.107314), (0.719341, 0.067631), (0.761313, 0.10655), (0.758374, 0.110929), (0.755446, 0.065406), (0.755446, 0.065406), (0.758374, 0.110929), (0.739691, 0.108586), (0.739691, 0.108586), (0.738928, 0.068648), (0.755446, 0.065406), (0.755446, 0.065406), (0.738928, 0.068648), (0.737401, 0.048553), (0.737401, 0.048553), (0.753807, 0.0455), (0.755446, 0.065406), (0.755446, 0.065406), (0.753807, 0.0455), (0.760421, 0.0455), (0.760421, 0.0455), (0.760609, 0.064605), (0.755446, 0.065406), (0.760609, 0.064605), (0.760421, 0.0455), (0.767034, 0.0455), (0.767034, 0.0455), (0.766639, 0.063626), (0.760609, 0.064605), (0.760609, 0.064605), (0.766639, 0.063626), (0.765269, 0.10655), (0.781663, 0.110875), (0.765269, 0.10655), (0.766639, 0.063626), (0.765269, 0.10655), (0.781663, 0.110875), (0.780391, 0.116217), (0.780391, 0.116217), (0.781663, 0.110875), (0.80150497, 0.112147), (0.80150497, 0.112147), (0.805793, 0.116217), (0.780391, 0.116217), (0.755446, 0.065406), (0.760609, 0.064605), (0.761313, 0.10655), (0.765269, 0.10655), (0.761313, 0.10655), (0.760609, 0.064605), (0.761313, 0.10655), (0.765269, 0.10655), (0.758374, 0.110929), (0.780391, 0.116217), (0.758374, 0.110929), (0.765269, 0.10655), (0.70535, 0.082639), (0.702297, 0.085437), (0.696192, 0.080095), (0.696192, 0.080095), (0.697464, 0.075008), (0.70535, 0.082639), (0.70535, 0.082639), (0.697464, 0.075008), (0.715016, 0.0455), (0.715016, 0.0455), (0.719341, 0.067631), (0.70535, 0.082639), (0.27801698, 0.742526), (0.317856, 0.742714), (0.344793, 0.788533), (0.277828, 0.691861), (0.32352, 0.705833), (0.317856, 0.742714), (0.317856, 0.742714), (0.32352, 0.705833), (0.380165, 0.753791), (0.380165, 0.753791), (0.344793, 0.788533), (0.317856, 0.742714), (0.380165, 0.753791), (0.37085, 0.777581), (0.344793, 0.788533), (0.37085, 0.777581), (0.380165, 0.753791), (0.412137, 0.831582), (0.125582, 0.841589), (0.139617, 0.86003), (0.107393, 0.877023), (0.149435, 0.903583), (0.107393, 0.877023), (0.139617, 0.86003), (0.107393, 0.877023), (0.149435, 0.903583), (0.114064, 0.943611), (0.114064, 0.943611), (0.149435, 0.903583), (0.220681, 0.909499), (0.220681, 0.909499), (0.220429, 0.979863), (0.114064, 0.943611), (0.220429, 0.979863), (0.220681, 0.909499), (0.291926, 0.903772), (0.291926, 0.903772), (0.327171, 0.94411397), (0.220429, 0.979863), (0.327171, 0.94411397), (0.291926, 0.903772), (0.333968, 0.877401), (0.333968, 0.877401), (0.409116, 0.874883), (0.327171, 0.94411397), (0.409116, 0.874883), (0.333968, 0.877401), (0.356499, 0.834981), (0.356499, 0.834981), (0.412137, 0.831582), (0.409116, 0.874883), (0.412137, 0.831582), (0.356499, 0.834981), (0.389731, 0.817232), (0.412137, 0.831582), (0.389731, 0.817232), (0.37085, 0.777581), (0.37085, 0.777581), (0.389731, 0.817232), (0.356499, 0.834981), (0.356499, 0.834981), (0.344793, 0.788533), (0.37085, 0.777581), (0.299038, 0.792939), (0.344793, 0.788533), (0.356499, 0.834981), (0.344793, 0.788533), (0.299038, 0.792939), (0.27801698, 0.742526), (0.27801698, 0.742526), (0.299038, 0.792939), (0.265618, 0.780162), (0.265618, 0.780162), (0.299038, 0.792939), (0.289125, 0.828845), (0.114064, 0.943611), (0.032245, 0.874128), (0.107393, 0.877023), (0.085113, 0.834351), (0.107393, 0.877023), (0.032245, 0.874128), (0.107393, 0.877023), (0.085113, 0.834351), (0.125582, 0.841589), (0.032245, 0.874128), (0.029476, 0.830701), (0.085113, 0.834351), (0.333968, 0.877401), (0.30187, 0.860345), (0.316031, 0.84203), (0.30187, 0.860345), (0.333968, 0.877401), (0.291926, 0.903772), (0.316031, 0.84203), (0.356499, 0.834981), (0.333968, 0.877401), (0.356499, 0.834981), (0.316031, 0.84203), (0.299038, 0.792939), (0.316031, 0.84203), (0.289125, 0.828845), (0.299038, 0.792939), (0.289125, 0.828845), (0.316031, 0.84203), (0.30187, 0.860345), (0.289125, 0.828845), (0.30187, 0.860345), (0.265806, 0.865883), (0.291926, 0.903772), (0.265806, 0.865883), (0.30187, 0.860345), (0.220681, 0.867582), (0.265806, 0.865883), (0.291926, 0.903772), (0.291926, 0.903772), (0.220681, 0.909499), (0.220681, 0.867582), (0.175617, 0.865757), (0.220681, 0.867582), (0.220681, 0.909499), (0.220681, 0.909499), (0.149435, 0.903583), (0.175617, 0.865757), (0.139617, 0.86003), (0.175617, 0.865757), (0.149435, 0.903583), (0.175617, 0.865757), (0.139617, 0.86003), (0.15226799, 0.828467), (0.15226799, 0.828467), (0.139617, 0.86003), (0.125582, 0.841589), (0.125582, 0.841589), (0.142575, 0.792624), (0.15226799, 0.828467), (0.142575, 0.792624), (0.125582, 0.841589), (0.085113, 0.834351), (0.085113, 0.834351), (0.096945, 0.788029), (0.142575, 0.792624), (0.070888996, 0.776826), (0.096945, 0.788029), (0.085113, 0.834351), (0.0617, 0.753036), (0.096945, 0.788029), (0.070888996, 0.776826), (0.029476, 0.830701), (0.0617, 0.753036), (0.070888996, 0.776826), (0.070888996, 0.776826), (0.051882, 0.816603), (0.029476, 0.830701), (0.029476, 0.830701), (0.051882, 0.816603), (0.085113, 0.834351), (0.085113, 0.834351), (0.051882, 0.816603), (0.070888996, 0.776826), (0.176058, 0.779973), (0.15226799, 0.828467), (0.142575, 0.792624), (0.176058, 0.779973), (0.142575, 0.792624), (0.163848, 0.742337), (0.163848, 0.742337), (0.142575, 0.792624), (0.096945, 0.788029), (0.096945, 0.788029), (0.124134, 0.742211), (0.163848, 0.742337), (0.124134, 0.742211), (0.096945, 0.788029), (0.0617, 0.753036), (0.0617, 0.753036), (0.118596, 0.705329), (0.124134, 0.742211), (0.124134, 0.742211), (0.118596, 0.705329), (0.164415, 0.691609), (0.164415, 0.691609), (0.163848, 0.742337), (0.124134, 0.742211), (0.20481999, 0.717414), (0.163848, 0.742337), (0.164415, 0.691609), (0.183044, 0.754736), (0.163848, 0.742337), (0.20481999, 0.717414), (0.20481999, 0.717414), (0.205953, 0.738686), (0.183044, 0.754736), (0.205953, 0.738686), (0.20481999, 0.717414), (0.220932, 0.714644), (0.221184, 0.682546), (0.220932, 0.714644), (0.20481999, 0.717414), (0.221184, 0.682546), (0.237044, 0.717413), (0.220932, 0.714644), (0.163848, 0.742337), (0.183044, 0.754736), (0.176058, 0.779973), (0.164415, 0.691609), (0.221184, 0.682546), (0.20481999, 0.717414), (0.193869, 0.843981), (0.175617, 0.865757), (0.178135, 0.834603), (0.15226799, 0.828467), (0.178135, 0.834603), (0.175617, 0.865757), (0.15226799, 0.828467), (0.189212, 0.808044), (0.178135, 0.834603), (0.176058, 0.779973), (0.189212, 0.808044), (0.15226799, 0.828467), (0.195757, 0.79382), (0.189212, 0.808044), (0.176058, 0.779973), (0.176058, 0.779973), (0.189967, 0.768896), (0.195757, 0.79382), (0.189967, 0.768896), (0.176058, 0.779973), (0.183044, 0.754736), (0.220848, 0.763022), (0.189967, 0.768896), (0.183044, 0.754736), (0.183044, 0.754736), (0.205953, 0.738686), (0.220848, 0.763022), (0.205953, 0.738686), (0.220932, 0.743469), (0.220848, 0.763022), (0.220932, 0.714644), (0.220932, 0.743469), (0.205953, 0.738686), (0.235785, 0.738686), (0.220932, 0.743469), (0.220932, 0.714644), (0.220932, 0.714644), (0.237044, 0.717413), (0.235785, 0.738686), (0.258695, 0.754861), (0.235785, 0.738686), (0.237044, 0.717413), (0.237044, 0.717413), (0.27801698, 0.742526), (0.258695, 0.754861), (0.27801698, 0.742526), (0.237044, 0.717413), (0.221184, 0.682546), (0.221184, 0.682546), (0.277828, 0.691861), (0.27801698, 0.742526), (0.317856, 0.742714), (0.27801698, 0.742526), (0.277828, 0.691861), (0.220848, 0.763022), (0.220932, 0.743469), (0.235785, 0.738686), (0.220848, 0.763022), (0.235785, 0.738686), (0.258695, 0.754861), (0.220765, 0.818743), (0.220681, 0.84442097), (0.193869, 0.843981), (0.189212, 0.808044), (0.220765, 0.818743), (0.193869, 0.843981), (0.193869, 0.843981), (0.178135, 0.834603), (0.189212, 0.808044), (0.220764, 0.804645), (0.220765, 0.818743), (0.189212, 0.808044), (0.220764, 0.804645), (0.189212, 0.808044), (0.195757, 0.79382), (0.195757, 0.79382), (0.220681, 0.796169), (0.220764, 0.804645), (0.265618, 0.780162), (0.258695, 0.754861), (0.27801698, 0.742526), (0.251646, 0.769148), (0.258695, 0.754861), (0.265618, 0.780162), (0.258695, 0.754861), (0.251646, 0.769148), (0.220848, 0.763022), (0.220764, 0.7694), (0.220848, 0.763022), (0.251646, 0.769148), (0.189967, 0.768896), (0.220848, 0.763022), (0.220764, 0.7694), (0.189967, 0.768896), (0.220764, 0.7694), (0.220764, 0.775442), (0.189967, 0.768896), (0.220764, 0.775442), (0.195757, 0.79382), (0.220764, 0.775442), (0.220764, 0.787106), (0.195757, 0.79382), (0.220764, 0.787106), (0.220681, 0.796169), (0.195757, 0.79382), (0.220764, 0.787106), (0.245855, 0.794071), (0.220681, 0.796169), (0.220764, 0.804645), (0.220681, 0.796169), (0.245855, 0.794071), (0.245855, 0.794071), (0.252401, 0.808169), (0.220764, 0.804645), (0.252401, 0.808169), (0.220765, 0.818743), (0.220764, 0.804645), (0.247555, 0.84404397), (0.220765, 0.818743), (0.252401, 0.808169), (0.220765, 0.818743), (0.247555, 0.84404397), (0.220681, 0.84442097), (0.220681, 0.867582), (0.220681, 0.84442097), (0.247555, 0.84404397), (0.193869, 0.843981), (0.220681, 0.84442097), (0.220681, 0.867582), (0.220681, 0.867582), (0.175617, 0.865757), (0.193869, 0.843981), (0.251646, 0.769148), (0.220764, 0.775442), (0.220764, 0.7694), (0.245855, 0.794071), (0.220764, 0.787106), (0.220764, 0.775442), (0.245855, 0.794071), (0.220764, 0.775442), (0.251646, 0.769148), (0.245855, 0.794071), (0.251646, 0.769148), (0.265618, 0.780162), (0.265618, 0.780162), (0.252401, 0.808169), (0.245855, 0.794071), (0.265618, 0.780162), (0.289125, 0.828845), (0.252401, 0.808169), (0.252401, 0.808169), (0.289125, 0.828845), (0.263478, 0.834603), (0.265806, 0.865883), (0.263478, 0.834603), (0.289125, 0.828845), (0.263478, 0.834603), (0.265806, 0.865883), (0.247555, 0.84404397), (0.247555, 0.84404397), (0.265806, 0.865883), (0.220681, 0.867582), (0.252401, 0.808169), (0.263478, 0.834603), (0.247555, 0.84404397), (0.638264, 0.505891), (0.642299, 0.514237), (0.615587, 0.551744), (0.462328, 0.504792), (0.464588, 0.490051), (0.500918, 0.495319), (0.500918, 0.495319), (0.499227, 0.510716), (0.462328, 0.504792), (0.499227, 0.510716), (0.500918, 0.495319), (0.526463, 0.517061), (0.526463, 0.517061), (0.524791, 0.535939), (0.499227, 0.510716), (0.524791, 0.535939), (0.526463, 0.517061), (0.549099, 0.535954), (0.549099, 0.535954), (0.547889, 0.554411), (0.524791, 0.535939), (0.547889, 0.554411), (0.549099, 0.535954), (0.572543, 0.540783), (0.572543, 0.540783), (0.578848, 0.564134), (0.547889, 0.554411), (0.578848, 0.564134), (0.572543, 0.540783), (0.606756, 0.535732), (0.606756, 0.535732), (0.615587, 0.551744), (0.578848, 0.564134), (0.615587, 0.551744), (0.606756, 0.535732), (0.638264, 0.505891), (0.638264, 0.505891), (0.606756, 0.535732), (0.608432, 0.424877), (0.638264, 0.505891), (0.608432, 0.424877), (0.629019, 0.421361), (0.629686, 0.412018), (0.629019, 0.421361), (0.608432, 0.424877), (0.608432, 0.424877), (0.612406, 0.416437), (0.629686, 0.412018), (0.612406, 0.416437), (0.608432, 0.424877), (0.589337, 0.427521), (0.589337, 0.427521), (0.590838, 0.420193), (0.612406, 0.416437), (0.590838, 0.420193), (0.589337, 0.427521), (0.571171, 0.430825), (0.464588, 0.490051), (0.49213, 0.420641), (0.510888, 0.429495), (0.510888, 0.429495), (0.500918, 0.495319), (0.464588, 0.490051), (0.526463, 0.517061), (0.500918, 0.495319), (0.510888, 0.429495), (0.510888, 0.429495), (0.531273, 0.433582), (0.526463, 0.517061), (0.549099, 0.535954), (0.526463, 0.517061), (0.531273, 0.433582), (0.531273, 0.433582), (0.55387, 0.435314), (0.549099, 0.535954), (0.549099, 0.535954), (0.55387, 0.435314), (0.571171, 0.430825), (0.571171, 0.430825), (0.572543, 0.540783), (0.549099, 0.535954), (0.606756, 0.535732), (0.572543, 0.540783), (0.571171, 0.430825), (0.571171, 0.430825), (0.589337, 0.427521), (0.606756, 0.535732), (0.606756, 0.535732), (0.589337, 0.427521), (0.608432, 0.424877), (0.571171, 0.430825), (0.570631, 0.42391), (0.590838, 0.420193), (0.570631, 0.42391), (0.571171, 0.430825), (0.55387, 0.435314), (0.55387, 0.435314), (0.553951, 0.425835), (0.570631, 0.42391), (0.553951, 0.425835), (0.55387, 0.435314), (0.531273, 0.433582), (0.531273, 0.433582), (0.532272, 0.423963), (0.553951, 0.425835), (0.532272, 0.423963), (0.531273, 0.433582), (0.510888, 0.429495), (0.510888, 0.429495), (0.511827, 0.41953), (0.532272, 0.423963), (0.511827, 0.41953), (0.510888, 0.429495), (0.49213, 0.420641), (0.49213, 0.420641), (0.493232, 0.411159), (0.511827, 0.41953), (0.512884, 0.406777), (0.511827, 0.41953), (0.493232, 0.411159), (0.493232, 0.411159), (0.496014, 0.400551), (0.512884, 0.406777), (0.517508, 0.345461), (0.512884, 0.406777), (0.496014, 0.400551), (0.496014, 0.400551), (0.505758, 0.344479), (0.517508, 0.345461), (0.532272, 0.423963), (0.511827, 0.41953), (0.512884, 0.406777), (0.512884, 0.406777), (0.532811, 0.411393), (0.532272, 0.423963), (0.553951, 0.425835), (0.532272, 0.423963), (0.532811, 0.411393), (0.512884, 0.406777), (0.517508, 0.345461), (0.534649, 0.34645998), (0.534649, 0.34645998), (0.532811, 0.411393), (0.512884, 0.406777), (0.554195, 0.412892), (0.532811, 0.411393), (0.534649, 0.34645998), (0.532811, 0.411393), (0.554195, 0.412892), (0.553951, 0.425835), (0.553951, 0.425835), (0.554195, 0.412892), (0.570249, 0.410856), (0.570249, 0.410856), (0.570631, 0.42391), (0.553951, 0.425835), (0.590838, 0.420193), (0.570631, 0.42391), (0.570249, 0.410856), (0.570249, 0.410856), (0.592254, 0.408093), (0.590838, 0.420193), (0.612406, 0.416437), (0.590838, 0.420193), (0.592254, 0.408093), (0.592254, 0.408093), (0.610264, 0.405665), (0.612406, 0.416437), (0.629686, 0.412018), (0.612406, 0.416437), (0.610264, 0.405665), (0.610264, 0.405665), (0.626367, 0.40013), (0.629686, 0.412018), (0.626367, 0.40013), (0.610264, 0.405665), (0.603193, 0.339891), (0.603193, 0.339891), (0.620593, 0.343485), (0.626367, 0.40013), (0.583442, 0.339973), (0.603193, 0.339891), (0.610264, 0.405665), (0.610264, 0.405665), (0.592254, 0.408093), (0.583442, 0.339973), (0.56771, 0.340698), (0.583442, 0.339973), (0.592254, 0.408093), (0.592254, 0.408093), (0.570249, 0.410856), (0.56771, 0.340698), (0.56771, 0.340698), (0.570249, 0.410856), (0.554195, 0.412892), (0.554195, 0.412892), (0.550406, 0.343655), (0.56771, 0.340698), (0.534649, 0.34645998), (0.550406, 0.343655), (0.554195, 0.412892), (0.174921, 0.643553), (0.137789, 0.659176), (0.150473, 0.634417), (0.078788, 0.629139), (0.150473, 0.634417), (0.137789, 0.659176), (0.137789, 0.659176), (0.070563, 0.648283), (0.078788, 0.629139), (0.070563, 0.648283), (0.052671, 0.629224), (0.078788, 0.629139), (0.078788, 0.629139), (0.052671, 0.629224), (0.077932, 0.60364), (0.150473, 0.634417), (0.078788, 0.629139), (0.077932, 0.60364), (0.077932, 0.60364), (0.148692, 0.610924), (0.150473, 0.634417), (0.192981, 0.620523), (0.150473, 0.634417), (0.148692, 0.610924), (0.148692, 0.610924), (0.176644, 0.603529), (0.192981, 0.620523), (0.150473, 0.634417), (0.192981, 0.620523), (0.174921, 0.643553), (0.177251, 0.295598), (0.041293, 0.157979), (0.102859996, 0.106773), (0.102859996, 0.106773), (0.212275, 0.256424), (0.177251, 0.295598), (0.212275, 0.256424), (0.102859996, 0.106773), (0.174592, 0.060085), (0.174592, 0.060085), (0.255289, 0.225869), (0.212275, 0.256424), (0.255289, 0.225869), (0.174592, 0.060085), (0.228682, 0.037759), (0.228682, 0.037759), (0.285324, 0.214472), (0.255289, 0.225869), (0.285324, 0.214472), (0.228682, 0.037759), (0.281076, 0.023534), (0.281076, 0.023534), (0.312311, 0.207759), (0.285324, 0.214472), (0.312311, 0.207759), (0.281076, 0.023534), (0.338048, 0.017299), (0.338048, 0.017299), (0.34383, 0.203916), (0.312311, 0.207759), (0.375518, 0.205699), (0.34383, 0.203916), (0.338048, 0.017299), (0.338048, 0.017299), (0.395557, 0.02002), (0.375518, 0.205699), (0.402947, 0.210903), (0.375518, 0.205699), (0.395557, 0.02002), (0.395557, 0.02002), (0.448392, 0.030912), (0.402947, 0.210903), (0.433484, 0.220175), (0.402947, 0.210903), (0.448392, 0.030912), (0.448392, 0.030912), (0.503487, 0.049922), (0.433484, 0.220175), (0.478311, 0.248103), (0.433484, 0.220175), (0.503487, 0.049922), (0.503487, 0.049922), (0.578184, 0.09193), (0.478311, 0.248103), (0.578184, 0.09193), (0.643195, 0.139031), (0.515744, 0.285119), (0.515744, 0.285119), (0.478311, 0.248103), (0.578184, 0.09193), (0.478311, 0.248103), (0.515744, 0.285119), (0.488986, 0.302412), (0.488986, 0.302412), (0.460711, 0.271065), (0.478311, 0.248103), (0.433484, 0.220175), (0.478311, 0.248103), (0.460711, 0.271065), (0.460711, 0.271065), (0.421652, 0.246034), (0.433484, 0.220175), (0.402947, 0.210903), (0.433484, 0.220175), (0.421652, 0.246034), (0.421652, 0.246034), (0.395557, 0.238801), (0.402947, 0.210903), (0.375518, 0.205699), (0.402947, 0.210903), (0.395557, 0.238801), (0.395557, 0.238801), (0.37215, 0.234773), (0.375518, 0.205699), (0.34383, 0.203916), (0.375518, 0.205699), (0.37215, 0.234773), (0.37215, 0.234773), (0.344883, 0.233377), (0.34383, 0.203916), (0.312311, 0.207759), (0.34383, 0.203916), (0.344883, 0.233377), (0.344883, 0.233377), (0.317542, 0.236595), (0.312311, 0.207759), (0.285324, 0.214472), (0.312311, 0.207759), (0.317542, 0.236595), (0.317542, 0.236595), (0.294616, 0.242179), (0.285324, 0.214472), (0.255289, 0.225869), (0.285324, 0.214472), (0.294616, 0.242179), (0.294616, 0.242179), (0.268752, 0.251136), (0.255289, 0.225869), (0.212275, 0.256424), (0.255289, 0.225869), (0.268752, 0.251136), (0.268752, 0.251136), (0.231226, 0.278376), (0.212275, 0.256424), (0.177251, 0.295598), (0.212275, 0.256424), (0.231226, 0.278376), (0.231226, 0.278376), (0.205087, 0.311233), (0.177251, 0.295598), (0.087743, 0.424366), (0.117637, 0.332707), (0.127798, 0.33514), (0.127798, 0.33514), (0.103675, 0.429072), (0.087743, 0.424366), (0.123951, 0.430431), (0.103675, 0.429072), (0.127798, 0.33514), (0.127798, 0.33514), (0.140843, 0.337607), (0.123951, 0.430431), (0.123951, 0.430431), (0.140843, 0.337607), (0.151256, 0.33941), (0.151256, 0.33941), (0.142728, 0.428347), (0.123951, 0.430431), (0.163425, 0.424234), (0.142728, 0.428347), (0.151256, 0.33941), (0.151256, 0.33941), (0.162173, 0.340054), (0.163425, 0.424234), (0.163425, 0.424234), (0.162173, 0.340054), (0.178002, 0.338975), (0.178002, 0.338975), (0.183098, 0.418589), (0.163425, 0.424234), (0.163425, 0.424234), (0.183098, 0.418589), (0.184613, 0.427408), (0.085471, 0.434874), (0.087743, 0.424366), (0.103675, 0.429072), (0.103675, 0.429072), (0.102344, 0.438985), (0.085471, 0.434874), (0.102344, 0.438985), (0.103675, 0.429072), (0.123951, 0.430431), (0.123951, 0.430431), (0.123668, 0.43877), (0.102344, 0.438985), (0.123668, 0.43877), (0.123951, 0.430431), (0.142728, 0.428347), (0.142728, 0.428347), (0.142615, 0.437829), (0.123668, 0.43877), (0.142615, 0.437829), (0.142728, 0.428347), (0.163425, 0.424234), (0.163425, 0.424234), (0.165272, 0.433682), (0.142615, 0.437829), (0.184613, 0.427408), (0.165272, 0.433682), (0.163425, 0.424234), (0.166381, 0.444309), (0.165272, 0.433682), (0.184613, 0.427408), (0.184613, 0.427408), (0.187563, 0.43807), (0.166381, 0.444309), (0.171653, 0.518713), (0.166381, 0.444309), (0.187563, 0.43807), (0.143417, 0.447615), (0.166381, 0.444309), (0.171653, 0.518713), (0.171653, 0.518713), (0.14324, 0.529161), (0.143417, 0.447615), (0.12377, 0.44891), (0.143417, 0.447615), (0.14324, 0.529161), (0.14324, 0.529161), (0.117437, 0.535404), (0.12377, 0.44891), (0.12377, 0.44891), (0.117437, 0.535404), (0.091512, 0.529104), (0.084267, 0.549479), (0.091512, 0.529104), (0.117437, 0.535404), (0.187563, 0.43807), (0.19977, 0.501806), (0.171653, 0.518713), (0.08275, 0.444347), (0.085471, 0.434874), (0.102344, 0.438985), (0.102344, 0.438985), (0.100702, 0.449069), (0.08275, 0.444347), (0.100702, 0.449069), (0.102344, 0.438985), (0.123668, 0.43877), (0.142615, 0.437829), (0.165272, 0.433682), (0.166381, 0.444309), (0.166381, 0.444309), (0.143417, 0.447615), (0.142615, 0.437829), (0.123668, 0.43877), (0.142615, 0.437829), (0.143417, 0.447615), (0.143417, 0.447615), (0.12377, 0.44891), (0.123668, 0.43877), (0.123668, 0.43877), (0.12377, 0.44891), (0.100702, 0.449069), (0.091512, 0.529104), (0.100702, 0.449069), (0.12377, 0.44891), (0.08275, 0.444347), (0.100702, 0.449069), (0.091512, 0.529104), (0.091512, 0.529104), (0.065324, 0.511877), (0.08275, 0.444347), (0.065324, 0.511877), (0.066156, 0.43839), (0.08275, 0.444347), (0.085471, 0.434874), (0.08275, 0.444347), (0.066156, 0.43839), (0.066156, 0.43839), (0.069328, 0.428085), (0.085471, 0.434874), (0.087743, 0.424366), (0.085471, 0.434874), (0.069328, 0.428085), (0.069328, 0.428085), (0.073493, 0.419792), (0.087743, 0.424366), (0.117637, 0.332707), (0.087743, 0.424366), (0.073493, 0.419792), (0.073493, 0.419792), (0.101393, 0.32766598), (0.117637, 0.332707), (0.117437, 0.535404), (0.113649, 0.556252), (0.084267, 0.549479), (0.113649, 0.556252), (0.117437, 0.535404), (0.14324, 0.529161), (0.14324, 0.529161), (0.143437, 0.551524), (0.113649, 0.556252), (0.143437, 0.551524), (0.14324, 0.529161), (0.171653, 0.518713), (0.171653, 0.518713), (0.179906, 0.538577), (0.143437, 0.551524), (0.179906, 0.538577), (0.171653, 0.518713), (0.19977, 0.501806), (0.19977, 0.501806), (0.204726, 0.508451), (0.179906, 0.538577), (0.091512, 0.529104), (0.084267, 0.549479), (0.055545, 0.526675), (0.055545, 0.526675), (0.065324, 0.511877), (0.091512, 0.529104), (0.065324, 0.511877), (0.055545, 0.526675), (0.033503, 0.504105), (0.033503, 0.504105), (0.038434, 0.493582), (0.065324, 0.511877), (0.065324, 0.511877), (0.038434, 0.493582), (0.066156, 0.43839), (0.038434, 0.493582), (0.049949, 0.430902), (0.066156, 0.43839), (0.069328, 0.428085), (0.066156, 0.43839), (0.049949, 0.430902), (0.049949, 0.430902), (0.05367, 0.421584), (0.069328, 0.428085), (0.073493, 0.419792), (0.069328, 0.428085), (0.05367, 0.421584), (0.05367, 0.421584), (0.058038, 0.410794), (0.073493, 0.419792), (0.101393, 0.32766598), (0.073493, 0.419792), (0.058038, 0.410794), (0.058038, 0.410794), (0.084799, 0.323319), (0.101393, 0.32766598), (0.502103, 0.820823), (0.52637297, 0.820056), (0.527259, 0.856727), (0.471809, 0.820056), (0.46301, 0.76939), (0.508008, 0.782204), (0.508008, 0.782204), (0.502103, 0.820823), (0.471809, 0.820056), (0.502103, 0.820823), (0.508008, 0.782204), (0.52596, 0.791416), (0.52596, 0.791416), (0.52637297, 0.820056), (0.502103, 0.820823), (0.565524, 0.815863), (0.52637297, 0.820056), (0.52596, 0.791416), (0.527259, 0.856727), (0.52637297, 0.820056), (0.565524, 0.815863), (0.565524, 0.815863), (0.567296, 0.854719), (0.527259, 0.856727), (0.567296, 0.854719), (0.565524, 0.815863), (0.603672, 0.810194), (0.603672, 0.810194), (0.606152, 0.849523), (0.567296, 0.854719), (0.656227, 0.852003), (0.687053, 0.86298597), (0.642763, 0.875919), (0.631544, 0.848578), (0.656227, 0.852003), (0.642763, 0.875919), (0.642763, 0.875919), (0.621328, 0.91631), (0.631544, 0.848578), (0.621328, 0.91631), (0.606152, 0.849523), (0.631544, 0.848578), (0.606152, 0.849523), (0.621328, 0.91631), (0.572728, 0.953099), (0.572728, 0.953099), (0.567296, 0.854719), (0.606152, 0.849523), (0.567296, 0.854719), (0.572728, 0.953099), (0.519641, 0.923101), (0.519641, 0.923101), (0.527259, 0.856727), (0.567296, 0.854719), (0.501394, 0.85802597), (0.527259, 0.856727), (0.519641, 0.923101), (0.527259, 0.856727), (0.501394, 0.85802597), (0.502103, 0.820823), (0.502103, 0.820823), (0.501394, 0.85802597), (0.47718298, 0.862396), (0.47718298, 0.862396), (0.471809, 0.820056), (0.502103, 0.820823), (0.471809, 0.820056), (0.47718298, 0.862396), (0.447539, 0.875741), (0.447539, 0.875741), (0.446594, 0.822359), (0.471809, 0.820056), (0.471809, 0.820056), (0.446594, 0.822359), (0.435965, 0.772047), (0.435965, 0.772047), (0.46301, 0.76939), (0.471809, 0.820056), (0.46301, 0.76939), (0.435965, 0.772047), (0.443996, 0.721735), (0.443996, 0.721735), (0.474112, 0.721026), (0.46301, 0.76939), (0.687053, 0.86298597), (0.71587, 0.890386), (0.636386, 0.968925), (0.636386, 0.968925), (0.642763, 0.875919), (0.687053, 0.86298597), (0.636386, 0.968925), (0.621328, 0.91631), (0.642763, 0.875919), (0.621328, 0.91631), (0.636386, 0.968925), (0.572728, 0.953099), (0.519641, 0.923101), (0.572728, 0.953099), (0.511315, 0.976838), (0.511315, 0.976838), (0.493363, 0.885485), (0.519641, 0.923101), (0.493363, 0.885485), (0.501394, 0.85802597), (0.519641, 0.923101), (0.501394, 0.85802597), (0.493363, 0.885485), (0.47718298, 0.862396), (0.47718298, 0.862396), (0.493363, 0.885485), (0.447539, 0.875741), (0.447539, 0.875741), (0.493363, 0.885485), (0.511315, 0.976838), (0.511315, 0.976838), (0.422501, 0.907629), (0.447539, 0.875741), (0.508008, 0.782204), (0.46301, 0.76939), (0.474112, 0.721026), (0.474112, 0.721026), (0.537298, 0.738033), (0.508008, 0.782204), (0.52596, 0.791416), (0.508008, 0.782204), (0.537298, 0.738033), (0.537298, 0.738033), (0.56516, 0.757867), (0.52596, 0.791416), (0.52596, 0.791416), (0.56516, 0.757867), (0.565524, 0.815863), (0.565524, 0.815863), (0.56516, 0.757867), (0.602491, 0.777479), (0.602491, 0.777479), (0.603672, 0.810194), (0.565524, 0.815863), (0.627882, 0.810548), (0.603672, 0.810194), (0.602491, 0.777479), (0.606152, 0.849523), (0.603672, 0.810194), (0.627882, 0.810548), (0.627882, 0.810548), (0.631544, 0.848578), (0.606152, 0.849523), (0.656227, 0.852003), (0.631544, 0.848578), (0.627882, 0.810548), (0.627882, 0.810548), (0.660302, 0.810371), (0.656227, 0.852003), (0.687053, 0.86298597), (0.656227, 0.852003), (0.660302, 0.810371), (0.660302, 0.810371), (0.685635, 0.813619), (0.687053, 0.86298597), (0.697327, 0.772992), (0.685635, 0.813619), (0.660302, 0.810371), (0.660302, 0.810371), (0.670164, 0.761831), (0.697327, 0.772992), (0.697327, 0.772992), (0.670164, 0.761831), (0.682422, 0.72532), (0.608431, 0.730312), (0.682422, 0.72532), (0.670164, 0.761831), (0.682422, 0.72532), (0.711471, 0.740078), (0.697327, 0.772992), (0.670164, 0.761831), (0.622332, 0.767441), (0.608431, 0.730312), (0.56516, 0.757867), (0.608431, 0.730312), (0.622332, 0.767441), (0.622332, 0.767441), (0.602491, 0.777479), (0.56516, 0.757867), (0.602491, 0.777479), (0.622332, 0.767441), (0.627882, 0.810548), (0.660302, 0.810371), (0.627882, 0.810548), (0.622332, 0.767441), (0.622332, 0.767441), (0.670164, 0.761831), (0.660302, 0.810371), (0.810719, 0.206794), (0.839209, 0.198908), (0.842516, 0.206285), (0.80466, 0.15358), (0.819589, 0.192527), (0.810719, 0.206794), (0.839209, 0.198908), (0.810719, 0.206794), (0.819589, 0.192527), (0.819589, 0.192527), (0.838192, 0.195347), (0.839209, 0.198908), (0.779176, 0.15304), (0.80466, 0.15358), (0.799781, 0.159225), (0.810719, 0.206794), (0.799781, 0.159225), (0.80466, 0.15358), (0.779685, 0.15948), (0.799781, 0.159225), (0.810719, 0.206794), (0.799781, 0.159225), (0.779685, 0.15948), (0.779176, 0.15304), (0.760209, 0.156459), (0.779176, 0.15304), (0.779685, 0.15948), (0.779685, 0.15948), (0.761878, 0.162278), (0.760209, 0.156459), (0.761878, 0.162278), (0.757554, 0.162278), (0.760209, 0.156459), (0.842516, 0.206285), (0.845314, 0.210864), (0.820639, 0.234012), (0.820639, 0.234012), (0.809701, 0.227653), (0.842516, 0.206285), (0.842516, 0.206285), (0.809701, 0.227653), (0.810719, 0.206794), (0.784263, 0.203742), (0.810719, 0.206794), (0.809701, 0.227653), (0.810719, 0.206794), (0.784263, 0.203742), (0.779685, 0.15948), (0.779685, 0.15948), (0.784263, 0.203742), (0.764986, 0.207564), (0.764986, 0.207564), (0.761878, 0.162278), (0.779685, 0.15948), (0.761878, 0.162278), (0.764986, 0.207564), (0.759323, 0.207038), (0.759323, 0.207038), (0.757554, 0.162278), (0.761878, 0.162278), (0.757554, 0.162278), (0.759323, 0.207038), (0.753428, 0.206788), (0.753428, 0.206788), (0.75193, 0.163296), (0.757554, 0.162278), (0.733388, 0.162532), (0.75193, 0.163296), (0.753428, 0.206788), (0.753428, 0.206788), (0.734151, 0.20247), (0.733388, 0.162532), (0.733388, 0.162532), (0.734151, 0.20247), (0.71431, 0.204251), (0.710494, 0.22689), (0.71431, 0.204251), (0.734151, 0.20247), (0.71431, 0.204251), (0.710494, 0.22689), (0.691924, 0.197891), (0.691924, 0.197891), (0.699556, 0.189751), (0.71431, 0.204251), (0.699556, 0.189751), (0.691924, 0.197891), (0.690398, 0.192803), (0.690398, 0.192803), (0.696503, 0.187207), (0.699556, 0.189751), (0.71431, 0.204251), (0.712529, 0.164567), (0.733388, 0.162532), (0.759323, 0.207038), (0.764986, 0.207564), (0.765185, 0.225364), (0.765185, 0.225364), (0.758826, 0.225364), (0.759323, 0.207038), (0.753428, 0.206788), (0.759323, 0.207038), (0.758826, 0.225364), (0.758826, 0.225364), (0.752466, 0.225364), (0.753428, 0.206788), (0.753428, 0.206788), (0.752466, 0.225364), (0.73288, 0.22282), (0.73288, 0.22282), (0.734151, 0.20247), (0.753428, 0.206788), (0.734151, 0.20247), (0.73288, 0.22282), (0.710494, 0.22689), (0.716853, 0.238846), (0.710494, 0.22689), (0.73288, 0.22282), (0.73288, 0.22282), (0.730844, 0.239863), (0.716853, 0.238846), (0.750593, 0.241953), (0.730844, 0.239863), (0.73288, 0.22282), (0.73288, 0.22282), (0.752466, 0.225364), (0.750593, 0.241953), (0.757554, 0.242407), (0.750593, 0.241953), (0.752466, 0.225364), (0.752466, 0.225364), (0.758826, 0.225364), (0.757554, 0.242407), (0.766457, 0.24317), (0.757554, 0.242407), (0.758826, 0.225364), (0.758826, 0.225364), (0.765185, 0.225364), (0.766457, 0.24317), (0.766457, 0.24317), (0.765185, 0.225364), (0.78426397, 0.223329), (0.78426397, 0.223329), (0.765185, 0.225364), (0.764986, 0.207564), (0.764986, 0.207564), (0.784263, 0.203742), (0.78426397, 0.223329), (0.809701, 0.227653), (0.78426397, 0.223329), (0.784263, 0.203742), (0.78426397, 0.223329), (0.809701, 0.227653), (0.800035, 0.241898), (0.812753, 0.245205), (0.800035, 0.241898), (0.809701, 0.227653), (0.809701, 0.227653), (0.820639, 0.234012), (0.812753, 0.245205), (0.800035, 0.241898), (0.784517, 0.243424), (0.78426397, 0.223329), (0.78426397, 0.223329), (0.784517, 0.243424), (0.766457, 0.24317), (0.823318, 0.478569), (0.83828, 0.38681298), (0.854118, 0.450025), (0.823318, 0.478569), (0.854118, 0.450025), (0.865123, 0.49460799), (0.871152, 0.37413698), (0.883705, 0.427711), (0.854118, 0.450025), (0.854118, 0.450025), (0.83828, 0.38681298), (0.871152, 0.37413698), (0.865946, 0.324272), (0.871152, 0.37413698), (0.83828, 0.38681298), (0.83828, 0.38681298), (0.831585, 0.328135), (0.865946, 0.324272), (0.779513, 0.330985), (0.831585, 0.328135), (0.83828, 0.38681298), (0.83828, 0.38681298), (0.779513, 0.385777), (0.779513, 0.330985), (0.779513, 0.385777), (0.83828, 0.38681298), (0.823318, 0.478569), (0.823318, 0.478569), (0.779513, 0.479818), (0.779513, 0.385777), (0.779513, 0.479818), (0.823318, 0.478569), (0.80937, 0.539726), (0.80937, 0.539726), (0.823318, 0.478569), (0.865123, 0.49460799), (0.80937, 0.539726), (0.865123, 0.49460799), (0.878641, 0.520339), (0.715994, 0.385889), (0.7277, 0.328135), (0.779513, 0.330985), (0.779513, 0.330985), (0.779513, 0.385777), (0.715994, 0.385889), (0.735967, 0.480677), (0.715994, 0.385889), (0.779513, 0.385777), (0.779513, 0.385777), (0.779513, 0.479818), (0.735967, 0.480677), (0.735967, 0.480677), (0.779513, 0.479818), (0.779772, 0.536812), (0.80937, 0.539726), (0.779772, 0.536812), (0.779513, 0.479818), (0.779772, 0.536812), (0.80937, 0.539726), (0.802958, 0.571138), (0.838321, 0.283058), (0.878052, 0.283693), (0.865946, 0.324272), (0.865946, 0.324272), (0.831585, 0.328135), (0.838321, 0.283058), (0.779513, 0.285908), (0.838321, 0.283058), (0.831585, 0.328135), (0.831585, 0.328135), (0.779513, 0.330985), (0.779513, 0.285908), (0.720964, 0.283058), (0.779513, 0.285908), (0.779513, 0.330985), (0.779513, 0.330985), (0.7277, 0.328135), (0.720964, 0.283058), (0.683212, 0.282704), (0.720964, 0.283058), (0.7277, 0.328135), (0.7277, 0.328135), (0.692349, 0.325261), (0.683212, 0.282704), (0.692349, 0.325261), (0.7277, 0.328135), (0.715994, 0.385889), (0.715994, 0.385889), (0.684176, 0.372288), (0.692349, 0.325261), (0.684176, 0.372288), (0.715994, 0.385889), (0.708134, 0.45009), (0.735967, 0.480677), (0.708134, 0.45009), (0.715994, 0.385889), (0.69601, 0.495727), (0.708134, 0.45009), (0.735967, 0.480677), (0.69601, 0.495727), (0.735967, 0.480677), (0.749915, 0.539856), (0.779772, 0.536812), (0.749915, 0.539856), (0.735967, 0.480677), (0.756327, 0.571268), (0.749915, 0.539856), (0.779772, 0.536812), (0.779772, 0.536812), (0.779772, 0.573729), (0.756327, 0.571268), (0.802958, 0.571138), (0.779772, 0.573729), (0.779772, 0.536812), (0.779772, 0.573729), (0.802958, 0.571138), (0.808269, 0.599506), (0.708134, 0.45009), (0.678548, 0.423754), (0.684176, 0.372288), (0.677676, 0.51763), (0.69601, 0.495727), (0.749915, 0.539856), (0.808269, 0.599506), (0.779772, 0.599376), (0.779772, 0.573729), (0.756327, 0.571268), (0.779772, 0.573729), (0.779772, 0.599376), (0.779772, 0.599376), (0.751146, 0.599506), (0.756327, 0.571268), (0.857164, 0.876445), (0.835662, 0.881821), (0.804185, 0.81595397), (0.804185, 0.81595397), (0.856905, 0.808312), (0.857164, 0.876445), (0.856905, 0.808312), (0.804185, 0.81595397), (0.789289, 0.773903), (0.789289, 0.773903), (0.856646, 0.766861), (0.856905, 0.808312), (0.856646, 0.766861), (0.789289, 0.773903), (0.796974, 0.735111), (0.874004, 0.916212), (0.857424, 0.908181), (0.857164, 0.876445), (0.835662, 0.881821), (0.857164, 0.876445), (0.857424, 0.908181), (0.857424, 0.908181), (0.840584, 0.916471), (0.835662, 0.881821), (0.835662, 0.881821), (0.840584, 0.916471), (0.828926, 0.929424), (0.796974, 0.735111), (0.754088, 0.740647), (0.755142, 0.694291), (0.796974, 0.735111), (0.789289, 0.773903), (0.754088, 0.740647), (0.754088, 0.740647), (0.789289, 0.773903), (0.768175, 0.819451), (0.804185, 0.81595397), (0.768175, 0.819451), (0.789289, 0.773903), (0.768564, 0.852741), (0.768175, 0.819451), (0.804185, 0.81595397), (0.768564, 0.852741), (0.804185, 0.81595397), (0.835662, 0.881821), (0.76131, 0.877093), (0.768564, 0.852741), (0.835662, 0.881821), (0.835662, 0.881821), (0.819211, 0.898466), (0.76131, 0.877093), (0.819211, 0.898466), (0.835662, 0.881821), (0.828926, 0.929424), (0.828926, 0.929424), (0.818046, 0.92968297), (0.819211, 0.898466), (0.818046, 0.92968297), (0.828926, 0.929424), (0.818564, 0.963102), (0.818564, 0.963102), (0.805869, 0.959476), (0.818046, 0.92968297), (0.768175, 0.819451), (0.717837, 0.803), (0.754088, 0.740647), (0.885661, 0.929165), (0.874004, 0.916212), (0.878732, 0.881562), (0.857164, 0.876445), (0.878732, 0.881562), (0.874004, 0.916212), (0.909626, 0.815306), (0.878732, 0.881562), (0.857164, 0.876445), (0.857164, 0.876445), (0.856905, 0.808312), (0.909626, 0.815306), (0.926586, 0.772944), (0.909626, 0.815306), (0.856905, 0.808312), (0.856905, 0.808312), (0.856646, 0.766861), (0.926586, 0.772944), (0.926586, 0.772944), (0.856646, 0.766861), (0.856646, 0.735384), (0.796974, 0.735111), (0.856646, 0.735384), (0.856646, 0.766861), (0.856646, 0.735384), (0.796974, 0.735111), (0.796762, 0.695003), (0.755142, 0.694291), (0.796762, 0.695003), (0.796974, 0.735111), (0.796762, 0.695003), (0.755142, 0.694291), (0.737493, 0.657228), (0.737493, 0.657228), (0.79188, 0.651188), (0.796762, 0.695003), (0.856387, 0.693157), (0.796762, 0.695003), (0.79188, 0.651188), (0.79188, 0.651188), (0.856128, 0.65067), (0.856387, 0.693157), (0.856387, 0.693157), (0.856128, 0.65067), (0.920635, 0.650411), (0.920635, 0.650411), (0.921872, 0.694986), (0.856387, 0.693157), (0.970066, 0.696558), (0.921872, 0.694986), (0.920635, 0.650411), (0.919941, 0.733375), (0.921872, 0.694986), (0.970066, 0.696558), (0.856646, 0.735384), (0.919941, 0.733375), (0.926586, 0.772944), (0.921872, 0.694986), (0.919941, 0.733375), (0.856646, 0.735384), (0.856646, 0.735384), (0.856387, 0.693157), (0.921872, 0.694986), (0.796762, 0.695003), (0.856387, 0.693157), (0.856646, 0.735384), (0.920635, 0.650411), (0.986437, 0.652889), (0.970066, 0.696558), (0.970066, 0.696558), (0.972836, 0.736696), (0.919941, 0.733375), (0.919941, 0.733375), (0.972836, 0.736696), (0.926586, 0.772944), (0.94903, 0.816742), (0.926586, 0.772944), (0.972836, 0.736696), (0.909626, 0.815306), (0.926586, 0.772944), (0.94903, 0.816742), (0.909626, 0.815306), (0.94903, 0.816742), (0.948282, 0.851704), (0.878732, 0.881562), (0.909626, 0.815306), (0.948282, 0.851704), (0.898889, 0.898077), (0.878732, 0.881562), (0.948282, 0.851704), (0.948282, 0.851704), (0.960628, 0.870789), (0.898889, 0.898077), (0.972836, 0.736696), (0.98354, 0.793852), (0.94903, 0.816742), (0.900054, 0.929165), (0.90846, 0.958957), (0.895765, 0.962584), (0.895765, 0.962584), (0.885661, 0.929165), (0.900054, 0.929165), (0.898889, 0.898077), (0.900054, 0.929165), (0.885661, 0.929165), (0.885661, 0.929165), (0.878732, 0.881562), (0.898889, 0.898077)] ( + interpolation = "faceVarying" + ) + bool[] primvars:sharp_face = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] ( + interpolation = "uniform" + ) + uniform token subdivisionScheme = "none" + } + } + } + + def Scope "_materials" + { + def Material "hulagirl_a_mtl_material0" + { + token outputs:surface.connect = + + def Shader "Principled_BSDF" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:clearcoat = 0 + float inputs:clearcoatRoughness = 0.03 + color3f inputs:diffuseColor.connect = + float inputs:ior = 0 + float inputs:metallic = 0 + float inputs:opacity = 1 + float inputs:roughness = 0.5 + float inputs:specular = 0 + token outputs:surface + } + + def Shader "Image_Texture" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @./textures/hulagirl_a_dif.dds@ + token inputs:sourceColorSpace = "sRGB" + float2 inputs:st.connect = + token inputs:wrapS = "repeat" + token inputs:wrapT = "repeat" + float3 outputs:rgb + } + + def Shader "uvmap" + { + uniform token info:id = "UsdPrimvarReader_float2" + token inputs:varname = "HulaGirl_LowerBody-mesh-UV" + float2 outputs:result + } + } + } +} + diff --git a/CgfConverterIntegrationTests/UnitTests/HashingExtensionTests.cs b/CgfConverterIntegrationTests/UnitTests/HashingExtensionTests.cs index 65aba76e..a8f4e0f2 100644 --- a/CgfConverterIntegrationTests/UnitTests/HashingExtensionTests.cs +++ b/CgfConverterIntegrationTests/UnitTests/HashingExtensionTests.cs @@ -58,4 +58,13 @@ public void Crc32_Nose() Assert.AreEqual(expectedCrc, actualCrc); } + + [TestMethod] + public void Crc32_Bip01_VerifyArcheAgeBoneHashing() + { + // ArcheAge CAF files use CRC32 with original case bone names + // This verifies the algorithm matches what ArcheAge expects + Assert.AreEqual((uint)0x78561A24, Crc32CryEngine.Compute("Bip01"), "Bip01 CRC32"); + Assert.AreEqual((uint)0xC8E05E07, Crc32CryEngine.Compute("l_wing_00"), "l_wing_00 CRC32"); + } } diff --git a/CgfConverterIntegrationTests/UnitTests/HelperMethodsTests.cs b/CgfConverterIntegrationTests/UnitTests/HelperMethodsTests.cs new file mode 100644 index 00000000..96484d26 --- /dev/null +++ b/CgfConverterIntegrationTests/UnitTests/HelperMethodsTests.cs @@ -0,0 +1,172 @@ +using CgfConverter.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.IO; +using System.Text; + +namespace CgfConverterTests.UnitTests; + +[TestClass] +[TestCategory("unit")] +public class HelperMethodsTests +{ + [TestMethod] + public void GetNullSeparatedStrings_BasicAscii_ReturnsCorrectStrings() + { + // "bone1\0bone2\0bone3\0" + var buffer = Encoding.UTF8.GetBytes("bone1\0bone2\0bone3\0"); + + using var stream = new MemoryStream(buffer); + using var reader = new BinaryReader(stream); + + var result = HelperMethods.GetNullSeparatedStrings(3, reader); + + Assert.AreEqual(3, result.Count); + Assert.AreEqual("bone1", result[0]); + Assert.AreEqual("bone2", result[1]); + Assert.AreEqual("bone3", result[2]); + } + + [TestMethod] + public void GetNullSeparatedStrings_Utf8MultiByte_ReturnsCorrectStrings() + { + // Test with multi-byte UTF-8 characters (e.g., accented characters, emoji) + var buffer = Encoding.UTF8.GetBytes("café\0naïve\0日本語\0"); + + using var stream = new MemoryStream(buffer); + using var reader = new BinaryReader(stream); + + var result = HelperMethods.GetNullSeparatedStrings(3, reader); + + Assert.AreEqual(3, result.Count); + Assert.AreEqual("café", result[0]); + Assert.AreEqual("naïve", result[1]); + Assert.AreEqual("日本語", result[2]); + } + + [TestMethod] + public void GetNullSeparatedStrings_EmptyStrings_ReturnsEmptyStrings() + { + // Three empty strings: "\0\0\0" + var buffer = new byte[] { 0, 0, 0 }; + + using var stream = new MemoryStream(buffer); + using var reader = new BinaryReader(stream); + + var result = HelperMethods.GetNullSeparatedStrings(3, reader); + + Assert.AreEqual(3, result.Count); + Assert.AreEqual("", result[0]); + Assert.AreEqual("", result[1]); + Assert.AreEqual("", result[2]); + } + + [TestMethod] + public void GetNullSeparatedStrings_SingleString_ReturnsCorrectString() + { + var buffer = Encoding.UTF8.GetBytes("single_bone\0"); + + using var stream = new MemoryStream(buffer); + using var reader = new BinaryReader(stream); + + var result = HelperMethods.GetNullSeparatedStrings(1, reader); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("single_bone", result[0]); + } + + [TestMethod] + public void GetNullSeparatedStrings_Bounded_ReadsExactByteCount() + { + // Buffer contains "bone1\0bone2\0" followed by garbage + var stringData = Encoding.UTF8.GetBytes("bone1\0bone2\0"); + var garbage = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC }; + var buffer = new byte[stringData.Length + garbage.Length]; + stringData.CopyTo(buffer, 0); + garbage.CopyTo(buffer, stringData.Length); + + using var stream = new MemoryStream(buffer); + using var reader = new BinaryReader(stream); + + // Only read the string data portion + var result = HelperMethods.GetNullSeparatedStrings(2, stringData.Length, reader); + + Assert.AreEqual(2, result.Count); + Assert.AreEqual("bone1", result[0]); + Assert.AreEqual("bone2", result[1]); + + // Verify stream position is exactly at end of string table + Assert.AreEqual(stringData.Length, stream.Position); + } + + [TestMethod] + public void GetNullSeparatedStrings_Bounded_StopsAtBufferEnd() + { + // Request more strings than exist in buffer + var buffer = Encoding.UTF8.GetBytes("bone1\0bone2\0"); + + using var stream = new MemoryStream(buffer); + using var reader = new BinaryReader(stream); + + // Ask for 5 strings but buffer only has 2 + var result = HelperMethods.GetNullSeparatedStrings(5, buffer.Length, reader); + + // Should return only what's available + Assert.AreEqual(2, result.Count); + Assert.AreEqual("bone1", result[0]); + Assert.AreEqual("bone2", result[1]); + } + + [TestMethod] + public void GetNullSeparatedStrings_ByteArray_ParsesCorrectly() + { + var buffer = Encoding.UTF8.GetBytes("alpha\0beta\0gamma\0"); + + var result = HelperMethods.GetNullSeparatedStrings(3, buffer); + + Assert.AreEqual(3, result.Count); + Assert.AreEqual("alpha", result[0]); + Assert.AreEqual("beta", result[1]); + Assert.AreEqual("gamma", result[2]); + } + + [TestMethod] + public void GetNullSeparatedStrings_ByteArray_HandlesNoTrailingNull() + { + // Last string without null terminator + var buffer = Encoding.UTF8.GetBytes("bone1\0bone2\0bone3"); + + var result = HelperMethods.GetNullSeparatedStrings(3, buffer); + + Assert.AreEqual(3, result.Count); + Assert.AreEqual("bone1", result[0]); + Assert.AreEqual("bone2", result[1]); + Assert.AreEqual("bone3", result[2]); + } + + [TestMethod] + public void GetNullSeparatedStrings_ByteArray_EmptyBuffer() + { + var buffer = Array.Empty(); + + var result = HelperMethods.GetNullSeparatedStrings(3, buffer); + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void GetNullSeparatedStrings_Bounded_Utf8MultiByte() + { + // Ensure bounded version handles UTF-8 correctly + var stringData = Encoding.UTF8.GetBytes("Ñoño\0größe\0"); + + using var stream = new MemoryStream(stringData); + using var reader = new BinaryReader(stream); + + var result = HelperMethods.GetNullSeparatedStrings(2, stringData.Length, reader); + + Assert.AreEqual(2, result.Count); + Assert.AreEqual("Ñoño", result[0]); + Assert.AreEqual("größe", result[1]); + } +} diff --git a/CgfConverterIntegrationTests/UnitTests/IvoTangentFrameTests.cs b/CgfConverterIntegrationTests/UnitTests/IvoTangentFrameTests.cs new file mode 100644 index 00000000..b74dbe6b --- /dev/null +++ b/CgfConverterIntegrationTests/UnitTests/IvoTangentFrameTests.cs @@ -0,0 +1,221 @@ +using CgfConverter.Models.Structs; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Numerics; + +namespace CgfConverterTests.UnitTests; + +[TestClass] +[TestCategory("unit")] +public class IvoTangentFrameTests +{ + private const float Epsilon = 0.02f; // Tolerance for quantization error (10-bit precision) + + /// + /// Test data from Ivo_Normals.md documentation. + /// Box with 6 axis-aligned faces. + /// + [TestMethod] + public void Decode_ZNegativeFace_ReturnsCorrectNormal() + { + // Bytes: FE FF FF 1F FF 3F 00 00 + // Expected: (0, 0, -1) + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x1FFF, + Word2 = 0x3FFF, + Word3 = 0x0000 + }; + + var (normal, _, _) = frame.Decode(); + + AssertVector3Equal(new Vector3(0, 0, -1), normal, Epsilon); + } + + [TestMethod] + public void Decode_ZPositiveFace_ReturnsCorrectNormal() + { + // Bytes: FE FF FF 9F FF 3F 00 80 + // Expected: (0, 0, +1) + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x9FFF, + Word2 = 0x3FFF, + Word3 = 0x8000 + }; + + var (normal, _, _) = frame.Decode(); + + AssertVector3Equal(new Vector3(0, 0, 1), normal, Epsilon); + } + + [TestMethod] + public void Decode_YNegativeFace_ReturnsCorrectNormal() + { + // Bytes: FE FF FF 9F FF BF FF DF + // Expected: (0, -1, 0) + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x9FFF, + Word2 = 0xBFFF, + Word3 = 0xDFFF + }; + + var (normal, _, _) = frame.Decode(); + + AssertVector3Equal(new Vector3(0, -1, 0), normal, Epsilon); + } + + [TestMethod] + public void Decode_XPositiveFace_ReturnsCorrectNormal() + { + // Bytes: FF 3F FF BF FF BF FF DF + // Expected: (+1, 0, 0) + var frame = new IvoTangentFrame + { + Word0 = 0x3FFF, + Word1 = 0xBFFF, + Word2 = 0xBFFF, + Word3 = 0xDFFF + }; + + var (normal, _, _) = frame.Decode(); + + AssertVector3Equal(new Vector3(1, 0, 0), normal, Epsilon); + } + + [TestMethod] + public void Decode_YPositiveFace_ReturnsCorrectNormal() + { + // Bytes: FE FF FF 1F FF BF FF 5F + // Expected: (0, +1, 0) + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x1FFF, + Word2 = 0xBFFF, + Word3 = 0x5FFF + }; + + var (normal, _, _) = frame.Decode(); + + AssertVector3Equal(new Vector3(0, 1, 0), normal, Epsilon); + } + + [TestMethod] + public void Decode_XNegativeFace_ReturnsCorrectNormal() + { + // Bytes: FF 3F FF 3F FF BF FF 5F + // Expected: (-1, 0, 0) - note: documentation says (-0.99, 0, 0) due to quantization + // This face uses d2=1 (heuristic) which may have some precision issues + var frame = new IvoTangentFrame + { + Word0 = 0x3FFF, + Word1 = 0x3FFF, + Word2 = 0xBFFF, + Word3 = 0x5FFF + }; + + var (normal, _, _) = frame.Decode(); + + // Primary X component should be close to -1 + Assert.IsTrue(normal.X < -0.98f, $"Expected X near -1, got {normal.X}"); + // The heuristic for d2=1 may introduce some Z-axis error + Assert.IsTrue(Math.Abs(normal.Y) < 0.15f, $"Y should be near 0, got {normal.Y}"); + } + + [TestMethod] + public void Decode_ReturnsNormalizedNormal() + { + // Any decoded normal should be approximately unit length + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x1FFF, + Word2 = 0x3FFF, + Word3 = 0x0000 + }; + + var (normal, _, _) = frame.Decode(); + float length = normal.Length(); + + Assert.AreEqual(1.0f, length, 0.01f, $"Normal length should be ~1.0, got {length}"); + } + + [TestMethod] + public void Decode_ReturnsTangentAndBitangent() + { + // Z-positive face + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x9FFF, + Word2 = 0x3FFF, + Word3 = 0x8000 + }; + + var (normal, tangent, bitangent) = frame.Decode(); + + // Verify they form an orthonormal basis (approximately) + float dot1 = Vector3.Dot(normal, tangent); + float dot2 = Vector3.Dot(normal, bitangent); + float dot3 = Vector3.Dot(tangent, bitangent); + + Assert.AreEqual(0, dot1, 0.1f, "Normal and tangent should be perpendicular"); + Assert.AreEqual(0, dot2, 0.1f, "Normal and bitangent should be perpendicular"); + Assert.AreEqual(0, dot3, 0.1f, "Tangent and bitangent should be perpendicular"); + } + + [TestMethod] + public void Decode_ColumnSelector0_UsesNegativeBitangent() + { + // d2 = 0 (bits 30-31 of value2 = 0) means use -Bitangent as normal + // Word3 high bits = 00 means d2 = 0 + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x1FFF, + Word2 = 0x3FFF, + Word3 = 0x0000 // d2 = 0 + }; + + var (normal, _, _) = frame.Decode(); + + // For this specific frame, the result should be (0, 0, -1) + Assert.IsTrue(normal.Z < -0.9f, $"Expected Z-negative normal, got {normal}"); + } + + [TestMethod] + public void Decode_ColumnSelector2_UsesPositiveNormal() + { + // d2 = 2 (bits 30-31 of value2 = 10b = 2) means use +Normal column + // Word3 = 0x8000 means high bits = 10b, so d2 = 2 + var frame = new IvoTangentFrame + { + Word0 = 0xFFFE, + Word1 = 0x9FFF, + Word2 = 0x3FFF, + Word3 = 0x8000 // d2 = 2 + }; + + var (normal, _, _) = frame.Decode(); + + // For this specific frame, the result should be (0, 0, +1) + Assert.IsTrue(normal.Z > 0.9f, $"Expected Z-positive normal, got {normal}"); + } + + private static void AssertVector3Equal(Vector3 expected, Vector3 actual, float epsilon) + { + bool xMatch = Math.Abs(expected.X - actual.X) <= epsilon; + bool yMatch = Math.Abs(expected.Y - actual.Y) <= epsilon; + bool zMatch = Math.Abs(expected.Z - actual.Z) <= epsilon; + + if (!xMatch || !yMatch || !zMatch) + { + Assert.Fail($"Vectors not equal within epsilon {epsilon}.\nExpected: {expected}\nActual: {actual}"); + } + } +} diff --git a/Cryengine Converter.sln b/Cryengine Converter.sln index c2e340c6..e5b805eb 100644 --- a/Cryengine Converter.sln +++ b/Cryengine Converter.sln @@ -17,6 +17,13 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02B37E78-2794-42F9-955C-35B7D6581897}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .gitignore = .gitignore + CLAUDE.md = CLAUDE.md + CONTRIBUTING.md = CONTRIBUTING.md + DEVNOTES.md = DEVNOTES.md + LICENSE = LICENSE + README.md = README.md + TODO = TODO EndProjectSection EndProject Global diff --git a/DEVNOTES.md b/DEVNOTES.md new file mode 100644 index 00000000..eae8aadb --- /dev/null +++ b/DEVNOTES.md @@ -0,0 +1,847 @@ +# Development Notes + +Active development notes, debugging history, and in-progress feature work for Cryengine Converter. + +## USD Export - Active Development + +### Shader-Based Material System (IN PROGRESS) + +**Goal**: Implement shader definition parsing to accurately interpret material properties based on `StringGenMask` flags, enabling proper material node graphs in USD format. + +**Shader File Format (.ext)**: +- Location: `/Shaders/.ext` (e.g., `d:\depot\mwo\Shaders\MechCockpit.ext`) +- Text-based property definitions with Name, Mask, Description, Dependencies +- 40 shader files total in MWO, 17 use `UsesCommonGlobalFlags` +- Property blocks define how texture channels map to material inputs + +**UsesCommonGlobalFlags**: Metadata directive indicating the shader uses standard material properties (diffuse, specular, environment maps, etc.). Not an inheritance mechanism - each .ext file contains complete property definitions. Shaders without this flag are typically post-processing or special effects that don't need material properties. **Decision**: Ignore this directive; parse complete property list from each shader file. + +**Example StringGenMask Parsing**: +- Material has `Shader="mechcockpit"` and `StringGenMask="%ALPHAGLOW%ENVIRONMENT_MAP%GLOSS_MAP%SPECULARPOW_GLOSSALPHA%VERTCOLORS"` +- Parser splits flags and looks up definitions in MechCockpit.ext: + - `%ALPHAGLOW` (Mask 0x2000): "Use alpha channel of diffuse texture for glow" → Connect diffuse.alpha to emissiveColor + - `%SPECULARPOW_GLOSSALPHA` (Mask 0x800): "Use specular map alpha channel as gloss map" → Connect specular.alpha to roughness + - `%ENVIRONMENT_MAP` (Mask 0x80): "Use environment map as separate texture" → Enable environment mapping + - `%GLOSS_MAP` (Mask 0x10): "Use gloss map as separate texture" → Enable gloss texture + - `%VERTCOLORS` (Mask 0x400000): "Use vertex colors" → Enable vertex color attribute + +**Implementation Plan**: + +1. **Data Models** (`CgfConverter/Models/Shaders/`) + - `ShaderProperty.cs` - Single property from .ext (Name, Mask, Description, Dependencies) + - `ShaderDefinition.cs` - Complete shader (ShaderName, Properties dictionary) + - `MaterialRule.cs` - Applied rule (PropertyName, TextureSlot, TargetChannel, OutputTarget) + +2. **Parser** (`CgfConverter/Parsers/ShaderExtParser.cs`) + - Parse .ext text format property blocks + - Build `ShaderDefinition` objects with property dictionaries + - Cache parsed shaders for reuse + +3. **Rules Engine** (`CgfConverter/Renderers/USD/ShaderRulesEngine.cs`) + - Takes Material + ShaderDefinition + - Parses StringGenMask into flags (split by '%') + - Generates MaterialRule list to apply + - Handles channel routing (e.g., diffuse.alpha → emissive vs opacity) + +4. **UsdRenderer Integration** (in `UsdRenderer.Materials.cs`) + ``` + Startup: + - Load all .ext files from /Shaders + - Parse and cache ShaderDefinition objects + + Per Material: + 1. Look up ShaderDefinition from material.Shader property (case-insensitive) + 2. Parse StringGenMask into active flags + 3. Use ShaderRulesEngine to generate MaterialRules + 4. Create texture shader nodes for all supported types (Diffuse, Specular, Bumpmap) + 5. Apply MaterialRules to configure connections (channel routing, alpha handling) + 6. Create PrincipledBSDF with all connections based on rules + ``` + +5. **Texture Support Extensions** + - Specular textures: Connect to `inputs:specularColor` or create channel router nodes + - Bumpmap textures: Connect to normal input (may need UsdNormalMap node) + - Environment textures: Skip (USD PreviewSurface doesn't support cubemaps) + - Detail textures: Handle with blending where applicable + - Unknown shader properties: Log at Debug level, continue processing + +**Test Case**: Adder cockpit (`d:\depot\mwo\objects\mechs\cockpit_standard\adder_a_cockpit_standard.cga`) +- Material file: `d:\depot\mwo\objects\mechs\adder\cockpit_standard\adder_a_cockpit_standard.mtl` +- "cockpit_shared" material uses MechCockpit shader with %ALPHAGLOW flag +- Verify diffuse.alpha connects to emissiveColor (not opacity) in exported USD + +**Current Status**: Planning complete, ready to implement data models and parser. + +### Future Material Improvements +- **Metallic detection heuristic**: Investigate using `Shader`, `MtlFlags`, or specular properties to auto-detect metallic materials + - Check if `Shader="Metal"` exists in any materials + - Consider using `Glossiness` property (currently unused) + - Analyze specular color patterns (colored specular might indicate metallic) +- **Material validation**: Ensure all MTL properties map correctly to USD PBR workflow + +### Animation Support + +**Current Status**: USD animation export fully working for MWO, Armored Warfare, and ArcheAge. Both `.dba` (animation databases) and `.caf` (individual clips) are supported. + +#### What's Working +- **MWO mech animations**: All power up/down animations from `.dba` files import perfectly into Blender +- **MWO pilot.chr**: Full skeleton with all CAF animations (joystick, throttle, etc.) working correctly +- **Armored Warfare**: CAF animations via ChunkController_829 fully working (chicken walk/idle) +- **ArcheAge**: CAF animations fully working (chicken uses ChunkCompiledBones_801 + .cal file) +- **Kingdom Come Deliverance 2 (KCD2)**: DBA animations working (pig skeleton uses ChunkController_905 in-place streaming mode) +- Animations exported as separate `.usda` files for Blender NLA workflow (one file per animation) +- First animation automatically bound as skeleton's `skel:animationSource` + +#### What Needs Testing/Fixing +- **Star Citizen #ivo animations**: Next target for animation support (see section below) + +#### Animation File Formats + +**DBA Files** (Animation Databases): +- Container format holding multiple animations +- Referenced via `.chrparams` file's `$TracksDatabase` entry (supports wildcards like `*.dba`) +- Parsed by `ChunkController_905` which contains: + - `NumAnims` animations, each with `MotionParams905` metadata + - Compressed keyframe data (positions, rotations, times) + - Per-animation `AssetFlags` in `MotionParams905.AssetFlags` +- **Two storage modes** detected via offset sign: + - **Standard mode** (positive offsets): Track data → Animation entries → Controllers + - **In-place streaming mode** (negative offsets): Padding → Animation entries → Controllers → Track data + - In-place mode: offsets are relative to END of track data block (add negative offset to trackDataEnd) + - 4-byte alignment required before track data in in-place mode + +**CAF Files** (Individual Animation Clips): +- Single animation per file +- Referenced via `.chrparams` `` entries +- Support wildcard patterns (e.g., `animations/pilot/*.caf`) +- Animation metadata in `ChunkGlobalAnimationHeaderCAF` chunk (version 971) +- Controller data in `ChunkController_829`, `ChunkController_830`, `ChunkController_831` +- Star Citizen uses `ChunkIvoCAF` for #ivo format animations + +#### Animation Asset Flags + +Defined in `ChunkController_905.AssetFlags` (also applies to CAF via `ChunkGlobalAnimationHeaderCAF.Flags`): + +| Flag | Value | Meaning | +|------|-------|---------| +| `Additive` | 0x001 | Animation stores deltas from rest pose, not absolute transforms | +| `Cycle` | 0x002 | Animation is meant to loop (not used in export - Blender controls this) | +| `Loaded` | 0x004 | Runtime flag | +| `Lmg` | 0x008 | Locomotion group | +| `LmgValid` | 0x020 | Locomotion group valid | +| `Created` | 0x800 | Runtime flag | +| `Aimpose` | 0x4000 | Aim pose animation | +| `BigEndian` | 0x80000000 | File is big-endian | + +#### Additive Animation Handling (FIXED) + +**Problem**: Additive animations (like `additive_pilot_joystick_down`) store bone transforms as *deltas* from the rest pose. When exported directly to USD, the skeleton collapses because translations are near-zero and rotations are near-identity. + +**Identification**: +- CAF files: Check `ChunkGlobalAnimationHeaderCAF.Flags & 0x001` +- DBA files: Check `MotionParams905.AssetFlags.Additive` +- Naming convention: Often prefixed with `additive_` + +**Solution** (implemented in `UsdRenderer.Animation.cs`): +```csharp +if (isAdditive) +{ + // Convert additive deltas to absolute transforms + absolute_rotation = restRotation * additiveRotation; + absolute_translation = restTranslation + additiveTranslation; +} +``` + +The `BuildRestRotationMapping()` and `BuildRestTranslationMapping()` methods extract rest pose from the skeleton's bind matrices for this conversion. + +#### Key Implementation Files + +- `CgfConverter/CryEngine/CryEngine.cs`: `LoadCafAnimations()`, `ParseCafModel()` - loads CAF files from chrparams +- `CgfConverter/Models/CafAnimation.cs`: Animation data model with `IsAdditive` property +- `CgfConverter/CryEngineCore/Chunks/ChunkController_905.cs`: DBA animation parsing, `AssetFlags` enum +- `CgfConverter/CryEngineCore/Chunks/ChunkGlobalAnimationHeaderCAF_971.cs`: CAF header with flags +- `CgfConverter/Renderers/USD/UsdRenderer.Animation.cs`: USD export, additive conversion, per-animation file export + +#### Blender Import Notes + +- USD SkelAnimation has no native looping property - cycle behavior controlled in Blender's NLA editor +- Each animation exported as separate `.usda` file (e.g., `model_anim_walk.usda`) for NLA workflow +- Blender only reads the single bound animation per file, hence separate files needed + +**glTF Frame Rate**: glTF stores animation times in seconds (not frames), dividing by 30fps during export. When Blender imports, it converts seconds back to frames using Blender's scene frame rate. If Blender is set to 24fps (default), keyframes will appear at different frame numbers than the original. **Set Blender to 30fps before importing glTF animations** to match the original frame timing. USD doesn't have this issue because it includes `TimeCodesPerSecond = 30` metadata. + +--- + +## Animation Data Flow: CryEngine → USD (Reference for New Chunk Support) + +This section documents the vetted animation pipeline to help implement support for new CompiledBones and Controller chunk versions. + +### Vetted Chunks (Known Working) + +| Chunk Type | Version | Game | Status | +|------------|---------|------|--------| +| `ChunkCompiledBones` | 0x800 | MWO, KCD2 | ✅ Vetted | +| `ChunkCompiledBones` | 0x801 | ArcheAge | ✅ Vetted | +| `ChunkController` | 0x905 | MWO (DBA), KCD2 (DBA in-place streaming) | ✅ Vetted | +| `ChunkController` | 0x829 | Armored Warfare (CAF) | ✅ Vetted | +| `ChunkCompiledBones` | 0x900, 0x901 | Unknown | ⚠️ Untested for animations | + +### CryEngine Matrix Convention + +**CRITICAL**: CryEngine stores translation in **column 4** (M14, M24, M34), not row 4. + +``` +CryEngine Matrix3x4 layout (row-major storage): +┌─────────────────────────────────┐ +│ M11 M12 M13 M14 (trans X) │ Row 1 +│ M21 M22 M23 M24 (trans Y) │ Row 2 +│ M31 M32 M33 M34 (trans Z) │ Row 3 +└─────────────────────────────────┘ + ↑ + Column 4 = Translation +``` + +When converted to `System.Numerics.Matrix4x4` via `Matrix3x4.ConvertToTransformMatrix()`: +- Translation stays in M14, M24, M34 +- M41, M42, M43 are set to 0 +- M44 is set to 1 + +**WARNING**: `Matrix4x4.Translation` property reads M41, M42, M43 (row 4) - this returns ZEROS for CryEngine matrices! Always extract translation manually: +```csharp +var translation = new Vector3(matrix.M14, matrix.M24, matrix.M34); // Correct +var translation = matrix.Translation; // WRONG - returns zeros! +``` + +### ChunkCompiledBones_800 (Vetted) + +**Data per bone** (584 bytes): +- `ControllerID` (uint32): CRC32 hash for animation binding +- `PhysicsGeometry[2]`: Physics data (live/ragdoll) +- `Mass` (float) +- `LocalTransformMatrix` (Matrix3x4): Local bone transform, used directly as BindPoseMatrix +- `WorldTransformMatrix` (Matrix3x4): World space transform +- `BoneName` (256 chars) +- `LimbId`, `OffsetParent`, `NumberOfChildren`, `OffsetChild` + +**BindPoseMatrix calculation**: +```csharp +LocalTransformMatrix = b.ReadMatrix3x4(); +BindPoseMatrix = LocalTransformMatrix.ConvertToTransformMatrix(); +``` + +### ChunkController_905 (Vetted - DBA Animations) + +**Structure**: +- Header: `NumKeyPos`, `NumKeyRot`, `NumKeyTime`, `NumAnims` +- Track data: Compressed keyframes (positions as Vector3, rotations as Quaternion) +- Per-animation: `MotionParams905` + `CControllerInfo[]` + +**Key fields in CControllerInfo**: +- `ControllerID`: CRC32 of bone name for skeleton binding +- `PosKeyTimeTrack`, `PosTrack`: Indices into KeyTimes/KeyPositions arrays +- `RotKeyTimeTrack`, `RotTrack`: Indices into KeyTimes/KeyRotations arrays + +**Position data**: Stored as **absolute local transforms**, not deltas from rest pose. + +### USD SkelAnimation Requirements + +USD `SkelAnimation` needs these attributes for Blender import: + +```usda +def SkelAnimation "AnimationName" +{ + uniform token[] joints = ["Bip01", "Bip01/Pelvis", ...] # Joint paths + + float3[] translations.timeSamples = { + 0: [(x,y,z), (x,y,z), ...], # One Vector3 per joint per frame + 1: [...], + } + + quatf[] rotations.timeSamples = { + 0: [(w,x,y,z), (w,x,y,z), ...], # One Quaternion per joint per frame + 1: [...], + } + + half3[] scales.timeSamples = { + 0: [(1,1,1), (1,1,1), ...], # Usually uniform scale + 1: [...], + } +} +``` + +**Joint path format**: Hierarchical with `/` separator (e.g., `Bip01/bip_01_Pelvis/bip_01_Spine`) + +### Translation Handling (The Critical Fix) + +**For bones WITH position animation**: Use animation data directly (it's absolute local position) + +**For bones WITHOUT position animation**: Use rest pose translation from skeleton + +```csharp +Vector3 position = SamplePosition(track, frame, restTranslation); +// If track has position data → returns interpolated animation position +// If track has NO position data → returns restTranslation +``` + +**Rest translation extraction** (from `BuildRestTranslationMapping()`): +```csharp +// Compute local transform relative to parent +if (bone.ParentBone == null) +{ + // Root: invert BindPoseMatrix to get boneToWorld + Matrix4x4.Invert(bone.BindPoseMatrix, out var boneToWorld); + restMatrix = boneToWorld; +} +else +{ + // Child: localTransform = parentWorldToBone * childBoneToWorld + Matrix4x4.Invert(bone.BindPoseMatrix, out var childBoneToWorld); + restMatrix = bone.ParentBone.BindPoseMatrix * childBoneToWorld; +} + +// Extract translation from column 4 (NOT .Translation property!) +var translation = new Vector3(restMatrix.M14, restMatrix.M24, restMatrix.M34); +``` + +### Adding Support for New Chunk Versions + +When implementing a new `ChunkCompiledBones_XXX`: + +1. **Identify how BindPoseMatrix is stored/computed** + - Is it stored directly as Matrix3x4? + - Is it computed from quaternion + translation? + - Where is translation stored (column 4 or row 4)? + +2. **Ensure translation ends up in M14, M24, M34** + - Use `Matrix3x4.ConvertToTransformMatrix()` if reading Matrix3x4 + - If building from quat+translation: `m.M14 = t.X; m.M24 = t.Y; m.M34 = t.Z;` + +3. **Test with non-additive animation first** (simpler code path) + +4. **Verify rest translations are non-zero** for child bones + +When implementing a new `ChunkController_XXX`: + +1. **Identify position data format** + - Absolute local transforms? (like 905) → Use directly + - Deltas from rest pose? → Add to rest translation + +2. **Identify key time format** + - Float frames? UInt16? Byte? + - Separate times for position vs rotation? + +3. **Map ControllerID to skeleton bones** + - Usually CRC32 of bone name + - Check case sensitivity (some games use lowercase) + +### Debugging Animation Issues + +**Symptom: All bones collapse to one point** +- Check rest translations - are they all zeros? +- Likely cause: extracting translation from wrong matrix elements + +**Symptom: Animation floats/offset from rest pose** +- Check if position data is being treated as delta when it's absolute (or vice versa) +- Compare animation position values to rest translation values + +**Symptom: Bones don't match between skeleton and animation** +- Check ControllerID matching (CRC32 hash) +- Check case sensitivity of bone names in hash +- Log which controllers couldn't find matching bones + +### Star Citizen #ivo Animation Format (IN PROGRESS) + +**Status**: Rotation animation parsing implemented and working. Position animation skipped (compression not decoded). Both USD and glTF renderers have Ivo DBA animation support. Double-transform issue on child meshes has been diagnosed and solution documented (see below). + +#### Ivo DBA Animation - Double Transform Issue (RESOLVED) + +**Test Asset**: `BEHR_LaserCannon_S2.cga` with `BEHR_LaserCannon_S2.dba` + +**Symptoms**: +- Body mesh: Correct position in all modes (edit, object, pose) +- Barrel mesh: Double transform - positioned too far in +Y and -Z +- Both USD and glTF exports have the **same issue**, suggesting the problem is in model reading, not rendering +- Animation playback looks correct (barrel recoil motion is correct relative to body) +- When `skel:animationSource` is removed from USD, geometry is correct + +**Root Cause**: Animation values stored as deltas, not absolute local transforms. + +CryEngine/Ivo stores animation values as **deltas from rest pose**: +``` +animation_translation = (0, 0.045, 0) // small recoil delta +animation_rotation = identity // no rotation change +``` + +But USD SkelAnimation expects **absolute joint-local transforms**: +``` +usd_translation = rest_local_translation + animation_delta +usd_rotation = rest_local_rotation * animation_delta_rotation +``` + +When Blender imports USD, it computes animation curves as: +```cpp +// From Blender's usd_skel_convert.cc lines 244-246 +bone_xform = joint_local_xforms[i] * joint_local_bind_xforms[i].GetInverse(); +``` + +If the animation provides delta `(0, 0.045, 0)` but local_bind is `(0, -0.876, 0.41)`: +``` +delta = (0, 0.045, 0) * inv((0, -0.876, 0.41)) + = (0, 0.045, 0) * (0, 0.876, -0.41) + = (0, 0.92, -0.41) // HUGE offset - the "doubling" effect! +``` + +**Solution**: Convert animation deltas to absolute local transforms before writing to USD. + +For **translations** (additive): +```csharp +usd_translation = rest_local_translation + animation_delta_translation; +// Example: (0, -0.876, 0.41) + (0, 0.045, 0) = (0, -0.831, 0.41) +``` + +For **rotations** (quaternion multiplication): +```csharp +usd_rotation = rest_local_rotation * animation_delta_rotation; +// Order matters: rest * delta applies delta in local space +// Try delta * rest if results are wrong (parent space) +``` + +For **scales** (multiplicative): +```csharp +usd_scale = rest_local_scale * animation_delta_scale; +// Usually (1,1,1) * (1,1,1) = (1,1,1) for most animations +``` + +**Corrected USD Output Example**: +```usda +// WRONG - delta values only: +float3[] translations.timeSamples = { + 0: [(0, 0.045113, 0)], // recoil delta + 1: [(0, 0, 0)], // "rest" but actually zero +} + +// CORRECT - absolute local transforms: +float3[] translations.timeSamples = { + 0: [(0, -0.831381, 0.410605)], // rest + recoil delta + 1: [(0, -0.876494, 0.410605)], // actual rest position +} +``` + +**Implementation Notes**: +- The rest local transform can be obtained from `restTransforms` array in the skeleton +- For bones without animation tracks, use rest transform directly (no conversion needed) +- This applies to ALL Ivo animations, not just additive ones - the Ivo format stores deltas by default + +**Files to Modify**: +- `CgfConverter/Renderers/USD/UsdRenderer.Animation.cs` - Add rest transform to Ivo animation values +- `CgfConverter/Renderers/Gltf/BaseGltfRenderer.Animation.cs` - Same fix for glTF export + +**Verification**: +1. Export USDA with animation +2. Check that animation translation values match rest position (not near-zero) +3. Import into Blender - child bones should stay attached to parents +4. Play animation - should see small recoil motion, not large offset + +--- + +**Test Asset**: `aloprat_skel.chr` with 13 CAF animations from `aloprat/*.caf` + +#### What's Implemented + +**ChunkIvoCAF_900** (`CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF_900.cs`): +- Parses `#caf` signature block (magic 0xAA55 for DBA, 0xFFFF for CAF) +- Reads bone hash array (CRC32 identifiers for each bone) +- Reads controller entries (24 bytes each): rotation track (12 bytes) + position track (12 bytes) +- Parses rotation keyframe data (supports formats 0x40, 0x42, 0x43) +- Parses rotation time keys (uint16 normalized to frame indices) + +**Controller Entry Structure** (per 010 template `Animation.bt`): +``` +// 24 bytes per bone +struct AnimControllerEntry_Ivo { + // Rotation track (12 bytes) + uint16 numRotKeys; // Number of rotation keyframes + uint16 rotFormatFlags; // e.g., 0x8042 + uint32 rotTimeOffset; // Offset from controller start to time data + uint32 rotDataOffset; // Offset from controller start to rotation data + + // Position track (12 bytes) + uint16 numPosKeys; // Meaning varies by format - NOT always a count + uint16 posFormatFlags; // e.g., 0xC040, 0xC142, 0xC242 + uint32 posTimeOffset; // Offset from controller start to time data + uint32 posDataOffset; // Offset from controller start to position data +}; +``` + +**IMPORTANT**: All offsets are relative to the **start of each controller**, not the start of the controllers array. + +**Rotation Format Flags** (0x80xx) - determines time structure: +| Flag | Time Format | Data Format | +|------|-------------|-------------| +| `0x8040` | ubyte array[numRotKeys] (padded to 4 bytes) | numRotKeys × Quaternion (16 bytes each) | +| `0x8042` | 8-byte header (uint16 × 2 + uint32 marker) | numRotKeys × Quaternion (16 bytes each) | + +**Rotation Time Header** (for 0x8042): +- 8-byte header: `uint16 startTime`, `uint16 endTime`, `uint32 marker` +- Followed by `numRotKeys` quaternions (16 bytes each, uncompressed) + +**Position Format Flags** (0xC0xx) - determines structure: +| Flag | Time Format | Data Format | numPosKeys Meaning | +|------|-------------|-------------|-------------------| +| `0xC040` | ubyte array[numPosKeys] | numPosKeys × Vector3, no header | Actual position count | +| `0xC142` | 8-byte header (uint16 × 2 + uint32 marker) | 2 × Vector3, no header | Unknown (not count) | +| `0xC242` | 8-byte header (uint16 × 2 + uint32 marker) | 8-byte header + 1 × Vector3 | Unknown (not count) | + +**Position Time Header** (for 0xC142, 0xC242): +``` +struct PosTimeBlock { + uint16 timeStart; // Usually 0 + uint16 timeEnd; // Usually 30 (0x1E) + uint32 marker; // e.g., 0x65294A55 +}; +``` + +**Position Data Header** (for 0xC242 only): +- 8 bytes including a NaN marker (0x7F7FFFFF pattern observed) +- Followed by 1 Vector3 + +**Format Flags Breakdown** (Unvetted - Under Investigation): + +Flag Structure: `0xABCD` (16-bit) + +| Nibble | Position | Values Observed | Meaning | +|--------|----------|-----------------|---------| +| A | 1st | 8, C | Data type: 8 = Quaternion (rotation), C = Vec3 (position) | +| B | 2nd | 0, 1, 2 | Compression/format variant (see below) | +| C | 3rd | 4 (always) | Likely indicates uncompressed/raw data | +| D | 4th | 0, 2 | Time format: 0 = ubyte, 2 = ushort | + +**Rotation Flags (Nibble A = 8)**: + +| Flag | Time Format | Data Format | +|------|-------------|-------------| +| 0x8040 | ubyte | Uncompressed Quat (4 floats, 16 bytes/key) | +| 0x8042 | ushort | Uncompressed Quat (4 floats, 16 bytes/key) | + +**Position Flags (Nibble A = C)**: + +| Flag | Time Format | Data Format | Notes | +|------|-------------|-------------|-------| +| 0xC040 | ubyte | Full Vec3 (3 floats, 12 bytes/key) | Standard uncompressed | +| 0xC042 | ushort | Full Vec3 (3 floats, 12 bytes/key) | Standard uncompressed | +| 0xC140 | ubyte | Unknown variant | Possibly CryHalf3? | +| 0xC142 | ushort | Unknown variant | Possibly CryHalf3? | +| 0xC240 | ubyte | Compressed - possibly SNORM16×2 + shared Z | See notes below | +| 0xC242 | ushort | Compressed - base Vec3 + delta indices? | See notes below | + +**2nd Nibble Hypothesis (Position Compression Level)**: + +| Value | Likely Meaning | +|-------|----------------| +| 0 | Full precision (3 × float32 = 12 bytes/key) | +| 1 | Half precision (3 × float16 = 6-8 bytes/key)? | +| 2 | Delta/indexed compression (base Vec3 + uint16 offsets) | + +**0xC240 Format (Observed)**: + +For a 3-key static position: +- `[SNORM16 x][SNORM16 y]` × 3 keys = 12 bytes +- `[float32 Z or scale]` = 4 bytes +- `0xFFFF` as SNORM16 ≈ 0.0 +- `0x7F7F` as SNORM16 ≈ 0.996 (near 1.0) + +**0xC242 Format (Under Investigation)**: + +For 14 keys, data structure appears to be: +- `[4 bytes header?]` 00 00 1E 00 +- `[unknown 8 bytes]` +- `[Base Vec3]` 3 floats (12 bytes) +- `[possible 4th float]` +- `[14 × uint16 delta/index values]` + +The repeating pattern `03 02 01 00 06 02 00 00` suggests indexed or delta-encoded positions referencing the base Vec3. + +**Reference: Lumberyard SNORM16 Packing** (from `VertexFormats.h` PackingSNorm namespace): +```cpp +int16 tPackF2B(float f) { return (int16)(f * 32767.0f); } +float tPackB2F(int16 i) { return (float)i / 32767.0f; } +``` +- `0x7FFF` (32767) = +1.0 +- `0x8001` (-32767) = -1.0 +- `0x0000` = 0.0 + +**Reference: Lumberyard Time Formats** (`EKeyTimesFormat`): + +| Value | Name | Description | +|-------|------|-------------| +| 0 | eF32 | 32-bit float | +| 1 | eUINT16 | 16-bit unsigned | +| 2 | eByte | 8-bit unsigned | +| 6 | eBitset | Bitset encoded | + +Note: Star Citizen may have remapped these values (0=byte, 2=ushort observed) + +**Lumberyard Reference Files**: +- `Code/CryEngine/CryCommon/CryCharAnimationParams.h` - Animation flag enums +- `Code/CryEngine/CryCommon/CryHalf.inl` - CryHalf implementation +- `Code/CryEngine/CryCommon/VertexFormats.h` - SNORM packing (PackingSNorm) +- `Code/Tools/RC/ResourceCompilerPC/CGA/ControllerPQ.h` - Controller formats +- `Gems/CryLegacy/Code/Source/CryAnimation/ControllerOpt.h` - Runtime controller + +**DONE**: Warnings are now logged in `ChunkIvoCAF_900.cs` when encountering format flags not in the known set (0x8040/0x8042 for rotation, 0xC040/0xC142/0xC242 for position). + +#### Current Issues + +**Position data not usable**: +- Format `0x40` positions appear to be root motion direction vectors (unit length ~1.0), not bone translations +- Format `0x42` positions use SmallTree compression (6 bytes) which we can't decode as 12-byte Vector3 +- Reading compressed data as uncompressed produces garbage that looks valid (e.g., `(0.619, 0.778, -0.178)`) +- **Current workaround**: Skip all CAF position tracks, use rest translations from skeleton + +**Armature display issue**: +- Edit mode: Armature looks correct (restTransforms working) +- Pose/Object mode: Armature positions are wrong (animation translations issue) +- Translations in USD are all rest pose values (constant across frames) since we skip position tracks +- Rotations are being read and vary per frame +- Issue may be in rotation data interpretation or bone ordering + +**Observed symptoms in Blender** (walk animation): +- World bone at (0,0,0) - correct +- Lower leg/tail/foot bones clustered near origin with small offsets +- Spine2 significantly offset (was reading garbage before position skip fix) +- Spine3 ~1m off with another cluster (arms, head, etc.) +- Animation plays but skeleton structure is wrong + +#### Research Needed + +1. **SmallTree position compression**: The 6-byte position format (0x42) needs reverse engineering + - May be similar to SmallTree48BitQuat but for Vector3 + - Could be quantized/normalized positions requiring scale factor + +2. **Root motion data**: Format 0x40 positions look like direction vectors + - Need to understand what this data represents + - May need to extract root motion separately from bone animation + +3. **Rotation data validation**: Even with position fix, armature is wrong + - Verify quaternion decoding for all formats + - Check if rotation data is local or world space + - Verify bone hash to joint path mapping is correct + +4. **Bone ordering**: Animation joints array order may matter + - Currently built from `cafAnim.BoneTracks` dictionary iteration order + - May need to match skeleton's joint order + +#### Key Files + +- `CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF_900.cs` - Main #ivo CAF parser +- `CgfConverter/CryEngineCore/Chunks/ChunkIvoCAF.cs` - Base class with dictionaries for rotation/position data +- `CgfConverter/Models/Structs/IvoAnimationStructs.cs` - Controller entry struct +- `CgfConverter/Renderers/USD/UsdRenderer.Animation.cs` - USD export (CreateSkelAnimationFromCaf) +- `010-Templates/chunks/Animation.bt` - 010 Editor template with format documentation + +#### Test Commands + +```bash +# Run aloprat CAF animation test +dotnet test --filter "FullyQualifiedName~Aloprat_Skel_USD_WithCAF" + +# Output files generated at: +# D:\depot\SC4.1\Data\Objects\Characters\Creatures\aloprat\aloprat_skel_anim_*.usda +``` + +### Known Issues + +#### Fixed Issues +- ~~**GeomSubset indices**: "invalid indices" warning in Blender~~ - FIXED: Convert vertex indices to face indices for elementType="face" +- ~~**Normal count mismatch**: "Loop normal count mismatch" warning~~ - FIXED: Expand normals array to match faceVertexIndices for faceVarying interpolation +- ~~**Ivo format file size explosion**: 700MB+ output files~~ - FIXED: Extract only per-subset vertices and remap indices (same fix as Collada/glTF renderers) +- ~~**Skeleton infinite recursion**: Stack overflow in BuildJointPaths~~ - FIXED: Added cycle detection to skip already-processed bones +- ~~**USD Skinning bone-to-vertex mapping incorrect for Star Citizen .skin files**~~ - FIXED: `BoneIndex` values in skinning data are indices into `CompiledBones` array order, but USD's `skel:jointIndices` expects indices into the `joints` array built from `jointPaths`. Since `BuildJointPaths()` walks the bone hierarchy depth-first, it produces different ordering than `CompiledBones`. Fix adds `_compiledBoneIndexToJointIndex` mapping in `CreateSkeleton()` and uses it in `AddSkinningAttributes()`. + +#### Open Issues +- **Node transforms incorrect**: Child nodes not positioned correctly in complex models (e.g., Avenger spaceship). Currently using `node.Transform` directly. + - Attempted: `node.LocalTransform` (full transpose) - Translations lost, all objects at origin + - Attempted: Transpose 3x3 rotation only + move translation to row 4 - Translations wrong + - **Priority**: Fix before armature/skinning work on complex multi-node models + +- **glTF: ArcheAge models skeleton/geometry incorrect**: ArcheAge uses `ChunkCompiledBones_801` which stores B2W (boneToWorld) matrix and computes BindPoseMatrix (W2B) by inversion. The glTF bone hierarchy building produces incorrect results - skeleton and geometry are malformed in Blender. USD export works correctly for the same files. + - Tested with: `archeage/game/objects/characters/animals/chicken/chicken.chr` + - Multiple fix attempts: changed root bone detection, rewrote bone transform computation to match USD approach, tried two-pass bone index mapping + - Symptoms: Geometry appears but skeleton is collapsed/wrong, bone transforms incorrect + - **Workaround**: Use USD export for ArcheAge models (works correctly with animations) + +### Multiple UV Layer Support (IN PROGRESS) + +**Status**: USD renderer infrastructure complete. Parsing not yet implemented. + +**USD Support**: Fully supported via primvars. Each UV set is a separate named primvar: +- Primary UV: `primvars:{nodeName}_UV` (texCoord2f[]) +- Secondary UV: `primvars:{nodeName}_UV2` (texCoord2f[]) +- Shaders reference UV sets via `UsdPrimvarReader_float2` with `varname` input + +**Implementation**: +- `GeometryInfo.UVs2` property added for second UV layer +- `UsdRenderer.Geometry.cs` outputs `_UV2` primvar when `UVs2` is populated +- Collada multi-UV support: TODO (uses multiple `` elements with `TEXCOORD` semantic and `set` indices) + +**CryEngine Data Sources** (research needed): +- Vertex format `eVF_P3S_C4B_T2S_T2S` exists in `Enums.cs` with comment "For UV2 support" +- Vertex format `eVF_P3F_C4B_T2F_T2F` also supports dual texture coordinates +- These formats are defined but **not yet parsed** in `ChunkDataStream` +- Need to identify which games/assets actually use dual-UV vertex formats +- Parsing would occur in `ChunkDataStream_800.cs` / `ChunkDataStream_801.cs` in the `VERTSUVS` case + +**Next Steps**: +1. Find sample assets that use dual-UV vertex formats +2. Extend `ChunkDataStream` parsing to extract second UV set +3. Populate `GeometryInfo.UVs2` during geometry aggregation +4. Test with USD export to verify primvar output + +--- + +## Collada Animation Export - Blender Compatibility (FIXED) + +### Problem +Animations exported to Collada were not importing into Blender. Blender reported "removed X unused curves". + +### Root Causes (Two Issues) + +1. **Wrong SID**: Joint nodes used `sid="matrix"` instead of `sid="transform"` +2. **Wrong element order**: Animation XML had `` before `` elements + +Blender's Collada importer expects: +- Joint nodes with `` (not decomposed transforms or `sid="matrix"`) +- Animation channels targeting `BoneName/transform` +- Animation elements in order: `source`, `sampler`, `channel` (not `channel` first) + +### Solution Implemented +Changed joint nodes and animations to use matrix-based transforms with SID `transform`: + +**Joint nodes** (`ColladaModelRenderer.Skeleton.cs`): +```xml + + ... + +``` + +**Animation channels** (`ColladaModelRenderer.Animation.cs`): +```xml + + /* 16 floats per keyframe */ + + + +``` + +### Key Technical Details +- Animation output uses `float4x4` matrices (16 values per keyframe) +- Channel targets use `/transform` suffix to match joint's matrix SID +- Removed `library_animation_clips` (Blender doesn't require it) +- Matrices built from position (Vector3) + rotation (Quaternion) at each keyframe +- **Element order fix** in `ColladaAnimation.cs`: Reordered property declarations so XML serializer outputs `source`, `sampler`, `channel` in correct Collada spec order + +--- + +## Collada Animation - Blender Multi-Animation Limitation + +### Problem +When importing a Collada file with multiple animations (e.g., `turret_a.dae` with `turret_a_open`, `turret_a_close`, etc.), Blender: +1. Only creates **one action** named "ArmatureAction" +2. Merges all animation curves from all clips into that single action +3. Ignores any `` elements that might group animations + +### Root Cause (Blender Source Code Analysis) + +**Blender source location**: `source/blender/io/collada/` (prior to removal in Blender 4.5) + +1. **Action naming** (`source/blender/animrig/intern/animdata.cc:195-249`): + ```cpp + SNPRINTF_UTF8(actname, DATA_("%sAction"), id->name + 2); + ``` + The action name is generated from the Blender object's name, not from Collada XML. For an Armature named "Armature", this creates "ArmatureAction". + +2. **`` support never implemented** (`DocumentImporter.cpp:1076`): + ```cpp + bool DocumentImporter::writeAnimationClip(const COLLADAFW::AnimationClip *animationClip) + { + return true; + /* TODO: implement import of AnimationClips */ + } + ``` + This function is a no-op. Animation clips are parsed by OpenCollada but completely ignored by Blender. + +3. **All animations merged** (`AnimationImporter.cpp`): + - `translate_Animations()` is called per node + - All FCurves from all `` elements are attached to one action via `ensure_action_and_slot_for_id()` + - No boundary checking between different animation clips + +### Collada Spec Support (Not Used by Blender) + +The Collada 1.4.1 spec supports multiple animation clips via: +```xml + + + + + + + + + + +``` + +But Blender's importer ignores this entirely. + +### Solution: Separate .dae Files Per Animation + +Since Blender cannot import multiple animations from a single Collada file, we export each animation to a separate file (same approach as USD renderer): + +- `model.dae` - Base model with skeleton and geometry +- `model_anim_walk.dae` - Walk animation only +- `model_anim_run.dae` - Run animation only + +This allows users to import each animation as a separate action in Blender. + +### Additional Issues Found and Fixed + +1. **Matrix translation convention** (FIXED): + - Animation code was using `matrix.Translation = position` which sets M41/M42/M43 + - CryEngine/Collada stores translation in column 4 (M14/M24/M34) + - Fixed by setting `matrix.M14 = position.X`, etc. + +2. **Rest pose fallback** (FIXED): + - Bones without position tracks were getting `Vector3.Zero` translation + - Fixed by storing rest transforms and using them as fallback + +### References +- Blender Collada importer was deprecated in 4.2 and removed in 4.5 +- glTF is the preferred format for multi-animation import in Blender + +--- + +## Reference Data + +### Mechanic.chr Bone Matrices (for skeleton debugging) + +Cryengine Matrix3x4 format, column 4 is translation, row major. Z up, Y forward. + +**Bip01**: +``` +worldToBone: [[-0.000000, 1.000000, 0.000000, -0.000000] [-1.000000, -0.000000, -0.000000, -0.000000] [-0.000000, -0.000000, 1.000000, -0.000000]] +boneToWorld: [[-0.000000, -1.000000, -0.000000, -0.000000] [1.000000, -0.000000, -0.000000, 0.000000] [0.000000, -0.000000, 1.000000, 0.000000]] +``` + +**Bip01_Pelvis**: +``` +worldToBone: [[0.000000, 0.000000, 1.000000, -0.950611] [-0.000003, 1.000000, -0.000000, -0.000000] [-1.000000, -0.000003, 0.000000, -0.000000]] +boneToWorld: [[0.000000, -0.000003, -1.000000, 0.000000] [0.000000, 1.000000, -0.000003, 0.000000] [1.000000, -0.000000, 0.000000, 0.950611]] +``` + +**Bip01_L_Thigh**: +``` +worldToBone: [[-0.141242, -0.078662, -0.986845, 0.920858] [-0.011145, 0.996901, -0.077868, 0.072651] [0.989912, 0.000000, -0.141681, 0.232642]] +boneToWorld: [[-0.141242, -0.011145, 0.989912, -0.099421] [-0.078662, 0.996901, 0.000000, 0.000010] [-0.986845, -0.077868, -0.141681, 0.947363]] +``` + +Use this data to verify restTransforms and bindTransforms calculations for skeleton export. diff --git a/TODO b/TODO index b927495b..a5bd2e7f 100644 --- a/TODO +++ b/TODO @@ -1,39 +1,6 @@ TODO File -Currently at version 0.1, which can successfully export an obj file to the console, which can be cut/paste and -imported into Blender. +2.0 release +- Animations +- USD support -Version 0.8 -- Usage output **COMPLETE** 7/30 -- Switch implementation: **COMPLETE** 8/1 - - -flipUV (probably default on, since I've never had to not do this) - - -obj (export to obj) - - -blend (export to .blend, to be implemented in 1.0) (can be used in conjunction with -obj) - - -output (where to write the output file; default to .cgf/.cga input directory) -- Figure out how to convert 8 byte vertex info into 3 vertices **COMPLETE** 7/25 -- Refactor to use Where(a=>a.blahblah==blahblahblah) and clean up classes. **COMPLETE** 7/30 -- .mtl files: Find the associated mtl file for the .cgf file from the MtlName chunk. **COMPLETE** 8/1 -- Add support for Cryengine 3.6 and newer files **COMPLETE** 8/1 - -Version 0.9 -- .mtl files - convert Cryengine .mtl files to .obj .mtl files. Better mtl file handling (search given obj dirs) **COMPLETE** -- Break classes into separate files. **COMPLETE** -- Input validation, error checking, etc. **COMPLETE** -- Move version, id, type and offset so they are read from the chunk table, not the chunks. **COMPLETE** 8/29/15 -- add build version into the output **COMPLETE** -- Bug fix so some .obj files import properly to Blender (exploded geometry issue) **COMPLETE** 8/29 -- Output to a user defined file (default to ) **COMPLETE** 12/2015 -- Icon for the exe. ** COMPLETE 9/18/17 ** -- Implement controller and meshphysics chunks ** COMPLETE 10/31/15 ** -- Collada exporter ** IN PROGRESS - 90% ** - -Version 1.0 -- Implement animation files. -- Implement armature files. ** IN PROGRESS ** (Armatures imported, but can't export yet) - -Version 1.1 -- Export directly to .blend files **IN PROGRESS** - -Version 1.2 -- TBD - diff --git a/brfl_fps_behr_p4ar-chr-data.md b/brfl_fps_behr_p4ar-chr-data.md new file mode 100644 index 00000000..6a1e7e11 --- /dev/null +++ b/brfl_fps_behr_p4ar-chr-data.md @@ -0,0 +1,263 @@ +# brfl_fps_behr_p4ar.chr Animation Analysis + +## File Locations +- **Model**: `{sc41ObjectDir}\Objects\fps_weapons\weapons_v7\behr\rifle\p4ar\brfl_fps_behr_p4ar.chr` +- **Animation DBA**: `{sc41ObjectDir}\Animations\fps_weapons\weapons_v7\behr\rifle\p4ar.dba` + +## Overview +- Number of animations: 9 +- Number of bones: 26 + +## Test Animations + +### Anim A: Rotation Only fire_trigger_01 +- **Name**: `animations/fps_weapons/weapons_v7/behr/rifle/p4ar/brfl_behr_p4ar_fire_trigger_01.caf` (index 2) +- **Animated bone**: `trigger02` (CRC32: 0xC9E2516F) + +### Anim B: Mixed (1 pos only + 1 rot only) fire_01 +- **Name**: `animations/fps_weapons/weapons_v7/behr/rifle/p4ar/brfl_behr_p4ar_fire_01.caf` (index 0) +- **Animated bones**: + - `bolt` (CRC32: 0x6DC0FD07) - position only + - `coverPlate` (CRC32: 0xD9A6C1AD) - rotation only + +### Anim C: +- **Name**: `animations/fps_weapons/weapons_v7/behr/rifle/p4ar/stocked_alerted_stand_brfl_behr_p4ar_reload_full_01_add.caf` (index 8) +- ** Animated bones**: + - `safe` (CRC32: 530851983 / 0x1FA4288F) + - `bolt` (CRC32: 1841364231 / 0x6DC0FD07) + - `magAttach` (CRC32: 1909618695 / 0x71D27807) + - `coverPlate` (CRC32: 3651584429 / 0xD9A6C1AD) + +## Bone Rest Poses (from skeleton) + +### root +- relative rot: (0, 0, 0, 1) [identity] +- relative pos: (0, 0, 0) +- world rot: (0, 0, 0, 1) +- world pos: (0, 0, 0) + +### trigger01 (child of root) +- relative rot: (0, 0, 0, 1) [identity] +- relative pos: (0, 0.044686, 0.042497) +- world rot: (0, 0, 0, 1) +- world pos: (0, 0.044686, 0.042497) + +### trigger02 (child of trigger01) +- relative rot: (0, 0, 0, 1) [identity] +- relative pos: (0, 0.011537, 0) +- world rot: (0, 0, 0, 1) +- world pos: (0, 0.056223, 0.042497) + +### bolt (child of root) +- relative rot: (0, 0, 0, 1) [identity] +- relative pos: (0, 0.242, 0.132063) +- world rot: (0, 0, 0, 1) +- world pos: (0, 0.242, 0.132063) + +### coverPlate (child of root) +- **relative rot: (0, 0.999048, 0, -0.043619)** [NOT identity! ~175° around Y] +- relative pos: (0.02241, 0.15797, 0.10213) +- world rot: (0, 0.999048, 0, -0.043619) +- world pos: (0.02241, 0.15797, 0.10213) + +### safe (child of root) +- relative rot: (0, 0, 0, 1) [identity] +- relative pos: 0.000000, 0.033961, 0.057669 +- world rot: (0, 0, 0, 1) +- world pos: 0.000000, 0.033961, 0.057669 + +### magAttach +- relative rot: 0.124774, 0.000000, 0.000000, 0.992185 +- relative pos: 0.000000, 0.173156, -0.010600 +- world rot: 0.124774, 0.000000, 0.000000, 0.992185 +- world pos: 0.000000, 0.173156, -0.010600 + +--- + +## Animation A Details + +### Metadata +- Start position: (0, 0, 0) +- Start rotation: (0, 0, 0, 1) [identity] + +### Animation Data (trigger02) +- 8 rotation keys, format 0x8042 +- Bone rest rotation: **identity (0, 0, 0, 1)** + +| Frame | Quaternion (x, y, z, w) | Magnitude | Approx Angle | +|-------|-------------------------|-----------|--------------| +| 0 | (0, 0, 0, 1) | 1.0 | 0° | +| 1 | (-0.1908, 0, 0, 0.9816) | 1.0 | ~22° around X | +| 2 | (-0.2322, 0, 0, 0.9727) | 1.0 | ~27° around X | +| 3 | (-0.2084, 0, 0, 0.9780) | 1.0 | ~24° around X | +| 4 | (-0.1513, 0, 0, 0.9885) | 1.0 | ~17° around X | +| 5 | (-0.0824, 0, 0, 0.9966) | 1.0 | ~9° around X | +| 6 | (-0.0244, 0, 0, 0.9997) | 1.0 | ~3° around X | +| 7 | (0, 0, 0, 1) | 1.0 | 0° | + +**Interpretation**: Trigger pull animation - rotates around X axis (typical trigger motion) + +--- + +## Animation B Details: + +### Metadata +- Start position: (0, 0, 0) +- Start rotation: (0, 0, 0, 1) [identity] + +### Animation Data (bolt) - Position Only +- 4 position keys, times: (0, 1, 2, 3) +- Bone rest position: **(0, 0.242, 0.132063)** +- Format: SNORM (need scale factor) + +| Frame | SNORM Values | Notes | +|-------|--------------|-------| +| 0 | (0, -8781, 0) | Bolt back? | +| 1 | (0, 0, 0) | At rest? | +| 2 | (0, 0, 0) | At rest? | +| 3 | (0, -8781, 0) | Bolt back? | + +**Missing**: SNORM scale factor to convert -8781 to meters + +### Animation Data (coverPlate) - Rotation Only +- 4 rotation keys, format 0x8040, times: (0, 1, 2, 3) +- Bone rest rotation: **(0, 0.999048, 0, -0.043619)** [~175° around Y] + +| Frame | Quaternion (x, y, z, w) | Magnitude | +|-------|-------------------------|-----------| +| 0 | (0, 0, 0, 1) | 1.0 | +| 1 | (0, 0, 0, 1) | 1.0 | +| 2 | (0, 0, 0, 1) | 1.0 | +| 3 | (0, 0, 0, 1) | 1.0 | + +**Note**: All identity quaternions - either this animation doesn't rotate coverPlate, or this is a test case for delta vs absolute interpretation. + +--- + +## Animation C Details: +- Additive animation? Not sure if this makes a difference +- Time data: 111 ticks. Both rot and pos time header has start anim at 0, end anim at 110 + +### Animation data (magRelease) - Rotation and position +Controller 0 (safe) is unanimated. +Controller 1 (bolt) pos Only +Controller 2 (magAttach) Both +Controller 3 (coverPlate) Both + +sample pos data (50 total positions, scale (?) = -0.375232, -0.224156, -0.374677) + +magAttach animation (controller index 2) +Rot=111 keys (flags=0x8042), Pos=50 keys (flags=0xC142) +Pos info: X=ON Y=ON Z=ON | Scale: (-0.375232, -0.224156, -0.374677) +Sample rots: +0-11: (0.1248, 0.0000, 0.0000, 0.9922) mag=1.0000 [OK] +12-14: (0.3215, 0.2606, 0.0855, 0.9063) mag=1.0000 [OK] + +Sample positions +0-1: (0.068847, 0.146881, 0.147506) +2: (0.024621, 0.110255, 0.000011) +3: (0.021277, 0.110125, 0.002390) +4: (0.019582, 0.110255, 0.004917) + + +coverPlate Animation (controller index 3) +Rot=111 keys (flags=0x8042), Pos=3 keys (flags=0xC240) +Pos info: X=OFF Y=OFF Z=OFF | Scale: (0.029439, 0.157912, 0.102597). 3 pos keys all 0,0,0 +Rot info: 111 rotation keys, all set to 0,0,0,1. + + +--- + +## Analysis Notes (Claude Code - 2025-12-27) + +### CRITICAL FINDING: magAttach Proves Rotations are ABSOLUTE + +The magAttach bone in Animation C provides definitive proof: + +**magAttach rest rotation**: `(0.124774, 0, 0, 0.992185)` ≈ 14.3° around X axis + +**Animation frames 0-11**: `(0.1248, 0, 0, 0.9922)` - **SAME as rest rotation!** + +This proves rotations are **ABSOLUTE**, not deltas: + +| Interpretation | Calculation | Result | Matches Rest? | +|----------------|-------------|--------|---------------| +| **ABSOLUTE** | `final = animValue` | 14.3° around X | ✓ YES | +| **DELTA** | `final = rest * delta = rest * rest` | ~28.6° around X | ✗ NO (doubled!) | + +If the animation were storing deltas, identity (0,0,0,1) would mean "no change from rest". But the animation stores the rest rotation value itself, proving it's absolute. + +### Bolt Position Confirms Delta Pattern + +From Animation B (fire_01): +- Scale: `(0, 0.155398, 0.132063)` with only Y active +- Animation value after SNORM decode: `(0, -0.041644, 0)` +- Rest position: `(0, 0.242, 0.132063)` + +The bolt retracts ~4.2cm backwards. This matches `rest + delta` pattern: +- Final Y = 0.242 + (-0.041644) = 0.200356m ✓ + +### CONFIRMED CONVENTION + +**Ivo DBA uses DIFFERENT conventions for positions vs rotations:** + +| Component | Convention | Formula | Rationale | +|-----------|------------|---------|-----------| +| **Positions** | DELTA | `final = rest + animDelta` | SNORM compression works well for small deltas | +| **Rotations** | ABSOLUTE | `final = animValue` | Quaternions are already normalized, no benefit from delta encoding | + +### Why trigger02/coverPlate Tests Were Inconclusive + +1. **trigger02**: Rest rotation = identity, so `absolute` and `rest * delta` give same result +2. **coverPlate**: Animation shows identity quaternions - likely means "straighten to identity" (absolute interpretation), not "no change from rest" + +The coverPlate having all identity rotations in animations B and C, while having a ~175° rest rotation, suggests those animations intentionally reset it to identity (dust cover opens). + +### Previous Working Theory (SUPERSEDED) + +- **Positions**: `final = rest + animDelta` (CONFIRMED) +- **Rotations**: `final = rest * animDelta` (ASSUMED, needs verification) + +The trigger02 animation is consistent with both absolute and delta interpretations because rest = identity. + +### What Would Be Ideal Test Case + +Find an animation where: +1. A bone has non-identity rest rotation (like coverPlate with ~175° Y) +2. The animation has non-identity rotation values +3. We can verify visually in Blender what the expected result should be + +### Checking Other Animations in the DBA + +The DBA has 9 animations. Worth checking: +- Index 1, 3, 4, 5, 6, 7, 8 for animations that might have more revealing rotation data +- Particularly reload animations which likely animate more bones + +--- + +## Raw Data Reference + +### Anim A Bone Hierarchy +``` +root -> trigger01 -> trigger02 +``` + +### Anim B Bone Hierarchy +``` +root -> bolt +root -> coverPlate +``` + +### Anim C Bone Hierarchy +``` +root -> safe +root -> magAttach +root -> bolt +root -> coverPlate +``` + +### Coordinate System +- Rifle facing +Y direction +- Rifle dimensions: ~0.38m in Y, ~0.18m in Z (height) +- trigger02 head position: Y=0.056223m, Z=0.042497m +- magAttach only bone with slight -z pos diff --git a/cgf-converter/cgf-converter.cs b/cgf-converter/cgf-converter.cs index 5c990650..00be14af 100644 --- a/cgf-converter/cgf-converter.cs +++ b/cgf-converter/cgf-converter.cs @@ -1,6 +1,7 @@ using CgfConverter.Renderers; using CgfConverter.Renderers.Collada; using CgfConverter.Renderers.Gltf; +using CgfConverter.Renderers.USD; using CgfConverter.Renderers.Wavefront; using CgfConverter.Terrain; using CgfConverter.Utilities; @@ -129,6 +130,8 @@ private void ExportSingleModel(string inputFile) renderers.Add(new ColladaModelRenderer(_args, data)); if (_args.OutputGLB || _args.OutputGLTF) renderers.Add(new GltfModelRenderer(_args, data, _args.OutputGLTF, _args.OutputGLB)); + if (_args.OutputUSD) + renderers.Add(new UsdRenderer(_args, data)); RunRenderersAndThrowAggregateExceptionIfAny(renderers); } diff --git a/cgf-converter/cgf-converter.csproj b/cgf-converter/cgf-converter.csproj index 92a17802..00782bd3 100644 --- a/cgf-converter/cgf-converter.csproj +++ b/cgf-converter/cgf-converter.csproj @@ -24,8 +24,8 @@ Cryengine Converter Cryengine Converter Heffay Presents - 1.7.1.0 - 1.7.1 + 2.0.0.0 + 2.0.0 Converts Cryengine game files to commonly supported 3D formats. ©2015-2025 https://github.com/Markemp/Cryengine-Converter/ diff --git a/docs/blender_usd_animation_research.md b/docs/blender_usd_animation_research.md new file mode 100644 index 00000000..09ad599e --- /dev/null +++ b/docs/blender_usd_animation_research.md @@ -0,0 +1,342 @@ +# Blender USD Skeletal Animation Import Research + +This document analyzes how Blender imports USD skeletal animations (UsdSkelAnimation) and converts them to Blender Actions. This research is intended for implementing a USD exporter that works well with Blender's importer. + +## Executive Summary + +Blender's USD importer: +- **Only imports ONE animation per skeleton** - the one referenced by `skel:animationSource` +- Does NOT traverse the stage looking for multiple SkelAnimation prims +- Names the resulting Action after the SkelAnimation prim name +- Relies heavily on USD's `UsdSkelSkeletonQuery` API to resolve animation bindings + +--- + +## 1. SkelAnimation Import Process + +### 1.1 How `skel:animationSource` is Read + +Blender does **NOT** directly read the `skel:animationSource` relationship. Instead, it uses the USD SDK's `UsdSkelSkeletonQuery` API which internally resolves this relationship. + +**Code path:** + +``` +import_skeleton() [usd_skel_convert.cc:726] + └─> UsdSkelCache::GetSkelQuery(skel) [line 737] + └─> Returns UsdSkelSkeletonQuery that internally resolves animationSource + └─> skel_query.GetAnimQuery() [line 102] + └─> Returns UsdSkelAnimQuery for the bound animation +``` + +**Key code (`usd_skel_convert.cc:102-107`):** +```cpp +const pxr::UsdSkelAnimQuery &anim_query = skel_query.GetAnimQuery(); + +if (!anim_query) { + /* No animation is defined. */ + return; +} +``` + +The `GetAnimQuery()` method follows the `skel:animationSource` relationship automatically. If no animation is bound, the function returns early. + +### 1.2 Time Samples to Keyframes Conversion + +**Location:** `import_skeleton_curves()` in `source/blender/io/usd/intern/usd_skel_convert.cc:86-320` + +**Step 1: Get time samples** +```cpp +std::vector samples; +anim_query.GetJointTransformTimeSamples(&samples); // line 110 +``` + +**Step 2: Create FCurves (10 per joint: 3 location + 4 rotation + 3 scale)** +```cpp +constexpr int curves_per_joint = 10; // line 129 + +// For each joint, create curves for: +// - pose.bones[""].location (indices 0-2) +// - pose.bones[""].rotation_quaternion (indices 3-6) +// - pose.bones[""].scale (indices 7-9) +``` + +**Step 3: Sample animation at each time code** +```cpp +for (const double frame : samples) { + pxr::VtMatrix4dArray joint_local_xforms; + skel_query.ComputeJointLocalTransforms(&joint_local_xforms, frame); // line 230 + + // Decompose each joint's transform + pxr::UsdSkelDecomposeTransform(bone_xform, &t, &qrot, &s); // line 252 + + // Set keyframe values using set_fcurve_sample() +} +``` + +**Step 4: Finalize curves** +```cpp +for (FCurve *fcu : fcurves) { + BKE_fcurve_handles_recalc(fcu); // line 317 +} +``` + +### 1.3 Transform Computation + +The importer computes animation relative to the bind pose: +```cpp +// line 245-246 +const pxr::GfMatrix4d bone_xform = joint_local_xforms.AsConst()[i] * + joint_local_bind_xforms[i].GetInverse(); +``` + +This means the USD animation values represent the full joint-local transform, and Blender computes the delta from the bind pose. + +--- + +## 2. Multiple Animation Handling + +### 2.1 Current Behavior + +**Blender only imports the SINGLE animation bound via `skel:animationSource`.** There is: +- No code to discover multiple SkelAnimation prims in the file +- No import option to select which animation to import +- No code to import multiple animations as separate Actions + +### 2.2 Evidence from Code + +The stage traversal in `usd_reader_stage.cc` only creates readers for `UsdSkelSkeleton` prims, not `UsdSkelAnimation` prims: + +```cpp +// usd_reader_stage.cc:271-272 +if (params_.import_skeletons && prim.IsA()) { + return new USDSkeletonReader(prim, params_, settings_); +} +``` + +There is no `USDSkelAnimationReader` class. Animations are imported as a side effect of importing skeletons. + +### 2.3 What This Means for USD File Structure + +If your USD file contains multiple SkelAnimation prims (e.g., `walk`, `run`, `idle`): +- Blender will only import the one pointed to by `skel:animationSource` on the Skeleton +- Other animations will be completely ignored +- There is no way to import them without modifying the `skel:animationSource` binding + +--- + +## 3. Action Creation Details + +### 3.1 Action Creation Location + +**File:** `source/blender/io/usd/intern/usd_skel_convert.cc` +**Function:** `import_skeleton_curves()` starting at line 86 + +**Action creation (lines 119-120):** +```cpp +bAction *act = blender::animrig::id_action_ensure(bmain, &arm_obj->id); +BKE_id_rename(*bmain, act->id, anim_query.GetPrim().GetName().GetText()); +``` + +### 3.2 Action Naming + +The Action is named after the **SkelAnimation prim's name**, not the path: +- USD path `/Root/Skeleton/walk_anim` → Action name: `walk_anim` +- The prim name comes from `anim_query.GetPrim().GetName().GetText()` + +### 3.3 FCurve Creation + +Curves are created using Blender's new animation system: +```cpp +blender::animrig::Channelbag &channelbag = blender::animrig::action_channelbag_ensure( + *act, arm_obj->id); + +// FCurve paths follow Blender conventions: +// "pose.bones[\"BoneName\"].location" +// "pose.bones[\"BoneName\"].rotation_quaternion" +// "pose.bones[\"BoneName\"].scale" +``` + +--- + +## 4. Import Options Affecting Animation + +### 4.1 Relevant Import Parameters + +From `source/blender/io/usd/usd.hh`: + +```cpp +struct USDImportParams { + bool import_skeletons; // Must be true to import armatures AND their animations + bool import_blendshapes; // For blend shape animation (separate from skeletal) + // Note: There is NO separate "import_animation" toggle for skeletal animation +}; +``` + +### 4.2 Hidden `import_anim` Parameter + +The `import_skeleton()` function accepts an `import_anim` parameter: +```cpp +void import_skeleton(Main *bmain, + Object *arm_obj, + const pxr::UsdSkelSkeleton &skel, + ReportList *reports, + bool import_anim = true); // Defaults to true +``` + +However, this parameter is **not exposed in the UI** - it always defaults to `true`. There's no user option to import skeleton without animation. + +--- + +## 5. Hardcoded Limitations and TODOs + +### 5.1 Negative Determinant Matrices + +**Location:** `usd_skel_convert.cc:849-888` + +Bones with negative determinant matrices (from mirroring operations) prevent animation import: +```cpp +if (negative_determinant) { + valid_skeleton = false; + BKE_reportf(reports, RPT_WARNING, + "USD Skeleton Import: bone matrices with negative determinants detected..." + "The skeletal animation won't be imported"); +} +``` + +### 5.2 No Multiple Animation Support + +There are no TODOs or FIXMEs related to multiple animation import. This appears to be an intentional design decision rather than missing functionality. + +### 5.3 Rest Pose vs Bind Pose Handling + +The importer distinguishes between: +- **Bind transforms:** World-space joint transforms at bind time (`GetJointWorldBindTransforms`) +- **Rest transforms:** Optional rest pose that differs from bind (`HasRestPose()`, `ComputeJointLocalTransforms` with `atRest=true`) + +--- + +## 6. Complete Import Code Path + +``` +USD_import() [usd_capi_import.cc] + └─> USDStageReader::collect_readers() + └─> create_reader_if_allowed() for UsdSkelSkeleton + └─> new USDSkeletonReader(prim, params, settings) + + └─> reader->read_object_data() [for each reader] + └─> USDSkeletonReader::read_object_data() [usd_reader_skeleton.cc:24] + └─> import_skeleton(bmain, object_, skel_, reports()) + [usd_skel_convert.cc:726] + + └─> UsdSkelCache::GetSkelQuery(skel) // Creates skeleton query + └─> Creates bones from joint hierarchy + └─> Gets bind transforms + └─> Sets bone parenting and lengths + └─> set_rest_pose() // If rest pose differs from bind + └─> import_skeleton_curves() // If import_anim && valid_skeleton + └─> skel_query.GetAnimQuery() // Resolves skel:animationSource + └─> anim_query.GetJointTransformTimeSamples() + └─> Creates Action, names it after SkelAnimation prim + └─> Creates FCurves for each joint + └─> Samples animation at each time code + └─> Recalculates curve handles +``` + +--- + +## 7. Recommendations for USD Exporter + +### 7.1 File Structure for Best Blender Compatibility + +``` +/Root + /Armature (Xform - becomes SkelRoot) + /Skeleton (UsdSkelSkeleton) + skel:animationSource -> + + /Animation (UsdSkelAnimation - CHILD of skeleton) + joints: [...] + translations: [...] (time-sampled) + rotations: [...] (time-sampled) + scales: [...] (time-sampled) + + /SkinnedMesh (UsdGeomMesh with skel binding) +``` + +### 7.2 Key Points + +1. **Single animation per skeleton**: Place ONE SkelAnimation as a child of the skeleton and bind it via `skel:animationSource`. Blender will ignore any other animations. + +2. **Animation prim naming**: The SkelAnimation prim name becomes the Blender Action name. Choose descriptive names. + +3. **Use relative paths for animationSource**: Blender's exporter uses relative paths: + ```cpp + // usd_writer_armature.cc:115-116 + usd_skel_api.CreateAnimationSourceRel().SetTargets( + pxr::SdfPathVector({pxr::SdfPath(skel_anim.GetPath().GetName())})); + ``` + +4. **Avoid negative scale/mirroring**: Bone matrices with negative determinants will cause animation import to fail. + +5. **Store decomposed transforms**: Blender expects translations, rotations (as quaternions), and scales. Using `SetTransforms()` on UsdSkelAnimation will automatically decompose matrices. + +6. **Joint ordering must match**: The joint order in the SkelAnimation must match the Skeleton's joint order. + +### 7.3 For Multiple Animations Workflow + +Since Blender only imports one animation, if you need multiple animations: + +1. **Export separate USD files** per animation, each with the animation bound via `skel:animationSource` + +2. **Or** modify the USD file's `skel:animationSource` relationship between imports + +3. **Or** wait for future Blender versions that may add multi-animation support + +### 7.4 Blender Export Structure Reference + +When Blender exports, it creates this structure (from `usd_writer_armature.cc`): + +```cpp +// Skeleton path: /Root/Armature/Skeleton +// Animation path: /Root/Armature/Skeleton/ + +pxr::SdfPath anim_path = usd_export_context_.usd_path.AppendChild(anim_name); +skel_anim = pxr::UsdSkelAnimation::Define(stage, anim_path); +``` + +The animation is always a direct child of the skeleton, and the action name (sanitized) is used as the prim name. + +--- + +## 8. Blend Shape Animation Notes + +Blend shape animation follows a similar pattern but uses: +- `GetInheritedAnimationSource()` on the skeleton's binding API +- Falls back to `GetAnimationSource()` if inherited is not available +- Looks for `blendShapeWeights` attribute on the SkelAnimation + +**Location:** `usd_skel_convert.cc:555-678` + +```cpp +pxr::UsdPrim anim_prim = skel_api.GetInheritedAnimationSource(); + +if (!anim_prim) { + skel_api.GetAnimationSource(&anim_prim); +} +``` + +This means blend shape and skeletal animation should use the **same** SkelAnimation prim for Blender to import both correctly. + +--- + +## File References + +| File | Description | +|------|-------------| +| `source/blender/io/usd/intern/usd_skel_convert.cc` | Main skeletal animation import logic | +| `source/blender/io/usd/intern/usd_skel_convert.hh` | Header with function declarations | +| `source/blender/io/usd/intern/usd_reader_skeleton.cc` | Skeleton prim reader | +| `source/blender/io/usd/intern/usd_armature_utils.cc` | FCurve creation utilities | +| `source/blender/io/usd/intern/usd_writer_armature.cc` | Export counterpart (for reference) | +| `source/blender/io/usd/intern/usd_reader_stage.cc` | Stage traversal and reader creation | +| `source/blender/io/usd/usd.hh` | Import/export parameters | diff --git a/docs/usd_animation_research.md b/docs/usd_animation_research.md new file mode 100644 index 00000000..e3410856 --- /dev/null +++ b/docs/usd_animation_research.md @@ -0,0 +1,468 @@ +# USD Skeletal Animation Architecture Research + +This document provides research findings for implementing a USD exporter with skeletal animation support. It covers animation binding, multiple animations, and recommended organization patterns. + +--- + +## 1. Multiple Animations Per Skeleton + +### Answer: One Animation Source Per Skeleton Instance + +The `skel:animationSource` relationship is **single-target only**. A skeleton can only have one active animation at any given time. + +### Evidence + +**Schema Definition** (`pxr/usd/usdSkel/schema.usda:162-169`): +```usda +rel skel:animationSource ( + customData = { + string apiName = "animationSource" + } + doc = """Animation source to be bound to Skeleton primitives at or + beneath the location at which this property is defined. + """ +) +``` + +**Warning for Multiple Targets** (`pxr/usd/usdSkel/bindingAPI.cpp:361-386`): +```cpp +UsdPrim +_GetFirstTargetPrimForRel(const UsdRelationship& rel, + const SdfPathVector& targets) +{ + if (targets.size() > 0) { + if (targets.size() > 1) { + TF_WARN("%s -- relationship has more than one target. " + "Only the first will be used.", + rel.GetPath().GetText()); + } + const SdfPath& target = targets.front(); + // ... + } + return UsdPrim(); +} +``` + +This function explicitly warns and discards additional targets if multiple are specified. + +### API Signatures + +**bindingAPI.h:328-338**: +```cpp +/// Animation source to be bound to Skeleton primitives at or +/// beneath the location at which this property is defined. +USDSKEL_API +UsdRelationship GetAnimationSourceRel() const; + +USDSKEL_API +UsdRelationship CreateAnimationSourceRel() const; +``` + +**bindingAPI.h:440-450** (Query methods): +```cpp +/// Returns true if an animation source binding is defined, and sets +/// \p prim to the target prim. +USDSKEL_API +bool GetAnimationSource(UsdPrim* prim) const; + +/// Returns the animation source bound at this prim, or one of its ancestors. +USDSKEL_API +UsdPrim GetInheritedAnimationSource() const; +``` + +--- + +## 2. Animation Switching/Blending Support + +### Answer: No Native Blending - Use Value Clips for Sequencing + +USD does **not** have native animation blending or clip switching within UsdSkel. However: + +1. **Value Clips**: The official USD mechanism for animation sequencing +2. **Future Support**: Architecture anticipates compound animation sources + +### No SkelAnimationClip Schema + +There is no `SkelAnimationClip`, `AnimationLibrary`, or animation stack concept in UsdSkel. The schema only defines: +- `UsdSkelSkeleton` - The skeleton definition +- `UsdSkelAnimation` - A single animation source +- `UsdSkelBindingAPI` - Binds animations to geometry + +### Future Animation Blending (Not Yet Implemented) + +**From `pxr/usd/usdSkel/doxygen/objectModel.dox:83-87`**: +``` +A UsdSkelAnimQuery is created through a UsdSkelCache instance. This is because +we anticipate adding _compound_ animation sources like animation blenders +in the future, and expect that different instances of blenders may reference +many of the same animations, so requiring a UsdSkelAnimQuery to be constructed +through a UsdSkelCache presents an opportunity to share references to queries +internally. +``` + +### Value Clips: The Official Approach + +**From `pxr/usd/usdSkel/doxygen/schemas.dox:14-16`**: +``` +Animations can be published individually and referenced back into scenes. +This not only allows re-use, but also enables sequencing of animations as +Value Clips. +``` + +Value Clips allow you to sequence multiple USD files containing animation data, switching between them over time. + +--- + +## 3. Recommended Organization for Multiple Animations + +### Pattern A: Separate Animation Files with Value Clips (Recommended) + +Store each animation (walk, run, idle) as a separate `.usda` file with a `SkelAnimation`, then use Value Clips to sequence them. + +**Animation File Structure:** +``` +/assets/ + character.usda # Main character with Skeleton + animations/ + walk.usda # SkelAnimation for walk + run.usda # SkelAnimation for run + idle.usda # SkelAnimation for idle + instances/ + character_walking.usda # References character + walk clips + character_running.usda # References character + run clips +``` + +**walk.usda:** +```usda +#usda 1.0 + +def SkelAnimation "WalkAnim" { + uniform token[] joints = ["Root", "Root/Spine", "Root/Spine/Head", ...] + + float3[] translations.timeSamples = { + 0: [(0,0,0), (0,1,0), ...], + 1: [(0.1,0,0), (0,1,0), ...], + // ... + } + + quatf[] rotations.timeSamples = { + 0: [(1,0,0,0), (1,0,0,0), ...], + 1: [(0.99,0.1,0,0), ...], + // ... + } + + half3[] scales.timeSamples = { + 0: [(1,1,1), (1,1,1), ...], + } +} +``` + +**Sequence with Value Clips:** +```usda +#usda 1.0 + +def SkelRoot "Character" ( + references = @./character.usda@ + prepend apiSchemas = ["SkelBindingAPI"] + + clips = { + dictionary default = { + asset[] assetPaths = [ + @./animations/idle.usda@, + @./animations/walk.usda@, + @./animations/run.usda@ + ] + string primPath = "/WalkAnim" # Path within each clip file + + # (stage_time, clip_index) - which clip is active when + double2[] active = [ + (0, 0), # idle at frame 0 + (30, 1), # walk at frame 30 + (60, 2), # run at frame 60 + (90, 0) # back to idle at frame 90 + ] + + # (stage_time, clip_time) - time remapping + double2[] times = [ + (0, 0), (30, 30), # idle: stage 0-30 maps to clip 0-30 + (30, 0), (60, 30), # walk: stage 30-60 maps to clip 0-30 + (60, 0), (90, 30), # run: stage 60-90 maps to clip 0-30 + (90, 0), (120, 30) # idle: stage 90-120 maps to clip 0-30 + ] + } + } +) { + rel skel:animationSource = + rel skel:skeleton = +} +``` + +### Pattern B: Hierarchical Binding Override + +Different skeleton instances can use different animations via inheritance override. + +**From test `pxr/usd/usdSkel/testenv/testUsdSkelCache/populate.usda`:** +```usda +def SkelRoot "AnimBinding" { + def Scope "Scope" (prepend apiSchemas = ["SkelBindingAPI"]) { + rel skel:animationSource = + + def Skeleton "Inherit" {} + + def Skeleton "Override" (prepend apiSchemas = ["SkelBindingAPI"]) { + rel skel:animationSource = + } + } +} +``` + +- `Skeleton "Inherit"` uses `` (inherited from parent) +- `Skeleton "Override"` uses `` (explicitly overridden) + +### Pattern C: Instance-Based Animation + +**From `pxr/usd/usdSkel/doxygen/schemas.dox:486-542`:** +```usda +over "A" (prepend apiSchemas = ["SkelBindingAPI"]) { + rel skel:skeleton = + rel skel:animationSource = + + over "B" (prepend apiSchemas = ["SkelBindingAPI"]) { + rel skel:animationSource = + } + over "C" (prepend apiSchemas = ["SkelBindingAPI"]) { + rel skel:animationSource = + rel skel:skeleton = + } +} +``` + +This creates two skeleton instances: +- One at `` with animation `` +- One at `` with animation `` + +--- + +## 4. SkelAnimation Schema Definition + +**Full schema from `pxr/usd/usdSkel/schema.usda:92-142`:** + +```usda +class SkelAnimation "SkelAnimation" ( + inherits = + doc = """Describes a skel animation, where joint animation is stored in a + vectorized form.""" + customData = { + string className = "Animation" + } +) { + uniform token[] joints ( + doc = """Array of tokens identifying which joints this animation's + data applies to. The tokens for joints correspond to the tokens of + Skeleton primitives. The order of the joints as listed here may + vary from the order of joints on the Skeleton itself.""" + ) + + float3[] translations ( + doc = """Joint-local translations of all affected joints. Array length + should match the size of the *joints* attribute.""" + ) + + quatf[] rotations ( + doc = """Joint-local unit quaternion rotations of all affected joints, + in 32-bit precision. Array length should match the size of the + *joints* attribute.""" + ) + + half3[] scales ( + doc = """Joint-local scales of all affected joints, in + 16 bit precision. Array length should match the size of the *joints* + attribute.""" + ) + + uniform token[] blendShapes ( + doc = """Array of tokens identifying which blend shapes this + animation's data applies to.""" + ) + + float[] blendShapeWeights ( + doc = """Array of weight values for each blend shape.""" + ) +} +``` + +### Key Characteristics + +| Feature | Details | +|---------|---------| +| Sparse Animation | Can animate subset of joints; `joints` array specifies which | +| Joint Order | Can differ from Skeleton's joint order | +| Time-Sampling | `translations`, `rotations`, `scales` support `.timeSamples` | +| Precision | rotations=32-bit float, scales=16-bit half | +| Blend Shapes | Supported via `blendShapes` + `blendShapeWeights` | + +--- + +## 5. Value Clips API Reference + +For animation sequencing, use `UsdClipsAPI` from `pxr/usd/usd/clipsAPI.h`. + +### Key Metadata + +| Metadata | Purpose | +|----------|---------| +| `assetPaths` | Array of clip file paths | +| `primPath` | Path within clips to source data from | +| `active` | `(stage_time, clip_index)` pairs - which clip at what time | +| `times` | `(stage_time, clip_time)` pairs - time remapping | +| `manifestAssetPath` | Optional manifest for optimization | + +### Example from `pxr/usd/usd/testenv/testUsdValueClips/multiclip/root.usda`: +```usda +def "Model_1" ( + references = @./ref.usda@ + + clips = { + dictionary default = { + asset[] assetPaths = [@./clip1.usda@, @./clip2.usda@] + string primPath = "/Model" + double2[] active = [(0.0, 0), (16.0, 1)] + double2[] times = [(0.0, 0.0), (16.0, 16.0), (16.0, 0.0), (32.0, 16.0)] + } + } +) +{ +} +``` + +This sequences `clip1.usda` (frames 0-16) then `clip2.usda` (frames 16-32). + +--- + +## 6. Implementation Recommendations for USD Exporter + +### Critical Requirements + +1. **Single Animation Per Skeleton Instance** + - Do NOT attempt multiple targets on `skel:animationSource` + - Export one `SkelAnimation` per binding location + - If source has multiple animations, export to separate files + +2. **Animation Source Validation** + ```cpp + // Validate before export + if (!UsdSkelIsSkelAnimationPrim(animPrim)) { + // Invalid animation source + } + ``` + +3. **Array Size Consistency** + - `joints.size() == translations.size() == rotations.size() == scales.size()` + - All must match or export will fail silently + +4. **Sparse Animation Support** + - Animation can reference subset of skeleton joints + - Order doesn't need to match skeleton's joint order + - UsdSkel handles remapping internally + +5. **Joint Order Note** + - **Skeleton** joints: Must be topologically sorted (parent before children) + - **Animation** joints: No ordering requirement + +### Recommended Export Strategy + +``` +For each character with multiple animations (walk, run, idle): + +1. Export skeleton definition once: + /Character/Skeleton (SkelSkeleton with joints, bindTransforms, restTransforms) + +2. Export each animation to separate prim or file: + /Character/Animations/Walk (SkelAnimation) + /Character/Animations/Run (SkelAnimation) + /Character/Animations/Idle (SkelAnimation) + +3. For single-animation use: + - Bind directly: rel skel:animationSource = + +4. For animation sequencing: + - Use Value Clips metadata to sequence animation files + - Each animation in separate .usda file + - Clips metadata specifies timing +``` + +### Code Example (Pseudocode) + +```cpp +// Create animation prim +UsdSkelAnimation anim = UsdSkelAnimation::Define(stage, SdfPath("/Anim")); + +// Set joints (can be sparse subset) +VtTokenArray joints = {"Root", "Root/Spine", "Root/Spine/Head"}; +anim.GetJointsAttr().Set(joints); + +// Set time-sampled transforms +for (double time : keyframeTimes) { + VtVec3fArray translations = GetTranslationsAtTime(time); + VtQuatfArray rotations = GetRotationsAtTime(time); + VtVec3hArray scales = GetScalesAtTime(time); + + anim.GetTranslationsAttr().Set(translations, UsdTimeCode(time)); + anim.GetRotationsAttr().Set(rotations, UsdTimeCode(time)); + anim.GetScalesAttr().Set(scales, UsdTimeCode(time)); +} + +// Bind animation to skeleton +UsdSkelBindingAPI binding = UsdSkelBindingAPI::Apply(skelRootPrim); +binding.CreateAnimationSourceRel().SetTargets({anim.GetPath()}); +``` + +--- + +## 7. Limitations and Caveats + +### Current Limitations + +1. **No Native Blending**: Cannot blend walk+run animations simultaneously +2. **Single Active Animation**: Only one animation per skeleton instance at runtime +3. **No Animation Layers**: Unlike Maya/Blender, no layered animation system +4. **Value Clips Overhead**: Runtime clip switching has performance implications + +### Workarounds + +| Need | Solution | +|------|----------| +| Multiple animations | Separate files + Value Clips | +| Blending | Pre-bake blended result, or custom runtime code | +| Animation layers | Flatten to single animation on export | +| Runtime switching | Value Clips or application-level logic | + +### Future Considerations + +The UsdSkel team anticipates adding "compound animation sources" including blenders. The current architecture (UsdSkelAnimQuery through UsdSkelCache) is designed to support this future expansion without breaking existing code. + +--- + +## 8. File References + +| File | Content | +|------|---------| +| `pxr/usd/usdSkel/schema.usda` | Schema definitions for Skeleton, Animation | +| `pxr/usd/usdSkel/bindingAPI.h/cpp` | Animation binding API | +| `pxr/usd/usdSkel/animQuery.h` | Animation query interface | +| `pxr/usd/usdSkel/doxygen/schemaOverview.dox` | Architecture documentation | +| `pxr/usd/usdSkel/doxygen/schemas.dox` | Detailed schema docs | +| `pxr/usd/usdSkel/doxygen/objectModel.dox` | Object model & future plans | +| `pxr/usd/usd/clipsAPI.h` | Value Clips API | +| `pxr/usd/usdSkel/testenv/testUsdSkelCache/populate.usda` | Binding examples | +| `pxr/usd/usd/testenv/testUsdValueClips/multiclip/root.usda` | Clip sequencing example | + +--- + +## Summary + +- **One animation per skeleton instance** - `skel:animationSource` is single-target +- **Use Value Clips for sequencing** - Official USD approach for walk/run/idle switching +- **Export animations to separate files** - Enables reuse and Value Clips composition +- **No native blending** - Pre-bake or handle at application level +- **Sparse animations supported** - Can animate subset of joints in any order