diff --git a/.gitattributes b/.gitattributes index 8d0eceb0..63a8bc19 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ *.png filter=lfs diff=lfs merge=lfs -text *.glb filter=lfs diff=lfs merge=lfs -text *.bin filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/ChangeLog.md b/ChangeLog.md index a48ac995..5ac1d657 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,7 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unrelease] - ### Added +- Experimental glTF Editor Export (under main menu `File > Export` and via API `GLTFast.Export.GameObjectExport`; #249) - *Generate Lightmap UVs* option in the glTF import inspector lets you create a secondary texture coordinate set (similar to the Model Import Settings from other formats; #238) +- Generic `ICodeLogger` methods that don't require a `LogCode` +### Changed +- Raised required Unity version to 2019.4.7f1 (fixes Burst 1.4 compiler issue #252). If you're on 2019.x, make sure to update to the latest LTS release! +- Less GC due to `CollectingLogger` creating the item list on demand ## [4.3.4] - 2021-10-26 ### Added @@ -474,4 +479,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [hybridherbst]: https://github.com/hybridherbst [Bersaelor]: https://github.com/Bersaelor [zharry]: https://github.com/zharry -[ReadyPlayerMe]: https://readyplayer.me \ No newline at end of file +[ReadyPlayerMe]: https://readyplayer.me diff --git a/Documentation~/features.md b/Documentation~/features.md index 21d58fa2..6dbb1372 100644 --- a/Documentation~/features.md +++ b/Documentation~/features.md @@ -1,14 +1,15 @@ # Features -## Overview +*glTFast* requires Unity 2019.4 or newer. -- [x] Run-time import - - [x] GameObjects - - [x] ⚠️ Entities (see [DOTS](#data-oriented-technology-stack)) -- [x] Fast and small footprint JSON parsing -- [x] Multi-threading via C# job system -- [x] Design-time (Editor) import -- [ ] Export +### Legend + +- ✅ Fully supported +- ☑️ Partially supported +- ℹ️ Planned (click for issue) +- ⛔️ No plan to support (click for issue) +- `?`: Unknown / Untested +- `n/a`: Not available ## Platforms @@ -23,99 +24,120 @@ All of Unity's platforms are supported. glTFast is tested or was reported to run - Universal Windows Platform - Lumin (Magic Leap) +## Workflows + +| | Runtime | Editor (design-time) +|----------| ------ | ------ +| | | +| **GameObject** +| Import | ✅️ | ✅ +| Export | [ℹ️][RuntimeExport] | 1 ☑️ +| | | +| **Entities (see [DOTS](#data-oriented-technology-stack))** +| Import | [☑️](#data-oriented-technology-stack) | `n/a` +| Export | | `n/a` + +1: glTF export currently only works on Unity 2020.2 or newer. + ## Core glTF features The glTF 2.0 specification is fully supported, with only a few minor remarks. -
Detailed list of glTF 2.0 core feature support - -- [x] glTF (gltf + buffers + textures) -- [x] glTF binary (glb) - -- [x] Scene - - [x] Node hierarchy - - [x] Camera -- [x] Buffers - - [x] External URIs - - [x] glTF binary main buffer - - [x] Embed buffers or textures (base-64 encoded within JSON) -- [x] Images - - [x] PNG - - [x] Jpeg - - [x] 2KTX with Basis Universal super compression (via [KTX/Basis Texture Unity Package](https://github.com/atteneder/KtxUnity)) -- [x] Materials (see section [Materials](#materials) for details) - - [x] [Universal Render Pipeline (URP)][URP] - - [x] [High Definition Render Pipeline (HDRP)][HDRP] - - [x] Built-in Render Pipeline -- Primitive Types - - [x] TRIANGLES - - [x] POINTS - - [x] 1LINES - - [x] LINE_STRIP - - [x] 1LINE_LOOP - - [ ] TRIANGLE_STRIP - - [ ] TRIANGLE_FAN -- [x] Meshes - - [x] Positions - - [x] Normals - - [x] Tangents - - [x] Texture coordinates - - [x] Vertex colors - - [x] Draco mesh compression (via [Draco 3D Data Compression Unity Package](https://github.com/atteneder/DracoUnity)) - - [x] Implicit (no) indices - - [x] Per primitive material - - [x] Two texture coordinates / UV sets - - [ ] Three or more texture coordinates / UV sets ([issue][UVsets]) - - [x] Joints (up to 4 per vertex) - - [x] Weights (up to 4 per vertex) -- [x] Texture sampler - - [x] Filtering (see ([limitations](#Known-issues))) - - [x] Wrap modes -- [x] Morph targets -- [x] 3Sparse accessors -- [x] [Skins][Skins] (sponsored by [Embibe](https://www.embibe.com)) -- [x] Animation - - [x] via legacy Animation System - - [ ] via Playable API ([issue][AnimationPlayables]) - - [ ] via Mecanim ([issue][AnimationMecanim]) +| | Import | Export +|------------| ------ | ------ +| **Format** +|glTF (.gltf) | ✅ | ✅ +|glTF-Binary (.glb) | ✅ | ✅ +| | | +| **Buffer type** +| External URIs | ✅ | ✅ +| GLB main buffer | ✅ | ✅ +| Embed buffers or textures (base-64 encoded within JSON) | ✅ | +| | | +| **Basics** +| Scenes | ✅ | ✅ +| Node hierarchies | ✅ | ✅ +| Cameras | ✅ | +| | | +| **Images** +| PNG | ✅ | ✅ +| Jpeg | ✅ | ✅ +| KTX with Basis Universal compression (via [KtxUnity](https://github.com/atteneder/KtxUnity)) | ✅ | +| | | +| **Texture sampler** +| Filtering | ✅ with [limitations](#Known-issues) | +| Wrap modes | ✅ | +| | | +| **Materials Overview** (see [details](#materials-details)) +| [Universal Render Pipeline (URP)][URP] | ✅ | +| [High Definition Render Pipeline (HDRP)][HDRP] | ✅ | +| Built-in Render Pipeline | ✅ | ☑️ +| | | +| **Topologies / Primitive Types** +| TRIANGLES | ✅ | ✅ +| POINTS | ✅ | ✅ +| 1LINES | ✅ | ✅ +| LINE_STRIP | ✅ | ✅ +| 1LINE_LOOP | ✅ | ✅ +| TRIANGLE_STRIP | | +| TRIANGLE_FAN | | +| Quads | `n/a` | ✅ via triangulation +| | | +| **Meshes** +| Positions | ✅ | ✅ +| Normals | ✅ | ✅ +| Tangents | ✅ | ✅ +| Texture coordinates | ✅ | ✅ +| Vertex colors | ✅ | `?` +| Draco mesh compression (via [DracoUnity](https://github.com/atteneder/DracoUnity)) | ✅ | +| Implicit (no) indices | ✅ | +| Per primitive material | ✅ | ✅ +| Two texture coordinates / UV sets | ✅ | `?` +| Three or more texture coordinates / UV sets | [issue][UVsets] | `?` +| Joints (up to 4 per vertex) | ✅ | +| Weights (up to 4 per vertex) | ✅ | +| | | +| **Morph Targets / Blend Shapes** +| Sparse accessors | 2 ✅ | +| [Skins][Skins] (sponsored by [Embibe](https://www.embibe.com)) | ✅ | +| | | +| **Animation** +| via legacy Animation System | ✅ | +| via Playable API ([issue][AnimationPlayables]) | | +| via Mecanim ([issue][AnimationMecanim]) | | 1: Untested due to lack of demo files. -2: Beta - -3: Not on all accessor types; morph targets and vertex positions only - -
+2: Not on all accessor types; morph targets and vertex positions only ## Extensions ### Official Khronos extensions -- [x] KHR_draco_mesh_compression -- [x] KHR_materials_pbrSpecularGlossiness -- [x] KHR_materials_unlit -- [x] KHR_texture_transform -- [x] KHR_mesh_quantization -- [x] KHR_texture_basisu -- [ ] KHR_lights_punctual ([issue][PointLights]) -- [ ] KHR_materials_clearcoat ([issue][ClearCoat]) -- [ ] KHR_materials_sheen ([issue][Sheen]) -- [ ] KHR_materials_transmission ([issue][Transmission]) -- [ ] KHR_materials_variants ([issue][Variants]) -- [ ] KHR_materials_ior ([issue][IOR]) -- [ ] KHR_materials_specular ([issue][Specular]) -- [ ] KHR_materials_volume ([issue][Volume]) -- [ ] KHR_xmp - -Will not be supported: - -- KHR_techniques_webgl - -### Vendor extensions - -- [x] 1EXT_mesh_gpu_instancing -- [ ] EXT_meshopt_compression ([issue][MeshOpt]) -- [ ] EXT_lights_image_based ([issue][IBL]) +| | Import | Export +|------------| ------ | ------ +| | | +| **Khronos** +| KHR_draco_mesh_compression | ✅ | +| KHR_materials_pbrSpecularGlossiness | ✅ | +| KHR_materials_unlit | ✅ | ✅ +| KHR_texture_transform | ✅ | ✅ +| KHR_mesh_quantization | ✅ | +| KHR_texture_basisu | ✅ | +| KHR_lights_punctual | [ℹ️][PointLights] | +| KHR_materials_clearcoat | [ℹ️][ClearCoat] | +| KHR_materials_sheen | [ℹ️][Sheen] | +| KHR_materials_transmission | [ℹ️][Transmission] | +| KHR_materials_variants | [ℹ️][Variants] | +| KHR_materials_ior | [ℹ️][IOR] | +| KHR_materials_specular | [ℹ️][Specular] | +| KHR_materials_volume | [ℹ️][Volume] | +| KHR_xmp |️ | +| | | +| **Vendor** +| 1EXT_mesh_gpu_instancing | ✅ | +| EXT_meshopt_compression | [ℹ️][MeshOpt] | +| EXT_lights_image_based | [ℹ️][IBL] | 1: Without support for custom vertex attributes (e.g. `_ID`) @@ -130,13 +152,16 @@ Not investigated yet: Will not be supported: +- KHR_techniques_webgl - ADOBE_materials_clearcoat_specular (prefer KHR_materials_clearcoat) - ADOBE_materials_thin_transparency (prefer KHR_materials_transmission) - EXT_texture_webp (prefer KTX/basisu) - FB_geometry_metadata (prefer KTX_xmp) - MSFT_texture_dds (prefer KTX/basisu) -## Materials +## Materials Details + +### Material Import | Material Feature | URP | HDRP | Built-In | |-------------------------------|-----|------|----------| @@ -151,13 +176,13 @@ Will not be supported: | Vertex colors | ✅ | ✅ | ✅ | | Multiple UV sets | ✅2 | ✅2 | ✅2 | | Texture Transform | ✅ | ✅ | ✅ | -| Clear coat | [ℹ][ClearCoat] | [ℹ][ClearCoat] | [❌][ClearCoat] | -| Sheen | [ℹ][Sheen] | [ℹ][Sheen] | [❌][Sheen] | -| Transmission | [✓][Transmission]3 | [✓][Transmission]4 | [✓][Transmission]4 | -| Variants | [ℹ][Variants] | [ℹ][Variants] | [ℹ][Variants] | -| IOR | [ℹ][IOR] | [ℹ][IOR] | [❌][IOR] | -| Specular | [ℹ][Specular] | [ℹ][Specular] | [❌][Specular] | -| Volume | [ℹ][Volume] | [ℹ][Volume] | [❌][Volume] | +| Clear coat | [ℹ️][ClearCoat] | [ℹ️][ClearCoat] | [⛔️][ClearCoat] | +| Sheen | [ℹ️][Sheen] | [ℹ️][Sheen] | [⛔️][Sheen] | +| Transmission | [☑️][Transmission]3 | [☑️][Transmission]4 | [☑️][Transmission]4 | +| Variants | [ℹ️][Variants] | [ℹ️][Variants] | [ℹ️][Variants] | +| IOR | [ℹ️][IOR] | [ℹ️][IOR] | [⛔️][IOR] | +| Specular | [ℹ️][Specular] | [ℹ️][Specular] | [⛔️][Specular] | +| Volume | [ℹ️][Volume] | [ℹ️][Volume] | [⛔️][Volume] | 1: Physically-Based Rendering (PBR) material model @@ -167,18 +192,42 @@ Will not be supported: 4: Transmission in Built-In and HD render pipeline does not support transmission textures and is only 100% correct in certain cases like clear glass (100% transmission, white base color). Otherwise it's an approximation. -Legend: +### Material Export -- ✅ Fully supported -- ✓ Supported partially -- ℹ Planned (click for issue) -- ❌ No plan to support (click to create issue) + + +| Material Feature | URP1 | HDRP2 | Built-In3 | +|-------------------------------|-----|------|----------| +| PBR Metallic-Roughness | `?` | `?` | ✅ | +| PBR Specular-Glossiness | | | | +| Unlit | `?` | `?` | ✅ | +| Normal texture | `?` | `?` | ✅ | +| Occlusion texture | `?` | `?` | | +| Emission texture | `?` | `?` | | +| Alpha modes OPAQUE/MASK/BLEND | `?` | `?` | ✅ | +| Double sided / Two sided | `?` | `?` | ✅ | +| Vertex colors | `?` | `?` | `?` | +| Multiple UV sets | `?` | `?` | `?` | +| Texture Transform | ✅ | ✅ | ✅ | +| Clear coat | | | `n/a` | +| Sheen | `?` | `?` | `n/a` | +| Transmission | | | `n/a` | +| Variants | | | | +| IOR | | | `n/a` | +| Specular | | | | +| Volume | | | `n/a` | + +1: Universal Render Pipeline Lit Shader + +2: High Definition Render Pipeline Lit Shader + +3: Built-In Render Pipeline Standard and Unlit Shader ## Data-Oriented Technology Stack > ⚠️ Note: DOTS is highly experimental and many features don't work yet. Do not use it for production ready projects! -Unity's [Data-Oriented Technology Stack (DOTS)][DOTS] allows users to create high performance gameplay. glTFast has initial, experimental support for it. +Unity's [Data-Oriented Technology Stack (DOTS)][DOTS] allows users to create high performance gameplay. glTFast has experimental import support for it. Instead of traditional GameObjects, glTFast will instantiate Entities with Hybrid Renderer (version 2) components. @@ -199,7 +248,7 @@ Possibly incomplete list of things that are known to not work with Entities yet: ## Known issues - 1Vertex accessors (positions, normals, etc.) that are used across meshes are duplicated and result in higher memory usage and slower loading (see [this comment](https://github.com/atteneder/glTFast/issues/52#issuecomment-583837852)) -- 1When using more than one samplers on an image, that image is duplicated and results in higher memory usage +- 1When using more than one sampler on one image, that image is duplicated and results in higher memory usage - Texture sampler minification/magnification filter limitations (see [issue][SamplerFilter]): - 1There's no differentiation between `minFilter` and `magFilter`. `minFilter` settings are prioritized. - 1`minFilter` mode `NEAREST_MIPMAP_LINEAR` is not supported and will result in `NEAREST`. @@ -216,9 +265,10 @@ Possibly incomplete list of things that are known to not work with Entities yet: [MeshOpt]: https://github.com/atteneder/glTFast/issues/106 [newIssue]: https://github.com/atteneder/glTFast/issues/new [PointLights]: https://github.com/atteneder/glTFast/issues/17 +[RuntimeExport]: https://github.com/atteneder/glTFast/issues/259 [SamplerFilter]: https://github.com/atteneder/glTFast/issues/61 [Sheen]: https://github.com/atteneder/glTFast/issues/110 -[Skins]: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#skins +[Skins]: https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#skins [Specular]: https://github.com/atteneder/glTFast/issues/208 [Transmission]: https://github.com/atteneder/glTFast/issues/111 [URP]: https://unity.com/srp/universal-render-pipeline diff --git a/Documentation~/glTFast.md b/Documentation~/glTFast.md index 15efb28a..23208135 100644 --- a/Documentation~/glTFast.md +++ b/Documentation~/glTFast.md @@ -15,7 +15,75 @@ Try the [WebGL Demo][gltfast-web-demo] and check out the [demo project](https:// *glTFast* supports the full [glTF 2.0 specification][gltf-spec] and many extensions. It works with Universal, High Definition and the Built-In Render Pipelines on all platforms. -See all details at the [list of features/extensions](./features.md). +See the [comprehensive list of supported features and extensions](./features.md). + +### Workflows + +There are four use-cases for glTF within Unity + +- Import + - [Runtime Import/Loading](#runtime-importloading) in games/applications + - [Editor Import](#editor-import-design-time) (i.e. import assets at design-time) +- Export + - [Runtime Export](#runtime-export) (save and share dynamic, user-generated 3D content) + - [Editor Export](#editor-export) (Unity as glTF authoring tool) + +![Schematic diagram of the four glTF workflows](./img/Unity-glTF-workflows.png "The four glTF workflows") + +#### Runtime Import/Loading + +Load and instantiate glTF files at runtime in your game/application via Script or the `GltfAsset` component. + +#### Benefits of Runtime Import + +- Efficiently load dynamic and/or third-party assets + - Make use of state-of-the art mesh and texture compression methods, like KTX/Basis Universal, Draco and meshoptimizer. +- No need to re-build your application or Asset Bundles upon Unity version upgrades + +*glTF* was specifically designed for vendor-independent transmission and runtime loading and naturally plays its strengths there. + +#### Editor Import (Design-Time) + +Although primarily designed for runtime, *glTF*'s effective design and its modern, physically-based material definition make it great for most simple DCC (digital content creation) interchange as well. + +Read about [usage](#editor-import) below. + +##### Benefits of Editor Import + +- Less friction between artists and developers due to *glTF* as standardized interface + - Example: artists don't need to know or follow Unity shader specific conventions and thus developers don't need to instruct them +- Enables adding rich interaction and behaviour to assets (e.g. custom scripts or animation controllers) +- In conjunction with [Editor Export](#editor-export), Unity becomes a complete tool for re-mixing 3D content +- 1Use default Lit (URP/HDRP) or Standard (Built-in render pipeline) materials + +1: Not yet supported (see [issue](https://github.com/atteneder/glTFast/issues/258)) + +#### Editor Export + +Use the Unity Editor as an authoring tool and export your scenes and GameObjects as *glTFs*. + +> Note: This feature is experimental + +##### Use-cases for Editor Export + +- [Unity runtime loading](#runtime-import-loading) +- Social media sharing +- Use within the [vast glTF eco system][gltf-projects], like third-party viewers or asset pipelines +- Archiving + +#### Runtime Export + +Allows your Unity-powered application/game to export scenes/GameObjects to glTF at runtime. + +##### Use-cases for Runtime Export + +- Preserve dynamic, user-generated 3D content + - Create metaverse-ready 3D snapshots of a current state / game action + - 3D product configurations (e-commerce) +- Build high level editing and authoring tools with Unity +- Social media sharing + +> Note: This feature is coming soon (see [issue](https://github.com/atteneder/glTFast/issues/259)) ## Usage @@ -210,7 +278,96 @@ async Task CustomDeferAgent() { ### Editor Import -To convert your glTF asset into a native Unity prefab, just move/copy it and all its companioning buffer and texture files into the *Assets* folder of your Unity project. It'll get imported into the Asset Database automatically. Select it in the Project view to see detailed settings and import reports in the Inspector. Expand it in the Project View to see the components (Scenes, Meshes, Materials, AnimationClips and Textures) that were imported. +You can move/copy *glTF* files into your project's *Assets* folder, similar to other 3D formats. *glTFast* will import them to native Unity prefabs and add them to the asset database. + +![Editor Import][import-gif] + +Don't forget to also copy over companion buffer (`.bin`) and image files! The file names and relative paths cannot be changed, otherwise references may break. + +Select a glTF in the Project view to see its import settings and eventual warnings/errors in the Inspector. Expand it in the Project View to see the imported components (Scenes, Meshes, Materials, AnimationClips and Textures). + + +### Editor Export + +#### Export from the Main Menu + +The main menu has a couple of entries for export under `File > Export`: + +- `Scene (glTF)` exports the entire active scene to glTF (`.gltf` plus external buffer and texture files) +- `Scene (glTF-Binary)` exports the entire active scene to glTF-Binary (`.glb`) +- `Selection (glTF)` exports the currently selected GameObject (with its hierarchy) to glTF (`.gltf` plus external buffer and texture files) +- `Selection (glTF-Binary)` exports the currently selected GameObject (with its hierarchy) to glTF-Binary (`.glb`) + +Clicking any of these will open a file selection dialog. If additional files are to be generated (e.g. a buffer or image files) and there's a conflict (i.e. an existing file in that location), a follow-up dialog will as for permission to overwrite. + +#### Export via Script + +To export a GameObject hierarchy/scene from script, create an instance of `GLTFast.Export.GameObjectExport`, +add content via `AddScene` and finally call `SaveToFileAndDispose` to export to a file. + +```c# +using GLTFast.Export; + +async void SimpleExport() { + + // Example of gathering GameObjects to be exported (recursively) + var rootLevelNodes = FindGameObjectsWithTag("ExportMe"); + + // GameObjectExport lets you create glTFs from GameObject hierarchies + var export = new GameObjectExport(); + + // Add a scene + export.AddScene(rootLevelNodes); + + // Async glTF export + bool success = await export.SaveToFileAndDispose(path); + + if(!success) { + Debug.LogError("Something went wrong exporting a glTF"); + } +} +``` + +After calling `SaveToFileAndDispose` the GameObjectExport instance becomes invalid. Do not re-use it. + +Further, the export can be customized by passing settings and injectables to the `GameObjectExport`'s +constructor: + +```c# +using GLTFast.Export; + +async void AdvancedExport() { + + // CollectingLogger lets you programatically go through + // errors and warnings the export raised + var logger = new CollectingLogger(); + + // ExportSettings allow you to configure the export + // Check its source for details + var exportSettings = new ExportSettings { + format = GltfFormat.Binary, + fileConflictResolution = FileConflictResolution.Overwrite + }; + + // GameObjectExport lets you create glTFs from GameObject hierarchies + var export = new GameObjectExport( exportSettings, logger: logger); + + // Example of gathering GameObjects to be exported (recursively) + var rootLevelNodes = FindGameObjectsWithTag("ExportMe"); + + // Add a scene + export.AddScene(rootLevelNodes, "My new glTF scene"); + + // Async glTF export + bool success = await export.SaveToFileAndDispose(path); + + if(!success) { + Debug.LogError("Something went wrong exporting a glTF"); + // Log all exporter messages + logger.LogAll(); + } +} +``` ## Project Setup @@ -218,9 +375,9 @@ To convert your glTF asset into a native Unity prefab, just move/copy it and all ❗ IMPORTANT ❗ -glTF materials might require many shader/features combinations. You **have** to make sure all shader variants your project will ever use are included, or the materials will not work in builds (even if they work in the Editor). +For runtime import, glTF materials might require many shader/features combinations. You **have** to make sure all shader variants your project will ever use are included, or the materials will not work in builds (even if they work in the Editor). -*glTFast* uses custom shaders that are derived from the Unity Standard shaders (and have a similar big number of variants). Including all those variants can make your build big. There's an easy way to find the right subset, if you already know what files you'll expect: +*glTFast* import uses custom shaders that are derived from the Unity *Lit* or *Standard* shaders (and have a similar big number of variants). Including all those variants can make your build big. There's an easy way to find the right subset, if you already know what files you'll expect: - Run your scene that loads all glTFs you expect in the editor. - Go to Edit->Project Settings->Graphics @@ -240,7 +397,7 @@ Motivations for this might be using meshes as physics colliders amongst [other c ### Safe Mode -Arbitrary (and potentially broken) input data is a challenge to software's robustness and safety. Some measurments to make glTFast more robust have a negative impact on its performance though. +Arbitrary (and potentially broken) input data is a challenge to software's robustness and safety. Some measurements to make glTFast more robust have a negative impact on its performance though. For this reason some pedantic safety checks in glTFast are not performed by default. You can enable safe-mode by adding the scripting define `GLTFAST_SAFE` to your project. @@ -264,7 +421,7 @@ When upgrading from an older version to 4.x or newer the most notable difference To counter-act this in applications that used older versions of *glTFast* before, make sure you rotate the parent `Transform` by 180° around the Y-axis, which brings the model back to where it should be. -This change was implemented to conform more closely to the [glTF specification](https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#coordinate-system-and-units), which says: +This change was implemented to conform more closely to the [glTF specification][gltf-spec-coords], which says: > The front of a glTF asset faces +Z. @@ -323,15 +480,18 @@ In the future materials can be created before textures are available/downloaded *glTFast* uses [Unity's JsonUtility](https://docs.unity3d.com/ScriptReference/JsonUtility.html) for parsing, which has little overhead, is fast and memory-efficient (See ). -It also uses fast low-level memory copy methods, [Unity's Job system](https://docs.unity3d.com/Manual/JobSystem.html) and the [Advanced Mesh API](https://docs.unity3d.com/ScriptReference/Mesh.html). +It also uses fast low-level memory copy methods, [Unity's Job system](https://docs.unity3d.com/Manual/JobSystem.html), [Mathematics](https://docs.unity3d.com/Packages/com.unity.mathematics@1.0/manual/index.html), the [Burst compiler](https://docs.unity3d.com/Packages/com.unity.burst@1.6/manual/index.html) and the [Advanced Mesh API](https://docs.unity3d.com/ScriptReference/Mesh.html). [unity]: https://unity.com [gltf]: https://www.khronos.org/gltf -[gltf-spec]: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md +[gltf-projects]: https://github.khronos.org/glTF-Project-Explorer +[gltf-spec]: https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html +[gltf-spec-coords]: https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#coordinate-system-and-units [gltfast-web-demo]: https://gltf.pixel.engineer [gltfasset_component]: ./img/gltfasset_component.png "Inspector showing a GltfAsset component added to a GameObject" [gltfast3to4]: ./img/gltfast3to4.png "3D scene view showing BoomBoxWithAxes model twice. One with the legacy axis conversion and one with the new orientation" [GltfAsset]: ../Runtime/Scripts/GltfAsset.cs [GltfImport]: ../Runtime/Scripts/GltfImport.cs [IGltfReadable]: ../Runtime/Scripts/IGltfReadable.cs -[MRTK]: https://github.com/microsoft/MixedRealityToolkit-Unity \ No newline at end of file +[import-gif]: ./img/import.gif "Video showing glTF files being copied into the Assets folder and imported" +[MRTK]: https://github.com/microsoft/MixedRealityToolkit-Unity diff --git a/Documentation~/img/Unity-glTF-workflows.png b/Documentation~/img/Unity-glTF-workflows.png new file mode 100644 index 00000000..46211ec2 --- /dev/null +++ b/Documentation~/img/Unity-glTF-workflows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:863cfd7cb36fdf3434a306a2c74972a0732d344bc99090ef7a233b8f235a44cc +size 46311 diff --git a/Documentation~/img/import.gif b/Documentation~/img/import.gif new file mode 100644 index 00000000..0b32f11a --- /dev/null +++ b/Documentation~/img/import.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e12eaf1a2d8115905586bb0ff8e3b567ffa56271e5c2e539a59caa5b3602da3 +size 2610769 diff --git a/Editor/Scripts/GltfImporter.cs b/Editor/Scripts/GltfImporter.cs index c381a307..4d9f3fd3 100644 --- a/Editor/Scripts/GltfImporter.cs +++ b/Editor/Scripts/GltfImporter.cs @@ -191,7 +191,9 @@ public override void OnImportAsset(AssetImportContext ctx) { assetDependencies = deps.ToArray(); var reportItemList = new List(); - reportItemList.AddRange(logger.items); + if (logger.items != null) { + reportItemList.AddRange(logger.items); + } if (instantiationLogger?.items != null) { reportItemList.AddRange(instantiationLogger.items); } diff --git a/Editor/Scripts/MenuEntries.cs b/Editor/Scripts/MenuEntries.cs new file mode 100644 index 00000000..e36f3a3a --- /dev/null +++ b/Editor/Scripts/MenuEntries.cs @@ -0,0 +1,157 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.IO; +using System.Linq; +using GLTFast.Export; +using UnityEditor; +using UnityEngine; +using UnityEngine.SceneManagement; + +#if GLTF_VALIDATOR +using Unity.glTF.Validator; +#endif + +namespace GLTFast.Editor { + + public static class MenuEntries { + + const string k_GltfExtension = "gltf"; + const string k_GltfBinaryExtension = "glb"; + + static string SaveFolderPath { + get { + var saveFolderPath = EditorUserSettings.GetConfigValue("glTF.saveFilePath"); + if (string.IsNullOrEmpty(saveFolderPath)) { + saveFolderPath = Application.streamingAssetsPath; + } + return saveFolderPath; + } + set => EditorUserSettings.SetConfigValue("glTF.saveFilePath",value); + } + + [MenuItem("File/Export/Selection (glTF)", true)] + static bool ExportSelectionValidate() { + return TryGetExportNameAndGameObjects(out _, out _); + } + + [MenuItem("File/Export/Selection (glTF)", false, 10)] + static void ExportSelectionMenu() { + ExportSelection(false); + } + + [MenuItem("File/Export/Selection (glTF-Binary)", true)] + static bool ExportSelectionBinaryValidate() { + return TryGetExportNameAndGameObjects(out _, out _); + } + + [MenuItem("File/Export/Selection (glTF-Binary)", false, 11)] + static void ExportSelectionBinaryMenu() { + ExportSelection(true); + } + + static void ExportSelection(bool binary) { + if (TryGetExportNameAndGameObjects(out var name, out var gameObjects)) { + var extension = binary ? k_GltfBinaryExtension : k_GltfExtension; + var path = EditorUtility.SaveFilePanel( + "glTF Export Path", + SaveFolderPath, + $"{name}.{extension}", + extension + ); + if (!string.IsNullOrEmpty(path)) { + var settings = GetDefaultSettings(binary); + var export = new GameObjectExport(settings, logger: new ConsoleLogger()); + export.AddScene(gameObjects, name); + AsyncHelpers.RunSync(() => export.SaveToFileAndDispose(path)); + +#if GLTF_VALIDATOR + var report = Validator.Validate(path); + report.Log(); +#endif + } + } + else { + Debug.LogError("Can't export glTF: selection is empty"); + } + } + + static ExportSettings GetDefaultSettings(bool binary) { + var settings = new ExportSettings { + format = binary ? GltfFormat.Binary : GltfFormat.Json + }; + return settings; + } + + [MenuItem("File/Export/Scene (glTF)", false, 100)] + static void ExportSceneMenu() { + ExportScene(false); + } + + [MenuItem("File/Export/Scene (glTF-Binary)", false, 101)] + static void ExportSceneBinaryMenu() { + ExportScene(true); + } + + static void ExportScene(bool binary) { + var scene = SceneManager.GetActiveScene(); + var gameObjects = scene.GetRootGameObjects(); + var extension = binary ? k_GltfBinaryExtension : k_GltfExtension; + + var path = EditorUtility.SaveFilePanel( + "glTF Export Path", + SaveFolderPath, + $"{scene.name}.{extension}", + extension + ); + if (!string.IsNullOrEmpty(path)) { + SaveFolderPath = Directory.GetParent(path)?.FullName; + var settings = GetDefaultSettings(binary); + var export = new GameObjectExport(settings, logger: new ConsoleLogger()); + export.AddScene(gameObjects, scene.name); + AsyncHelpers.RunSync(() => export.SaveToFileAndDispose(path)); +#if GLTF_VALIDATOR + var report = Validator.Validate(path); + report.Log(); +#endif + } + } + + static bool TryGetExportNameAndGameObjects(out string name, out GameObject[] gameObjects) + { + if (Selection.transforms.Length > 1) { + name = SceneManager.GetActiveScene().name; + gameObjects = Selection.gameObjects; + return true; + } + + if (Selection.transforms.Length == 1) { + name = Selection.activeGameObject.name; + gameObjects = Selection.gameObjects; + return true; + } + + if (Selection.objects.Any() && Selection.objects.All(x => x is GameObject)) { + name = Selection.objects.First().name; + gameObjects = Selection.objects.Select(x => (x as GameObject)).ToArray(); + return true; + } + + name = null; + gameObjects = null; + return false; + } + } +} diff --git a/Editor/Scripts/MenuEntries.cs.meta b/Editor/Scripts/MenuEntries.cs.meta new file mode 100644 index 00000000..f7a0e160 --- /dev/null +++ b/Editor/Scripts/MenuEntries.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97759b33473f943e5a24b78714284966 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/OnScriptsReloadHandler.cs b/Editor/Scripts/OnScriptsReloadHandler.cs new file mode 100644 index 00000000..1dacb044 --- /dev/null +++ b/Editor/Scripts/OnScriptsReloadHandler.cs @@ -0,0 +1,59 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using UnityEditor; +using UnityEngine; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; + +namespace GLTFast { + public static class OnScriptsReloadHandler { + +// Only run this check if glTFast is in Packages/manifest.json testables +// (which indicates you're developing it) +#if UNITY_INCLUDE_TESTS + + static ListRequest s_Request; + + [UnityEditor.Callbacks.DidReloadScripts] + static void OnScriptsReloaded() { + s_Request = Client.List(); + EditorApplication.update += Progress; + } + + static void Progress() + { + if (s_Request.IsCompleted) + { + if (s_Request.Status == StatusCode.Success) { + foreach (var package in s_Request.Result) { + if (package.name == "com.atteneder.gltfast") { + var version = package.version; + if (Export.Constants.version != version) { + Debug.LogWarning($"Version mismatch in Constants.cs (is {Export.Constants.version}, should be {version}). Please update!"); + } + } + } + } + else if (s_Request.Status >= StatusCode.Failure) { + Debug.Log(s_Request.Error.message); + } + + EditorApplication.update -= Progress; + } + } +#endif + } +} \ No newline at end of file diff --git a/Editor/Scripts/OnScriptsReloadHandler.cs.meta b/Editor/Scripts/OnScriptsReloadHandler.cs.meta new file mode 100644 index 00000000..f396b632 --- /dev/null +++ b/Editor/Scripts/OnScriptsReloadHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd9d2e50cd6f34502ad61d8555c226bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Scripts/glTFastEditor.asmdef b/Editor/Scripts/glTFastEditor.asmdef index a4e1b8d4..0a0f94e8 100644 --- a/Editor/Scripts/glTFastEditor.asmdef +++ b/Editor/Scripts/glTFastEditor.asmdef @@ -1,12 +1,15 @@ { "name": "glTFastEditor", + "rootNamespace": "", "references": [ "Unity.Mathematics", "glTFast", "glTFastSchema", + "glTFast.Export", "glTFastTests", "UnityEngine.TestTools.Graphics", - "Unity.RenderPipelines.HighDefinition.Runtime" + "Unity.RenderPipelines.HighDefinition.Runtime", + "com.unity.formats.gltf.validator.Editor" ], "includePlatforms": [ "Editor" @@ -37,6 +40,11 @@ "name": "com.unity.modules.animation", "expression": "1.0.0", "define": "UNITY_ANIMATION" + }, + { + "name": "com.unity.formats.gltf.validator", + "expression": "", + "define": "GLTF_VALIDATOR" } ], "noEngineReferences": false diff --git a/README.md b/README.md index 765739fc..ea1355ae 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,37 @@ [![GitHub issues](https://img.shields.io/github/issues/atteneder/glTFast)](https://github.com/atteneder/glTFast/issues) [![GitHub license](https://img.shields.io/github/license/atteneder/glTFast)](https://github.com/atteneder/glTFast/blob/main/LICENSE.md) -*glTFast* enables loading [glTF™ (GL Transmission Format)][gltf] asset files in [Unity][unity]. +*glTFast* enables use of [glTF™ (GL Transmission Format)][gltf] asset files in [Unity][unity]. -It focuses on speed, memory efficiency and a small build footprint. +It focuses on speed, memory efficiency and a small build footprint while also providing: -Two workflows are supported +- 100% [glTF 2.0 specification][gltf-spec] compliance +- Ease of use +- Robustness and Stability +- Customization and extensibility for advanced users -- Load glTF assets at runtime -- Import glTF assets as prefabs into the asset database at design-time in the Unity Editor - -Try the [WebGL Demo][gltfast-web-demo] and check out the [demo project](https://github.com/atteneder/glTFastDemo). +Check out the [demo project](https://github.com/atteneder/glTFastDemo) and try the [WebGL Demo][gltfast-web-demo]. ## Features *glTFast* supports the full [glTF 2.0 specification][gltf-spec] and many extensions. It works with Universal, High Definition and the Built-In Render Pipelines on all platforms. -See all details at the [list of features/extensions](./Documentation~/features.md). +See the [comprehensive list of supported features and extensions](./Documentation~/features.md). + +### Workflows + +There are four use-cases for glTF within Unity + +- Import + - [Runtime Import/Loading](./Documentation~/glTFast.md#runtime-importloading) in games/applications + - [Editor Import](./Documentation~/glTFast.md#editor-import-design-time) (i.e. import assets at design-time) +- Export + - [Runtime Export](./Documentation~/glTFast.md#runtime-export) (save and share dynamic, user-generated 3D content) + - [Editor Export](./Documentation~/glTFast.md#editor-export) (Unity as glTF authoring tool) + +[![Schematic diagram of the four glTF workflows](./Documentation~/img/Unity-glTF-workflows.png "The four glTF workflows")][workflows] + +Read more about the workflows in the [documentation][workflows]. ## Installing @@ -75,23 +90,22 @@ var gltf = gameObject.AddComponent(); gltf.url = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF/Duck.gltf"; ``` -See [Runtime Loading via Script](./Documentation~/glTFast.md#runtime-loading-via-script) in the detailed documentation for instructions how to customize the loading behaviour via script. +See [Runtime Loading via Script](./Documentation~/glTFast.md#runtime-loading-via-script) in the documentation for more details and instructions how to [customize the loading behaviour](./Documentation~/glTFast.md#customize-loading-behavior) via script. -#### Customizing Runtime Loading Behavior +### Editor Import -The loading behavior can be highly customized: +Move or copy *glTF* files into your project's *Assets* folder, similar to other 3D formats: -- Customize [instantiation](./Documentation~/glTFast.md#instantiation) -- Load glTF once and instantiate it many times (see [example](./Documentation~/glTFast.md#custom-post-loading-behaviour)) -- Access data of glTF scene (for example get material; see [example](./Documentation~/glTFast.md#custom-post-loading-behaviour)) -- Load [reports](./Documentation~/glTFast.md#report) allow reacting and communicating incidents during loading and instantiation -- Tweak and optimize loading performance +![Editor Import][import-gif] -See the [Documentation](./Documentation~/glTFast.md) for details. +*glTFast* will import them to native Unity prefabs and add them to the asset database. -### Editor Import +See [Editor Import](./Documentation~/glTFast.md#editor-import) in the documentation for details. -To convert your glTF asset into a native Unity prefab, just move/copy it and all its companioning buffer and texture files into the *Assets* folder of your Unity project. It'll get imported into the Asset Database automatically. Select it in the Project view to see detailed settings and import reports in the Inspector. Expand it in the Project View to see the components (Scenes, Meshes, Materials, AnimationClips and Textures) that were imported. +### Editor Export + +The main menu has a couple of [entries for glTF export](./Documentation~/glTFast.md#export-from-the-main-menu) under `File > Export` and glTFs can also be +created [via script](./Documentation~/glTFast.md#export-via-script). ## Project Setup @@ -103,33 +117,9 @@ To convert your glTF asset into a native Unity prefab, just move/copy it and all Read the section *Materials and Shader Variants* in the [Documentation](./Documentation~/glTFast.md#materials-and-shader-variants) for details. -## Roadmap - -Find plans for upcoming changes at the [milestones](https://github.com/atteneder/glTFast/milestones). - -## Motivation - -### Goals - -- Stay fast, memory efficient and small -- Feature completeness - - Support 100% of the glTF 2.0 specification - - Support all official Khronos extensions - - Support selected vendor extension -- Universally usable… - - …across all popular Unity versions - - …across all platforms and devices - - …across different project setups (all important render pipelines, GameObject or entity component system based, DOTS, Tiny, etc.) -- Allow customization - -### Extended goals - -- glTF Authoring (create optimized glTFs from prefabs) -- glTF Runtime Export - ## Get involved -Contributions like ideas, comments, critique, bug reports, pull requests are highly appreciated. Feel free to get in contact if you consider using or improving *glTFast*. +Contributions in the form of ideas, comments, critique, bug reports, pull requests are highly appreciated. Feel free to get in contact if you consider using or improving *glTFast*. ## Supporters @@ -161,9 +151,11 @@ limitations under the License. [unity]: https://unity.com [gltf]: https://www.khronos.org/gltf -[gltf-spec]: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md +[gltf-spec]: https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html [gltfast-web-demo]: https://gltf.pixel.engineer [khronos]: https://www.khronos.org [embibe]: https://www.embibe.com [gltfasset_component]: ./Documentation~/img/gltfasset_component.png "Inspector showing a GltfAsset component added to a GameObject" +[import-gif]: ./Documentation~/img/import.gif "Video showing glTF files being copied into the Assets folder and imported" [upm_install]: ./Documentation~/img/upm_install.png "Unity Package Manager add menu" +[workflows]: ./Documentation~/glTFast.md#workflows \ No newline at end of file diff --git a/Runtime/Scripts/Export.meta b/Runtime/Scripts/Export.meta new file mode 100644 index 00000000..f13f5e1b --- /dev/null +++ b/Runtime/Scripts/Export.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 59ebae2cdae594e61a848659876e2444 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/Constants.cs b/Runtime/Scripts/Export/Constants.cs new file mode 100644 index 00000000..4bd0e9b2 --- /dev/null +++ b/Runtime/Scripts/Export/Constants.cs @@ -0,0 +1,22 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using UnityEngine; + +namespace GLTFast.Export { + static class Constants { + public const string version = "4.4.0"; + } +} diff --git a/Runtime/Scripts/Export/Constants.cs.meta b/Runtime/Scripts/Export/Constants.cs.meta new file mode 100644 index 00000000..04d64e35 --- /dev/null +++ b/Runtime/Scripts/Export/Constants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0cbddf2c39bbb4469b0eed7673800b76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/ExportJobs.cs b/Runtime/Scripts/Export/ExportJobs.cs new file mode 100644 index 00000000..87b900ce --- /dev/null +++ b/Runtime/Scripts/Export/ExportJobs.cs @@ -0,0 +1,111 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace GLTFast.Export { + + [BurstCompile] + public static class ExportJobs { + + [BurstCompile] + public struct ConvertIndicesFlippedJob : IJobParallelFor where T : struct { + + [ReadOnly] + public NativeArray input; + + [WriteOnly] + [NativeDisableParallelForRestriction] + public NativeArray result; + + public void Execute(int i) { + result[i*3+0] = input[i*3+0]; + result[i*3+1] = input[i*3+2]; + result[i*3+2] = input[i*3+1]; + } + } + + [BurstCompile] + public struct ConvertIndicesQuadFlippedJob : IJobParallelFor where T : struct { + + [ReadOnly] + public NativeArray input; + + [WriteOnly] + [NativeDisableParallelForRestriction] + public NativeArray result; + + public void Execute(int i) { + result[i*6+0] = input[i*4+0]; + result[i*6+1] = input[i*4+2]; + result[i*6+2] = input[i*4+1]; + result[i*6+3] = input[i*4+2]; + result[i*6+4] = input[i*4+0]; + result[i*6+5] = input[i*4+3]; + } + } + + [BurstCompile] + public unsafe struct ConvertPositionFloatJob : IJobParallelFor { + + public uint byteStride; + + [ReadOnly] + [NativeDisableUnsafePtrRestriction] + public byte* input; + + [WriteOnly] + [NativeDisableUnsafePtrRestriction] + public byte* output; + + public void Execute(int i) { + var inPtr = (float3*)(input + i * byteStride); + var outPtr = (float3*)(output + i * byteStride); + + var tmp = *inPtr; + tmp.x *= -1; + *outPtr = tmp; + } + } + + [BurstCompile] + public unsafe struct ConvertTangentFloatJob : IJobParallelFor { + + public uint byteStride; + + [ReadOnly] + [NativeDisableUnsafePtrRestriction] + public byte* input; + + [WriteOnly] + [NativeDisableUnsafePtrRestriction] + public byte* output; + + public void Execute(int i) { + var inPtr = (float4*)(input + i * byteStride); + var outPtr = (float4*)(output + i * byteStride); + + var tmp = *inPtr; + tmp.z *= -1; + *outPtr = tmp; + } + } + } +} diff --git a/Runtime/Scripts/Export/ExportJobs.cs.meta b/Runtime/Scripts/Export/ExportJobs.cs.meta new file mode 100644 index 00000000..ab1728cb --- /dev/null +++ b/Runtime/Scripts/Export/ExportJobs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6674c1f674c774250b3ff7a9abb099d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/ExportSettings.cs b/Runtime/Scripts/Export/ExportSettings.cs new file mode 100644 index 00000000..3397f21e --- /dev/null +++ b/Runtime/Scripts/Export/ExportSettings.cs @@ -0,0 +1,65 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using UnityEngine; + +namespace GLTFast.Export { + + /// + /// glTF format + /// + public enum GltfFormat { + /// + /// JSON-based glTF (.gltf file extension) + /// + Json, + /// + /// glTF-binary (.glb file extension) + /// + Binary + } + + /// + /// Destination for image files + /// + public enum ImageDestination { + /// + /// Automatic decision. Main buffer for glTF-binary, separate files for JSON-based glTFs. + /// + Automatic, + /// + /// Embeds images in main buffer + /// + MainBuffer, + /// + /// Saves images as separate files relative to glTF file + /// + SeparateFile + } + + public enum FileConflictResolution { + Abort, + Overwrite + } + + /// + /// glTF export settings + /// + public class ExportSettings { + public GltfFormat format = GltfFormat.Json; + public ImageDestination imageDestination = ImageDestination.Automatic; + public FileConflictResolution fileConflictResolution = FileConflictResolution.Abort; + } +} diff --git a/Runtime/Scripts/Export/ExportSettings.cs.meta b/Runtime/Scripts/Export/ExportSettings.cs.meta new file mode 100644 index 00000000..919cb50b --- /dev/null +++ b/Runtime/Scripts/Export/ExportSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c28d37ec26924a698bafba3e3fe631f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/GameObjectExport.cs b/Runtime/Scripts/Export/GameObjectExport.cs new file mode 100644 index 00000000..58262349 --- /dev/null +++ b/Runtime/Scripts/Export/GameObjectExport.cs @@ -0,0 +1,142 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; + +namespace GLTFast.Export { + + public class GameObjectExport { + + GltfWriter m_Writer; + + /// + /// Provides glTF export of GameObject based scenes and hierarchies. + /// + /// Export settings + /// Defer agent; decides when/if to preempt + /// export to preserve a stable frame rate + /// Interface for logging (error) messages + /// + public GameObjectExport( + ExportSettings exportSettings = null, + IDeferAgent deferAgent = null, + ICodeLogger logger = null + ) { + m_Writer = new GltfWriter(exportSettings, deferAgent, logger); + } + + /// + /// Adds a scene to the glTF. + /// If the conversion to glTF was not flawless (i.e. parts of the scene + /// were not converted 100% correctly) you still might be able to + /// export a glTF. You may use the + /// to analyze what exactly went wrong. + /// + /// Root level GameObjects (will get added recursively) + /// Name of the scene + /// True if the scene was added flawlessly, false otherwise + public bool AddScene(GameObject[] gameObjects, string name = null) { + CertifyNotDisposed(); + var rootNodes = new List(gameObjects.Length); + var tempMaterials = new List(); + var success = true; + for (var index = 0; index < gameObjects.Length; index++) { + var gameObject = gameObjects[index]; + if(!gameObject.activeInHierarchy) continue; + success &= AddGameObject(gameObject,tempMaterials, out var nodeId); + if (nodeId >= 0) { + rootNodes.Add((uint)nodeId); + } + } + if (rootNodes.Count > 0) { + m_Writer.AddScene(rootNodes.ToArray(), name); + } + + return success; + } + + /// + /// Exports the collected scenes/content as glTF and disposes this object. + /// After the export this instance cannot be re-used! + /// + /// glTF destination file path + /// True if the glTF file was created successfully, false otherwise + public async Task SaveToFileAndDispose(string path) { + CertifyNotDisposed(); + var success = await m_Writer.SaveToFileAndDispose(path); + m_Writer = null; + return success; + } + + void CertifyNotDisposed() { + if (m_Writer == null) { + throw new InvalidOperationException("GameObjectExport was already disposed"); + } + } + bool AddGameObject(GameObject gameObject, List tempMaterials, out int nodeId ) { + if (!gameObject.activeInHierarchy) { + nodeId = -1; + return true; + } + + var success = true; + var childCount = gameObject.transform.childCount; + uint[] children = null; + if (childCount > 0) { + var childList = new List(gameObject.transform.childCount); + for (var i = 0; i < childCount; i++) { + var child = gameObject.transform.GetChild(i); + success &= AddGameObject(child.gameObject, tempMaterials, out var childNodeId); + if (childNodeId >= 0) { + childList.Add((uint)childNodeId); + } + } + if (childList.Count > 0) { + children = childList.ToArray(); + } + } + + var transform = gameObject.transform; + nodeId = (int) m_Writer.AddNode( + transform.localPosition, + transform.localRotation, + transform.localScale, + children, + gameObject.name + ); + Mesh mesh = null; + + tempMaterials.Clear(); + + if (gameObject.TryGetComponent(out MeshFilter meshFilter)) { + mesh = meshFilter.sharedMesh; + if (gameObject.TryGetComponent(out Renderer renderer)) { + renderer.GetSharedMaterials(tempMaterials); + } + } else + if (gameObject.TryGetComponent(out SkinnedMeshRenderer smr)) { + mesh = smr.sharedMesh; + smr.GetSharedMaterials(tempMaterials); + } + if (mesh != null) { + success &= m_Writer.AddMeshToNode(nodeId,mesh,tempMaterials); + } + return success; + } + } +} diff --git a/Runtime/Scripts/Export/GameObjectExport.cs.meta b/Runtime/Scripts/Export/GameObjectExport.cs.meta new file mode 100644 index 00000000..145ff613 --- /dev/null +++ b/Runtime/Scripts/Export/GameObjectExport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c45eecded6f2d47c4a6f6162481119df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/GltfWriter.cs b/Runtime/Scripts/Export/GltfWriter.cs new file mode 100644 index 00000000..4028cda0 --- /dev/null +++ b/Runtime/Scripts/Export/GltfWriter.cs @@ -0,0 +1,1226 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if UNITY_2020_2_OR_NEWER +#define GLTFAST_MESH_DATA +#endif + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using GLTFast.Schema; +using JetBrains.Annotations; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Assertions; +using UnityEngine.Profiling; +using UnityEngine.Rendering; +using Buffer = GLTFast.Schema.Buffer; +using Debug = UnityEngine.Debug; +using Material = GLTFast.Schema.Material; +using Mesh = GLTFast.Schema.Mesh; +using Texture = GLTFast.Schema.Texture; + +#if DEBUG +using System.Text; +#endif +#if UNITY_EDITOR +using UnityEditor; +#endif + +[assembly: InternalsVisibleTo("glTFastEditor")] +[assembly: InternalsVisibleTo("glTF-test-framework.Tests")] + +namespace GLTFast.Export { + + public class GltfWriter : IGltfWritable { + + enum State { + Initialized, + ContentAdded, + Disposed + } + +#region Constants + const int k_MAXStreamCount = 4; + const int k_DefaultInnerLoopBatchCount = 512; +#endregion Constants + +#region Private + State m_State; + + ExportSettings m_Settings; + IDeferAgent m_DeferAgent; + ICodeLogger m_Logger; + + Root m_Gltf; + + HashSet m_ExtensionsUsedOnly; + HashSet m_ExtensionsRequired; + + List m_Scenes; + List m_Nodes; + List m_Meshes; + List m_Materials; + List m_Textures; + List m_Images; + List m_Accessors; + List m_BufferViews; + + List m_UnityMaterials; + List m_UnityMeshes; + List m_UnityTextures; + Dictionary m_NodeMaterials; + Dictionary m_ImagePathsToAdd; + + Stream m_BufferStream; + string m_BufferPath; +#endregion Private + + /// + /// Provides glTF export independent of workflow (GameObjects/Entities) + /// + /// Export settings + /// Defer agent; decides when/if to preempt + /// export to preserve a stable frame rate + /// Interface for logging (error) messages + /// + public GltfWriter( + ExportSettings exportSettings = null, + IDeferAgent deferAgent = null, + ICodeLogger logger = null + ) + { + m_Gltf = new Root(); + m_Settings = exportSettings ?? new ExportSettings(); + m_Logger = logger; + m_State = State.Initialized; + m_DeferAgent = deferAgent ?? new UninterruptedDeferAgent(); + } + + /// + /// Adds a node to the glTF + /// + /// Local translation of the node (in Unity-space) + /// Local rotation of the node (in Unity-space) + /// Local scale of the node (in Unity-space) + /// Array of node indices that are parented to + /// this newly created node + /// Name of the node + /// glTF node index + public uint AddNode( + float3? translation = null, + quaternion? rotation = null, + float3? scale = null, + uint[] children = null, + string name = null + ) + { + CertifyNotDisposed(); + m_State = State.ContentAdded; + var node = new Node { + name = name, + children = children, + }; + if( translation.HasValue && !translation.Equals(float3.zero) ) { + node.translation = new[] { -translation.Value.x, translation.Value.y, translation.Value.z }; + } + if( rotation.HasValue && !rotation.Equals(quaternion.identity) ) { + node.rotation = new[] { rotation.Value.value.x, -rotation.Value.value.y, -rotation.Value.value.z, rotation.Value.value.w }; + } + if( scale.HasValue && !scale.Equals(new float3(1f)) ) { + node.scale = new[] { scale.Value.x, scale.Value.y, scale.Value.z }; + } + m_Nodes = m_Nodes ?? new List(); + m_Nodes.Add(node); + return (uint) m_Nodes.Count - 1; + } + + /// + /// Assigns a mesh to a previously added node + /// + /// Index of the node to add the mesh to + /// Unity mesh to be assigned and exported + /// Materials to be assigned and exported + /// (multiple in case of sub-meshes) + /// True if the conversion was flawless, false otherwise (use + /// for analysing errors) + public bool AddMeshToNode(int nodeId, [NotNull] UnityEngine.Mesh uMesh, List uMaterials) { + CertifyNotDisposed(); + var node = m_Nodes[nodeId]; + + var success = true; + if (uMaterials != null && uMaterials.Count > 0) { + var materialIds = new int[uMaterials.Count]; + for (var i = 0; i < uMaterials.Count; i++) { + var uMaterial = uMaterials[i]; + if (uMaterial == null) { + materialIds[i] = -1; + } else { + success &= AddMaterial(uMaterial, out materialIds[i]); + } + } + m_NodeMaterials = m_NodeMaterials ?? new Dictionary(); + m_NodeMaterials[nodeId] = materialIds; + } + + node.mesh = AddMesh(uMesh); + return success; + } + + /// + /// Adds a scene to the glTF + /// + /// Root level nodes + /// Name of the scene + /// glTF scene index + public uint AddScene(uint[] nodes, string name = null) { + CertifyNotDisposed(); + m_Scenes = m_Scenes ?? new List(); + var scene = new Scene { + name = name, + nodes = nodes + }; + m_Scenes.Add(scene); + if (m_Scenes.Count == 1) { + m_Gltf.scene = 0; + } + return (uint) m_Scenes.Count - 1; + } + + public int AddImage( UnityEngine.Texture uTexture ) { + CertifyNotDisposed(); + int imageId; + if (m_UnityTextures != null) { + imageId = m_UnityTextures.IndexOf(uTexture); + if (imageId >= 0) { + return imageId; + } + } else { + m_UnityTextures = new List(); + m_Images = new List(); + } + + imageId = m_UnityTextures.Count; + + // TODO: Create sampler, if required + // TODO: KTX encoding + +#if UNITY_EDITOR + + var assetPath = AssetDatabase.GetAssetPath(uTexture); + if (File.Exists(assetPath)) { + var mimeType = GetMimeType(assetPath); + if (!string.IsNullOrEmpty(mimeType)) { + var image = new Image { + name = uTexture.name, + mimeType = mimeType + }; + + m_ImagePathsToAdd = m_ImagePathsToAdd ?? new Dictionary(); + m_ImagePathsToAdd[imageId] = assetPath; + + m_UnityTextures.Add(uTexture); + m_Images.Add(image); + } else { + m_Logger?.Error(LogCode.ImageFormatUnknown,uTexture.name,assetPath); + return -1; + } + } +#else + throw new NotImplementedException("Exporting textures at runtime is not yet implemented"); +#endif + + return imageId; + } + + public int AddTexture(int imageId) { + CertifyNotDisposed(); + m_Textures = m_Textures ?? new List(); + + var texture = new Texture { + source = imageId + }; + m_Textures.Add(texture); + return m_Textures.Count - 1; + } + + public void RegisterExtensionUsage(Extension extension, bool required = true) { + CertifyNotDisposed(); + if (required) { + m_ExtensionsRequired = m_ExtensionsRequired ?? new HashSet(); + m_ExtensionsRequired.Add(extension); + } else { + if (m_ExtensionsRequired == null || !m_ExtensionsRequired.Contains(extension)) { + m_ExtensionsUsedOnly = m_ExtensionsUsedOnly ?? new HashSet(); + m_ExtensionsUsedOnly.Add(extension); + } + } + } + + /// + /// Exports the collected scenes/content as glTF and disposes this object. + /// After the export this instance cannot be re-used! + /// + /// glTF destination file path + /// True if the glTF file was created successfully, false otherwise + public async Task SaveToFileAndDispose(string path) { + CertifyNotDisposed(); +#if DEBUG + if (m_State != State.ContentAdded) { + Debug.LogWarning("Exporting empty glTF"); + } +#endif + var ext = Path.GetExtension(path); + var binary = m_Settings.format == GltfFormat.Binary; + m_BufferPath = null; + if (!binary) { + if (string.IsNullOrEmpty(ext)) { + m_BufferPath = path + ".bin"; + } else { + m_BufferPath = path.Substring(0, path.Length - ext.Length) + ".bin"; + } + } + + var success = await Bake(Path.GetFileName(m_BufferPath), Path.GetDirectoryName(path)); + + if (!success) { + m_BufferStream?.Close(); + Dispose(); + return false; + } + + var json = GetJson(); + LogSummary(json.Length, m_BufferStream?.Length ?? 0); + + if (binary) { + const uint headerSize = 12; // 4 bytes magic + 4 bytes version + 4 bytes length (uint each) + const uint chunkOverhead = 8; // 4 bytes chunk length + 4 bytes chunk type (uint each) + var glb = new FileStream(path,FileMode.Create); + glb.Write(BitConverter.GetBytes(GltfGlobals.GLB_MAGIC)); + glb.Write(BitConverter.GetBytes((uint)2)); + + var jsonPad = GetPadByteCount((uint)json.Length); + var binPad = 0; + var totalLength = (uint) (headerSize + chunkOverhead + json.Length + jsonPad); + var hasBufferContent = (m_BufferStream?.Length ?? 0) > 0; + if (hasBufferContent) { + binPad = GetPadByteCount((uint)m_BufferStream.Length); + totalLength += (uint) (chunkOverhead + m_BufferStream.Length + binPad); + } + + glb.Write(BitConverter.GetBytes(totalLength)); + + glb.Write(BitConverter.GetBytes((uint)(json.Length+jsonPad))); + glb.Write(BitConverter.GetBytes((uint)ChunkFormat.JSON)); + var sw = new StreamWriter(glb); + sw.Write(json); + for (int i = 0; i < jsonPad; i++) { + sw.Write(' '); + } + sw.Flush(); + + if (hasBufferContent) { + glb.Write(BitConverter.GetBytes((uint)(m_BufferStream.Length+binPad))); + glb.Write(BitConverter.GetBytes((uint)ChunkFormat.BIN)); + var ms = (MemoryStream)m_BufferStream; + ms.WriteTo(glb); + ms.Flush(); + for (int i = 0; i < binPad; i++) { + sw.Write('\0'); + } + } + sw.Close(); + glb.Close(); + } + else { + File.WriteAllText(path,json); + } + + Dispose(); + return true; + } + + void CertifyNotDisposed() { + if (m_State == State.Disposed) { + throw new InvalidOperationException("GltfWriter was already disposed"); + } + } + + static string GetMimeType(string assetPath) { + string mimeType = null; + if (assetPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { + mimeType = "image/png"; + } + else if (assetPath.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + assetPath.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase)) { + mimeType = "image/jpeg"; + } + + return mimeType; + } + + ImageDestination GetFinalImageDestination() { + var imageDest = m_Settings.imageDestination; + if (imageDest == ImageDestination.Automatic) { + imageDest = m_Settings.format == GltfFormat.Binary + ? ImageDestination.MainBuffer + : ImageDestination.SeparateFile; + } + + return imageDest; + } + + bool AddMaterial(UnityEngine.Material uMaterial, out int materialId) { + + if (m_Materials!=null) { + materialId = m_UnityMaterials.IndexOf(uMaterial); + if (materialId >= 0) { + return true; + } + } else { + m_Materials = new List(); + m_UnityMaterials = new List(); + } + + var success = StandardMaterialExport.ConvertMaterial(uMaterial, out var material, this, m_Logger); + + materialId = m_Materials.Count; + m_Materials.Add(material); + m_UnityMaterials.Add(uMaterial); + return success; + } + + int GetPadByteCount(uint length) { + return (4 - (int)(length & 3) ) & 3; + } + +#if DEBUG + [Conditional("DEBUG")] + void LogSummary(int jsonLength, long bufferLength) { + var sb = new StringBuilder("glTF summary: "); + sb.AppendFormat("{0} bytes JSON + {1} bytes buffer", jsonLength, bufferLength); + if (m_Gltf != null) { + sb.AppendFormat(", {0} nodes", m_Gltf.nodes?.Length ?? 0); + sb.AppendFormat(" ,{0} meshes", m_Gltf.meshes?.Length ?? 0); + sb.AppendFormat(" ,{0} materials", m_Gltf.materials?.Length ?? 0); + sb.AppendFormat(" ,{0} images", m_Gltf.images?.Length ?? 0); + } + m_Logger?.Info(sb.ToString()); + } +#endif + + async Task Bake(string bufferPath, string directory) { + if (m_Meshes != null) { +#if GLTFAST_MESH_DATA + await BakeMeshes(); +#else + throw new NotImplementedException("glTF export (containing meshes) is currently not supported on Unity 2020.1 and older"); +#endif + } + + AssignMaterialsToMeshes(); + + var success = await BakeImages(directory); + + if (!success) return false; + + if (m_BufferStream != null && m_BufferStream.Length > 0) { + m_Gltf.buffers = new[] { + new Buffer { + uri = bufferPath, + byteLength = (uint) m_BufferStream.Length + } + }; + } + + m_Gltf.scenes = m_Scenes?.ToArray(); + m_Gltf.nodes = m_Nodes?.ToArray(); + m_Gltf.meshes = m_Meshes?.ToArray(); + m_Gltf.accessors = m_Accessors?.ToArray(); + m_Gltf.bufferViews = m_BufferViews?.ToArray(); + m_Gltf.materials = m_Materials?.ToArray(); + m_Gltf.images = m_Images?.ToArray(); + m_Gltf.textures = m_Textures?.ToArray(); + + m_Gltf.asset = new Asset { + version = "2.0", + generator = $"Unity {Application.unityVersion} glTFast {Constants.version}" + }; + + BakeExtensions(); + return true; + } + + void BakeExtensions() { + if (m_ExtensionsRequired != null) { + var usedOnlyCount = m_ExtensionsUsedOnly == null ? 0 : m_ExtensionsUsedOnly.Count; + m_Gltf.extensionsRequired = new string[m_ExtensionsRequired.Count]; + m_Gltf.extensionsUsed = new string[m_ExtensionsRequired.Count + usedOnlyCount]; + var i = 0; + foreach (var extension in m_ExtensionsRequired) { + var name = extension.GetName(); + m_Gltf.extensionsRequired[i] = name; + m_Gltf.extensionsUsed[i] = name; + i++; + } + } + + if (m_ExtensionsUsedOnly != null) { + var i = 0; + if (m_Gltf.extensionsUsed == null) { + m_Gltf.extensionsUsed = new string[m_ExtensionsUsedOnly.Count]; + } + else { + i = m_Gltf.extensionsUsed.Length - m_ExtensionsUsedOnly.Count; + } + + foreach (var extension in m_ExtensionsUsedOnly) { + m_Gltf.extensionsUsed[i++] = extension.GetName(); + } + } + } + + void AssignMaterialsToMeshes() { + if (m_NodeMaterials != null && m_Meshes != null) { + var meshMaterialCombos = new Dictionary(m_Meshes.Count); + var originalCombos = new Dictionary(m_Meshes.Count); + foreach (var nodeMaterial in m_NodeMaterials) { + var nodeId = nodeMaterial.Key; + var materialIds = nodeMaterial.Value; + var node = m_Nodes[nodeId]; + var originalMeshId = node.mesh; + var mesh = m_Meshes[originalMeshId]; + + var meshMaterialCombo = new MeshMaterialCombination(originalMeshId, materialIds); + + if (!originalCombos.ContainsKey(originalMeshId)) { + // First usage of the original -> assign materials to original + AssignMaterialsToMesh(materialIds, mesh); + originalCombos[originalMeshId] = meshMaterialCombo; + meshMaterialCombos[meshMaterialCombo] = originalMeshId; + } else { + // Mesh is re-used -> check if this exact materials set was used before + if (meshMaterialCombos.TryGetValue(meshMaterialCombo, out var meshId)) { + // Materials are identical -> re-use Mesh object + node.mesh = meshId; + } else { + // Materials differ -> clone Mesh object and assign materials to clone + var clonedMeshId = DuplicateMesh(originalMeshId); + mesh = m_Meshes[clonedMeshId]; + AssignMaterialsToMesh(materialIds, mesh); + node.mesh = clonedMeshId; + meshMaterialCombos[meshMaterialCombo] = clonedMeshId; + } + } + } + } + m_NodeMaterials = null; + } + + static void AssignMaterialsToMesh(int[] materialIds, Mesh mesh) { + for (var i = 0; i < materialIds.Length; i++) { + mesh.primitives[i].material = materialIds[i] >= 0 ? materialIds[i] : -1; + } + } + + int DuplicateMesh(int meshId) { + var src = m_Meshes[meshId]; + var copy = (Mesh)src.Clone(); + m_Meshes.Add(copy); + return m_Meshes.Count - 1; + } + +#if GLTFAST_MESH_DATA + + async Task BakeMeshes() { + Profiler.BeginSample("BakeMeshes"); + Profiler.BeginSample("AcquireReadOnlyMeshData"); + var meshDataArray = UnityEngine.Mesh.AcquireReadOnlyMeshData(m_UnityMeshes); + Profiler.EndSample(); + for (var meshId = 0; meshId < m_Meshes.Count; meshId++) { + await BakeMesh(meshId, meshDataArray[meshId]); + await m_DeferAgent.BreakPoint(); + } + meshDataArray.Dispose(); + Profiler.EndSample(); + } + + async Task BakeMesh(int meshId, UnityEngine.Mesh.MeshData meshData) { + + Profiler.BeginSample("BakeMesh"); + + var mesh = m_Meshes[meshId]; + var uMesh = m_UnityMeshes[meshId]; + + var vertexAttributes = uMesh.GetVertexAttributes(); + var strides = new int[k_MAXStreamCount]; + var alignments = new int[k_MAXStreamCount]; + + var attributes = new Attributes(); + var vertexCount = uMesh.vertexCount; + var attrDataDict = new Dictionary(); + + foreach (var attribute in vertexAttributes) { + var attrData = new AttributeData { + offset = strides[attribute.stream], + stream = attribute.stream + }; + + var attributeSize = GetAttributeSize(attribute.format); + var size = attribute.dimension * attributeSize; + strides[attribute.stream] += size; + alignments[attribute.stream] = math.max(alignments[attribute.stream], attributeSize); + + // Adhere data alignment rules + Assert.IsTrue(attrData.offset % 4 == 0); + + var accessor = new Accessor { + byteOffset = attrData.offset, + componentType = Accessor.GetComponentType(attribute.format), + count = vertexCount, + typeEnum = Accessor.GetAccessorAttributeType(attribute.dimension), + }; + + var accessorId = AddAccessor(accessor); + + attrData.accessorId = accessorId; + attrDataDict[attribute.attribute] = attrData; + + switch (attribute.attribute) { + case VertexAttribute.Position: + Assert.AreEqual(VertexAttributeFormat.Float32,attribute.format); + Assert.AreEqual(3,attribute.dimension); + var bounds = uMesh.bounds; + var max = bounds.max; + var min = bounds.min; + accessor.min = new[] { -max.x, min.y, min.z }; + accessor.max = new[] { -min.x, max.y, max.z }; + attributes.POSITION = accessorId; + break; + case VertexAttribute.Normal: + Assert.AreEqual(VertexAttributeFormat.Float32,attribute.format); + Assert.AreEqual(3,attribute.dimension); + attributes.NORMAL = accessorId; + break; + case VertexAttribute.Tangent: + Assert.AreEqual(VertexAttributeFormat.Float32,attribute.format); + Assert.AreEqual(4,attribute.dimension); + attributes.TANGENT = accessorId; + break; + case VertexAttribute.Color: + attributes.COLOR_0 = accessorId; + break; + case VertexAttribute.TexCoord0: + attributes.TEXCOORD_0 = accessorId; + break; + case VertexAttribute.TexCoord1: + attributes.TEXCOORD_1 = accessorId; + break; + case VertexAttribute.TexCoord2: + attributes.TEXCOORD_2 = accessorId; + break; + case VertexAttribute.TexCoord3: + attributes.TEXCOORD_3 = accessorId; + break; + case VertexAttribute.TexCoord4: + attributes.TEXCOORD_4 = accessorId; + break; + case VertexAttribute.TexCoord5: + attributes.TEXCOORD_5 = accessorId; + break; + case VertexAttribute.TexCoord6: + attributes.TEXCOORD_6 = accessorId; + break; + case VertexAttribute.TexCoord7: + attributes.TEXCOORD_7 = accessorId; + break; + case VertexAttribute.BlendWeight: + attributes.WEIGHTS_0 = accessorId; + break; + case VertexAttribute.BlendIndices: + attributes.JOINTS_0 = accessorId; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + var streamCount = 1; + for (var stream = 0; stream < strides.Length; stream++) { + var stride = strides[stream]; + if (stride <= 0) continue; + streamCount = stream + 1; + } + + var indexComponentType = uMesh.indexFormat == IndexFormat.UInt16 ? GLTFComponentType.UnsignedShort : GLTFComponentType.UnsignedInt; + mesh.primitives = new MeshPrimitive[meshData.subMeshCount]; + var indexAccessors = new Accessor[meshData.subMeshCount]; + var indexOffset = 0; + MeshTopology? topology = null; + for (var subMeshIndex = 0; subMeshIndex < meshData.subMeshCount; subMeshIndex++) { + var subMesh = meshData.GetSubMesh(subMeshIndex); + if (!topology.HasValue) { + topology = subMesh.topology; + } else { + Assert.AreEqual(topology.Value, subMesh.topology, "Mixed topologies are not supported!"); + } + var mode = GetDrawMode(subMesh.topology); + if (!mode.HasValue) { + m_Logger?.Error(LogCode.TopologyUnsupported, subMesh.topology.ToString()); + mode = DrawMode.Points; + } + + Accessor indexAccessor; + + indexAccessor = new Accessor { + typeEnum = GLTFAccessorAttributeType.SCALAR, + byteOffset = indexOffset, + componentType = indexComponentType, + count = subMesh.indexCount, + + // min = new []{}, // TODO + // max = new []{}, // TODO + }; + + if (subMesh.topology == MeshTopology.Quads) { + indexAccessor.count = indexAccessor.count / 2 * 3; + } + + var indexAccessorId = AddAccessor(indexAccessor); + indexAccessors[subMeshIndex] = indexAccessor; + + indexOffset += indexAccessor.count * Accessor.GetComponentTypeSize(indexComponentType); + + mesh.primitives[subMeshIndex] = new MeshPrimitive { + mode = mode.Value, + attributes = attributes, + indices = indexAccessorId, + }; + } + Assert.IsTrue(topology.HasValue); + + Profiler.BeginSample("ScheduleIndexJob"); + int indexBufferViewId; + if (uMesh.indexFormat == IndexFormat.UInt16) { + var indexData16 = meshData.GetIndexData(); + if (topology.Value == MeshTopology.Quads) { + var quadCount = indexData16.Length / 4; + var destIndices = new NativeArray(quadCount*6,Allocator.TempJob); + var job = new ExportJobs.ConvertIndicesQuadFlippedJob { + input = indexData16, + result = destIndices + }.Schedule(quadCount, k_DefaultInnerLoopBatchCount); + while (!job.IsCompleted) { + await Task.Yield(); + } + job.Complete(); // TODO: Wait until thread is finished + indexBufferViewId = WriteBufferViewToBuffer( + destIndices.Reinterpret(sizeof(ushort)), + byteAlignment:sizeof(ushort) + ); + destIndices.Dispose(); + } else { + var triangleCount = indexData16.Length / 3; + var destIndices = new NativeArray(indexData16.Length,Allocator.TempJob); + var job = new ExportJobs.ConvertIndicesFlippedJob { + input = indexData16, + result = destIndices + }.Schedule(triangleCount, k_DefaultInnerLoopBatchCount); + while (!job.IsCompleted) { + await Task.Yield(); + } + job.Complete(); // TODO: Wait until thread is finished + indexBufferViewId = WriteBufferViewToBuffer( + destIndices.Reinterpret(sizeof(ushort)), + byteAlignment:sizeof(ushort) + ); + destIndices.Dispose(); + } + } else { + var indexData32 = meshData.GetIndexData(); + if (topology.Value == MeshTopology.Quads) { + var quadCount = indexData32.Length / 4; + var destIndices = new NativeArray(quadCount*6,Allocator.TempJob); + var job = new ExportJobs.ConvertIndicesQuadFlippedJob { + input = indexData32, + result = destIndices + }.Schedule(quadCount, k_DefaultInnerLoopBatchCount); + while (!job.IsCompleted) { + await Task.Yield(); + } + job.Complete(); // TODO: Wait until thread is finished + indexBufferViewId = WriteBufferViewToBuffer( + destIndices.Reinterpret(sizeof(uint)), + byteAlignment:sizeof(uint) + ); + destIndices.Dispose(); + } else { + var triangleCount = indexData32.Length / 3; + var destIndices = new NativeArray(indexData32.Length, Allocator.TempJob); + var job = new ExportJobs.ConvertIndicesFlippedJob { + input = indexData32, + result = destIndices + }.Schedule(triangleCount, k_DefaultInnerLoopBatchCount); + while (!job.IsCompleted) { + await Task.Yield(); + } + job.Complete(); // TODO: Wait until thread is finished + indexBufferViewId = WriteBufferViewToBuffer( + destIndices.Reinterpret(sizeof(uint)), + byteAlignment:sizeof(uint) + ); + destIndices.Dispose(); + } + } + Profiler.EndSample(); + + foreach (var accessor in indexAccessors) { + accessor.bufferView = indexBufferViewId; + } + + var inputStreams = new NativeArray[streamCount]; + var outputStreams = new NativeArray[streamCount]; + + for (var stream = 0; stream < streamCount; stream++) { + inputStreams[stream] = meshData.GetVertexData(stream); + outputStreams[stream] = new NativeArray(inputStreams[stream], Allocator.TempJob); + } + + Profiler.BeginSample("ScheduleVertexJob"); + foreach (var pair in attrDataDict) { + var vertexAttribute = pair.Key; + var attrData = pair.Value; + switch (vertexAttribute) { + case VertexAttribute.Position: + case VertexAttribute.Normal: + await ConvertPositionAttribute( + attrData, + (uint)strides[attrData.stream], + vertexCount, + inputStreams[attrData.stream], + outputStreams[attrData.stream] + ); + break; + case VertexAttribute.Tangent: + await ConvertTangentAttribute( + attrData, + (uint)strides[attrData.stream], + vertexCount, + inputStreams[attrData.stream], + outputStreams[attrData.stream] + ); + break; + } + } + Profiler.EndSample(); + + var bufferViewIds = new int[streamCount]; + for (var stream = 0; stream < streamCount; stream++) { + bufferViewIds[stream] = WriteBufferViewToBuffer( + outputStreams[stream], + strides[stream], + alignments[stream] + ); + inputStreams[stream].Dispose(); + outputStreams[stream].Dispose(); + } + + foreach (var pair in attrDataDict) { + var attrData = pair.Value; + m_Accessors[attrData.accessorId].bufferView = bufferViewIds[attrData.stream]; + } + + Profiler.EndSample(); + } + + int AddAccessor(Accessor accessor) { + m_Accessors = m_Accessors ?? new List(); + var accessorId = m_Accessors.Count; + m_Accessors.Add(accessor); + return accessorId; + } + +#endif // #if GLTFAST_MESH_DATA + + async Task BakeImages(string directory) { + if (m_ImagePathsToAdd != null) { + var imageDest = GetFinalImageDestination(); + var overwrite = m_Settings.fileConflictResolution == FileConflictResolution.Overwrite; + if (!overwrite && imageDest == ImageDestination.SeparateFile) { + var fileExists = false; + foreach (var pair in m_ImagePathsToAdd) { + var assetPath = pair.Value; + var fileName = Path.GetFileName(assetPath); + var destPath = Path.Combine(directory,fileName); + if (File.Exists(destPath)) { + fileExists = true; + break; + } + } + + if (fileExists) { +#if UNITY_EDITOR + overwrite = EditorUtility.DisplayDialog( + "Image file conflicts", + "Some image files at the destination will be overwritten", + "Overwrite", "Cancel"); + if (!overwrite) { + return false; + } +#else + if (m_Settings.fileConflictResolution == FileConflictResolution.Abort) { + return false; + } +#endif + } + } + + foreach (var pair in m_ImagePathsToAdd) { + var imageId = pair.Key; + var assetPath = pair.Value; + if (imageDest == ImageDestination.MainBuffer) { + // TODO: Write from file to buffer stream directly + var imageBytes = File.ReadAllBytes(assetPath); + m_Images[imageId].bufferView = WriteBufferViewToBuffer(imageBytes); + } else if (imageDest == ImageDestination.SeparateFile) { + var fileName = Path.GetFileName(assetPath); + File.Copy(assetPath, Path.Combine(directory,fileName), overwrite); + m_Images[imageId].uri = fileName; + } + await m_DeferAgent.BreakPoint(); + } + } + + m_ImagePathsToAdd = null; + return true; + } + + static async Task ConvertPositionAttribute( + AttributeData attrData, + uint byteStride, + int vertexCount, + NativeArray inputStream, + NativeArray outputStream + ) + { + var job = CreateConvertPositionAttributeJob(attrData, byteStride, vertexCount, inputStream, outputStream); + while (!job.IsCompleted) { + await Task.Yield(); + } + job.Complete(); // TODO: Wait until thread is finished + } + + static unsafe JobHandle CreateConvertPositionAttributeJob( + AttributeData attrData, + uint byteStride, + int vertexCount, + NativeArray inputStream, + NativeArray outputStream + ) + { + var job = new ExportJobs.ConvertPositionFloatJob { + input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.offset, + byteStride = byteStride, + output = (byte*)outputStream.GetUnsafePtr() + attrData.offset + }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); + return job; + } + + static async Task ConvertTangentAttribute( + AttributeData attrData, + uint byteStride, + int vertexCount, + NativeArray inputStream, + NativeArray outputStream + ) + { + var job = CreateConvertTangentAttributeJob(attrData, byteStride, vertexCount, inputStream, outputStream); + while (!job.IsCompleted) { + await Task.Yield(); + } + job.Complete(); // TODO: Wait until thread is finished + } + + static unsafe JobHandle CreateConvertTangentAttributeJob( + AttributeData attrData, + uint byteStride, + int vertexCount, + NativeArray inputStream, + NativeArray outputStream + ) { + var job = new ExportJobs.ConvertTangentFloatJob { + input = (byte*)inputStream.GetUnsafeReadOnlyPtr() + attrData.offset, + byteStride = byteStride, + output = (byte*)outputStream.GetUnsafePtr() + attrData.offset + }.Schedule(vertexCount, k_DefaultInnerLoopBatchCount); + return job; + } + + static DrawMode? GetDrawMode(MeshTopology topology) { + switch (topology) { + case MeshTopology.Quads: + return DrawMode.Triangles; + case MeshTopology.Triangles: + return DrawMode.Triangles; + case MeshTopology.Lines: + return DrawMode.Lines; + case MeshTopology.LineStrip: + return DrawMode.LineStrip; + case MeshTopology.Points: + return DrawMode.Points; + default: + return null; + } + } + + string GetJson() { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + m_Gltf.GltfSerialize(writer); + writer.Flush(); + stream.Seek(0,SeekOrigin.Begin); + var reader = new StreamReader( stream ); + var json = reader.ReadToEnd(); + reader.Close(); + return json; + } + + int AddMesh([NotNull] UnityEngine.Mesh uMesh) { + int meshId; + +#if !UNITY_EDITOR + if (!uMesh.isReadable) { + m_Logger?.Error(LogCode.MeshNotReadable, uMesh.name); + return -1; + } +#endif + + if (m_UnityMeshes!=null) { + meshId = m_UnityMeshes.IndexOf(uMesh); + if (meshId >= 0) { + return meshId; + } + } + + var mesh = new Mesh { + name = uMesh.name + }; + m_Meshes = m_Meshes ?? new List(); + m_UnityMeshes = m_UnityMeshes ?? new List(); + m_Meshes.Add(mesh); + m_UnityMeshes.Add(uMesh); + meshId = m_Meshes.Count - 1; + return meshId; + } + + unsafe int WriteBufferViewToBuffer( byte[] bufferViewData, int? byteStride = null) { + var bufferHandle = GCHandle.Alloc(bufferViewData,GCHandleType.Pinned); + fixed (void* bufferAddress = &bufferViewData[0]) { + var nativeData = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(bufferAddress,bufferViewData.Length,Allocator.None); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + var safetyHandle = AtomicSafetyHandle.Create(); + NativeArrayUnsafeUtility.SetAtomicSafetyHandle(array: ref nativeData, safetyHandle); +#endif + var bufferViewId = WriteBufferViewToBuffer(nativeData, byteStride); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + AtomicSafetyHandle.Release(safetyHandle); +#endif + bufferHandle.Free(); + return bufferViewId; + } + } + + Stream CertifyBuffer() { + if (m_BufferStream == null) { + // Delayed, implicit stream generation. + // if `m_BufferPath` was set, we need a FileStream + if (m_BufferPath != null) { + m_BufferStream = new FileStream(m_BufferPath,FileMode.Create); + } else { + m_BufferStream = new MemoryStream(); + } + } + return m_BufferStream; + } + + /// + /// Writes the given data to the main buffer, creates a bufferView and returns its index + /// + /// Content to write to buffer + /// The byte size of an element. Provide it, + /// if it cannot be inferred from the accessor + /// If not zero, the offsets of the bufferView + /// will be multiple of it to please alignment rules (padding bytes will be added, + /// if required; see https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#data-alignment ) + /// + /// Buffer view index + int WriteBufferViewToBuffer(NativeArray bufferViewData, int? byteStride = null, int byteAlignment = 0) { + Profiler.BeginSample("WriteBufferViewToBuffer"); + var buffer = CertifyBuffer(); + var byteOffset = buffer.Length; + + if (byteAlignment > 0) { + Assert.IsTrue(byteAlignment<5); // There is no componentType that requires more than 4 bytes + var alignmentByteCount = (byteAlignment-(byteOffset % byteAlignment)) % byteAlignment; + for (var i = 0; i < alignmentByteCount; i++) { + buffer.WriteByte(0); + } + // Update byteOffset + byteOffset = buffer.Length; + } + + buffer.Write(bufferViewData); + + var bufferView = new BufferView { + buffer = 0, + byteOffset = (int)byteOffset, + byteLength = bufferViewData.Length, + }; + if (byteStride.HasValue) { + // Adhere data alignment rules + Assert.IsTrue(byteStride.Value % 4 == 0); + bufferView.byteStride = byteStride.Value; + } + m_BufferViews = m_BufferViews ?? new List(); + var bufferViewId = m_BufferViews.Count; + m_BufferViews.Add(bufferView); + Profiler.EndSample(); + return bufferViewId; + } + + void Dispose() { + m_Settings = null; + + m_Logger = null; + m_Gltf = null; + m_ExtensionsUsedOnly = null; + m_ExtensionsRequired = null; + m_UnityMaterials = null; + m_UnityMeshes = null; + m_UnityTextures = null; + m_NodeMaterials = null; + m_ImagePathsToAdd = null; + m_BufferStream = null; + m_BufferPath = null; + + m_Scenes = null; + m_Nodes = null; + m_Meshes = null; + m_Accessors = null; + m_BufferViews = null; + m_Materials = null; + m_Images = null; + m_Textures = null; + + m_State = State.Disposed; + } + + static unsafe int GetAttributeSize(VertexAttributeFormat format) { + switch (format) { + case VertexAttributeFormat.Float32: + return sizeof(float); + case VertexAttributeFormat.Float16: + return sizeof(half); + case VertexAttributeFormat.UNorm8: + return sizeof(byte); + case VertexAttributeFormat.SNorm8: + return sizeof(sbyte); + case VertexAttributeFormat.UNorm16: + return sizeof(ushort); + case VertexAttributeFormat.SNorm16: + return sizeof(short); + case VertexAttributeFormat.UInt8: + return sizeof(byte); + case VertexAttributeFormat.SInt8: + return sizeof(sbyte); + case VertexAttributeFormat.UInt16: + return sizeof(ushort); + case VertexAttributeFormat.SInt16: + return sizeof(short); + case VertexAttributeFormat.UInt32: + return sizeof(uint); + case VertexAttributeFormat.SInt32: + return sizeof(int); + default: + throw new ArgumentOutOfRangeException(nameof(format), format, null); + } + } + + internal struct MeshMaterialCombination { + readonly int m_MeshId; + readonly int[] m_MaterialIds; + + public MeshMaterialCombination(int meshId, int[] materialIds) { + m_MeshId = meshId; + m_MaterialIds = materialIds; + } + + public override bool Equals(object obj) { + //Check for null and compare run-time types. + if (obj == null || ! GetType().Equals(obj.GetType())) { + return false; + } + return Equals((MeshMaterialCombination)obj); + } + + bool Equals(MeshMaterialCombination other) { + return m_MeshId == other.m_MeshId && Equals(m_MaterialIds, other.m_MaterialIds); + } + + static bool Equals(int[] a, int[] b) { + if (a == null && b == null) { + return true; + } + if (a == null ^ b == null) { + return false; + } + if (a.Length != b.Length) { + return false; + } + for (var i = 0; i < a.Length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + public override int GetHashCode() { +#if NET_4_6 + return HashCode.Combine(meshId, materialIds); +#else + var hash = 17; + hash = hash * 31 + m_MeshId.GetHashCode(); + hash = hash * 31 + m_MaterialIds.GetHashCode(); + return hash; +#endif + } + } + } + + struct AttributeData { + public int stream; + public int offset; + public int accessorId; + } +} diff --git a/Runtime/Scripts/Export/GltfWriter.cs.meta b/Runtime/Scripts/Export/GltfWriter.cs.meta new file mode 100644 index 00000000..9cd87dce --- /dev/null +++ b/Runtime/Scripts/Export/GltfWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d9076bab5d8c46be9d03094dca7c95b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/IGltfWritable.cs b/Runtime/Scripts/Export/IGltfWritable.cs new file mode 100644 index 00000000..9909dcbd --- /dev/null +++ b/Runtime/Scripts/Export/IGltfWritable.cs @@ -0,0 +1,43 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using UnityEngine; + +namespace GLTFast.Export { + public interface IGltfWritable { + + + /// + /// Registers the use of a glTF extension + /// + /// Extension's name + /// True if extension is required and used. False if it's used only + void RegisterExtensionUsage(Extension extension, bool required = true); + + /// + /// Adds a Unity Texture to the glTF and returns the resulting image index + /// + /// Unity Texture + /// glTF image index + int AddImage(Texture uTexture); + + /// + /// Creates a glTF texture from with a given image index + /// + /// glTF image index returned by + /// glTF texture index + int AddTexture(int imageId); + } +} diff --git a/Runtime/Scripts/Export/IGltfWritable.cs.meta b/Runtime/Scripts/Export/IGltfWritable.cs.meta new file mode 100644 index 00000000..ff16b204 --- /dev/null +++ b/Runtime/Scripts/Export/IGltfWritable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3023c5f428f814fa882cdf62151c871e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/StandardMaterialExport.cs b/Runtime/Scripts/Export/StandardMaterialExport.cs new file mode 100644 index 00000000..54aea5d8 --- /dev/null +++ b/Runtime/Scripts/Export/StandardMaterialExport.cs @@ -0,0 +1,372 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using GLTFast.Materials; +using UnityEngine; +using UnityEngine.Rendering; + +namespace GLTFast.Export { + + using Schema; + + public static class StandardMaterialExport { + + const string k_KeywordBumpMap = "_BUMPMAP"; + + static readonly int k_Cutoff = Shader.PropertyToID("_Cutoff"); + static readonly int k_Cull = Shader.PropertyToID("_Cull"); + static readonly int k_EmissionColor = Shader.PropertyToID("_EmissionColor"); + static readonly int k_EmissionMap = Shader.PropertyToID("_EmissionMap"); + static readonly int k_BumpMap = Shader.PropertyToID("_BumpMap"); + static readonly int k_OcclusionMap = Shader.PropertyToID("_OcclusionMap"); + static readonly int k_BaseMap = Shader.PropertyToID("_BaseMap"); + static readonly int k_ColorTexture = Shader.PropertyToID("_ColorTexture"); + static readonly int k_BaseColor = Shader.PropertyToID("_BaseColor"); + static readonly int k_MainTex = Shader.PropertyToID("_MainTex"); + static readonly int k_TintColor = Shader.PropertyToID("_TintColor"); + static readonly int k_Color = Shader.PropertyToID("_Color"); + static readonly int k_Metallic = Shader.PropertyToID("_Metallic"); + static readonly int k_MetallicGlossMap = Shader.PropertyToID("_MetallicGlossMap"); + static readonly int k_Smoothness = Shader.PropertyToID("_Smoothness"); + static readonly int k_Glossiness = Shader.PropertyToID("_Glossiness"); + static readonly int k_GlossMapScale = Shader.PropertyToID("_GlossMapScale"); + + enum TextureMapType { + Main, + Bump, + SpecGloss, + Emission, + MetallicGloss, + Light, + Occlusion + } + + /// + /// Converts a Unity material to a glTF material. + /// + /// Source material + /// Resulting material + /// Associated IGltfWriter. Is used for adding images and textures. + /// Logger used for reporting + /// True if no errors occured, false otherwise + internal static bool ConvertMaterial(UnityEngine.Material uMaterial, out Material material, IGltfWritable gltf, ICodeLogger logger ) { + var success = true; + material = new Material { + name = uMaterial.name, + pbrMetallicRoughness = new PbrMetallicRoughness { + metallicFactor = 0, + roughnessFactor = 1.0f + } + }; + + switch (uMaterial.GetTag("RenderType", false, "")) + { + case "TransparentCutout": + if (uMaterial.HasProperty(k_Cutoff)) + { + material.alphaCutoff = uMaterial.GetFloat(k_Cutoff); + } + material.alphaModeEnum = Material.AlphaMode.MASK; + break; + case "Transparent": + case "Fade": + material.alphaModeEnum = Material.AlphaMode.BLEND; + break; + default: + material.alphaModeEnum = Material.AlphaMode.OPAQUE; + break; + } + + material.doubleSided = uMaterial.HasProperty(k_Cull) && + uMaterial.GetInt(k_Cull) == (int) CullMode.Off; + + if(uMaterial.IsKeywordEnabled("_EMISSION")) { + if (uMaterial.HasProperty(k_EmissionColor)) { + material.emissive = uMaterial.GetColor(k_EmissionColor); + } + + if (uMaterial.HasProperty(k_EmissionMap)) { + // var emissionTex = uMaterial.GetTexture(k_EmissionMap); + // + // if (emissionTex != null) { + // if(emissionTex is Texture2D) { + // material.emissiveTexture = ExportTextureInfo(emissionTex, TextureMapType.Emission); + // ExportTextureTransform(material.EmissiveTexture, uMaterial, "_EmissionMap"); + // } else { + // logger?.Error(LogCode.TextureInvalidType, "emission", material.name ); + // success = false; + // } + // } + } + } + if ( + uMaterial.HasProperty(k_BumpMap) + && (uMaterial.IsKeywordEnabled( BuiltInMaterialGenerator.KW_NORMALMAP) + || uMaterial.IsKeywordEnabled(k_KeywordBumpMap)) + ) + { + var normalTex = uMaterial.GetTexture(k_BumpMap); + + if (normalTex != null) { + if(normalTex is Texture2D) { + material.normalTexture = ExportNormalTextureInfo(normalTex, TextureMapType.Bump, uMaterial, gltf); + ExportTextureTransform(material.normalTexture, uMaterial, k_BumpMap, gltf); + } else { + logger?.Error(LogCode.TextureInvalidType, "normal", uMaterial.name ); + success = false; + } + } + } + + if (uMaterial.HasProperty(k_OcclusionMap)) { + var occTex = uMaterial.GetTexture(k_OcclusionMap); + if (occTex != null) { + // if(occTex is Texture2D) { + // material.occlusionTexture = ExportOcclusionTextureInfo(occTex, TextureMapType.Occlusion, uMaterial); + // ExportTextureTransform(material.OcclusionTexture, uMaterial, "_OcclusionMap"); + // } else { + // logger?.Error(LogCode.TextureInvalidType, "occlusion", material.name ); + // success = false; + // } + } + } + if(IsUnlit(uMaterial)) { + ExportUnlit(material, uMaterial, gltf, logger); + } + else if (IsPbrMetallicRoughness(uMaterial)) + { + success &= ExportPbrMetallicRoughness(uMaterial, out material.pbrMetallicRoughness, gltf, logger); + } + else if (IsPbrSpecularGlossiness(uMaterial)) + { + // ExportPBRSpecularGlossiness(material, uMaterial); + } + else if (uMaterial.HasProperty(k_BaseMap)) + { + var mainTex = uMaterial.GetTexture(k_BaseMap); + material.pbrMetallicRoughness = new PbrMetallicRoughness { + baseColor = uMaterial.HasProperty(k_BaseColor) + ? uMaterial.GetColor(k_BaseColor) + : Color.white, + baseColorTexture = mainTex==null ? null : ExportTextureInfo( mainTex, TextureMapType.Main, gltf) + }; + } + else if (uMaterial.HasProperty(k_ColorTexture)) + { + var mainTex = uMaterial.GetTexture(k_ColorTexture); + material.pbrMetallicRoughness = new PbrMetallicRoughness { + baseColor = uMaterial.HasProperty(k_BaseColor) + ? uMaterial.GetColor(k_BaseColor) + : Color.white, + baseColorTexture = mainTex==null ? null : ExportTextureInfo(mainTex, TextureMapType.Main, gltf) + }; + } + else if (uMaterial.HasProperty(k_MainTex)) //else export main texture + { + var mainTex = uMaterial.GetTexture(k_MainTex); + + if (mainTex != null) { + material.pbrMetallicRoughness = new PbrMetallicRoughness { + metallicFactor = 0, roughnessFactor = 1.0f, + baseColorTexture = ExportTextureInfo(mainTex, TextureMapType.Main, gltf) + }; + + // ExportTextureTransform(material.pbrMetallicRoughness.baseColorTexture, uMaterial, "_MainTex"); + } + if (uMaterial.HasProperty(k_TintColor)) { + //particles use _TintColor instead of _Color + material.pbrMetallicRoughness = material.pbrMetallicRoughness ?? new PbrMetallicRoughness { metallicFactor = 0, roughnessFactor = 1.0f }; + + material.pbrMetallicRoughness.baseColor = uMaterial.GetColor(k_TintColor); + } + material.doubleSided = true; + } + return success; + } + + static bool IsUnlit(UnityEngine.Material material) { + return material.shader.name.ToLowerInvariant().Contains("unlit"); + } + + static bool IsPbrMetallicRoughness(UnityEngine.Material material) { + return material.HasProperty("_Metallic") && (material.HasProperty("_MetallicGlossMap") || material.HasProperty(k_Glossiness)); + } + + static bool IsPbrSpecularGlossiness(UnityEngine.Material material) { + return material.HasProperty("_SpecColor") && material.HasProperty("_SpecGlossMap"); + } + + static bool ExportPbrMetallicRoughness(UnityEngine.Material material, out PbrMetallicRoughness pbr, IGltfWritable gltf, ICodeLogger logger) { + var success = true; + pbr = new PbrMetallicRoughness { metallicFactor = 0, roughnessFactor = 1.0f }; + + if (material.HasProperty(k_BaseColor)) + { + pbr.baseColor = material.GetColor(k_BaseColor); + } else + if (material.HasProperty(k_Color)) { + pbr.baseColor = material.GetColor(k_Color); + } + + if (material.HasProperty(k_TintColor)) { + //particles use _TintColor instead of _Color + float white = 1; + if (material.HasProperty(k_Color)) + { + var c = material.GetColor(k_Color); + white = (c.r + c.g + c.b) / 3.0f; //multiply alpha by overall whiteness of TintColor + } + + pbr.baseColor = material.GetColor(k_TintColor) * white; + } + + if (material.HasProperty(k_MainTex) || material.HasProperty("_BaseMap")) { + // TODO if additive particle, render black into alpha + // TODO use private Material.GetFirstPropertyNameIdByAttribute here, supported from 2020.1+ + var mainTexProperty = material.HasProperty(k_BaseMap) ? k_BaseMap : k_MainTex; + var mainTex = material.GetTexture(mainTexProperty); + + if (mainTex) { + if(mainTex is Texture2D) { + pbr.baseColorTexture = ExportTextureInfo(mainTex, TextureMapType.Main, gltf); + ExportTextureTransform(pbr.baseColorTexture, material, mainTexProperty, gltf); + } else { + logger?.Error(LogCode.TextureInvalidType, "main", material.name ); + success = false; + } + } + } + + if (material.HasProperty(k_Metallic) && !material.IsKeywordEnabled("_METALLICGLOSSMAP")) { + pbr.metallicFactor = material.GetFloat(k_Metallic); + } + + if (material.HasProperty(k_Glossiness) || material.HasProperty(k_Smoothness)) { + var smoothnessPropertyName = material.HasProperty(k_Smoothness) ? k_Smoothness : k_Glossiness; + var metallicGlossMap = material.GetTexture(k_MetallicGlossMap); + float smoothness = material.GetFloat(smoothnessPropertyName); + // legacy workaround: the UnityGLTF shaders misuse k_Glossiness as roughness but don't have a keyword for it. + if (material.shader.name.Equals("GLTF/PbrMetallicRoughness", StringComparison.Ordinal)) { + smoothness = 1 - smoothness; + } + pbr.roughnessFactor = (metallicGlossMap!=null && material.HasProperty(k_GlossMapScale)) + ? material.GetFloat(k_GlossMapScale) + : 1f - smoothness; + } + + if (material.HasProperty(k_MetallicGlossMap)) { + var mrTex = material.GetTexture(k_MetallicGlossMap); + + if (mrTex != null) { + if(mrTex is Texture2D) { + // pbr.metallicRoughnessTexture = ExportTextureInfo(mrTex, TextureMapType.MetallicGloss); + // if (material.IsKeywordEnabled("_METALLICGLOSSMAP")) + // pbr.metallicFactor = 1.0f; + // ExportTextureTransform(pbr.MetallicRoughnessTexture, material, k_MetallicGlossMap); + } else { + logger?.Error(LogCode.TextureInvalidType, "metallic/gloss", material.name ); + success = false; + } + } + } + + return success; + } + + static void ExportUnlit(Material material, UnityEngine.Material uMaterial, IGltfWritable gltf, ICodeLogger logger){ + + gltf.RegisterExtensionUsage(Extension.MaterialsUnlit); + material.extensions = material.extensions ?? new MaterialExtension(); + material.extensions.KHR_materials_unlit = new MaterialUnlit(); + + var pbr = material.pbrMetallicRoughness ?? new PbrMetallicRoughness(); + + if (uMaterial.HasProperty(k_Color)) { + pbr.baseColor = uMaterial.GetColor(k_Color); + } + + if (uMaterial.HasProperty(k_MainTex)) { + var mainTex = uMaterial.GetTexture(k_MainTex); + if (mainTex != null) { + if(mainTex is Texture2D) { + pbr.baseColorTexture = ExportTextureInfo(mainTex, TextureMapType.Main, gltf); + ExportTextureTransform(pbr.baseColorTexture, uMaterial, k_MainTex, gltf); + } else { + logger?.Error(LogCode.TextureInvalidType, "main", material.name ); + } + } + } + + material.pbrMetallicRoughness = pbr; + } + + static TextureInfo ExportTextureInfo( UnityEngine.Texture texture, TextureMapType textureMapType, IGltfWritable gltf) { + var imageId = gltf.AddImage(texture); + if (imageId < 0) { + return null; + } + var textureId = gltf.AddTexture(imageId); + var info = new TextureInfo { + index = textureId, + // texCoord = 0 // TODO: figure out which UV set was used + }; + return info; + } + + static NormalTextureInfo ExportNormalTextureInfo( + UnityEngine.Texture texture, + TextureMapType textureMapType, + UnityEngine.Material material, + IGltfWritable gltf + ) + { + var imageId = gltf.AddImage(texture); + if (imageId < 0) { + return null; + } + var textureId = gltf.AddTexture(imageId); + var info = new NormalTextureInfo { + index = textureId, + // texCoord = 0 // TODO: figure out which UV set was used + }; + + if (material.HasProperty(MaterialGenerator.PROP_BUMP_SCALE)) { + info.scale = material.GetFloat(MaterialGenerator.PROP_BUMP_SCALE); + } + + return info; + } + + static void ExportTextureTransform(TextureInfo def, UnityEngine.Material mat, int texPropertyId, IGltfWritable gltf) { + var offset = mat.GetTextureOffset(texPropertyId); + var scale = mat.GetTextureScale(texPropertyId); + + // Counter measure for Unity/glTF texture coordinate difference + // TODO: Offer UV conversion as alternative + offset.y = 1 - offset.x; + scale.y *= -1; + + if (offset != Vector2.zero || scale != Vector2.one) { + gltf.RegisterExtensionUsage(Extension.TextureTransform); + def.extensions = def.extensions ?? new TextureInfoExtension(); + def.extensions.KHR_texture_transform = new TextureTransform { + scale = new[] { scale.x, scale.y }, + offset = new[] { offset.x, offset.y } + }; + } + } + } +} diff --git a/Runtime/Scripts/Export/StandardMaterialExport.cs.meta b/Runtime/Scripts/Export/StandardMaterialExport.cs.meta new file mode 100644 index 00000000..367e0546 --- /dev/null +++ b/Runtime/Scripts/Export/StandardMaterialExport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 145ccf8370f9e47348bba3fc099214f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/StreamExtension.cs b/Runtime/Scripts/Export/StreamExtension.cs new file mode 100644 index 00000000..0c284403 --- /dev/null +++ b/Runtime/Scripts/Export/StreamExtension.cs @@ -0,0 +1,44 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; +using UnityEngine; + +#if NET_STANDARD +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +#else +using System.Collections.Generic; +#endif + +namespace GLTFast.Export { + public static class StreamExtension { + +#if NET_STANDARD + public static unsafe void Write(this Stream stream, NativeArray array) { + var span = new ReadOnlySpan(array.GetUnsafeReadOnlyPtr(), array.Length); + stream.Write(span); + } +#else + public static void Write(this Stream stream, IEnumerable array) { + // TODO: Is there a faster way? + foreach (var b in array) { + stream.WriteByte(b); + } + } +#endif + } +} diff --git a/Runtime/Scripts/Export/StreamExtension.cs.meta b/Runtime/Scripts/Export/StreamExtension.cs.meta new file mode 100644 index 00000000..336ace47 --- /dev/null +++ b/Runtime/Scripts/Export/StreamExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce842f523046d45de9f138cb1ad861ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Export/glTFast.Export.asmdef b/Runtime/Scripts/Export/glTFast.Export.asmdef new file mode 100644 index 00000000..788007aa --- /dev/null +++ b/Runtime/Scripts/Export/glTFast.Export.asmdef @@ -0,0 +1,19 @@ +{ + "name": "glTFast.Export", + "rootNamespace": "", + "references": [ + "glTFast", + "glTFastSchema", + "Unity.Mathematics", + "Unity.Burst" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Scripts/Export/glTFast.Export.asmdef.meta b/Runtime/Scripts/Export/glTFast.Export.asmdef.meta new file mode 100644 index 00000000..80d5969a --- /dev/null +++ b/Runtime/Scripts/Export/glTFast.Export.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5e7ba8d6d26c84943b64dd07bd1c9510 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Extensions.cs b/Runtime/Scripts/Extensions.cs index bd6e03d1..07849384 100644 --- a/Runtime/Scripts/Extensions.cs +++ b/Runtime/Scripts/Extensions.cs @@ -14,6 +14,17 @@ // namespace GLTFast { + + public enum Extension { + DracoMeshCompression, + MaterialsPbrSpecularGlossiness, + MaterialsTransmission, + MaterialsUnlit, + MeshGPUInstancing, + MeshQuantization, + TextureBasisUniversal, + TextureTransform, + } /// /// Collection of glTF extension names @@ -27,5 +38,28 @@ public static class Extensions { public const string MeshQuantization = "KHR_mesh_quantization"; public const string TextureBasisUniversal = "KHR_texture_basisu"; public const string TextureTransform = "KHR_texture_transform"; + + public static string GetName(this Extension extension) { + switch (extension) { + case Extension.DracoMeshCompression: + return DracoMeshCompression; + case Extension.MaterialsPbrSpecularGlossiness: + return MaterialsPbrSpecularGlossiness; + case Extension.MaterialsTransmission: + return MaterialsTransmission; + case Extension.MaterialsUnlit: + return MaterialsUnlit; + case Extension.MeshGPUInstancing: + return MeshGPUInstancing; + case Extension.MeshQuantization: + return MeshQuantization; + case Extension.TextureBasisUniversal: + return TextureBasisUniversal; + case Extension.TextureTransform: + return TextureTransform; + default: + return null; + } + } } } diff --git a/Runtime/Scripts/GltfGlobals.cs b/Runtime/Scripts/GltfGlobals.cs index 0b892dc6..1088ffd7 100644 --- a/Runtime/Scripts/GltfGlobals.cs +++ b/Runtime/Scripts/GltfGlobals.cs @@ -23,9 +23,17 @@ enum ImageFormat { Jpeg, KTX } + + enum ChunkFormat : uint { + JSON = 0x4e4f534a, + BIN = 0x004e4942 + } public static class GltfGlobals { + public const string glbExt = ".glb"; + public const string gltfExt = ".gltf"; + public const uint GLB_MAGIC = 0x46546c67; // represents glTF in ASCII public static bool IsGltfBinary(byte[] data) { diff --git a/Runtime/Scripts/GltfImport.cs b/Runtime/Scripts/GltfImport.cs index f82286d9..bfff49ba 100644 --- a/Runtime/Scripts/GltfImport.cs +++ b/Runtime/Scripts/GltfImport.cs @@ -40,6 +40,7 @@ #endif [assembly: InternalsVisibleTo("glTFastEditorTests")] +[assembly: InternalsVisibleTo("glTFast.Export")] namespace GLTFast { @@ -90,12 +91,6 @@ public class GltfImport : IGltfReadable, IGltfBuffers { Extensions.MeshGPUInstancing, }; - enum ChunkFormat : uint - { - JSON = 0x4e4f534a, - BIN = 0x004e4942 - } - static IDeferAgent defaultDeferAgent; IDownloadProvider downloadProvider; diff --git a/Runtime/Scripts/Logging/CollectingLogger.cs b/Runtime/Scripts/Logging/CollectingLogger.cs index e5d54639..011bef21 100644 --- a/Runtime/Scripts/Logging/CollectingLogger.cs +++ b/Runtime/Scripts/Logging/CollectingLogger.cs @@ -22,23 +22,55 @@ namespace GLTFast { [Serializable] public class CollectingLogger : ICodeLogger { - public List items = new List(); + public List items; public void Error(LogCode code, params string[] messages) { + if (items == null) { + items = new List(); + } items.Add(new LogItem(LogType.Error, code, messages)); } public void Warning(LogCode code, params string[] messages) { + if (items == null) { + items = new List(); + } items.Add(new LogItem(LogType.Warning, code, messages)); } public void Info(LogCode code, params string[] messages) { + if (items == null) { + items = new List(); + } items.Add(new LogItem(LogType.Log, code, messages)); } + public void Error(string message) { + if (items == null) { + items = new List(); + } + items.Add(new LogItem(LogType.Error, LogCode.None, message )); + } + + public void Warning(string message) { + if (items == null) { + items = new List(); + } + items.Add(new LogItem(LogType.Warning, LogCode.None, message )); + } + + public void Info(string message) { + if (items == null) { + items = new List(); + } + items.Add(new LogItem(LogType.Log, LogCode.None, message )); + } + public void LogAll() { - foreach (var item in items) { - item.Log(); + if (items != null) { + foreach (var item in items) { + item.Log(); + } } } } diff --git a/Runtime/Scripts/Logging/ConsoleLogger.cs b/Runtime/Scripts/Logging/ConsoleLogger.cs index 1ec83432..58d69915 100644 --- a/Runtime/Scripts/Logging/ConsoleLogger.cs +++ b/Runtime/Scripts/Logging/ConsoleLogger.cs @@ -30,6 +30,18 @@ public void Warning(LogCode code, params string[] messages) { public void Info(LogCode code, params string[] messages) { Debug.Log(LogMessages.GetFullMessage(code,messages)); } + + public void Error(string message) { + Debug.Log(message); + } + + public void Warning(string message) { + Debug.Log(message); + } + + public void Info(string message) { + Debug.Log(message); + } } } diff --git a/Runtime/Scripts/Logging/ICodeLogger.cs b/Runtime/Scripts/Logging/ICodeLogger.cs index cce3c1be..22306a34 100644 --- a/Runtime/Scripts/Logging/ICodeLogger.cs +++ b/Runtime/Scripts/Logging/ICodeLogger.cs @@ -21,5 +21,9 @@ public interface ICodeLogger { void Error(LogCode code, params string[] messages); void Warning(LogCode code, params string[] messages); void Info(LogCode code, params string[] messages); + + void Error(string message); + void Warning(string message); + void Info(string message); } } diff --git a/Runtime/Scripts/Logging/LogMessages.cs b/Runtime/Scripts/Logging/LogMessages.cs index e839f384..ae092b94 100644 --- a/Runtime/Scripts/Logging/LogMessages.cs +++ b/Runtime/Scripts/Logging/LogMessages.cs @@ -20,13 +20,12 @@ using System; using System.Collections.Generic; using UnityEngine; -#if !GLTFAST_REPORT using System.Text; -#endif namespace GLTFast { public enum LogCode : uint { + None, AccessorAttributeTypeUnknown, AccessorInconsistentUsage, AccessorsShared, @@ -54,6 +53,7 @@ public enum LogCode : uint { MaterialTransmissionApprox, MaterialTransmissionApproxURP, MeshBoundsMissing, + MeshNotReadable, MissingImageURL, MorphTargetContextFail, NamingOverride, @@ -63,8 +63,10 @@ public enum LogCode : uint { SkinMissing, SparseAccessor, TextureDownloadFailed, + TextureInvalidType, TextureLoadFailed, TextureNotFound, + TopologyUnsupported, TypeUnsupported, UVMulti, } @@ -102,6 +104,7 @@ This may result in low performance and high memory usage. Try optimizing the glT { LogCode.MaterialTransmissionApproxURP, "Chance of incorrect materials! glTF transmission" + " is approximated. Enable Opaque Texture access in Universal Render Pipeline!" }, { LogCode.MeshBoundsMissing, "No bounds for mesh {0} => calculating them." }, + { LogCode.MeshNotReadable, "Skipping non-readable mesh {0}" }, { LogCode.MissingImageURL, "Image URL missing" }, { LogCode.MorphTargetContextFail, "Retrieving morph target failed" }, { LogCode.NamingOverride, "Overriding naming method to be OriginalUnique (animation requirement)" }, @@ -111,14 +114,26 @@ This may result in low performance and high memory usage. Try optimizing the glT { LogCode.SkinMissing, "Skin missing" }, { LogCode.SparseAccessor, "Sparse Accessor not supported ({0})" }, { LogCode.TextureDownloadFailed, "Download texture {1} failed: {0}" }, + { LogCode.TextureInvalidType, "Invalid {0} texture type (material: {1})" }, { LogCode.TextureLoadFailed, "Texture #{0} not loaded" }, { LogCode.TextureNotFound, "Texture #{0} not found" }, + { LogCode.TopologyUnsupported, "Unsupported topology {0}" }, { LogCode.TypeUnsupported, "Unsupported {0} type {1}" }, { LogCode.UVMulti, "UV set index {0} is not supported in current render pipeline!" }, }; #endif public static string GetFullMessage(LogCode code, params string[] messages) { + if (code == LogCode.None) { + var sb = new StringBuilder(); + foreach (var message in messages) { + if (sb.Length > 0) { + sb.Append(";"); + } + sb.Append(message); + } + return sb.ToString(); + } #if GLTFAST_REPORT return messages != null ? string.Format(fullMessages[code], messages) diff --git a/Runtime/Scripts/MaterialGenerator.cs b/Runtime/Scripts/MaterialGenerator.cs index 03b5a731..d3e136ee 100644 --- a/Runtime/Scripts/MaterialGenerator.cs +++ b/Runtime/Scripts/MaterialGenerator.cs @@ -45,6 +45,8 @@ protected enum MaterialType { public const string KW_UV_ROTATION = "_UV_ROTATION"; public const string KW_UV_CHANNEL_SELECT = "_UV_CHANNEL_SELECT"; + internal const string PROP_BUMP_SCALE = "_BumpScale"; + public static readonly int bumpMapPropId = Shader.PropertyToID("_BumpMap"); public static readonly int bumpMapRotationPropId = Shader.PropertyToID("_BumpMapRotation"); public static readonly int bumpMapScaleTransformPropId = Shader.PropertyToID("_BumpMap_ST"); diff --git a/Runtime/Scripts/Schema/Accessor.cs b/Runtime/Scripts/Schema/Accessor.cs index 0f6a8a33..7e8092cc 100644 --- a/Runtime/Scripts/Schema/Accessor.cs +++ b/Runtime/Scripts/Schema/Accessor.cs @@ -17,6 +17,9 @@ using UnityEngine; using UnityEngine.Assertions; +// GLTF_EXPORT +using UnityEngine.Rendering; + namespace GLTFast.Schema { public enum GLTFComponentType @@ -88,7 +91,9 @@ public class Accessor { [UnityEngine.SerializeField] string type; - private GLTFAccessorAttributeType _typeEnum = GLTFAccessorAttributeType.Undefined; + [NonSerialized] + GLTFAccessorAttributeType _typeEnum = GLTFAccessorAttributeType.Undefined; + public GLTFAccessorAttributeType typeEnum { get { if (_typeEnum != GLTFAccessorAttributeType.Undefined) { @@ -101,9 +106,10 @@ public GLTFAccessorAttributeType typeEnum { return GLTFAccessorAttributeType.Undefined; } } - //set { - // _typeEnum = value; - //} + set { + _typeEnum = value; + type = value.ToString(); + } } /// @@ -163,7 +169,47 @@ public static int GetComponentTypeSize( GLTFComponentType componentType ) { throw new ArgumentOutOfRangeException(nameof(type), componentType, null); } } + + public static GLTFComponentType GetComponentType(VertexAttributeFormat format) { + switch (format) { + case VertexAttributeFormat.Float32: + case VertexAttributeFormat.Float16: + return GLTFComponentType.Float; + case VertexAttributeFormat.UNorm8: + case VertexAttributeFormat.UInt8: + return GLTFComponentType.UnsignedByte; + case VertexAttributeFormat.SNorm8: + case VertexAttributeFormat.SInt8: + return GLTFComponentType.Byte; + case VertexAttributeFormat.UNorm16: + case VertexAttributeFormat.UInt16: + return GLTFComponentType.UnsignedShort; + case VertexAttributeFormat.SNorm16: + case VertexAttributeFormat.SInt16: + return GLTFComponentType.Short; + case VertexAttributeFormat.UInt32: + case VertexAttributeFormat.SInt32: + return GLTFComponentType.UnsignedInt; + default: + throw new ArgumentOutOfRangeException(nameof(format), format, null); + } + } + public static GLTFAccessorAttributeType GetAccessorAttributeType(int dimension) { + switch (dimension) { + case 1: + return GLTFAccessorAttributeType.SCALAR; + case 2: + return GLTFAccessorAttributeType.VEC2; + case 3: + return GLTFAccessorAttributeType.VEC3; + case 4: + return GLTFAccessorAttributeType.VEC4; + default: + throw new ArgumentOutOfRangeException(nameof(dimension), dimension, null); + } + } + public static int GetAccessorAttributeTypeLength( GLTFAccessorAttributeType type ) { switch (type) { @@ -201,5 +247,34 @@ public static int GetAccessorAttributeTypeLength( GLTFAccessorAttributeType type } public bool isSparse => sparse != null; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + if (bufferView >= 0) { + writer.AddProperty("bufferView", bufferView); + } + writer.AddProperty("componentType", (int)componentType); + writer.AddProperty("count", count); + writer.AddProperty("type", type); + if (byteOffset > 0) { + writer.AddProperty("byteOffset", byteOffset); + } + if (normalized) { + writer.AddProperty("normalized", normalized); + } + if (max!=null) { + writer.AddArrayProperty("max", max); + } + if (min!=null) { + writer.AddArrayProperty("min", min); + } + + if (sparse != null) { + writer.AddProperty("sparse"); + sparse.GltfSerialize(writer); + writer.Close(); + } + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/AccessorSparse.cs b/Runtime/Scripts/Schema/AccessorSparse.cs index eed3dcde..4eac0697 100644 --- a/Runtime/Scripts/Schema/AccessorSparse.cs +++ b/Runtime/Scripts/Schema/AccessorSparse.cs @@ -36,5 +36,19 @@ public class AccessorSparse { /// public AccessorSparseValues values; + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.AddProperty("count",count); + if (indices != null) { + writer.AddProperty("indices"); + indices.GltfSerialize(writer); + } + if (values != null) { + writer.AddProperty("values"); + values.GltfSerialize(writer); + } + writer.Close(); + } + } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/AccessorSparseIndices.cs b/Runtime/Scripts/Schema/AccessorSparseIndices.cs index 4350b25c..738024f2 100644 --- a/Runtime/Scripts/Schema/AccessorSparseIndices.cs +++ b/Runtime/Scripts/Schema/AccessorSparseIndices.cs @@ -36,5 +36,15 @@ public class AccessorSparseIndices { /// `5125` (UNSIGNED_INT) /// public GLTFComponentType componentType; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.AddProperty("bufferView", bufferView); + writer.AddProperty("componentType", componentType); + if (byteOffset >= 0) { + writer.AddProperty("byteOffset", byteOffset); + } + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/AccessorSparseValues.cs b/Runtime/Scripts/Schema/AccessorSparseValues.cs index 725d5daa..82893b6d 100644 --- a/Runtime/Scripts/Schema/AccessorSparseValues.cs +++ b/Runtime/Scripts/Schema/AccessorSparseValues.cs @@ -28,5 +28,14 @@ public class AccessorSparseValues { /// 0 /// public int byteOffset = 0; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.AddProperty("bufferView", bufferView); + if (byteOffset >= 0) { + writer.AddProperty("byteOffset", byteOffset); + } + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/Asset.cs b/Runtime/Scripts/Schema/Asset.cs index 9cb14985..84ac0a3e 100644 --- a/Runtime/Scripts/Schema/Asset.cs +++ b/Runtime/Scripts/Schema/Asset.cs @@ -36,5 +36,22 @@ public class Asset { /// The minimum glTF version that this asset targets. /// public string minVersion; + + public void GltfSerialize(JsonWriter writer) { + writer.OpenBrackets(); + if (!string.IsNullOrEmpty(version)) { + writer.AddProperty("version", version); + } + if (!string.IsNullOrEmpty(generator)) { + writer.AddProperty("generator", generator); + } + if (!string.IsNullOrEmpty(copyright)) { + writer.AddProperty("copyright", copyright); + } + if (!string.IsNullOrEmpty(minVersion)) { + writer.AddProperty("minVersion", minVersion); + } + writer.Close(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/Buffer.cs b/Runtime/Scripts/Schema/Buffer.cs index b91d783e..44c337a9 100644 --- a/Runtime/Scripts/Schema/Buffer.cs +++ b/Runtime/Scripts/Schema/Buffer.cs @@ -19,5 +19,14 @@ namespace GLTFast.Schema { public class Buffer { public uint byteLength; public string uri; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + if (!string.IsNullOrEmpty(uri)) { + writer.AddProperty("uri", uri); + } + writer.AddProperty("byteLength",byteLength); + writer.Close(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/BufferView.cs b/Runtime/Scripts/Schema/BufferView.cs index 6e36d599..6531d7c3 100644 --- a/Runtime/Scripts/Schema/BufferView.cs +++ b/Runtime/Scripts/Schema/BufferView.cs @@ -58,5 +58,21 @@ public class BufferView : BufferSlice { /// When this is not provided, the bufferView contains animation or skin data. /// public int target; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.AddProperty("buffer", buffer); + writer.AddProperty("byteLength", byteLength); + if (byteOffset > 0) { + writer.AddProperty("byteOffset", byteOffset); + } + if (byteStride > 0) { + writer.AddProperty("byteStride", byteStride); + } + if (target > 0) { + writer.AddProperty("target", target); + } + writer.Close(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/Constants.cs b/Runtime/Scripts/Schema/Constants.cs new file mode 100644 index 00000000..b0173077 --- /dev/null +++ b/Runtime/Scripts/Schema/Constants.cs @@ -0,0 +1,20 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace GLTFast.Schema { + static class Constants { + public const float epsilon = .001f; + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Schema/Constants.cs.meta b/Runtime/Scripts/Schema/Constants.cs.meta new file mode 100644 index 00000000..c25fce57 --- /dev/null +++ b/Runtime/Scripts/Schema/Constants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9bc735a73730843b7a877cadf120268f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Schema/GltfAnimation.cs b/Runtime/Scripts/Schema/GltfAnimation.cs index 5696dfe3..a79bfb14 100644 --- a/Runtime/Scripts/Schema/GltfAnimation.cs +++ b/Runtime/Scripts/Schema/GltfAnimation.cs @@ -34,6 +34,13 @@ public class GltfAnimation : RootChild /// interpolation algorithm to define a keyframe graph (but not its target). /// public AnimationSampler[] samplers; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } [Serializable] @@ -58,6 +65,10 @@ public enum Path { /// The index of the node and TRS property to target. /// public AnimationChannelTarget target; + + public void GltfSerialize(JsonWriter writer) { + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } [Serializable] @@ -87,6 +98,10 @@ public AnimationChannel.Path pathEnum { return m_Path; } } + + public void GltfSerialize(JsonWriter writer) { + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } public enum InterpolationType diff --git a/Runtime/Scripts/Schema/GltfCamera.cs b/Runtime/Scripts/Schema/GltfCamera.cs index 2a4ef8cc..541d43dd 100644 --- a/Runtime/Scripts/Schema/GltfCamera.cs +++ b/Runtime/Scripts/Schema/GltfCamera.cs @@ -49,6 +49,13 @@ public Type typeEnum { public CameraOrthographic orthographic; public CameraPerspective perspective; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } [Serializable] diff --git a/Runtime/Scripts/Schema/Image.cs b/Runtime/Scripts/Schema/Image.cs index 0c1f000a..d9c521b1 100644 --- a/Runtime/Scripts/Schema/Image.cs +++ b/Runtime/Scripts/Schema/Image.cs @@ -40,5 +40,20 @@ public class Image : RootChild #if KTX_UNITY public ImageExtension extensions; #endif + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + if (!string.IsNullOrEmpty(uri)) { + writer.AddProperty("uri", uri); + } + if (!string.IsNullOrEmpty(mimeType)) { + writer.AddProperty("mimeType", mimeType); + } + if (bufferView >= 0) { + writer.AddProperty("bufferView", bufferView); + } + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/JsonWriter.cs b/Runtime/Scripts/Schema/JsonWriter.cs new file mode 100644 index 00000000..aba6c793 --- /dev/null +++ b/Runtime/Scripts/Schema/JsonWriter.cs @@ -0,0 +1,131 @@ +// Copyright 2020-2021 Andreas Atteneder +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace GLTFast.Schema { + + public class JsonWriter { + + StreamWriter m_Stream; + + bool separation; + + public JsonWriter(StreamWriter stream) { + m_Stream = stream; + OpenBrackets(); + } + + public void OpenBrackets() { + m_Stream.Write('{'); + separation = false; + } + + public void AddProperty(string name) { + Separate(); + m_Stream.Write('"'); + m_Stream.Write(name); + m_Stream.Write("\":"); + separation = false; + } + + public void AddObject() { + Separate(); + m_Stream.Write('{'); + separation = false; + } + + public void AddArray(string name) { + Separate(); + m_Stream.Write('"'); + m_Stream.Write(name); + m_Stream.Write("\":["); + separation = false; + } + + public void CloseArray() { + m_Stream.Write(']'); + separation = true; + } + + public void AddArrayProperty(string name, IEnumerable values) { + AddArray(name); + foreach (var value in values) { + Separate(); + m_Stream.Write(value.ToString()); + } + CloseArray(); + } + + public void AddArrayProperty(string name, IEnumerable values) { + AddArray(name); + foreach (var value in values) { + Separate(); + m_Stream.Write(value.ToString("R")); + } + CloseArray(); + } + + public void AddArrayProperty(string name, IEnumerable values) { + AddArray(name); + foreach (var value in values) { + Separate(); + m_Stream.Write('"'); + m_Stream.Write(value); + m_Stream.Write('"'); + } + CloseArray(); + } + + public void AddProperty(string name, T value) { + Separate(); + m_Stream.Write('"'); + m_Stream.Write(name); + m_Stream.Write("\":"); + m_Stream.Write(value.ToString()); + } + + public void AddProperty(string name, string value) { + Separate(); + m_Stream.Write('"'); + m_Stream.Write(name); + m_Stream.Write("\":\""); + m_Stream.Write(value); + m_Stream.Write('"'); + } + + public void AddProperty(string name, bool value) { + Separate(); + m_Stream.Write('"'); + m_Stream.Write(name); + m_Stream.Write("\":"); + m_Stream.Write(value?"true":"false"); + } + + void Separate() { + if (separation) { + m_Stream.Write(','); + } + separation = true; + } + + public void Close() { + m_Stream.Write('}'); + separation = true; + } + } +} diff --git a/Runtime/Scripts/Schema/JsonWriter.cs.meta b/Runtime/Scripts/Schema/JsonWriter.cs.meta new file mode 100644 index 00000000..1711d880 --- /dev/null +++ b/Runtime/Scripts/Schema/JsonWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: baf6df9cf1a2e4f6da292612bb67b11b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Schema/Material.cs b/Runtime/Scripts/Schema/Material.cs index e8b57c27..38c23e3f 100644 --- a/Runtime/Scripts/Schema/Material.cs +++ b/Runtime/Scripts/Schema/Material.cs @@ -13,6 +13,7 @@ // limitations under the License. // +using Unity.Mathematics; using UnityEngine; namespace GLTFast.Schema { @@ -86,6 +87,9 @@ public Color emissive { emissiveFactor[2] ); } + set { + emissiveFactor = new[] { value.r, value.g, value.b }; + } } /// @@ -98,9 +102,10 @@ public Color emissive { /// using the normal painting operation (i.e. the Porter and Duff over operator). /// [SerializeField] - string alphaMode; + public string alphaMode; AlphaMode? _alphaModeEnum; + public AlphaMode alphaModeEnum { get { if ( _alphaModeEnum.HasValue ) { @@ -110,8 +115,14 @@ public AlphaMode alphaModeEnum { _alphaModeEnum = (AlphaMode)System.Enum.Parse (typeof(AlphaMode), alphaMode, true); alphaMode = null; return _alphaModeEnum.Value; - } else { - return AlphaMode.OPAQUE; + } + + return AlphaMode.OPAQUE; + } + set { + _alphaModeEnum = value; + if (value != AlphaMode.OPAQUE) { + alphaMode = value.ToString(); } } } @@ -134,5 +145,43 @@ public AlphaMode alphaModeEnum { public bool requiresNormals => extensions?.KHR_materials_unlit == null; public bool requiresTangents => normalTexture!=null && normalTexture.index>=0; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + if(pbrMetallicRoughness!=null) { + writer.AddProperty("pbrMetallicRoughness"); + pbrMetallicRoughness.GltfSerialize(writer); + } + if(normalTexture!=null) { + writer.AddProperty("normalTexture"); + normalTexture.GltfSerialize(writer); + } + if(occlusionTexture!=null) { + writer.AddProperty("occlusionTexture"); + occlusionTexture.GltfSerialize(writer); + } + if(emissiveTexture!=null) { + writer.AddProperty("emissiveTexture"); + emissiveTexture.GltfSerialize(writer); + } + if (emissiveFactor != null) { + writer.AddArrayProperty("emissiveFactor", emissiveFactor); + } + if (!string.IsNullOrEmpty(alphaMode)) { + writer.AddProperty("alphaMode", alphaMode); + } + if (math.abs(alphaCutoff - .5f) > Constants.epsilon) { + writer.AddProperty("alphaCutoff", alphaCutoff); + } + if (doubleSided) { + writer.AddProperty("doubleSided", doubleSided); + } + if (extensions != null) { + writer.AddProperty("extensions"); + extensions.GltfSerialize(writer); + } + writer.Close(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/MaterialClearCoat.cs b/Runtime/Scripts/Schema/MaterialClearCoat.cs index 39b02416..1c6cbbdd 100644 --- a/Runtime/Scripts/Schema/MaterialClearCoat.cs +++ b/Runtime/Scripts/Schema/MaterialClearCoat.cs @@ -25,6 +25,12 @@ public class ClearCoat { public TextureInfo clearcoatRoughnessTexture = null; public TextureInfo clearcoatNormalTexture = null; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/MaterialExtension.cs b/Runtime/Scripts/Schema/MaterialExtension.cs index d38c5cce..778bd663 100644 --- a/Runtime/Scripts/Schema/MaterialExtension.cs +++ b/Runtime/Scripts/Schema/MaterialExtension.cs @@ -22,5 +22,30 @@ public class MaterialExtension { public Transmission KHR_materials_transmission; public ClearCoat KHR_materials_clearcoat; public Sheen KHR_materials_sheen; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + if(KHR_materials_pbrSpecularGlossiness!=null) { + writer.AddProperty("KHR_materials_pbrSpecularGlossiness"); + KHR_materials_pbrSpecularGlossiness.GltfSerialize(writer); + } + if(KHR_materials_unlit!=null) { + writer.AddProperty("KHR_materials_unlit"); + KHR_materials_unlit.GltfSerialize(writer); + } + if(KHR_materials_transmission!=null) { + writer.AddProperty("KHR_materials_transmission"); + KHR_materials_transmission.GltfSerialize(writer); + } + if(KHR_materials_clearcoat!=null) { + writer.AddProperty("KHR_materials_clearcoat"); + KHR_materials_clearcoat.GltfSerialize(writer); + } + if(KHR_materials_sheen!=null) { + writer.AddProperty("KHR_materials_sheen"); + KHR_materials_sheen.GltfSerialize(writer); + } + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/MaterialPbrMetallicRoughness.cs b/Runtime/Scripts/Schema/MaterialPbrMetallicRoughness.cs index aaa7fe25..c07910bf 100644 --- a/Runtime/Scripts/Schema/MaterialPbrMetallicRoughness.cs +++ b/Runtime/Scripts/Schema/MaterialPbrMetallicRoughness.cs @@ -13,6 +13,8 @@ // limitations under the License. // +using System; +using Unity.Mathematics; using UnityEngine; namespace GLTFast.Schema { @@ -32,13 +34,15 @@ public class PbrMetallicRoughness { public float[] baseColorFactor = {1,1,1,1}; public Color baseColor { - get { - return new Color( + get => + new Color( baseColorFactor[0], baseColorFactor[1], baseColorFactor[2], baseColorFactor[3] ); + set { + baseColorFactor = new[] { value.r, value.g, value.b, value.a }; } } @@ -78,5 +82,35 @@ public Color baseColor { /// they are ignored. /// public TextureInfo metallicRoughnessTexture; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + if (baseColorFactor != null && ( + math.abs(baseColorFactor[0] - 1f) > Constants.epsilon || + math.abs(baseColorFactor[1] - 1f) > Constants.epsilon || + math.abs(baseColorFactor[2] - 1f) > Constants.epsilon || + math.abs(baseColorFactor[3] - 1f) > Constants.epsilon + )) + { + writer.AddArrayProperty("baseColorFactor", baseColorFactor); + } + + if(metallicFactor < 1f) { + writer.AddProperty("metallicFactor", metallicFactor); + } + if(roughnessFactor < 1f) { + writer.AddProperty("roughnessFactor", roughnessFactor); + } + if(baseColorTexture!=null) { + writer.AddProperty("baseColorTexture"); + baseColorTexture.GltfSerialize(writer); + } + if(metallicRoughnessTexture!=null) { + writer.AddProperty("metallicRoughnessTexture"); + metallicRoughnessTexture.GltfSerialize(writer); + } + + writer.Close(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/MaterialSheen.cs b/Runtime/Scripts/Schema/MaterialSheen.cs index 7fd74888..7f53cb4d 100644 --- a/Runtime/Scripts/Schema/MaterialSheen.cs +++ b/Runtime/Scripts/Schema/MaterialSheen.cs @@ -33,5 +33,11 @@ public class Sheen { public float sheenRoughnessFactor = 0; public TextureInfo sheenRoughnessTexture = null; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/MaterialTransmission.cs b/Runtime/Scripts/Schema/MaterialTransmission.cs index 3b36f39c..665aba68 100644 --- a/Runtime/Scripts/Schema/MaterialTransmission.cs +++ b/Runtime/Scripts/Schema/MaterialTransmission.cs @@ -21,5 +21,10 @@ public class Transmission { public float transmissionFactor = 0; public TextureInfo transmissionTexture = null; + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/MaterialUnlit.cs b/Runtime/Scripts/Schema/MaterialUnlit.cs index 83778cf1..3b72550e 100644 --- a/Runtime/Scripts/Schema/MaterialUnlit.cs +++ b/Runtime/Scripts/Schema/MaterialUnlit.cs @@ -16,5 +16,9 @@ namespace GLTFast.Schema { [System.Serializable] public class MaterialUnlit { + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/Mesh.cs b/Runtime/Scripts/Schema/Mesh.cs index 8ac2b07d..9409afdb 100644 --- a/Runtime/Scripts/Schema/Mesh.cs +++ b/Runtime/Scripts/Schema/Mesh.cs @@ -13,10 +13,12 @@ // limitations under the License. // +using System; + namespace GLTFast.Schema { [System.Serializable] - public class Mesh : RootChild { + public class Mesh : RootChild, ICloneable { /// /// An array of primitives, each defining geometry to be rendered with @@ -32,10 +34,50 @@ public class Mesh : RootChild { public float[] weights; public MeshExtras extras; + + public object Clone() { + var clone = (Mesh)MemberwiseClone(); + if (primitives != null) { + clone.primitives = new MeshPrimitive[primitives.Length]; + for (var i = 0; i < primitives.Length; i++) { + clone.primitives[i] = (MeshPrimitive) primitives[i].Clone(); + } + } + return clone; + } + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + if (primitives != null) { + writer.AddArray("primitives"); + foreach (var primitive in primitives) { + primitive.GltfSerialize(writer); + } + writer.CloseArray(); + } + + if (weights != null) { + writer.AddArrayProperty("weights", weights); + } + + if (extras != null) { + writer.AddProperty("extras"); + extras.GltfSerialize(writer); + writer.Close(); + } + writer.Close(); + } } [System.Serializable] public class MeshExtras { public string[] targetNames; + + public void GltfSerialize(JsonWriter writer) { + if (targetNames != null) { + writer.AddArrayProperty("targetNames", targetNames); + } + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/MeshGpuInstancing.cs b/Runtime/Scripts/Schema/MeshGpuInstancing.cs index b6eb7493..f4c23c04 100644 --- a/Runtime/Scripts/Schema/MeshGpuInstancing.cs +++ b/Runtime/Scripts/Schema/MeshGpuInstancing.cs @@ -28,5 +28,9 @@ public class Attributes { } public Attributes attributes; + + public void GltfSerialize(JsonWriter writer) { + throw new System.NotImplementedException(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/MeshPrimitive.cs b/Runtime/Scripts/Schema/MeshPrimitive.cs index 9a50378a..0c93c003 100644 --- a/Runtime/Scripts/Schema/MeshPrimitive.cs +++ b/Runtime/Scripts/Schema/MeshPrimitive.cs @@ -13,6 +13,8 @@ // limitations under the License. // +using System; + namespace GLTFast.Schema { public enum DrawMode @@ -27,7 +29,7 @@ public enum DrawMode } [System.Serializable] - public class MeshPrimitive { + public class MeshPrimitive : ICloneable { /// /// A dictionary object, where each key corresponds to mesh attribute semantic @@ -115,18 +117,71 @@ public override int GetHashCode() } return hash; } + + public object Clone() { + return MemberwiseClone(); + } + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + if(attributes!=null) { + writer.AddProperty("attributes"); + attributes.GltfSerialize(writer); + } + if(indices>=0) { + writer.AddProperty("indices", indices); + } + if(material>=0) { + writer.AddProperty("material", material); + } + if( mode != DrawMode.Triangles) { + writer.AddProperty("mode",mode.ToString()); + } + if(targets!=null) { + writer.AddArray("targets"); + foreach (var target in targets) { + target.GltfSerialize(writer); + } + writer.CloseArray(); + } + if(extensions!=null) { + extensions.GltfSerialize(writer); + } + writer.Close(); + } } [System.Serializable] public class Attributes { + public int POSITION = -1; + public int NORMAL = -1; + public int TANGENT = -1; + public int TEXCOORD_0 = -1; + public int TEXCOORD_1 = -1; + + public int TEXCOORD_2 = -1; + + public int TEXCOORD_3 = -1; + + public int TEXCOORD_4 = -1; + + public int TEXCOORD_5 = -1; + + public int TEXCOORD_6 = -1; + + public int TEXCOORD_7 = -1; + public int COLOR_0 = -1; + public int JOINTS_0 = -1; + public int WEIGHTS_0 = -1; + public override bool Equals(object obj) { @@ -156,6 +211,25 @@ public override int GetHashCode() hash = hash * 7 + COLOR_0.GetHashCode(); return hash; } + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + if( POSITION >= 0 ) writer.AddProperty("POSITION", POSITION); + if( NORMAL >= 0 ) writer.AddProperty("NORMAL", NORMAL); + if( TANGENT >= 0 ) writer.AddProperty("TANGENT", TANGENT); + if( TEXCOORD_0 >= 0 ) writer.AddProperty("TEXCOORD_0", TEXCOORD_0); + if( TEXCOORD_1 >= 0 ) writer.AddProperty("TEXCOORD_1", TEXCOORD_1); + if( TEXCOORD_2 >= 0 ) writer.AddProperty("TEXCOORD_2", TEXCOORD_2); + if( TEXCOORD_3 >= 0 ) writer.AddProperty("TEXCOORD_3", TEXCOORD_3); + if( TEXCOORD_4 >= 0 ) writer.AddProperty("TEXCOORD_4", TEXCOORD_4); + if( TEXCOORD_5 >= 0 ) writer.AddProperty("TEXCOORD_5", TEXCOORD_5); + if( TEXCOORD_6 >= 0 ) writer.AddProperty("TEXCOORD_6", TEXCOORD_6); + if( TEXCOORD_7 >= 0 ) writer.AddProperty("TEXCOORD_7", TEXCOORD_7); + if( COLOR_0 >= 0 ) writer.AddProperty("COLOR_0", COLOR_0); + if( JOINTS_0 >= 0 ) writer.AddProperty("JOINTS_0", JOINTS_0); + if( WEIGHTS_0 >= 0 ) writer.AddProperty("WEIGHTS_0", WEIGHTS_0); + writer.Close(); + } } [System.Serializable] @@ -163,6 +237,15 @@ public class MeshPrimitiveExtensions { #if DRACO_UNITY public MeshPrimitiveDracoExtension KHR_draco_mesh_compression; #endif + + public void GltfSerialize(JsonWriter writer) { +#if DRACO_UNITY + if (KHR_draco_mesh_compression != null) { + writer.AddProperty("KHR_draco_mesh_compression"); + KHR_draco_mesh_compression.GltfSerialize(writer); + } +#endif + } } #if DRACO_UNITY @@ -170,6 +253,10 @@ public class MeshPrimitiveExtensions { public class MeshPrimitiveDracoExtension { public int bufferView; public Attributes attributes; + + public void GltfSerialize(JsonWriter writer) { + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } #endif @@ -200,5 +287,11 @@ public override int GetHashCode() hash = hash * 7 + TANGENT.GetHashCode(); return hash; } + + public void GltfSerialize(JsonWriter writer) { + if( POSITION >= 0 ) writer.AddProperty("POSITION", POSITION); + if( NORMAL >= 0 ) writer.AddProperty("NORMAL", NORMAL); + if( TANGENT >= 0 ) writer.AddProperty("TANGENT", TANGENT); + } } } diff --git a/Runtime/Scripts/Schema/Node.cs b/Runtime/Scripts/Schema/Node.cs index 43fef5cf..48265e90 100644 --- a/Runtime/Scripts/Schema/Node.cs +++ b/Runtime/Scripts/Schema/Node.cs @@ -65,6 +65,48 @@ public class Node : RootChild { public int camera = -1; public NodeExtensions extensions; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + + if (children != null) { + writer.AddArrayProperty("children", children); + } + + if (mesh >= 0) { + writer.AddProperty("mesh", mesh); + } + + if (translation!=null) { + writer.AddArrayProperty("translation", translation); + } + + if (rotation!=null) { + writer.AddArrayProperty("rotation", rotation); + } + + if (scale!=null) { + writer.AddArrayProperty("scale", scale); + } + + if (matrix!=null) { + writer.AddArrayProperty("matrix", matrix); + } + + if (skin >= 0) { + writer.AddProperty("skin", skin); + } + + if (camera >= 0) { + writer.AddProperty("camera", skin); + } + + if (extensions != null) { + extensions.GltfSerialize(writer); + } + writer.Close(); + } } [System.Serializable] @@ -73,5 +115,12 @@ public class NodeExtensions { // Whenever an extension is added, the JsonParser // (specifically step four of JsonParser.ParseJson) // needs to be updated! + + public void GltfSerialize(JsonWriter writer) { + if (EXT_mesh_gpu_instancing != null) { + writer.AddProperty("EXT_mesh_gpu_instancing"); + EXT_mesh_gpu_instancing.GltfSerialize(writer); + } + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/NormalTextureInfo.cs b/Runtime/Scripts/Schema/NormalTextureInfo.cs index c1ecc5c7..e78d86f0 100644 --- a/Runtime/Scripts/Schema/NormalTextureInfo.cs +++ b/Runtime/Scripts/Schema/NormalTextureInfo.cs @@ -13,6 +13,8 @@ // limitations under the License. // +using Unity.Mathematics; + namespace GLTFast.Schema{ [System.Serializable] public class NormalTextureInfo : TextureInfo { @@ -23,5 +25,14 @@ public class NormalTextureInfo : TextureInfo { /// This value is linear. /// public float scale = 1.0f; + + public override void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeTextureInfo(writer); + if (math.abs(scale - 1f) > Constants.epsilon) { + writer.AddProperty("scale", scale); + } + writer.Close(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/OcclusionTextureInfo.cs b/Runtime/Scripts/Schema/OcclusionTextureInfo.cs index 20f02a1b..575b2dca 100644 --- a/Runtime/Scripts/Schema/OcclusionTextureInfo.cs +++ b/Runtime/Scripts/Schema/OcclusionTextureInfo.cs @@ -27,5 +27,12 @@ public class OcclusionTextureInfo : TextureInfo { /// 1.0 /// public float strength = 1.0f; + + public override void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeTextureInfo(writer); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/PbrSpecularGlossiness.cs b/Runtime/Scripts/Schema/PbrSpecularGlossiness.cs index 8235d423..7b70a8eb 100644 --- a/Runtime/Scripts/Schema/PbrSpecularGlossiness.cs +++ b/Runtime/Scripts/Schema/PbrSpecularGlossiness.cs @@ -53,6 +53,12 @@ public Color specularColor { public float glossinessFactor = 1; public TextureInfo specularGlossinessTexture = null; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } } diff --git a/Runtime/Scripts/Schema/Root.cs b/Runtime/Scripts/Schema/Root.cs index 3c47b798..d8bcd3d6 100644 --- a/Runtime/Scripts/Schema/Root.cs +++ b/Runtime/Scripts/Schema/Root.cs @@ -13,6 +13,7 @@ // limitations under the License. // +using System.IO; using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("glTFastEditorTests")] @@ -121,5 +122,124 @@ public bool IsAccessorInterleaved( int accessorIndex ) { var elementSize = Accessor.GetAccessorAttributeTypeLength(accessor.typeEnum) * Accessor.GetComponentTypeSize(accessor.componentType); return bufferView.byteStride > elementSize; } + + public void GltfSerialize(StreamWriter stream) { + var writer = new JsonWriter(stream); + + if (asset != null) { + writer.AddProperty("asset"); + asset.GltfSerialize(writer); + } + if (nodes != null) { + writer.AddArray("nodes"); + foreach (var node in nodes) { + node.GltfSerialize(writer); + } + writer.CloseArray(); + } + + if (extensionsRequired != null) { + writer.AddArrayProperty("extensionsRequired", extensionsRequired); + } + + if (extensionsUsed != null) { + writer.AddArrayProperty("extensionsUsed", extensionsUsed); + } + + if (animations!=null) { + writer.AddArray("animations"); + foreach( var animation in animations) { + animation.GltfSerialize(writer); + } + writer.CloseArray(); + } + + if (buffers!=null) { + writer.AddArray("buffers"); + foreach( var buffer in buffers) { + buffer.GltfSerialize(writer); + } + writer.CloseArray(); + } + + if (bufferViews!=null) { + writer.AddArray("bufferViews"); + foreach( var bufferView in bufferViews) { + bufferView.GltfSerialize(writer); + } + writer.CloseArray(); + } + + if (accessors!=null) { + writer.AddArray("accessors"); + foreach( var accessor in accessors) { + accessor.GltfSerialize(writer); + } + writer.CloseArray(); + } + + if (cameras!=null) { + writer.AddArray("cameras"); + foreach( var camera in cameras) { + camera.GltfSerialize(writer); + } + writer.CloseArray(); + } + + if (images!=null) { + writer.AddArray("images"); + foreach( var image in images) { + image.GltfSerialize(writer); + } + writer.CloseArray(); + } + if (materials!=null) { + writer.AddArray("materials"); + foreach( var material in materials) { + material.GltfSerialize(writer); + } + writer.CloseArray(); + } + if (meshes!=null) { + writer.AddArray("meshes"); + foreach( var mesh in meshes) { + mesh.GltfSerialize(writer); + } + writer.CloseArray(); + } + if (samplers!=null) { + writer.AddArray("samplers"); + foreach( var sampler in samplers) { + sampler.GltfSerialize(writer); + } + writer.CloseArray(); + } + if (scene>=0) { + writer.AddProperty("scene",scene); + } + if (scenes!=null) { + writer.AddArray("scenes"); + foreach( var scene in scenes) { + scene.GltfSerialize(writer); + } + writer.CloseArray(); + } + if (skins!=null) { + writer.AddArray("skins"); + foreach( var skin in skins) { + skin.GltfSerialize(writer); + } + writer.CloseArray(); + } + if (textures!=null) { + writer.AddArray("textures"); + foreach( var texture in textures) { + texture.GltfSerialize(writer); + } + writer.CloseArray(); + } + + writer.Close(); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/RootChild.cs b/Runtime/Scripts/Schema/RootChild.cs index 0d357c86..3ccfb427 100644 --- a/Runtime/Scripts/Schema/RootChild.cs +++ b/Runtime/Scripts/Schema/RootChild.cs @@ -18,5 +18,11 @@ namespace GLTFast.Schema { [System.Serializable] public class RootChild { public string name; + + protected void GltfSerializeRoot(JsonWriter writer) { + if (!string.IsNullOrEmpty(name)) { + writer.AddProperty("name", name); + } + } } } diff --git a/Runtime/Scripts/Schema/Sampler.cs b/Runtime/Scripts/Schema/Sampler.cs index b6070a48..be502810 100644 --- a/Runtime/Scripts/Schema/Sampler.cs +++ b/Runtime/Scripts/Schema/Sampler.cs @@ -135,5 +135,12 @@ public void Apply(Texture2D image, magFilter == MagFilterMode.None ? defaultMagFilter : magFilter ); } + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } } \ No newline at end of file diff --git a/Runtime/Scripts/Schema/Scene.cs b/Runtime/Scripts/Schema/Scene.cs index b465852c..27a47721 100644 --- a/Runtime/Scripts/Schema/Scene.cs +++ b/Runtime/Scripts/Schema/Scene.cs @@ -18,5 +18,12 @@ namespace GLTFast.Schema { [System.Serializable] public class Scene : RootChild { public uint[] nodes; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + writer.AddArrayProperty("nodes",nodes); + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/Skin.cs b/Runtime/Scripts/Schema/Skin.cs index a4844543..80d1c18c 100644 --- a/Runtime/Scripts/Schema/Skin.cs +++ b/Runtime/Scripts/Schema/Skin.cs @@ -20,5 +20,12 @@ public class Skin : RootChild { public int inverseBindMatrices; public int skeleton = -1; public uint[] joints; + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + writer.Close(); + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } } diff --git a/Runtime/Scripts/Schema/Texture.cs b/Runtime/Scripts/Schema/Texture.cs index aa3b4071..d55fed87 100644 --- a/Runtime/Scripts/Schema/Texture.cs +++ b/Runtime/Scripts/Schema/Texture.cs @@ -16,7 +16,7 @@ namespace GLTFast.Schema { [System.Serializable] - public class Texture { + public class Texture : RootChild { /// /// The index of the sampler used by this texture. /// @@ -43,5 +43,21 @@ public bool isKtx { return extensions!=null && extensions.KHR_texture_basisu!=null; } } + + public void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeRoot(writer); + if (source >= 0) { + writer.AddProperty("source", source); + } + if (sampler >= 0) { + writer.AddProperty("sampler", sampler); + } + if (extensions!=null) { + writer.AddProperty("extensions"); + extensions.GltfSerialize(writer); + } + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/TextureExtension.cs b/Runtime/Scripts/Schema/TextureExtension.cs index 16c6f69b..1ad4ab2f 100644 --- a/Runtime/Scripts/Schema/TextureExtension.cs +++ b/Runtime/Scripts/Schema/TextureExtension.cs @@ -18,6 +18,10 @@ namespace GLTFast.Schema { [System.Serializable] public class TextureExtension { public TextureFormat KHR_texture_basisu = null; + + public void GltfSerialize(JsonWriter writer) { + throw new System.NotImplementedException($"GltfSerialize missing on {GetType()}"); + } } [System.Serializable] diff --git a/Runtime/Scripts/Schema/TextureInfo.cs b/Runtime/Scripts/Schema/TextureInfo.cs index b88a75a3..c9c37c3c 100644 --- a/Runtime/Scripts/Schema/TextureInfo.cs +++ b/Runtime/Scripts/Schema/TextureInfo.cs @@ -31,5 +31,25 @@ public class TextureInfo { public int texCoord = 0; public TextureInfoExtension extensions; + + protected void GltfSerializeTextureInfo(JsonWriter writer) { + if (index >= 0) { + writer.AddProperty("index", index); + } + if (texCoord > 0) { + writer.AddProperty("texCoord", texCoord); + } + + if (extensions != null) { + writer.AddProperty("extensions"); + extensions.GltfSerialize(writer); + } + } + + public virtual void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + GltfSerializeTextureInfo(writer); + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/TextureInfoExtension.cs b/Runtime/Scripts/Schema/TextureInfoExtension.cs index 29ee0dae..0e1ecf7a 100644 --- a/Runtime/Scripts/Schema/TextureInfoExtension.cs +++ b/Runtime/Scripts/Schema/TextureInfoExtension.cs @@ -17,5 +17,14 @@ namespace GLTFast.Schema { [System.Serializable] public class TextureInfoExtension { public TextureTransform KHR_texture_transform; + + public virtual void GltfSerialize(JsonWriter writer) { + if(KHR_texture_transform != null) { + writer.AddObject(); + writer.AddProperty("KHR_texture_transform"); + KHR_texture_transform.GltfSerialize(writer); + writer.Close(); + } + } } } diff --git a/Runtime/Scripts/Schema/TextureTransform.cs b/Runtime/Scripts/Schema/TextureTransform.cs index 78ba275b..922fb432 100644 --- a/Runtime/Scripts/Schema/TextureTransform.cs +++ b/Runtime/Scripts/Schema/TextureTransform.cs @@ -37,5 +37,22 @@ public class TextureTransform { /// Overrides the textureInfo texCoord value if supplied, and if this extension is supported. /// public int texCoord = -1; + + public virtual void GltfSerialize(JsonWriter writer) { + writer.AddObject(); + if (offset != null) { + writer.AddArrayProperty("offset", offset); + } + if (scale != null) { + writer.AddArrayProperty("scale", scale); + } + if(rotation != 0) { + writer.AddProperty("rotation", rotation); + } + if(texCoord >= 0) { + writer.AddProperty("texCoord", texCoord); + } + writer.Close(); + } } } diff --git a/Runtime/Scripts/Schema/glTFastSchema.asmdef b/Runtime/Scripts/Schema/glTFastSchema.asmdef index 5694a682..ce3e2cfa 100644 --- a/Runtime/Scripts/Schema/glTFastSchema.asmdef +++ b/Runtime/Scripts/Schema/glTFastSchema.asmdef @@ -1,6 +1,9 @@ { "name": "glTFastSchema", - "references": [], + "rootNamespace": "", + "references": [ + "Unity.Mathematics" + ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, diff --git a/Runtime/Scripts/UriHelper.cs b/Runtime/Scripts/UriHelper.cs index 2b534dec..a2dbcad1 100644 --- a/Runtime/Scripts/UriHelper.cs +++ b/Runtime/Scripts/UriHelper.cs @@ -21,11 +21,8 @@ namespace GLTFast { - public static class UriHelper - { - const string GLB_EXT = ".glb"; - const string GLTF_EXT = ".gltf"; - + public static class UriHelper { + public static Uri GetBaseUri( Uri uri ) { if(uri==null) return null; if (!uri.IsAbsoluteUri) { @@ -125,10 +122,10 @@ public static string RemoveDotSegments(string uri, out int parentLevels) { string path = uri.IsAbsoluteUri ? uri.LocalPath : uri.OriginalString; var index = path.LastIndexOf('.',path.Length-1, Mathf.Min(5,path.Length) ); if(index<0) return null; - if(path.EndsWith(GLB_EXT, StringComparison.OrdinalIgnoreCase)) { + if(path.EndsWith(GltfGlobals.glbExt, StringComparison.OrdinalIgnoreCase)) { return true; } - if(path.EndsWith(GLTF_EXT, StringComparison.OrdinalIgnoreCase)) { + if(path.EndsWith(GltfGlobals.gltfExt, StringComparison.OrdinalIgnoreCase)) { return false; } return null; @@ -157,15 +154,15 @@ internal static ImageFormat GetImageFormatFromUri(string uri) { /// Downside: less convenient // public static bool? IsGltfBinary( string uri ) { // // quick glTF-binary check - // if (uri.EndsWith(GLB_EXT, StringComparison.OrdinalIgnoreCase)) return true; - // if (uri.EndsWith(GLTF_EXT, StringComparison.OrdinalIgnoreCase)) return false; + // if (uri.EndsWith(GltfGlobals.glbExt, StringComparison.OrdinalIgnoreCase)) return true; + // if (uri.EndsWith(GltfGlobals.gltfExt, StringComparison.OrdinalIgnoreCase)) return false; // // thourough glTF-binary extension check that strips HTTP GET parameters // int getIndex = uri.LastIndexOf('?'); // if (getIndex >= 0) { - // var ext = uri.Substring(getIndex - GLTF_EXT.Length, GLTF_EXT.Length); - // if(ext.EndsWith(GLB_EXT, StringComparison.OrdinalIgnoreCase)) return true; - // if(ext.EndsWith(GLTF_EXT, StringComparison.OrdinalIgnoreCase)) return false; + // var ext = uri.Substring(getIndex - GltfGlobals.gltfExt.Length, GltfGlobals.gltfExt.Length); + // if(ext.EndsWith(GltfGlobals.glbExt, StringComparison.OrdinalIgnoreCase)) return true; + // if(ext.EndsWith(GltfGlobals.gltfExt, StringComparison.OrdinalIgnoreCase)) return false; // } // return null; // } diff --git a/Tests/Editor/LoggerTest.cs b/Tests/Editor/LoggerTest.cs index 082acd22..2e00476c 100644 --- a/Tests/Editor/LoggerTest.cs +++ b/Tests/Editor/LoggerTest.cs @@ -27,6 +27,7 @@ public static void CollectingLoggerTest() { var r = new CollectingLogger(); r.Error(LogCode.Download,"404", "https://something.com/nowherfound.glb"); + Assert.NotNull(r.items); Assert.AreEqual(1,r.items.Count); Assert.AreEqual("Download URL https://something.com/nowherfound.glb failed: 404", r.items[0].ToString()); } diff --git a/package.json b/package.json index 47f2148f..b8ff4c26 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "com.atteneder.gltfast", - "version": "4.3.4", + "version": "4.4.0", "displayName": "glTFast", "description": "Load glTF 3D files fast at runtime or import them into the asset database in the Editor", - "unity": "2019.3", + "unity": "2019.4", "keywords": [ "mesh", "gltf", @@ -27,5 +27,6 @@ "com.unity.mathematics": "1.2.1", "com.unity.burst": "1.4.11" }, - "type": "library" + "type": "library", + "unityRelease": "7f1" } \ No newline at end of file