From ecd4dad0d65f7f489875013781df3643438ec3b6 Mon Sep 17 00:00:00 2001 From: harfang3dadmin Date: Wed, 15 Dec 2021 20:23:12 +0100 Subject: [PATCH] 3.1.0 release. --- CMakeLists.txt | 2 +- LICENSE | 13 +- binding/bind_harfang.py | 52 +- doc/CMakeLists.txt | 63 +- doc/doc/Camera.md | 4 +- doc/doc/DuplicateNodeAndChildrenFromAssets.md | 4 +- doc/doc/DuplicateNodeAndChildrenFromFile.md | 4 +- doc/doc/DuplicateNodeFromAssets.md | 4 +- doc/doc/DuplicateNodeFromFile.md | 4 +- doc/doc/Frame.md | 6 +- doc/doc/FromHLS.md | 1 + doc/doc/Frustum.md | 2 +- doc/doc/GetClock.md | 4 +- doc/doc/GetColorTexture.md | 1 + doc/doc/GetDepthTexture.md | 1 + doc/doc/GetTextures.md | 1 + doc/doc/ImGuiBeginMainMenuBar.md | 4 +- doc/doc/ImGuiBeginMenu.md | 4 +- doc/doc/ImGuiBeginMenuBar.md | 4 +- doc/doc/ImGuiBeginPopup.md | 4 +- doc/doc/ImGuiCalcItemWidth.md | 4 +- doc/doc/ImGuiCheckbox.md | 6 +- doc/doc/ImGuiColumns.md | 8 +- doc/doc/ImGuiIsItemActive.md | 4 +- doc/doc/ImGuiOpenPopup.md | 16 +- doc/doc/ImGuiSelectable.md | 20 +- doc/doc/ImGuiTextWrapped.md | 4 +- doc/doc/InputInit.md | 10 +- doc/doc/MakeFrustum.md | 14 +- doc/doc/Node_SetCamera.md | 2 +- doc/doc/Node_SetLight.md | 4 +- doc/doc/Node_SetLuaScript.md | 4 +- doc/doc/Node_SetObject.md | 4 +- doc/doc/Node_SetTransform.md | 4 +- doc/doc/Open.md | 4 +- doc/doc/OpenText.md | 4 +- doc/doc/OpenVRStateToViewState.md | 4 +- doc/doc/OpenVRSubmitFrame.md | 4 +- doc/doc/OpenWrite.md | 4 +- doc/doc/OpenWriteText.md | 4 +- doc/doc/RaycastOut.md | 10 +- ...ewtonPhysics.md => SceneBullet3Physics.md} | 0 doc/doc/Scene_GetNodeEx.md | 44 +- doc/doc/SetSaturation.md | 3 + doc/doc/TickClock.md | 2 +- doc/doc/ToHLS.md | 1 + doc/doc/Touch.md | 4 +- doc/doc/man.AllFunctionIndex.md | 3 - .../man.AnApplicationUsingTheRenderSystem.md | 76 - doc/doc/man.AnApplicationUsingTheRenderer.md | 159 - doc/doc/man.Architecture.md | 1 - doc/doc/man.AssembleOverview.md | 5 - doc/doc/man.AssetCompiler.md | 92 +- doc/doc/man.Assets.md | 6 +- doc/doc/man.CPython.md | 14 + doc/doc/man.Classes.md | 3 - doc/doc/man.Component.md | 5 - doc/doc/man.Constants.md | 5 - doc/doc/man.CoreRuntimeResources.md | 3 - doc/doc/man.Dearimgui.md | 41 - doc/doc/man.Debugging.md | 13 - doc/doc/man.DrawingGraphicPrimitives.md | 76 - doc/doc/man.EngineDebugger.md | 24 - doc/doc/man.Enums.md | 5 - doc/doc/man.Examples.md | 7 - doc/doc/man.ExtendingTheEditor.md | 4 - doc/doc/man.ExtendingTheProjectExplorer.md | 14 - doc/doc/man.ExtendingTheScenePlugin.md | 24 - doc/doc/man.FeatureList.md | 51 - doc/doc/man.Functions.md | 5 - doc/doc/man.GeneratedTexturedCube.md | 152 - doc/doc/man.Installation.md | 73 - doc/doc/man.Overview.md | 38 +- doc/doc/man.Physics.md | 14 +- doc/doc/man.PostProcessing.md | 140 - doc/doc/man.Requirements.md | 34 +- doc/doc/man.Scene.md | 96 +- doc/doc/man.Scripting.md | 168 +- doc/doc/man.Shader.md | 198 +- doc/doc/man.Toolchain.md | 1 - doc/doc/man.TutorialFilesystemAssetsFolder.md | 15 - ...rialFilesystemRecursiveDirectoryListing.md | 15 - doc/doc/man.TutorialGUIBasicImGui.md | 15 - ...man.TutorialImmediateRenderingBasicLoop.md | 15 - ...an.TutorialImmediateRenderingNoPipeline.md | 15 - doc/doc/man.TutorialInputListDevices.md | 15 - doc/doc/man.TutorialInputPad.md | 15 - doc/doc/man.TutorialInputPad2.md | 15 - doc/doc/man.TutorialInputReadKeyboard.md | 15 - doc/doc/man.TutorialInputReadKeyboard2.md | 15 - doc/doc/man.TutorialInputReadKeyboard3.md | 15 - doc/doc/man.TutorialInputReadMouse.md | 15 - doc/doc/man.TutorialInputReadMouse2.md | 15 - doc/doc/man.TutorialInputReadMouse3.md | 15 - doc/doc/man.TutorialPhysicsCubes.md | 15 - doc/doc/man.TutorialPhysicsImpulse.md | 15 - doc/doc/man.TutorialPhysicsOverridesMatrix.md | 15 - doc/doc/man.TutorialPictureLoad.md | 15 - doc/doc/man.TutorialPictureSave.md | 15 - doc/doc/man.TutorialSceneDynamicsObjects.md | 15 - doc/doc/man.TutorialSceneKaplaShooting.md | 15 - doc/doc/man.TutorialSceneKaplaTower.md | 15 - doc/doc/man.TutorialSceneLightPriority.md | 15 - doc/doc/man.TutorialSceneMultiViewport.md | 15 - doc/doc/man.TutorialSceneWithPipeline.md | 15 - doc/doc/man.Tutorials.md | 5 - doc/doc/man.VR.md | 142 +- doc/doc/man.WritingAGraphicApplication.md | 46 - doc/doc/tree_desc.txt | 7 - doc/doc_editor.py | 34 - doc/doc_to_html.bat | 1 - doc/doc_to_html.py | 502 - doc/doc_to_hugo.py | 8 +- doc/img/assetc.gif | Bin 296476 -> 207207 bytes doc/search_index.py | 169 - extern/tiny-process-library/process_win.cpp | 2 +- harfang/CMakeLists.txt | 8 +- harfang/engine/aaa_blur.cpp | 9 + harfang/engine/aaa_blur.h | 2 + harfang/engine/bloom.cpp | 21 +- harfang/engine/bloom.h | 2 + harfang/engine/component.cpp | 13 + harfang/engine/dear_imgui.cpp | 3 +- harfang/engine/dear_imgui.h | 5 + harfang/engine/downsample.cpp | 33 +- harfang/engine/downsample.h | 2 + harfang/engine/hiz.cpp | 16 +- harfang/engine/hiz.h | 3 +- harfang/engine/iso_surface.cpp | 2 +- harfang/engine/meta.cpp | 4 +- harfang/engine/motion_blur.cpp | 10 + harfang/engine/motion_blur.h | 2 + harfang/engine/node.h | 6 +- harfang/engine/openvr_api.cpp | 4 +- harfang/engine/picture.cpp | 4 +- harfang/engine/recast_detour.cpp | 18 +- harfang/engine/render_pipeline.cpp | 86 +- harfang/engine/render_pipeline.h | 24 +- harfang/engine/sao.cpp | 19 +- harfang/engine/sao.h | 2 + harfang/engine/scene.cpp | 37 +- harfang/engine/scene.h | 14 +- harfang/engine/scene_bullet3_physics.cpp | 165 +- harfang/engine/scene_bullet3_physics.h | 24 +- harfang/engine/scene_forward_pipeline.cpp | 136 +- harfang/engine/scene_forward_pipeline.h | 3 + harfang/engine/scene_load_json.cpp | 2 + harfang/engine/scene_systems.cpp | 2 +- harfang/engine/ssgi.cpp | 11 +- harfang/engine/ssgi.h | 2 + harfang/engine/ssr.cpp | 11 +- harfang/engine/ssr.h | 2 + harfang/engine/taa.cpp | 7 +- harfang/engine/taa.h | 2 + harfang/engine/temporal_accumulation.cpp | 10 + harfang/engine/temporal_accumulation.h | 2 + harfang/engine/upsample.cpp | 11 +- harfang/engine/upsample.h | 2 + harfang/foundation/CMakeLists.txt | 2 + harfang/foundation/color.cpp | 143 +- harfang/foundation/color.h | 11 + harfang/foundation/file.cpp | 11 + harfang/foundation/file.h | 1 + harfang/foundation/log_file.cpp | 38 + harfang/foundation/log_file.h | 12 + harfang/foundation/math.h | 4 +- harfang/foundation/minmax.h | 2 +- harfang/foundation/obb.cpp | 2 +- harfang/foundation/profiler.cpp | 12 +- harfang/foundation/qmc.h | 2 +- harfang/foundation/string.cpp | 12 +- harfang/foundation/string.h | 3 +- harfang/foundation/time.h | 4 + harfang/foundation/time_to_string.cpp | 8 +- harfang/foundation/vector2.cpp | 3 +- harfang/foundation/vector_list.h | 8 +- harfang/foundation/version.cpp | 1 + harfang/platform/filesystem_watcher.cpp | 10 +- harfang/platform/glfw/input_system_glfw.cpp | 6 +- harfang/platform/glfw/window_system.cpp | 27 + harfang/platform/input_system.h | 4 +- harfang/platform/window_system.h | 4 + harfang/tests/t_dir.cpp | 2 +- harfang/version.txt | 2 +- .../hg_python/bdist_wheel/DESCRIPTION.rst | 9 +- readme.md | 3 +- release-notes.md | 110 + requirements.txt | 11 +- tools/assetc/assetc.cpp | 247 +- tools/assimp_converter/assimp_converter.cpp | 20 +- tools/gltf_converter/CMakeLists.txt | 76 +- tools/gltf_converter/gltf_exporter.cpp | 51 +- tools/gltf_converter/gltf_importer.cpp | 119 +- tools/gltf_converter/tiny_gltf.h | 15276 ++++++++-------- 194 files changed, 9517 insertions(+), 10695 deletions(-) create mode 100644 doc/doc/FromHLS.md create mode 100644 doc/doc/GetColorTexture.md create mode 100644 doc/doc/GetDepthTexture.md create mode 100644 doc/doc/GetTextures.md rename doc/doc/{SceneNewtonPhysics.md => SceneBullet3Physics.md} (100%) create mode 100644 doc/doc/SetSaturation.md create mode 100644 doc/doc/ToHLS.md delete mode 100644 doc/doc/man.AllFunctionIndex.md delete mode 100644 doc/doc/man.AnApplicationUsingTheRenderSystem.md delete mode 100644 doc/doc/man.AnApplicationUsingTheRenderer.md delete mode 100644 doc/doc/man.Architecture.md delete mode 100644 doc/doc/man.AssembleOverview.md delete mode 100644 doc/doc/man.Classes.md delete mode 100644 doc/doc/man.Component.md delete mode 100644 doc/doc/man.Constants.md delete mode 100644 doc/doc/man.CoreRuntimeResources.md delete mode 100644 doc/doc/man.Dearimgui.md delete mode 100644 doc/doc/man.Debugging.md delete mode 100644 doc/doc/man.DrawingGraphicPrimitives.md delete mode 100644 doc/doc/man.EngineDebugger.md delete mode 100644 doc/doc/man.Enums.md delete mode 100644 doc/doc/man.Examples.md delete mode 100644 doc/doc/man.ExtendingTheEditor.md delete mode 100644 doc/doc/man.ExtendingTheProjectExplorer.md delete mode 100644 doc/doc/man.ExtendingTheScenePlugin.md delete mode 100644 doc/doc/man.FeatureList.md delete mode 100644 doc/doc/man.Functions.md delete mode 100644 doc/doc/man.GeneratedTexturedCube.md delete mode 100644 doc/doc/man.Installation.md delete mode 100644 doc/doc/man.PostProcessing.md delete mode 100644 doc/doc/man.Toolchain.md delete mode 100644 doc/doc/man.TutorialFilesystemAssetsFolder.md delete mode 100644 doc/doc/man.TutorialFilesystemRecursiveDirectoryListing.md delete mode 100644 doc/doc/man.TutorialGUIBasicImGui.md delete mode 100644 doc/doc/man.TutorialImmediateRenderingBasicLoop.md delete mode 100644 doc/doc/man.TutorialImmediateRenderingNoPipeline.md delete mode 100644 doc/doc/man.TutorialInputListDevices.md delete mode 100644 doc/doc/man.TutorialInputPad.md delete mode 100644 doc/doc/man.TutorialInputPad2.md delete mode 100644 doc/doc/man.TutorialInputReadKeyboard.md delete mode 100644 doc/doc/man.TutorialInputReadKeyboard2.md delete mode 100644 doc/doc/man.TutorialInputReadKeyboard3.md delete mode 100644 doc/doc/man.TutorialInputReadMouse.md delete mode 100644 doc/doc/man.TutorialInputReadMouse2.md delete mode 100644 doc/doc/man.TutorialInputReadMouse3.md delete mode 100644 doc/doc/man.TutorialPhysicsCubes.md delete mode 100644 doc/doc/man.TutorialPhysicsImpulse.md delete mode 100644 doc/doc/man.TutorialPhysicsOverridesMatrix.md delete mode 100644 doc/doc/man.TutorialPictureLoad.md delete mode 100644 doc/doc/man.TutorialPictureSave.md delete mode 100644 doc/doc/man.TutorialSceneDynamicsObjects.md delete mode 100644 doc/doc/man.TutorialSceneKaplaShooting.md delete mode 100644 doc/doc/man.TutorialSceneKaplaTower.md delete mode 100644 doc/doc/man.TutorialSceneLightPriority.md delete mode 100644 doc/doc/man.TutorialSceneMultiViewport.md delete mode 100644 doc/doc/man.TutorialSceneWithPipeline.md delete mode 100644 doc/doc/man.Tutorials.md delete mode 100644 doc/doc/man.WritingAGraphicApplication.md delete mode 100644 doc/doc_editor.py delete mode 100644 doc/doc_to_html.bat delete mode 100644 doc/doc_to_html.py delete mode 100644 doc/search_index.py create mode 100644 harfang/foundation/log_file.cpp create mode 100644 harfang/foundation/log_file.h create mode 100644 release-notes.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 509a63c..ea2784d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,7 +293,7 @@ if(HG_BINDING_DEFINES_LIST) set(HG_BINDING_DEFINES "--defines" ${HG_BINDING_DEFINES_LIST}) endif() -if( HG_BUILD_CPP_SDK OR HG_BUILD_ASSIMP_CONVERTER OR HG_BUILD_FBX_CONVERTER OR HG_BUILD_GLTF_EXPORTER OR HG_BUILD_GLTF_IMPORTER OR HG_BUILD_ASSETC ) +if(HG_BUILD_HG_LUA OR HG_BUILD_CPP_SDK OR HG_BUILD_ASSIMP_CONVERTER OR HG_BUILD_FBX_CONVERTER OR HG_BUILD_GLTF_EXPORTER OR HG_BUILD_GLTF_IMPORTER OR HG_BUILD_ASSETC) add_subdirectory(extern) add_subdirectory(binding) endif() diff --git a/LICENSE b/LICENSE index 2ec3273..3c44432 100644 --- a/LICENSE +++ b/LICENSE @@ -1112,13 +1112,12 @@ Article 8 - COMMERCIAL USE of the SOFTWARE 8.1 COMMERCIALISATION DECLARATION -USERS who wish to use the SOFTWARE for COMMERCIAL PURPOSES should, prior to -placing a FINAL PRODUCT on the market, complete a COMMERCIALISATION DECLARATION -for NWNC, which can be accessed at the following address: -www.harfang3d.com/declaration. +USERS who wish to use the SOFTWARE for COMMERCIAL PURPOSES should, prior to +placing a FINAL PRODUCT on the market, complete a COMMERCIALISATION DECLARATION +for NWNC by sending an email to contact@harfang3d.com. Companies must pay the appropriate rates for the COMMERCIALISATION DECLARATION, -which can be viewed at the following address: www.harfang3d.com/declaration. +which can be consulted by sending an email to contact@harfang3d.com. The USER will be charged for any COMMERCIAL USE of the SOFTWARE in compliance with NWNC’s rates which entered into force on the same day as the @@ -1139,8 +1138,8 @@ FINAL PRODUCT lawfully with regard to NWNC. The USER may affix a certification number to the FINAL PRODUCT. -The certification number will be freely available to the FINAL USER at the -following address www.harfang3d.com/certificate-check. +The certification number will be freely available to the FINAL USER by sending +an email to contact@harfang3d.com. The USER consequently agrees to cite the CERTIFICATE OF COMPLIANCE and specify how the FINAL USER may access it on the FINAL PRODUCT or in the General diff --git a/binding/bind_harfang.py b/binding/bind_harfang.py index 95e0d07..d07c535 100644 --- a/binding/bind_harfang.py +++ b/binding/bind_harfang.py @@ -977,6 +977,8 @@ def bind_scene(gen): gen.bind_method(rigid_body, 'SetRestitution', 'void', ['float restitution']) gen.bind_method(rigid_body, 'GetFriction', 'float', []) gen.bind_method(rigid_body, 'SetFriction', 'void', ['float friction']) + gen.bind_method(rigid_body, 'GetRollingFriction', 'float', []) + gen.bind_method(rigid_body, 'SetRollingFriction', 'void', ['float rolling_friction']) gen.end_class(rigid_body) @@ -990,6 +992,8 @@ def bind_scene(gen): gen.bind_method(collision, 'GetType', 'hg::CollisionType', []) gen.bind_method(collision, 'SetType', 'void', ['hg::CollisionType type']) + gen.bind_method(collision, 'GetLocalTransform', 'hg::Mat4', []) + gen.bind_method(collision, 'SetLocalTransform', 'void', ['hg::Mat4 m']) gen.bind_method(collision, 'GetMass', 'float', []) gen.bind_method(collision, 'SetMass', 'void', ['float mass']) gen.bind_method(collision, 'GetRadius', 'float', []) @@ -1307,8 +1311,8 @@ def bind_scene(gen): protos = [('hg::Node', ['hg::Scene &scene', 'const hg::Mat4 &mtx', 'const hg::ModelRef &model', 'const std::vector &materials'], [])] gen.bind_function_overloads('hg::CreateObject', expand_std_vector_proto(gen, protos)) - gen.bind_function('hg::CreateInstanceFromFile', 'hg::Node', ['hg::Scene &scene', 'const hg::Mat4 &mtx', 'const std::string &name', 'hg::PipelineResources &resources', 'const hg::PipelineInfo &pipeline', '?uint32_t flags'], {'constants_group': {'flags': 'LoadSaveSceneFlags'}}) - gen.bind_function('hg::CreateInstanceFromAssets', 'hg::Node', ['hg::Scene &scene', 'const hg::Mat4 &mtx', 'const std::string &name', 'hg::PipelineResources &resources', 'const hg::PipelineInfo &pipeline', '?uint32_t flags'], {'constants_group': {'flags': 'LoadSaveSceneFlags'}}) + gen.bind_function('hg::CreateInstanceFromFile', 'hg::Node', ['hg::Scene &scene', 'const hg::Mat4 &mtx', 'const std::string &name', 'hg::PipelineResources &resources', 'const hg::PipelineInfo &pipeline', 'bool &success', '?uint32_t flags'], {'constants_group': {'flags': 'LoadSaveSceneFlags'}, 'arg_out': ['success']}) + gen.bind_function('hg::CreateInstanceFromAssets', 'hg::Node', ['hg::Scene &scene', 'const hg::Mat4 &mtx', 'const std::string &name', 'hg::PipelineResources &resources', 'const hg::PipelineInfo &pipeline', 'bool &success', '?uint32_t flags'], {'constants_group': {'flags': 'LoadSaveSceneFlags'}, 'arg_out': ['success']}) gen.bind_function('hg::CreateScript', 'hg::Node', ['hg::Scene &scene', '?const std::string &path']) @@ -1497,6 +1501,7 @@ static std::vector _GetSceneForwardPipelineLights(cons gen.bind_function('hg::CreateForwardPipelineAAAFromFile', 'hg::ForwardPipelineAAA', ['const char *path', 'const hg::ForwardPipelineAAAConfig &config', '?bgfx::BackbufferRatio::Enum ssgi_ratio', '?bgfx::BackbufferRatio::Enum ssr_ratio']) gen.bind_function('hg::CreateForwardPipelineAAAFromAssets', 'hg::ForwardPipelineAAA', ['const char *path', 'const hg::ForwardPipelineAAAConfig &config', '?bgfx::BackbufferRatio::Enum ssgi_ratio', '?bgfx::BackbufferRatio::Enum ssr_ratio']) gen.bind_function('hg::DestroyForwardPipelineAAA', 'void', ['hg::ForwardPipelineAAA &pipeline']) + gen.bind_function('hg::IsValid', 'bool', ['const hg::ForwardPipelineAAA &pipeline']) gen.bind_function('hg::UpdateForwardPipelineAAA', 'void', ['hg::ForwardPipeline &pipeline', 'const hg::Rect &rect', 'const hg::Mat4 &view', 'const hg::Mat44 &proj', 'const hg::Mat4 &prv_view', 'const hg::Mat44 &prv_proj', 'const hg::tVec2 &jitter', 'bgfx::BackbufferRatio::Enum ssgi_ratio', 'bgfx::BackbufferRatio::Enum ssr_ratio', 'float temporal_aa_weight', 'float motion_blur_strength', @@ -1544,7 +1549,7 @@ def bind_bullet3_physics(gen): # gen.bind_method(newton, 'CollectCollisionEvents', 'void', ['const hg::Scene &scene', 'hg::NodeNodeContacts &node_node_contacts']) - gen.bind_method(bullet, 'SyncKinematicBodiesFromScene', 'void', ['const hg::Scene &scene']) + gen.bind_method(bullet, 'SyncBodiesFromScene', 'void', ['const hg::Scene &scene']) gen.bind_method(bullet, 'GarbageCollect', 'size_t', ['const hg::Scene &scene']) gen.bind_method(bullet, 'GarbageCollectResources', 'size_t', []) @@ -1555,6 +1560,11 @@ def bind_bullet3_physics(gen): # gen.bind_method(bullet, 'NodeWake', 'void', ['const hg::Node &node']) + gen.bind_method(bullet, 'NodeSetDeactivation', 'void', ['const hg::Node &node', 'bool enable']) + gen.bind_method(bullet, 'NodeGetDeactivation', 'bool', ['const hg::Node &node']) + + gen.bind_method(bullet, 'NodeResetWorld', 'void', ['const hg::Node &node', 'const hg::Mat4 &world']) + gen.bind_method(bullet, 'NodeAddForce', 'void', ['const hg::Node &node', 'const hg::Vec3 &F', '?const hg::Vec3 &world_pos']) gen.bind_method(bullet, 'NodeAddImpulse', 'void', ['const hg::Node &node', 'const hg::Vec3 &dt_velocity', '?const hg::Vec3 &world_pos']) gen.bind_method(bullet, 'NodeGetPointVelocity', 'hg::Vec3', ['const hg::Node &node', 'const hg::Vec3 &world_pos']) @@ -1564,6 +1574,11 @@ def bind_bullet3_physics(gen): gen.bind_method(bullet, 'NodeGetAngularVelocity', 'hg::Vec3', ['const hg::Node &node']) gen.bind_method(bullet, 'NodeSetAngularVelocity', 'void', ['const hg::Node &node', 'const hg::Vec3 &W']) + gen.bind_method(bullet, 'NodeGetLinearLockAxes', 'void', ['const hg::Node &node', 'bool &X', 'bool &Y', 'bool &Z'], {'arg_out': ['X', 'Y', 'Z']}) + gen.bind_method(bullet, 'NodeSetLinearLockAxes', 'void', ['const hg::Node &node', 'bool X', 'bool Y', 'bool Z']) + gen.bind_method(bullet, 'NodeGetAngularLockAxes', 'void', ['const hg::Node &node', 'bool &X', 'bool &Y', 'bool &Z'], {'arg_out': ['X', 'Y', 'Z']}) + gen.bind_method(bullet, 'NodeSetAngularLockAxes', 'void', ['const hg::Node &node', 'bool X', 'bool Y', 'bool Z']) + # node_contacts = gen.begin_class('hg::NodeContacts') gen.end_class(node_contacts) @@ -1594,6 +1609,7 @@ static std::vector __GetNodeContacts(const hg::NodeContacts &ctcs, # gen.bind_method(bullet, 'RaycastFirstHit', 'hg::RaycastOut', ['const hg::Scene &scene', 'const hg::Vec3 &p0', 'const hg::Vec3 &p1']) + gen.bind_method(bullet, 'RaycastAllHits', 'std::vector', ['const hg::Scene &scene', 'const hg::Vec3 &p0', 'const hg::Vec3 &p1']) # gen.bind_method(bullet, 'RenderCollision', 'void', ['bgfx::ViewId view_id', 'const bgfx::VertexLayout &vtx_layout', 'bgfx::ProgramHandle prg', 'hg::RenderState render_state', 'uint32_t depth']) @@ -2114,6 +2130,8 @@ static void _SetViewTransform(bgfx::ViewId view_id, const hg::Mat4 &view, const ('TF_SamplerMinAnisotropic', 'BGFX_SAMPLER_MIN_ANISOTROPIC'), ('TF_SamplerMagPoint', 'BGFX_SAMPLER_MAG_POINT'), ('TF_SamplerMagAnisotropic', 'BGFX_SAMPLER_MAG_ANISOTROPIC'), + ('TF_BlitDestination', 'BGFX_TEXTURE_BLIT_DST'), + ('TF_ReadBack', 'BGFX_TEXTURE_READ_BACK'), ], 'TextureFlags') gen.bind_function('hg::LoadTextureFlagsFromFile', 'uint64_t', ['const std::string &path'], {'rval_constants_group': 'TextureFlags'}) @@ -2394,16 +2412,27 @@ static bgfx::TextureInfo _PipelineResources_GetTextureInfo(hg::PipelineResources gen.bind_function('hg::CreateMissingMaterialProgramValuesFromAssets', 'void', ['hg::Material &mat', 'const hg::PipelineResources &resources']) # - pipeline_frame_buffer = gen.begin_class('hg::PipelineFrameBuffer') - gen.bind_members(pipeline_frame_buffer, ['bgfx::FrameBufferHandle handle', 'hg::TextureRef color', 'hg::TextureRef depth']) - gen.end_class(pipeline_frame_buffer) + frame_buffer = gen.begin_class('hg::FrameBuffer') + gen.bind_member(frame_buffer, 'bgfx::FrameBufferHandle handle') + gen.end_class(frame_buffer) gen.bind_function_overloads('hg::CreateFrameBuffer', [ - ('hg::PipelineFrameBuffer', ['bgfx::TextureFormat::Enum color_format', 'bgfx::TextureFormat::Enum depth_format', 'int aa', 'hg::PipelineResources &res', 'const char *name'], []), - ('hg::PipelineFrameBuffer', ['int width', 'int height', 'bgfx::TextureFormat::Enum color_format', 'bgfx::TextureFormat::Enum depth_format', 'int aa', 'hg::PipelineResources &res', 'const char *name'], []) + ('hg::FrameBuffer', ['const hg::Texture &color', 'const hg::Texture &depth', 'const char *name'], []), + ('hg::FrameBuffer', ['bgfx::TextureFormat::Enum color_format', 'bgfx::TextureFormat::Enum depth_format', 'int aa', 'const char *name'], []), + ('hg::FrameBuffer', ['int width', 'int height', 'bgfx::TextureFormat::Enum color_format', 'bgfx::TextureFormat::Enum depth_format', 'int aa', 'const char *name'], []) ]) + + gen.bind_function('hg::GetColorTexture', 'hg::Texture', ['hg::FrameBuffer &frameBuffer']) + gen.bind_function('hg::GetDepthTexture', 'hg::Texture', ['hg::FrameBuffer &frameBuffer']) - gen.bind_function('hg::DestroyFrameBuffer', 'void', ['hg::PipelineResources &res', 'hg::PipelineFrameBuffer &frameBuffer']) + gen.insert_binding_code(''' +static void _FrameBuffer_GetTextures(hg::FrameBuffer &framebuffer, hg::Texture &color, hg::Texture &depth) { + color = hg::GetColorTexture(framebuffer); + depth = hg::GetDepthTexture(framebuffer); +} +''') + gen.bind_function('GetTextures', 'void', ['hg::FrameBuffer &framebuffer', 'hg::Texture &color', 'hg::Texture &depth'], {'route': route_lambda('_FrameBuffer_GetTextures'), 'arg_out': ['color', 'depth']}) + gen.bind_function('hg::DestroyFrameBuffer', 'void', ['hg::FrameBuffer &frameBuffer']) # vertices = gen.begin_class('hg::Vertices') @@ -2786,6 +2815,11 @@ def bind_color(gen): gen.bind_function('hg::ColorI', 'hg::Color', ['int r', 'int g', 'int b', '?int a']) + gen.bind_function('hg::ToHLS', 'hg::Color', ['const hg::Color &color']) + gen.bind_function('hg::FromHLS', 'hg::Color', ['const hg::Color &color']) + + gen.bind_function('hg::SetSaturation', 'hg::Color', ['const hg::Color &color', 'float saturation']) + bind_std_vector(gen, color) diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 1b4727d..3b60bfe 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -1,33 +1,38 @@ # Generates the Harfang API XML description used to generate the documentation. -add_custom_command( - OUTPUT - ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml - COMMAND - ${Python3_EXECUTABLE} bind.py ${CMAKE_CURRENT_SOURCE_DIR}/../binding/bind_harfang.py --xml --out ${CMAKE_CURRENT_BINARY_DIR}/harfang ${HG_BINDING_DEFINES} - MAIN_DEPENDENCY - ${CMAKE_SOURCE_DIR}/binding/bind_harfang.py - WORKING_DIRECTORY - ${HG_FABGEN_PATH} - COMMENT - "Generating Harfang API description file") -# online docs -add_custom_target(online_docs ALL - ${Python3_EXECUTABLE} doc_to_html.py --project_name Harfang --doc_path doc --api_path ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml --out_path ${CMAKE_INSTALL_PREFIX}/online_docs --version ${HG_VERSION} --online - WORKING_DIRECTORY - ${CMAKE_CURRENT_SOURCE_DIR} - DEPENDS - ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml) -install(DIRECTORY img DESTINATION online_docs COMPONENT online_docs) -set_target_properties(online_docs PROPERTIES FOLDER "harfang/doc") +add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml + COMMAND + ${Python3_EXECUTABLE} bind.py ${CMAKE_SOURCE_DIR}/binding/bind_harfang.py --xml --out ${CMAKE_CURRENT_BINARY_DIR}/harfang ${HG_BINDING_DEFINES} + MAIN_DEPENDENCY + ${CMAKE_SOURCE_DIR}/binding/bind_harfang.py + WORKING_DIRECTORY + ${HG_FABGEN_PATH} + COMMENT + "Generating Harfang API description file") + +add_custom_target(gen_api_xml ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml) + +# add_custom_command( +# OUTPUT +# ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml +# COMMAND +# ${Python3_EXECUTABLE} bind.py ${CMAKE_CURRENT_SOURCE_DIR}/../binding/bind_harfang.py --xml --out ${CMAKE_CURRENT_BINARY_DIR}/harfang ${HG_BINDING_DEFINES} +# MAIN_DEPENDENCY +# ${CMAKE_SOURCE_DIR}/binding/bind_harfang.py +# WORKING_DIRECTORY +# ${HG_FABGEN_PATH} +# COMMENT +# "Generating Harfang API description file") # offline docs -configure_file(doc/index.html.in ${CMAKE_INSTALL_PREFIX}/offline_docs/index.html @ONLY IMMEDIATE) -add_custom_target(offline_docs ALL - ${Python3_EXECUTABLE} doc_to_html.py --project_name Harfang --doc_path doc --api_path ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml --out_path ${CMAKE_INSTALL_PREFIX}/offline_docs --version ${HG_VERSION} - WORKING_DIRECTORY - ${CMAKE_CURRENT_SOURCE_DIR} - DEPENDS - ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml) -install(DIRECTORY img DESTINATION offline_docs/content COMPONENT offline_docs) -set_target_properties(offline_docs PROPERTIES FOLDER "harfang/doc") +#configure_file(doc/index.html.in ${CMAKE_INSTALL_PREFIX}/offline_docs/index.html @ONLY IMMEDIATE) +#add_custom_target(offline_docs ALL +# ${Python3_EXECUTABLE} doc_to_html.py --project_name Harfang --doc_path doc --api_path ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml --out_path ${CMAKE_INSTALL_PREFIX}/offline_docs --version ${HG_VERSION} +# WORKING_DIRECTORY +# ${CMAKE_CURRENT_SOURCE_DIR} +# DEPENDS +# ${CMAKE_CURRENT_BINARY_DIR}/harfang/api.xml) +#install(DIRECTORY img DESTINATION offline_docs/content COMPONENT offline_docs) +#set_target_properties(offline_docs PROPERTIES FOLDER "harfang/doc") diff --git a/doc/doc/Camera.md b/doc/doc/Camera.md index 51d6b26..cb696e2 100644 --- a/doc/doc/Camera.md +++ b/doc/doc/Camera.md @@ -1,3 +1,3 @@ -Add this component to a [Node] to implement the camera aspect. - +Add this component to a [Node] to implement the camera aspect. + Create a camera component with [Scene_CreateCamera], use [CreateCamera] to create a complete camera node. \ No newline at end of file diff --git a/doc/doc/DuplicateNodeAndChildrenFromAssets.md b/doc/doc/DuplicateNodeAndChildrenFromAssets.md index aed9bfa..5200e2b 100644 --- a/doc/doc/DuplicateNodeAndChildrenFromAssets.md +++ b/doc/doc/DuplicateNodeAndChildrenFromAssets.md @@ -1,3 +1,3 @@ -Duplicate a node and its child hierarchy. Resources will be load from the assets system. - +Duplicate a node and its child hierarchy. Resources will be load from the assets system. + See [man.Assets]. \ No newline at end of file diff --git a/doc/doc/DuplicateNodeAndChildrenFromFile.md b/doc/doc/DuplicateNodeAndChildrenFromFile.md index 1fb0694..c284f37 100644 --- a/doc/doc/DuplicateNodeAndChildrenFromFile.md +++ b/doc/doc/DuplicateNodeAndChildrenFromFile.md @@ -1,3 +1,3 @@ -Duplicate a node and its child hierarchy. Resources will be load from the local filesystem. - +Duplicate a node and its child hierarchy. Resources will be load from the local filesystem. + See [man.Assets]. \ No newline at end of file diff --git a/doc/doc/DuplicateNodeFromAssets.md b/doc/doc/DuplicateNodeFromAssets.md index d763c6f..e113416 100644 --- a/doc/doc/DuplicateNodeFromAssets.md +++ b/doc/doc/DuplicateNodeFromAssets.md @@ -1,3 +1,3 @@ -Duplicate a node. Resources will be load from the assets system. - +Duplicate a node. Resources will be load from the assets system. + See [man.Assets]. \ No newline at end of file diff --git a/doc/doc/DuplicateNodeFromFile.md b/doc/doc/DuplicateNodeFromFile.md index 5a32601..9c94f9e 100644 --- a/doc/doc/DuplicateNodeFromFile.md +++ b/doc/doc/DuplicateNodeFromFile.md @@ -1,3 +1,3 @@ -Duplicate a node. Resources will be load from the local filesystem. - +Duplicate a node. Resources will be load from the local filesystem. + See [man.Assets]. \ No newline at end of file diff --git a/doc/doc/Frame.md b/doc/doc/Frame.md index 31a20c4..a21b299 100644 --- a/doc/doc/Frame.md +++ b/doc/doc/Frame.md @@ -1,4 +1,4 @@ -Advance the rendering backend to the next frame, execute all queued rendering commands. -This function returns the backend current frame. - +Advance the rendering backend to the next frame, execute all queued rendering commands. +This function returns the backend current frame. + The frame counter is used by asynchronous functions such as [CaptureTexture]. You must wait for the frame counter to reach or exceed the value returned by an asynchronous function before accessing its result. \ No newline at end of file diff --git a/doc/doc/FromHLS.md b/doc/doc/FromHLS.md new file mode 100644 index 0000000..41d3be3 --- /dev/null +++ b/doc/doc/FromHLS.md @@ -0,0 +1 @@ +Convert input hue/luminance/saturation color to RGBA, alpha channel is left unmodified. \ No newline at end of file diff --git a/doc/doc/Frustum.md b/doc/doc/Frustum.md index e5f07e5..f2042b9 100644 --- a/doc/doc/Frustum.md +++ b/doc/doc/Frustum.md @@ -1,2 +1,2 @@ -A view frustum, perspective or orthographic, holding the necessary information to perform culling queries. +A view frustum, perspective or orthographic, holding the necessary information to perform culling queries. It can be used to test wether a volume is inside or outside the frustum it represents. \ No newline at end of file diff --git a/doc/doc/GetClock.md b/doc/doc/GetClock.md index e7f3971..58f9295 100644 --- a/doc/doc/GetClock.md +++ b/doc/doc/GetClock.md @@ -1 +1,3 @@ -Return the total elapsed time since the object creation or the last call to [ResetClock]. \ No newline at end of file +Return the current clock since the last call to [TickClock] or [ResetClock]. + +See [time_to_sec_f] to convert the returned time to second. \ No newline at end of file diff --git a/doc/doc/GetColorTexture.md b/doc/doc/GetColorTexture.md new file mode 100644 index 0000000..657cf15 --- /dev/null +++ b/doc/doc/GetColorTexture.md @@ -0,0 +1 @@ +Retrieves color texture attachment. diff --git a/doc/doc/GetDepthTexture.md b/doc/doc/GetDepthTexture.md new file mode 100644 index 0000000..5dece82 --- /dev/null +++ b/doc/doc/GetDepthTexture.md @@ -0,0 +1 @@ +Retrieves depth texture attachment. diff --git a/doc/doc/GetTextures.md b/doc/doc/GetTextures.md new file mode 100644 index 0000000..ce0cb96 --- /dev/null +++ b/doc/doc/GetTextures.md @@ -0,0 +1 @@ +Returns color and depth texture attachments. \ No newline at end of file diff --git a/doc/doc/ImGuiBeginMainMenuBar.md b/doc/doc/ImGuiBeginMainMenuBar.md index 1fafcaf..c223580 100644 --- a/doc/doc/ImGuiBeginMainMenuBar.md +++ b/doc/doc/ImGuiBeginMainMenuBar.md @@ -1,3 +1,3 @@ -Create and append to a full screen menu-bar. - +Create and append to a full screen menu-bar. + Note: Only call [ImGuiEndMainMenuBar] if this returns `true`. \ No newline at end of file diff --git a/doc/doc/ImGuiBeginMenu.md b/doc/doc/ImGuiBeginMenu.md index 1bb067f..37a02cf 100644 --- a/doc/doc/ImGuiBeginMenu.md +++ b/doc/doc/ImGuiBeginMenu.md @@ -1,3 +1,3 @@ -Create a sub-menu entry. - +Create a sub-menu entry. + Note: Only call [ImGuiEndMenu] if this returns `true`. \ No newline at end of file diff --git a/doc/doc/ImGuiBeginMenuBar.md b/doc/doc/ImGuiBeginMenuBar.md index 56d1fee..213f3f2 100644 --- a/doc/doc/ImGuiBeginMenuBar.md +++ b/doc/doc/ImGuiBeginMenuBar.md @@ -1,3 +1,3 @@ -Start append to the menu-bar of the current window (requires the `WindowFlags_MenuBar` flag). - +Start append to the menu-bar of the current window (requires the `WindowFlags_MenuBar` flag). + Note: Only call [ImGuiEndMenuBar] if this returns `true`. \ No newline at end of file diff --git a/doc/doc/ImGuiBeginPopup.md b/doc/doc/ImGuiBeginPopup.md index 272ebb1..46c2abb 100644 --- a/doc/doc/ImGuiBeginPopup.md +++ b/doc/doc/ImGuiBeginPopup.md @@ -1,3 +1,3 @@ -Return `true` if popup is opened and starts outputting to it. - +Return `true` if popup is opened and starts outputting to it. + Note: Only call [ImGuiEndPopup] if this returns `true`. \ No newline at end of file diff --git a/doc/doc/ImGuiCalcItemWidth.md b/doc/doc/ImGuiCalcItemWidth.md index 7dafebc..d019d32 100644 --- a/doc/doc/ImGuiCalcItemWidth.md +++ b/doc/doc/ImGuiCalcItemWidth.md @@ -1,3 +1,3 @@ -Returns the width of item given pushed settings and current cursor position. - +Returns the width of item given pushed settings and current cursor position. + Note: This is not necessarily the width of last item. \ No newline at end of file diff --git a/doc/doc/ImGuiCheckbox.md b/doc/doc/ImGuiCheckbox.md index d0cd85d..b938a48 100644 --- a/doc/doc/ImGuiCheckbox.md +++ b/doc/doc/ImGuiCheckbox.md @@ -1 +1,5 @@ -Checkbox widget returning the check state. \ No newline at end of file +Display a checkbox widget. Returns an interaction flag (user interacted with the widget) and the current widget state (checked or not after user interaction). + +```python +was_clicked, my_value = gs.ImGuiCheckBox('My value', my_value) +``` diff --git a/doc/doc/ImGuiColumns.md b/doc/doc/ImGuiColumns.md index 44147a5..a6e572e 100644 --- a/doc/doc/ImGuiColumns.md +++ b/doc/doc/ImGuiColumns.md @@ -1,5 +1,5 @@ -Begin a column layout section. - -To move to the next column use [ImGuiNextColumn]. To end a column layout section pass `1` to this function. - +Begin a column layout section. + +To move to the next column use [ImGuiNextColumn]. To end a column layout section pass `1` to this function. + **Note:** Current implementation supports a maximum of 64 columns. \ No newline at end of file diff --git a/doc/doc/ImGuiIsItemActive.md b/doc/doc/ImGuiIsItemActive.md index bfb6190..7326953 100644 --- a/doc/doc/ImGuiIsItemActive.md +++ b/doc/doc/ImGuiIsItemActive.md @@ -1,3 +1,3 @@ -Was the last item active. - +Was the last item active. + e.g. button being held, text field being edited - items that do not interact will always return `false`. \ No newline at end of file diff --git a/doc/doc/ImGuiOpenPopup.md b/doc/doc/ImGuiOpenPopup.md index 558d156..4d3fe63 100644 --- a/doc/doc/ImGuiOpenPopup.md +++ b/doc/doc/ImGuiOpenPopup.md @@ -1,9 +1,9 @@ -Mark a named popup as open. - -Popup windows are closed when the user: - -* Clicks outside of their client rect, -* Activates a pressable item, -* [ImGuiCloseCurrentPopup] is called within a [ImGuiBeginPopup]/[ImGuiEndPopup] block. - +Mark a named popup as open. + +Popup windows are closed when the user: + +* Clicks outside of their client rect, +* Activates a pressable item, +* [ImGuiCloseCurrentPopup] is called within a [ImGuiBeginPopup]/[ImGuiEndPopup] block. + Popup identifiers are relative to the current ID stack so [ImGuiOpenPopup] and [ImGuiBeginPopup] need to be at the same level of the ID stack. \ No newline at end of file diff --git a/doc/doc/ImGuiSelectable.md b/doc/doc/ImGuiSelectable.md index 15d5359..afb4b1d 100644 --- a/doc/doc/ImGuiSelectable.md +++ b/doc/doc/ImGuiSelectable.md @@ -1,11 +1,11 @@ -Selectable item. - -The following `width` values are possible: - -* `= 0.0`: Use remaining width. -* `> 0.0`: Specific width. - -The following `height` values are possible: - -* `= 0.0`: Use label height. +Selectable item. + +The following `width` values are possible: + +* `= 0.0`: Use remaining width. +* `> 0.0`: Specific width. + +The following `height` values are possible: + +* `= 0.0`: Use label height. * `> 0.0`: Specific height. \ No newline at end of file diff --git a/doc/doc/ImGuiTextWrapped.md b/doc/doc/ImGuiTextWrapped.md index b58e95e..29604d4 100644 --- a/doc/doc/ImGuiTextWrapped.md +++ b/doc/doc/ImGuiTextWrapped.md @@ -1,3 +1,3 @@ -Wrapped static text. - +Wrapped static text. + Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, you may need to set a size using [ImGuiSetNextWindowSize]. \ No newline at end of file diff --git a/doc/doc/InputInit.md b/doc/doc/InputInit.md index 7b10027..d2ac198 100644 --- a/doc/doc/InputInit.md +++ b/doc/doc/InputInit.md @@ -1,6 +1,6 @@ -Initialize the Input system. Must be invoked before any call to [WindowSystemInit] to work properly. - -```python -hg.InputInit() -hg.WindowSystemInit() +Initialize the Input system. Must be invoked before any call to [WindowSystemInit] to work properly. + +```python +hg.InputInit() +hg.WindowSystemInit() ``` \ No newline at end of file diff --git a/doc/doc/MakeFrustum.md b/doc/doc/MakeFrustum.md index add8f0a..607fa76 100644 --- a/doc/doc/MakeFrustum.md +++ b/doc/doc/MakeFrustum.md @@ -1,8 +1,8 @@ -Create a projection frustum. This object can then be used to perform culling using [TestVisibility]. - -```python -# Compute a perspective matrix -proj = hg.ComputePerspectiveProjectionMatrix(0.1, 1000, hg.FovToZoomFactor(math.pi/4), 1280/720) -# Make a frustum from this projection matrix -frustum = hg.MakeFrustum(proj) +Create a projection frustum. This object can then be used to perform culling using [TestVisibility]. + +```python +# Compute a perspective matrix +proj = hg.ComputePerspectiveProjectionMatrix(0.1, 1000, hg.FovToZoomFactor(math.pi/4), 1280/720) +# Make a frustum from this projection matrix +frustum = hg.MakeFrustum(proj) ``` \ No newline at end of file diff --git a/doc/doc/Node_SetCamera.md b/doc/doc/Node_SetCamera.md index d5d42f1..8f6d5b9 100644 --- a/doc/doc/Node_SetCamera.md +++ b/doc/doc/Node_SetCamera.md @@ -1,2 +1,2 @@ -Set the [Camera] component of a node. +Set the [Camera] component of a node. See [Scene_CreateCamera]. \ No newline at end of file diff --git a/doc/doc/Node_SetLight.md b/doc/doc/Node_SetLight.md index 35453f2..b86cb7e 100644 --- a/doc/doc/Node_SetLight.md +++ b/doc/doc/Node_SetLight.md @@ -1,3 +1,3 @@ -Set the [Light] component of a node. - +Set the [Light] component of a node. + See [Scene_CreateLight], [Scene_CreatePointLight], [Scene_CreateSpotLight] or [Scene_CreateLinearLight]. \ No newline at end of file diff --git a/doc/doc/Node_SetLuaScript.md b/doc/doc/Node_SetLuaScript.md index ae9b937..5927b9f 100644 --- a/doc/doc/Node_SetLuaScript.md +++ b/doc/doc/Node_SetLuaScript.md @@ -1,3 +1,3 @@ -Set the [LuaScript] component of a node. - +Set the [LuaScript] component of a node. + See [Scene_CreateLuaScript]. \ No newline at end of file diff --git a/doc/doc/Node_SetObject.md b/doc/doc/Node_SetObject.md index da0714d..6179bba 100644 --- a/doc/doc/Node_SetObject.md +++ b/doc/doc/Node_SetObject.md @@ -1,3 +1,3 @@ -Set the [Object] component of a node. - +Set the [Object] component of a node. + See [Scene_CreateObject]. \ No newline at end of file diff --git a/doc/doc/Node_SetTransform.md b/doc/doc/Node_SetTransform.md index 46b27ce..a97ed63 100644 --- a/doc/doc/Node_SetTransform.md +++ b/doc/doc/Node_SetTransform.md @@ -1,3 +1,3 @@ -Set the [Transform] component of a node. - +Set the [Transform] component of a node. + See [Scene_CreateTransform]. \ No newline at end of file diff --git a/doc/doc/Open.md b/doc/doc/Open.md index ba76159..57e4382 100644 --- a/doc/doc/Open.md +++ b/doc/doc/Open.md @@ -1,3 +1,3 @@ -Open a file in binary mode. - +Open a file in binary mode. + See [OpenText], [OpenWrite], [OpenWriteText] \ No newline at end of file diff --git a/doc/doc/OpenText.md b/doc/doc/OpenText.md index 148af42..09a96e8 100644 --- a/doc/doc/OpenText.md +++ b/doc/doc/OpenText.md @@ -1,3 +1,3 @@ -Open a file as text. Return a handle to the opened file. - +Open a file as text. Return a handle to the opened file. + See [Open], [OpenWrite], [OpenWriteText] \ No newline at end of file diff --git a/doc/doc/OpenVRStateToViewState.md b/doc/doc/OpenVRStateToViewState.md index 9afbb42..2407a77 100644 --- a/doc/doc/OpenVRStateToViewState.md +++ b/doc/doc/OpenVRStateToViewState.md @@ -1,3 +1,3 @@ -Compute the left and right eye view states from an OpenVR state. - +Compute the left and right eye view states from an OpenVR state. + See [OpenVRGetState]. \ No newline at end of file diff --git a/doc/doc/OpenVRSubmitFrame.md b/doc/doc/OpenVRSubmitFrame.md index 388ef9f..1b37f40 100644 --- a/doc/doc/OpenVRSubmitFrame.md +++ b/doc/doc/OpenVRSubmitFrame.md @@ -1,3 +1,3 @@ -Submit the left and right eye textures to the OpenVR compositor. - +Submit the left and right eye textures to the OpenVR compositor. + See [OpenVRCreateEyeFrameBuffer]. \ No newline at end of file diff --git a/doc/doc/OpenWrite.md b/doc/doc/OpenWrite.md index 4bf01d2..953ccbc 100644 --- a/doc/doc/OpenWrite.md +++ b/doc/doc/OpenWrite.md @@ -1,3 +1,3 @@ -Open a file as binary in write mode. - +Open a file as binary in write mode. + See [Open], [OpenText], [OpenWriteText] \ No newline at end of file diff --git a/doc/doc/OpenWriteText.md b/doc/doc/OpenWriteText.md index c3b19e9..0a647e4 100644 --- a/doc/doc/OpenWriteText.md +++ b/doc/doc/OpenWriteText.md @@ -1,3 +1,3 @@ -Open a file as text in write mode. - +Open a file as text in write mode. + See [Open], [OpenText], [OpenWrite] \ No newline at end of file diff --git a/doc/doc/RaycastOut.md b/doc/doc/RaycastOut.md index 632c8ee..9889b3a 100644 --- a/doc/doc/RaycastOut.md +++ b/doc/doc/RaycastOut.md @@ -1,6 +1,6 @@ -Contains the result of a physics raycast. - -* `P`: Position of the raycast hit -* `N`: Normal of the raycast hit -* `Node`: Node hit by the raycast +Contains the result of a physics raycast. + +* `P`: Position of the raycast hit +* `N`: Normal of the raycast hit +* `Node`: Node hit by the raycast * `t`: Parametric value of the intersection, ratio of the distance to the hit by the length of the raycast \ No newline at end of file diff --git a/doc/doc/SceneNewtonPhysics.md b/doc/doc/SceneBullet3Physics.md similarity index 100% rename from doc/doc/SceneNewtonPhysics.md rename to doc/doc/SceneBullet3Physics.md diff --git a/doc/doc/Scene_GetNodeEx.md b/doc/doc/Scene_GetNodeEx.md index e8640e0..d6eaaa4 100644 --- a/doc/doc/Scene_GetNodeEx.md +++ b/doc/doc/Scene_GetNodeEx.md @@ -1,22 +1,22 @@ -Get a node by its absolute path in the node hierarchy. - -A node path is constructed as follow: - -- Nodes are refered to by their name. -- To address the child of a node, use the `/` delimiter between its parent name and the child name. -- To address a node inside an instance component, use the `:` delimiter. -- There is no limit on the number of delimiters you can use. - -Examples: - -Get the node named `child` parented to the `root` node. - -```python -child = scene.GetNodeEx('root/child') -``` - -Get the node named `dummy` instantiated by the `root` node. - -```python -dummy = my_scene.GetNodeEx('root:dummy') -``` +Get a node by its absolute path in the node hierarchy. + +A node path is constructed as follow: + +- Nodes are refered to by their name. +- To address the child of a node, use the `/` delimiter between its parent name and the child name. +- To address a node inside an instance component, use the `:` delimiter. +- There is no limit on the number of delimiters you can use. + +Examples: + +Get the node named `child` parented to the `root` node. + +```python +child = scene.GetNodeEx('root/child') +``` + +Get the node named `dummy` instantiated by the `root` node. + +```python +dummy = my_scene.GetNodeEx('root:dummy') +``` diff --git a/doc/doc/SetSaturation.md b/doc/doc/SetSaturation.md new file mode 100644 index 0000000..80282a2 --- /dev/null +++ b/doc/doc/SetSaturation.md @@ -0,0 +1,3 @@ +Return a copy of the input RGBA color with its saturation set to the specified value, alpha channel is left unmodified. + +See [ToHLS] and [FromHLS]. \ No newline at end of file diff --git a/doc/doc/TickClock.md b/doc/doc/TickClock.md index c86c283..f00b8dd 100644 --- a/doc/doc/TickClock.md +++ b/doc/doc/TickClock.md @@ -1,3 +1,3 @@ -Record the elapsed time since the last call to this function. +Advance the engine clock and return the elapsed time since the last call to this function. See [GetClock] to retrieve the current clock. See [GetClockDt]. \ No newline at end of file diff --git a/doc/doc/ToHLS.md b/doc/doc/ToHLS.md new file mode 100644 index 0000000..5d16862 --- /dev/null +++ b/doc/doc/ToHLS.md @@ -0,0 +1 @@ +Convert input RGBA color to hue/luminance/saturation, alpha channel is left unmodified. \ No newline at end of file diff --git a/doc/doc/Touch.md b/doc/doc/Touch.md index a818217..5eef377 100644 --- a/doc/doc/Touch.md +++ b/doc/doc/Touch.md @@ -1,3 +1,3 @@ -Submit an empty primitive to the view. - +Submit an empty primitive to the view. + See [Frame]. \ No newline at end of file diff --git a/doc/doc/man.AllFunctionIndex.md b/doc/doc/man.AllFunctionIndex.md deleted file mode 100644 index a60610a..0000000 --- a/doc/doc/man.AllFunctionIndex.md +++ /dev/null @@ -1,3 +0,0 @@ -.title Function/member index - -%AllFunctionIndex% \ No newline at end of file diff --git a/doc/doc/man.AnApplicationUsingTheRenderSystem.md b/doc/doc/man.AnApplicationUsingTheRenderSystem.md deleted file mode 100644 index dcd0004..0000000 --- a/doc/doc/man.AnApplicationUsingTheRenderSystem.md +++ /dev/null @@ -1,76 +0,0 @@ -.title An application using the render system - -This page describes a complete Harfang application in Python displaying a triangle using the [RenderSystem]. - -.img("man.AnApplicationUsingTheRenderSystem.png") - -The complete source for this application can be found in the [man.Tutorials]. - -## Program overview - -To display a triangle using the render system we will need to: - -1. Create the renderer and a render system wrapping it, -* Display the triangle in a loop until the end of execution condition is met. - -Most steps of this program are explained in details in the [man.AnApplicationUsingTheRenderer] page. - -## Creating the render system - -This application uses [Renderer] and wraps it with [RenderSystem]. - -```python -# create the renderer -renderer = hg.CreateRenderer() -renderer.Open() - -# open a new window -win = hg.NewWindow(480, 240) - -# create a new output surface for the newly opened window -surface = renderer.NewOutputSurface(win) -renderer.SetOutputSurface(surface) - -# initialize the render system, which is used to draw through the renderer -render_system = hg.RenderSystem() -render_system.Initialize(renderer) -``` - -The render system is ready to work. - -## The application render loop - -### A word on vertex transformation - -Since we will be displaying the triangle using the render system we have less control over the shader that is going to be used. The render system core resources include shader to render all the common combination of vertex attributes. - -However, unlike the the shader we used in the equivalent renderer program, _all render system shaders make use of the renderer ModelViewProjection matrix_. So we first need to initialize it. - -For the purpose of this program a simple 2D projection system will do. The following call will set a projection matrix that maps vertex coordinate to pixels with (0;0) in the lower-left corner of the viewport with +X going right and +Y going up. - -```python -renderer.Set2DMatrices() -``` - -### Drawing the triangle - -The application loops until the default renderer window is closed and starts by clearing the render target to a solid green color. - -```python -while hg.IsWindowOpen(win): - renderer.Clear(hg.Color.Green) -``` - -Next, we tell the render system to draw the triangle using the helper function it provides for this task. - -```python - vertices = [hg.Vector3(0, 0, 0), hg.Vector3(0, 240, 0), hg.Vector3(480, 240, 0)] - render_system.DrawTriangleAuto(1, vertices, color) -``` - -Finally, the loop ends by showing the draw result and updating the renderer output window. - -```python - hg.Frame() - hg.UpdateWindow(win) -``` diff --git a/doc/doc/man.AnApplicationUsingTheRenderer.md b/doc/doc/man.AnApplicationUsingTheRenderer.md deleted file mode 100644 index 4c1e27e..0000000 --- a/doc/doc/man.AnApplicationUsingTheRenderer.md +++ /dev/null @@ -1,159 +0,0 @@ -.title An application using the renderer - -This page describes a complete Harfang application in Python displaying a triangle using the [Renderer]. - -The complete source for this application can be found in the [man.Tutorials]. - -## Program overview - -To display a triangle using the renderer we will need to: - -1. Create the renderer, -* Create a window and , -* Describe the geometry we intend to draw, -* Provide the geometry to the renderer along with a shader to draw it. -* Display the triangle in a loop until the end of execution condition is met. - -## Creating the renderer - -We first need to create an object of [Renderer] type. By default this application uses the OpenGL implementation of the renderer interface, but any other available implementation can be used in its place. - -```python -renderer = hg.CreateRenderer() -renderer.Open() -``` - -The [Renderer] object is first created then its [Renderer_Open] member function is called. At this point no window is created. - -## Creating the window - -We create a new [Window] using the [NewWindow] function. - -```python -win = hg.NewWindow(640, 480) -``` - -We create a [Surface] for the newly created window and set it as the [Renderer] new output surface. - -```python -surface = renderer.NewOutputSurface(win) -renderer.SetOutputSurface(surface) -``` - -## Describing the geometry to draw - -The [Renderer] API works at the lowest level of abstraction and uses [GpuBuffer] and [VertexLayout] together with a [Shader] to build and draw primitives. - -### Index buffer - -A triangle is build from 3 vertices which are connected in sequential order (vertex 0 to vertex 1 to vertex 2). So we need a vertex buffer of 3 vertices and an index buffer of 3 indexes containing the following values: 0, 1 and 2. - -The index values need to be packed into a memory buffer before they can be send to the gpu. The [BinaryBlob] class can be used to this effect. - -```python -data = gs.BinaryData() -data.WriteUInt16s([0, 1, 2]) # we use 16 bit packing of the index values by writing shorts -``` - -The index buffer is then very easily constructed from the binary blob. - -```python -idx = renderer.NewBuffer() -renderer.CreateBuffer(idx, data, hg.GpuBufferIndex) -``` - -### Vertex buffer - -A vertex can be made of any number of attributes (position, color, UV, etc...) which are then fed into the shader used to draw the primitives. To specify the layout of a vertex buffer we use the [VertexLayout] class. - -The triangle we will display is the simplest one possible, displaying a single color provided as a shader parameter, so its vertices only need a position attribute. The position of each vertex will be stored using 3 float values. - -```python -vtx_layout = hg.VertexLayout() -vtx_layout.AddAttribute(hg.VertexPosition, 3, hg.VertexFloat) -``` - -We then prepare the vertex buffer content using another binary blob. - -```python -data = hg.BinaryData() -data.WriteFloats([-0.5, -0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5]) -``` - -We can now create the vertex buffer. - -```python -vtx = renderer.NewBuffer() -renderer.CreateBuffer(vtx, data, hg.GpuBufferVertex) -``` - -The buffers are complete and ready to render. - -## Create a shader to render the geometry - -The shader we use to display the triangle takes the vertex position as it is and output the same color for each pixel. The output color is an input parameter named `u_color` which we will programmatically change. - -```glsl -in { vec4 u_color; } - -variant { - vertex { - out { vec4 v_color; } - - source %{ - v_color = u_color; - %out.position% = vec4(vPosition, 1.0); - %} - } - - pixel { - in { vec4 v_color; } - - source %{ - %out.color% = v_color; - %} - } -} -``` - -For more information on the shader structure, refer to the [man.Shader] page. - -To load the shader from a file we first mount a file driver (cf. [man.Assets]). - -```python -hg.MountFileDriver(hg.StdFileDriver()) - -shader_path = os.path.join(os.getcwd(), "../_data/shader_2d_color.isl") -shader = renderer.LoadShader(shader_path) -``` - -**Note:** A default value for `u_color` can be specified in the shader declaration so that we do not have to set it programmatically. - -## The application render loop - -Everything is now ready so we enter the main application loop in which we render the triangle. The application loops until the default renderer window is closed and starts by clearing the render target to a solid red color. - -```python -while hg.IsWindowOpen(win): - renderer.Clear(hg.Color.Red) -``` - -Next, the shader and its `u_color` input value are set and the index/vertex buffers are drawn to the render target. - -```python - renderer.SetShader(shader) - renderer.SetShaderFloat4("u_color", 0, 1, 0, 1) - hg.DrawBuffers(renderer, 3, idx, vtx, vtx_layout) -``` - -**Note:** The [DrawBuffers] call specifies the drawing of 3 indexes and uses the default value for the index type and primitive type. Those are 16 bit indexes and triangle primitives. - -Finally, the loop ends by commiting the draw call, showing the draw result and updating the renderer output window. - -```python - renderer.DrawFrame() - renderer.ShowFrame() - - hg.UpdateWindow(win) - hg.EndFrame() -``` \ No newline at end of file diff --git a/doc/doc/man.Architecture.md b/doc/doc/man.Architecture.md deleted file mode 100644 index 758dcba..0000000 --- a/doc/doc/man.Architecture.md +++ /dev/null @@ -1 +0,0 @@ -.title Architecture \ No newline at end of file diff --git a/doc/doc/man.AssembleOverview.md b/doc/doc/man.AssembleOverview.md deleted file mode 100644 index bd0122a..0000000 --- a/doc/doc/man.AssembleOverview.md +++ /dev/null @@ -1,5 +0,0 @@ -.title Overview - -Assemble is a scene editor for the Harfang library written in C++ using the DearImGui library. - -It can be used to prepare assets for your Harfang programs or as a stand-alone content creation tool with publishing capabilities. \ No newline at end of file diff --git a/doc/doc/man.AssetCompiler.md b/doc/doc/man.AssetCompiler.md index 1a51e5e..55323d1 100644 --- a/doc/doc/man.AssetCompiler.md +++ b/doc/doc/man.AssetCompiler.md @@ -1,46 +1,46 @@ -.title Compiling to Assets - -Compiling project resources into assets is done using the `assetc` command-line tool. - -Upon invocation, it will scan the input folder and compile all resources in a supported format to the output folder. Files in an unsupported format are copied unmodified to the output folder. - -*It is very important that you treat the compiled output folder as entirely disposable* and *only ever* perform modifications in the input folder. Output assets will be different for each platform you compile to. - -Again, *do not ever work in the assets folder*. - -If you are unclear on the resources/assets distinction see [man.Assets]. - -## Drag & drop - -The easiest way is to drag and drop the resources folder on the assetc executable: - -![assetc drag & drop](../img/assetc.gif) - -## Command-Line - -If you need more control over the compilation options, the command line gets the following parameters: - -``` -assetc [output PATH] [-daemon] [-platform PLATFORM] [-api API] [-defines DEFINES] [-job COUNT] - [-toolchain PATH] [-progress] [-log_to_std_out] [-debug] [-quiet] [-verbose] -``` - -Option | Shortcut | Description --------|----------|------------ -`-input` | | Input project resources folder. -`-output` | | Output compiled assets folder. If unspecified, the input folder path suffixed with `_compiled` is used. -`-daemon` | `-d` | Run the compiler in daemon mode. The compiler will constantly monitor the input folder and compile its content as it is modified. -`-platform` | `-p` | Platform to target. -`-api` | | Graphics API to target. Some platforms (eg. PC) might support multiple graphics API (DX11, DX12, GL, ...). -`-defines` | `-D` | Semicolon separated defines to pass to the shader compiler (eg. FLAG;VALUE=2). -`-job` | `-j` | Maximum number of parallel job (0 - automatic). -`-toolchain` | `-t` | Path to the toolchain folder. -`-progress` | | Output progress to the standard output. -`-log_to_std_out` | `-l` | Log errors to the standard output. -`-debug` | | Compile in debug mode (eg. output debug informations in shader). -`-quiet` | `-q` | Disable all build information but errors. -`-verbose` | `-v` | Output additional information about the compilation process. - -*Note:* When run in daemon mode `assetc` will not exit after its initial run and will keep watching the input folder. When a resource is modified it will automatically be compiled to the output folder. - -See [man.GLTF] and [man.FBX] to convert common 3d formats to Harfang resources. +.title Compiling to Assets + +Compiling project resources into assets is done using the `assetc` command-line tool. + +Upon invocation, it will scan the input folder and compile all resources in a supported format to the output folder. Files in an unsupported format are copied unmodified to the output folder. + +*It is very important that you treat the compiled output folder as entirely disposable* and *only ever* perform modifications in the input folder. Output assets will be different for each platform you compile to. + +Again, *do not ever work in the assets folder*. + +If you are unclear on the resources/assets distinction see [man.Assets]. + +## Drag & drop + +The easiest way is to drag and drop the resources folder on the assetc executable: + +![assetc drag & drop](../img/assetc.gif) + +## Command-Line + +If you need more control over the compilation options, the command line gets the following parameters: + +``` +assetc [output PATH] [-daemon] [-platform PLATFORM] [-api API] [-defines DEFINES] [-job COUNT] + [-toolchain PATH] [-progress] [-log_to_std_out] [-debug] [-quiet] [-verbose] +``` + +Option | Shortcut | Description +-------|----------|------------ +`-input` | | Input project resources folder. +`-output` | | Output compiled assets folder. If unspecified, the input folder path suffixed with `_compiled` is used. +`-daemon` | `-d` | Run the compiler in daemon mode. The compiler will constantly monitor the input folder and compile its content as it is modified. +`-platform` | `-p` | Platform to target. +`-api` | | Graphics API to target. Some platforms (eg. PC) might support multiple graphics API (DX11, DX12, GL, ...). +`-defines` | `-D` | Semicolon separated defines to pass to the shader compiler (eg. FLAG;VALUE=2). +`-job` | `-j` | Maximum number of parallel job (0 - automatic). +`-toolchain` | `-t` | Path to the toolchain folder. +`-progress` | | Output progress to the standard output. +`-log_to_std_out` | `-l` | Log errors to the standard output. +`-debug` | | Compile in debug mode (eg. output debug informations in shader). +`-quiet` | `-q` | Disable all build information but errors. +`-verbose` | `-v` | Output additional information about the compilation process. + +*Note:* When run in daemon mode `assetc` will not exit after its initial run and will keep watching the input folder. When a resource is modified it will automatically be compiled to the output folder. + +See [man.GLTF] and [man.FBX] to convert common 3d formats to Harfang resources. diff --git a/doc/doc/man.Assets.md b/doc/doc/man.Assets.md index 09e2538..29756aa 100644 --- a/doc/doc/man.Assets.md +++ b/doc/doc/man.Assets.md @@ -1,14 +1,14 @@ .title Resources & Assets -By convention, we call production files: **resources** (eg. *the project resources*). +By convention, production files are called: **resources** (eg. *the project resources*). -Files issued from the compilation of production files for a specific target are called: **assets** (eg. *the project assets for iOS*). +Files issued from the compilation of production files for a specific target are called: **assets** (eg. *the project assets for Windows PC*). [TOC] ## Resource Formats -During development resources are stored in *production formats*, these formats are meant for efficient editing. Before they can be loaded at runtime, resources must be compiled into their *runtime formats* as assets which are specific to and optimized for the target platform. +During development resources are stored in *production formats*, these formats are meant for efficient editing. Before they can be loaded at runtime, resources must be compiled into their *runtime formats* as assets which are specific to the target platform. To compile a project resources use: diff --git a/doc/doc/man.CPython.md b/doc/doc/man.CPython.md index 9c5151a..ba18617 100644 --- a/doc/doc/man.CPython.md +++ b/doc/doc/man.CPython.md @@ -16,6 +16,20 @@ Harfang 2.0.0 for CPython 3.2+ on windows-x64 (build ba08463ee9e6c0c93960230fb88 See http://harfang3d.com/license for licensing terms ``` +### Troubleshooting + +> _`pip install` fails with a message saying `harfang is not a supported wheel on this platform`._ +> +> Make sure that your pip install is up to date. Outdated pip versions have been known to cause such problems. + +> _The dynamic library fails to load when importing the `harfang` module in Python._ +> +> Make sure your system has the required runtime dependencies installed. It should have OpenAL and on Windows the Visual C++ 2017 redistributable installed. + +> _`ImportError: DLL load failed: %1 is not a valid Win32 application.` error when importing the `harfang` module._ +> +> This error usually happens when installing the incorrect version of Harfang for your Python version. For example when installing the 64 bit version of Harfang on a 32 bit install of the Python interpreter. + ## First Program Let's write a simple test program, create a new file named `test.py` and paste the following code into it. diff --git a/doc/doc/man.Classes.md b/doc/doc/man.Classes.md deleted file mode 100644 index 6d6fb9d..0000000 --- a/doc/doc/man.Classes.md +++ /dev/null @@ -1,3 +0,0 @@ -.title API Classes - -%ClassIndex% diff --git a/doc/doc/man.Component.md b/doc/doc/man.Component.md deleted file mode 100644 index 8c00c36..0000000 --- a/doc/doc/man.Component.md +++ /dev/null @@ -1,5 +0,0 @@ -.title Components - -## Node components - -[Transform], [Object], [Light], [Camera], [LuaScript], [RigidBody] and [Collision]. \ No newline at end of file diff --git a/doc/doc/man.Constants.md b/doc/doc/man.Constants.md deleted file mode 100644 index adfc726..0000000 --- a/doc/doc/man.Constants.md +++ /dev/null @@ -1,5 +0,0 @@ -.title API Constants - -%GlobalConstantsIndex% - -%GlobalConstantsDocumentation% diff --git a/doc/doc/man.CoreRuntimeResources.md b/doc/doc/man.CoreRuntimeResources.md deleted file mode 100644 index 5e23d0f..0000000 --- a/doc/doc/man.CoreRuntimeResources.md +++ /dev/null @@ -1,3 +0,0 @@ -.title Core runtime resources - -In order to properly work the Harfang library needs to access a number of core resources. \ No newline at end of file diff --git a/doc/doc/man.Dearimgui.md b/doc/doc/man.Dearimgui.md deleted file mode 100644 index 3dd8182..0000000 --- a/doc/doc/man.Dearimgui.md +++ /dev/null @@ -1,41 +0,0 @@ -.title DearImGui - -Harfang embeds the [dear imGui](https://github.com/ocornut/imgui) library. - -Dear imgui is an immediate GUI library designed to quickly build debugging/profiling user interface. - -It is available at all time through the ImGui* function and only requires a [Renderer] instance to work. Only a single instance of the library is available and its output is displayed right before executing a [Renderer_ShowFrame] call. - -Accessing ImGui from multiple threads require synchronization using the [ImGuiLock] and [ImGuiUnlock] functions. - -### Minimal sample code showing a window - -```python -import harfang as hg - -hg.LoadPlugins() - -renderer = hg.CreateRenderer() -renderer.Open() - -win = hg.NewWindow(640, 480) - -surface = renderer.NewOutputSurface(win) -renderer.SetOutputSurface(surface) - -hg.ImGuiSetOutputSurface(surface) - -while True: - hg.ImGuiBegin("window"): - hg.ImGuiEnd() - - renderer.Clear(hg.Color.Red) - renderer.ShowFrame() - - hg.UpdateWindow(win) - hg.EndFrame() - -renderer.DestroyOutputSurface(surface) -hg.DestroyWindow(win) -renderer.Close() -``` \ No newline at end of file diff --git a/doc/doc/man.Debugging.md b/doc/doc/man.Debugging.md deleted file mode 100644 index 364cc9c..0000000 --- a/doc/doc/man.Debugging.md +++ /dev/null @@ -1,13 +0,0 @@ -.title Debugging - -## Debugging general issues - -The first thing to check when your program fails is the engine log output. The log output sends all engine debugging messages to the console. - -The log system defaults to only displaying warning and error level messages. Enabling debug and standard level messages by calling `hg.SetLogLevel(hg.LogAll)` (see [SetLogLevel]) will simplify identifying why something is going wrong. - -Complex error messages might include detailed information on the error. By default, details are filtered out by the system and must be enabled using `hg.SetLogIsDetailed(True)` (see [SetLogIsDetailed]). - -## Debugging scene issues - -A scene is to complex an object to debug through the log system. The engine debugger includes a scene debugger which is the perfect tool for dwelving into the data structures of a scene. Refer to the [man.EngineDebugger] manual page for how to use it. \ No newline at end of file diff --git a/doc/doc/man.DrawingGraphicPrimitives.md b/doc/doc/man.DrawingGraphicPrimitives.md deleted file mode 100644 index 5d8227f..0000000 --- a/doc/doc/man.DrawingGraphicPrimitives.md +++ /dev/null @@ -1,76 +0,0 @@ -.title Drawing graphic primitives - -This page describes a complete Harfang application in Python displaying lines, triangles and polygons using low level graphics functionnalities. - -.img("man.AnApplicationUsingTheRenderSystem.png") - -The complete source for this application can be found in the [man.Tutorials]. - -## Program overview - -To display a triangle using the render system we will need to: - -1. Create the renderer and a render system wrapping it, -* Display the triangle in a loop until the end of execution condition is met. - -Most steps of this program are explained in details in the [man.AnApplicationUsingTheRenderer] page. - -## Creating the render system - -This application uses [Renderer] and wraps it with [RenderSystem]. - -```python -# create the renderer -renderer = hg.CreateRenderer() -renderer.Open() - -# open a new window -win = hg.NewWindow(480, 240) - -# create a new output surface for the newly opened window -surface = renderer.NewOutputSurface(win) -renderer.SetOutputSurface(surface) - -# initialize the render system, which is used to draw through the renderer -render_system = hg.RenderSystem() -render_system.Initialize(renderer) -``` - -The render system is ready to work. - -## The application render loop - -### A word on vertex transformation - -Since we will be displaying the triangle using the render system we have less control over the shader that is going to be used. The render system core resources include shader to render all the common combination of vertex attributes. - -However, unlike the the shader we used in the equivalent renderer program, _all render system shaders make use of the renderer ModelViewProjection matrix_. So we first need to initialize it. - -For the purpose of this program a simple 2D projection system will do. The following call will set a projection matrix that maps vertex coordinate to pixels with (0;0) in the lower-left corner of the viewport with +X going right and +Y going up. - -```python -renderer.Set2DMatrices() -``` - -### Drawing the triangle - -The application loops until the default renderer window is closed and starts by clearing the render target to a solid green color. - -```python -while hg.IsWindowOpen(win): - renderer.Clear(hg.Color.Green) -``` - -Next, we tell the render system to draw the triangle using the helper function it provides for this task. - -```python - vertices = [hg.Vector3(0, 0, 0), hg.Vector3(0, 240, 0), hg.Vector3(480, 240, 0)] - render_system.DrawTriangleAuto(1, vertices, color) -``` - -Finally, the loop ends by showing the draw result and updating the renderer output window. - -```python - hg.Frame() - hg.UpdateWindow(win) -``` diff --git a/doc/doc/man.EngineDebugger.md b/doc/doc/man.EngineDebugger.md deleted file mode 100644 index 21a360c..0000000 --- a/doc/doc/man.EngineDebugger.md +++ /dev/null @@ -1,24 +0,0 @@ -.title Engine debugger - -Harfang integrates a debugger written in [man.Dearimgui]. The debugger can be used to inspect, debug and profile many systems of the engine at runtime. - -Use the [SetEnableDebugger] function to enable and disable the debugger. The debugger interface will overlay itself over your program output before each call to [Renderer_ShowFrame]. - -## Engine systems - -The debugger monitors the following engine systems. - -* **Renderer:** Statistics for the current [Renderer]. -* **Render system:** Statistics for the current [RenderSystem]. -* **Texture cache:** Display the content of the engine texture cache. -* **Geometry cache:** Display the content of the engine geometry cache. -* **Material cache:** Display the content of the engine material cache. -* **Log window:** Display the engine log output. - -## Scene debugger - -.img("man.scene_debugger.jpg") - -The debugger tracks all scene creation and deletion and keeps a list of available scene in the *Scene debugger* menu. You can select a specific scene to monitor or select the *automatic* option from the *Scene debugger* menu to track the last displayed scene. - -The scene debugger can display the full scene tree, inspect and modify [Node] and their [man.Component]. \ No newline at end of file diff --git a/doc/doc/man.Enums.md b/doc/doc/man.Enums.md deleted file mode 100644 index bf5004d..0000000 --- a/doc/doc/man.Enums.md +++ /dev/null @@ -1,5 +0,0 @@ -.title API Enumerations - -%GlobalEnumIndex% - -%GlobalEnumDocumentation% diff --git a/doc/doc/man.Examples.md b/doc/doc/man.Examples.md deleted file mode 100644 index dca8483..0000000 --- a/doc/doc/man.Examples.md +++ /dev/null @@ -1,7 +0,0 @@ -.title Examples - -## Note on code examples - -Most examples presented in this manual are written in Python. - -The API exposed to both languages being identical, adapting the Python examples to Lua usually requires little more than language grammar change. diff --git a/doc/doc/man.ExtendingTheEditor.md b/doc/doc/man.ExtendingTheEditor.md deleted file mode 100644 index 3d9263c..0000000 --- a/doc/doc/man.ExtendingTheEditor.md +++ /dev/null @@ -1,4 +0,0 @@ -.title Extending the editor - -* [man.ExtendingTheProjectExplorer] -* [man.ExtendingTheScenePlugin] diff --git a/doc/doc/man.ExtendingTheProjectExplorer.md b/doc/doc/man.ExtendingTheProjectExplorer.md deleted file mode 100644 index 917ed40..0000000 --- a/doc/doc/man.ExtendingTheProjectExplorer.md +++ /dev/null @@ -1,14 +0,0 @@ -.title Extending the project explorer - -## Writing a project explorer plugin - -A scene tool plugin must be declared as a class extending the `plugin.IProjectExplorerPlugin` interface. - -```python -class Plugin(IProjectExplorerPlugin): - """ A new project explorer plugin """ -``` - -The following methods must be implemented by the plugin: - -* `process_drop_event(dropped_urls, target_url)`: Process a drop event over the project explorer. Return `plugin.InterruptPluginChain` to stop execution at your plugin. diff --git a/doc/doc/man.ExtendingTheScenePlugin.md b/doc/doc/man.ExtendingTheScenePlugin.md deleted file mode 100644 index cbfac11..0000000 --- a/doc/doc/man.ExtendingTheScenePlugin.md +++ /dev/null @@ -1,24 +0,0 @@ -.title Extending the scene plugin - -## Writing a scene tool plugin - -A scene tool plugin must be declared as a class extending the `plugin.ISceneToolPlugin` interface. - -```python -class Plugin(ISceneToolPlugin): - """ New Scene plugin """ -``` - -The following methods must be implemented by the plugin: - -* `on_selection_changed(selection)`: Called by the scene plugin whenever the selection is changed. The complete new selection is passed to the plugin. -* `on_node_selection_changed(node_selection)`: Same as above but a list of the scene nodes in the new selection is passed to the plugin. -* `on_frame_complete()`: This function is called when the current frame is complete. The plugin is given a chance to draw additional content at this point. -**Note:** This call is done from the rendering thread. The synchronous renderer object [^1] can be used from this location. The renderer matrix stack is automatically saved and restored around this call. -* `on_mouse_event(event, mouse, dt_frame)`: Called whenever a mouse event happens over the viewport. - -[^1]: Available through the `engine.renderer` symbol. - -## Mouse events - -TODO diff --git a/doc/doc/man.FeatureList.md b/doc/doc/man.FeatureList.md deleted file mode 100644 index 8417068..0000000 --- a/doc/doc/man.FeatureList.md +++ /dev/null @@ -1,51 +0,0 @@ -.title Feature List - -### General - -* Cross-platform -* Lightweight code base -* Small memory footprint - -### Interoperability - -* Command line FBX converter (extensive support including geometry skinning) -* Native support for many image file formats (PSD, JPG, PNG, TGA, ...) -* Native support for many sound file formats (OGG, WAV, AIFF, XM, S3M, ...) - -### Framework - -* Flexible file system abstraction (local, archive, network components with chaining) -* HID abstraction to access machine devices (DirectInput, XInput, ...) -* Data format abstraction (XML/JSON/Binary back-ends) -* 2D Vector graphics engine based on the Anti Grain Geometry library - -### Multi-threading - -* Task-based multi-threading -* Asynchronous interfaces to control key API objects from any thread or language - -### Audio - -* Audio API abstraction layer (OpenAL back-end) -* Any supported audio format can be streamed or loaded as a sound -* 3D audio support - -### Rendering - -* GPU-accelerated -* Graphic API abstraction layer (OpenGL 3.3/ES 2.0 & DirectX 11 back-ends) -* Shader-based rendering -* Draw TTF text to screen - -### Scene - -* Complete scene management -* Component/system architecture -* GPU skinning -* Light component with shadow mapping -* Post-processing (motion blur, depth of field, ...) -* Bullet/PhysX 3 physics system -* Recast/Detour navigation system -* Create new component using Lua scripts -* Multiple Lua scripts can run in parallel -* Each scene system executes tasks in parallel using a lock-free stepping algorithm diff --git a/doc/doc/man.Functions.md b/doc/doc/man.Functions.md deleted file mode 100644 index adc311e..0000000 --- a/doc/doc/man.Functions.md +++ /dev/null @@ -1,5 +0,0 @@ -.title API Functions - -%GlobalFunctionIndex% - -%GlobalFunctionDocumentation% diff --git a/doc/doc/man.GeneratedTexturedCube.md b/doc/doc/man.GeneratedTexturedCube.md deleted file mode 100644 index 0430977..0000000 --- a/doc/doc/man.GeneratedTexturedCube.md +++ /dev/null @@ -1,152 +0,0 @@ -.title Generated textured cube - -The [Geometry] class contains the functions needed to generate meshes. - -## Generated textured cube - -To generate a textured cube using [Geometry] API, you need the following functions: - -### Vertices -* [Geometry_AllocateVertex] : Set the number of vertices. -* [Geometry_SetVertex] : Set vertex coordinates. - -### Polygons -* [Geometry_AllocatePolygon] : Set number of polygons. -* [Geometry_SetPolygon] : Set the number of vertices and the polygon material. -* [Geometry_AllocatePolygonBinding] : Allocate memory to store the geometry polygon binding table. -* [Geometry_SetPolygonBinding] : Set the polygon binding table. - -### Normals -* [Geometry_AllocateVertexNormal] : Set the number of normals. -* [Geometry_SetVertexNormal] : Set normal coordinates. - -### UVs -* [Geometry_AllocateUVChannels] : Set number of UVs. -* [Geometry_SetUV] : Set UV coordinates. - -### Materials -* [Geometry_AllocateMaterialTable] : Set number of materials. -* [Geometry_SetMaterial] : Set material definition file path. - -## Code example - -### Vertices and polygons - -```python - - cube = hg.Geometry() - - # Create vertex - - s = hg.Vector3(1,1,1) # dimensions - cube.AllocateVertex(8) - ube.SetVertex(0, hg.Vector3(-s.x, -s.y, -s.z)) - cube.SetVertex(1, hg.Vector3(-s.x, -s.y, s.z)) - cube.SetVertex(2, hg.Vector3(-s.x, s.y, -s.z)) - cube.SetVertex(3, hg.Vector3(-s.x, s.y, s.z)) - cube.SetVertex(4, hg.Vector3(s.x, -s.y, -s.z)) - cube.SetVertex(5, hg.Vector3(s.x, -s.y, s.z)) - cube.SetVertex(6, hg.Vector3(s.x, s.y, -s.z)) - cube.SetVertex(7, hg.Vector3(s.x, s.y, s.z)) - - # Create polygons - - cube.AllocatePolygon(6) - cube.SetPolygon(0, 4, 0) - cube.SetPolygon(1, 4, 1) - cube.SetPolygon(2, 4, 2) - cube.SetPolygon(3, 4, 3) - cube.SetPolygon(4, 4, 4) - cube.SetPolygon(5, 4, 5) - - # Polygons bindings - - cube.AllocatePolygonBinding() - cube.SetPolygonBinding(0, hg.IntList([0, 2, 6, 4])) - cube.SetPolygonBinding(1, hg.IntList([4, 6, 7, 5])) - cube.SetPolygonBinding(2, hg.IntList([5, 7, 3, 1])) - cube.SetPolygonBinding(3, hg.IntList([1, 3, 2, 0])) - cube.SetPolygonBinding(4, hg.IntList([2, 3, 7, 6])) - cube.SetPolygonBinding(5, hg.IntList([4, 5, 1, 0])) - -``` - -### Normals. Each vertex for each polygon has a normal. So, 6 polygons X 4 vertices = 24 normals - -```python - - # Normals - - cube.AllocateVertexNormal(24) - cube.SetVertexNormal(0, hg.Vector3(0, 0, -1)) - cube.SetVertexNormal(1, hg.Vector3(0, 0, -1)) - cube.SetVertexNormal(2, hg.Vector3(0, 0, -1)) - cube.SetVertexNormal(3, hg.Vector3(0, 0, -1)) - cube.SetVertexNormal(4, hg.Vector3(1, 0, 0)) - cube.SetVertexNormal(5, hg.Vector3(1, 0, 0)) - cube.SetVertexNormal(6, hg.Vector3(1, 0, 0)) - cube.SetVertexNormal(7, hg.Vector3(1, 0, 0)) - cube.SetVertexNormal(8, hg.Vector3(0, 0, 1)) - cube.SetVertexNormal(9, hg.Vector3(0, 0, 1)) - cube.SetVertexNormal(10, hg.Vector3(0, 0, 1)) - cube.SetVertexNormal(11, hg.Vector3(0, 0, 1)) - cube.SetVertexNormal(12, hg.Vector3(-1, 0, 0)) - cube.SetVertexNormal(13, hg.Vector3(-1, 0, 0)) - cube.SetVertexNormal(14, hg.Vector3(-1, 0, 0)) - cube.SetVertexNormal(15, hg.Vector3(-1, 0, 0)) - cube.SetVertexNormal(16, hg.Vector3(0, 1, 0)) - cube.SetVertexNormal(17, hg.Vector3(0, 1, 0)) - cube.SetVertexNormal(18, hg.Vector3(0, 1, 0)) - cube.SetVertexNormal(19, hg.Vector3(0, 1, 0)) - cube.SetVertexNormal(20, hg.Vector3(0, -1, 0)) - cube.SetVertexNormal(21, hg.Vector3(0, -1, 0)) - cube.SetVertexNormal(22, hg.Vector3(0, -1, 0)) - cube.SetVertexNormal(23, hg.Vector3(0, -1, 0)) - -``` - -### UVs and materials - -```python - # Create UVs - - cube.AllocateUVChannels(1, 24) - cube.SetUV(0, 0, hg.Vector2List([hg.Vector2(0, 0), hg.Vector2(0, 1), hg.Vector2(1, 1), hg.Vector2(1, 0)])) - cube.SetUV(0, 1, hg.Vector2List([hg.Vector2(0, 0), hg.Vector2(0, 1), hg.Vector2(1, 1), hg.Vector2(1, 0)])) - cube.SetUV(0, 2, hg.Vector2List([hg.Vector2(0, 0), hg.Vector2(0, 1), hg.Vector2(1, 1), hg.Vector2(1, 0)])) - cube.SetUV(0, 3, hg.Vector2List([hg.Vector2(0, 0), hg.Vector2(0, 1), hg.Vector2(1, 1), hg.Vector2(1, 0)])) - cube.SetUV(0, 4, hg.Vector2List([hg.Vector2(0, 0), hg.Vector2(0, 1), hg.Vector2(1, 1), hg.Vector2(1, 0)])) - cube.SetUV(0, 5, hg.Vector2List([hg.Vector2(0, 0), hg.Vector2(0, 1), hg.Vector2(1, 1), hg.Vector2(1, 0)])) - - # Create materials - - cube.AllocateMaterialTable(6) - cube.SetMaterial(0, "assets/materials/face1.mat") - cube.SetMaterial(1, "assets/materials/face2.mat") - cube.SetMaterial(2, "assets/materials/face3.mat") - cube.SetMaterial(3, "assets/materials/face4.mat") - cube.SetMaterial(4, "assets/materials/face5.mat") - cube.SetMaterial(5, "assets/materials/face6.mat") - -``` - -### Validate Geometry and create Node - -After having determined the geometry, it's possible to validate your structure. If all is ok, you can create [Object] and [Node]. - -```python - if cube.Validate(): - geo = plus.GetRenderSystem().CreateGeometry(cube,False) - obj = hg.Object() - obj.SetGeometry(geo) - node = hg.Node() - node.SetName("generated_cube") - transform = hg.Transform(hg.Vector3(0,3,0)) - node.AddComponent(transform) - node.AddComponent(obj) - scene.AddNode(node) - return node -``` - - - diff --git a/doc/doc/man.Installation.md b/doc/doc/man.Installation.md deleted file mode 100644 index 7bd29ea..0000000 --- a/doc/doc/man.Installation.md +++ /dev/null @@ -1,73 +0,0 @@ -.title Installation - -## Quick Install - -* **Using PIP in a command line:** `pip install harfang` -* **Or download the wheel from :** [Downloads](https://www.harfang3d.com/downloads) - -If anything goes wrong, please look at the [TroubleShooting](#TroubleShooting) section.
-Further details on the installation are available below. - -## Prerequisites - -The following dependencies must be installed on your system for any Harfang project to work properly. - -* Functional OpenGL 3.3 hardware and drivers - -### Windows - -* OpenAL redistributable (`oalinst.exe`) -* Visual C++ 2017 redistributable (`vcredist.exe`) - -### Linux - -* OpenAL (`sudo apt-get install libopenal1`) - -## Installation - -Harfang is available for several programming languages as an extension or as a standalone executable. The following sections describe the installation procedure for each variant. - -### Python - -* Download the `.whl` package for your OS and Python version. ([Downloads](https://www.harfang3d.com/downloads)) - -#### Windows - -1. Open a command prompt as Administrator (`Win+X` then `Command Prompt (Admin)`). -1. Switch to the download directory and execute `pip install .whl --user`. - -#### OSX - -1. Open a terminal window (in `Applications/Utilities/Terminal.app`). -1. Switch to the download directory and execute `pip install .whl --user`. - -#### Linux - -1. Open a terminal window. -1. Switch to the download directory and execute `pip install .whl --user`. - -**Note:** You might need to explicitly use `pip3` to install the module if your system has both Python 2 and 3 installed. - -#### Confirm your installation #### - -Confirm your installation by starting your Python 3 interpreter and execute the following statement `import harfang as hg`. If you receive no error message, the installation was successful. - -### Lua - -Deploy the binary extension to your Lua interpreter or use the provided interpreter. - -## Troubleshooting - -### Python - -#### 1. `pip install` fails with a message saying `harfang is not a supported wheel on this platform`. - -Make sure that your pip install is up to date. Outdated pip versions have been known to cause such problems. - -#### 2. The dynamic library fails to load when importing the `harfang` module in Python. - -Make sure your system has the required runtime dependencies installed. It should have OpenAL and on Windows the Visual C++ 2017 redistributable installed. - -#### 3. `ImportError: DLL load failed: %1 is not a valid Win32 application.` error when importing the `harfang` module. - -This error usually happens when installing the incorrect version of Harfang for your Python version. For example when installing the 64 bit version of Harfang on a 32 bit install of the Python interpreter. diff --git a/doc/doc/man.Overview.md b/doc/doc/man.Overview.md index 5f12475..1974714 100644 --- a/doc/doc/man.Overview.md +++ b/doc/doc/man.Overview.md @@ -1,19 +1,19 @@ -.title Overview - -Harfang is a high-level software library to create applications that display 2D/3D visuals and play sound/music. - -It provides a unified API to write programs using different programming languages and is available for Windows, OSX and Linux (Debian/Ubuntu). - -## Supported Programming Languages - -For installation instructions for each supported language please refer to the corresponding manual page: - -* [man.CPython] -* [man.Lua] - -## Getting Help - -* For generic programming issues: - - [Stack Overflow](http://stackoverflow.com/) -* For game development related issues: - - [gamedev.net](http://www.gamedev.net) +.title Overview + +Harfang is a high-level software library to create applications that display 2D/3D visuals and play sound/music. + +It provides a unified API to write programs using different programming languages and is available for Windows and Linux (for more details, please refer to the [man.Requirements]). + +## Supported Programming Languages + +For installation instructions for each supported language please refer to the corresponding manual page: + +* [man.CPython] +* [man.Lua] + +## Getting Help + +* For generic programming issues: + - [Stack Overflow](http://stackoverflow.com/) +* For game development related issues: + - [gamedev.net](http://www.gamedev.net) diff --git a/doc/doc/man.Physics.md b/doc/doc/man.Physics.md index 3faa762..489100e 100644 --- a/doc/doc/man.Physics.md +++ b/doc/doc/man.Physics.md @@ -23,19 +23,19 @@ The rigid body intertia tensor is computed from its collision shape properties. ## Simulating Physics -Create a physics backend such as [SceneNewtonPhysics] and call [SceneNewtonPhysics_SceneCreatePhysicsFromAssets] to create the physics states corresponding to the scene declaration. +Create a physics backend such as [SceneNewtonPhysics] and call [SceneBullet3Physics_SceneCreatePhysicsFromAssets] to create the physics states corresponding to the scene declaration. -*Note:* [SceneNewtonPhysics_SceneCreatePhysicsFromAssets] means that if setting up the physics states requires access to an external resource, such as a mesh, it should be loaded from the assets system. If you are working from the filesystem, use [SceneNewtonPhysics_SceneCreatePhysicsFromFile]. +*Note:* [SceneBullet3Physics_SceneCreatePhysicsFromAssets] means that if setting up the physics states requires access to an external resource, such as a mesh, it should be loaded from the assets system. If you are working from the filesystem, use [SceneBullet3Physics_SceneCreatePhysicsFromFile]. ### Running the Simulation This involves 3 steps on each update: -1. Synchronize physics state with the scene declaration using [SceneNewtonPhysics_SceneCreatePhysicsFromAssets]. Alternatively, you can use a more fine-grained approach using [SceneNewtonPhysics_NodeCreatePhysicsFromAssets] to improve performance. -2. Step the simulation using [SceneNewtonPhysics_StepSimulation]. -3. Synchronize the updated physics transformations to the scene using [SceneNewtonPhysics_SyncDynamicBodiesToScene]. +1. Synchronize physics state with the scene declaration using [SceneBullet3Physics_SceneCreatePhysicsFromAssets]. Alternatively, you can use a more fine-grained approach using [SceneBullet3Physics_NodeCreatePhysicsFromAssets] to improve performance. +2. Step the simulation using [SceneBullet3Physics_StepSimulation]. +3. Synchronize the updated physics transformations to the scene using [SceneBullet3Physics_SyncDynamicBodiesToScene]. -*Note:* If you are using kinematic bodies you will also need to synchronize them from their node transformation on each update using [SceneNewtonPhysics_SyncKinematicBodiesFromScene]. +*Note:* If you are using kinematic bodies you will also need to synchronize them from their node transformation on each update using [SceneBullet3Physics_SyncKinematicBodiesFromScene]. ### The Easy Way @@ -45,7 +45,7 @@ When using a script system, this function will also dispatch collision events to ## Keeping the System Synchronized -Call the physics system garbage collect method (eg. [SceneNewtonPhysics_GarbageCollect]) on each update to ensure that destroyed nodes or components are properly removed. If you know that no node or component was destroyed during a particular update, not calling the garbage collector will save on performance. +Call the physics system garbage collect method (eg. [SceneBullet3Physics_GarbageCollect]) on each update to ensure that destroyed nodes or components are properly removed. If you know that no node or component was destroyed during a particular update, not calling the garbage collector will save on performance. ## Reading Physics Transformation diff --git a/doc/doc/man.PostProcessing.md b/doc/doc/man.PostProcessing.md deleted file mode 100644 index 5899e06..0000000 --- a/doc/doc/man.PostProcessing.md +++ /dev/null @@ -1,140 +0,0 @@ -.title Post processing - -Post processing occures after main scene rendering in order to add more realistic effects (motion blur, ambient occlusion...) - -.img("post_process_pipeline.png") - -## Post process - * [BloomPostProcess] - * [ChromaticDispersionPostProcess] - * [HSLPostProcess] - * [MotionBlurPostProcess] - * [RadialBlurPostProcess] - * [SAOPostProcess] - * [SharpenPostProcess] - -### Usage (python) - -All post-processings have the same init/remove procedures: - - -``` -import harfang as hg -... -camera = scene.GetCurrentCamera() -post_process = hg.BloomPostProcess() -camera.AddComponent(post_process) - -... - -camera.RemoveComponent(post_process) -... -``` - -_In that exemple we use "BloomPostProcess", but it could be "ChromaticDispersionPostProcess", "HSLPostProcess", and so on..._ - -## Post process Stack - -You can add as much PostProcessComponent as you like to the Camera. -It will be executed in the order of appearance in the Node's stack: this is the **Post Process stack**. - -The order of execution of the post-processes is important: - -.img("post_process_stack.png") - - ---- - -## BloomPostProcess - -Drops a glow around enlighten areas. This effect enhances the impression of brightness. - -_Bloom post-process:_ -.img("bloom_01.png") - -_No post-process:_ -.img("no_post_process.png") - - ---- - -## ChromaticDispersionPostProcess - -This filter works by independently offseting the red, green and blue components of the input image. - -_Chromatic dispersion post-process:_ -.img("chromatic_dispersion_01.png") - -_No post-process:_ -.img("no_post_process.png") - ---- - -## HSLPostProcess - -Post-process component implementing a Hue/Saturation/Brightness filter. - -* **Hue:** add a circular shift to pixel colors. -* **Saturation:** set the colors strength (0: picture in grayscale). -* **Brightness:** Set the brightnes level (0 sets the screen to black). This can be used to fade-in / fade-out effect. - -_HSL post-process:_ -.img("HSL_01.png") - -_No post-process:_ -.img("no_post_process_camaro.png") - ---- - -## MotioBlurPostProcess - -This effect reproduce the famous cinematographic effect that blurs moving parts. -The faster the part, the blurrier it is. Motion-blur increases the realism of the rendering by softening the movements. - -_Motion blur post-process:_ -.img("motionblur_01.png") - -_No post-process:_ -.img("no_post_process_camaro.png") - ---- - -## RadialBlurPostProcess - -This effect blurs the pixels from a point of the screen. The further the pixels are from the point, the blurrier they are. -Unlike motion-blur, the radial-blur is independent of motion. - -_Radial blur post-process:_ -.img("radial_blur_01.png") - -_No post-process:_ -.img("no_post_process_camaro.png") - ---- - -## SAOPostProcess - -S.A.O for Screen-space Ambient Occlusion. - -SAO is a fast real-time ambient occlusion rendering. It calculates pixels occlusions using the frame Z-Buffer. -It's an approximation of real ambient occlusion, but it's quite faster. -SAO is independant of scene complexity, as it works only on pixels datas (colors buffer & Z-Buffer). - - -_SAO post-process:_ -.img("SAO_01.png") - -_No post-process:_ -.img("no_post_process_camaro.png") - ---- - -## SharpenPostProcess - -This effect reinforce contrasted edges using a convolution matrice. - -_Sharpen post-process:_ -.img("Sharpen_01.png") - -_No post-process:_ -.img("no_post_process.png") diff --git a/doc/doc/man.Requirements.md b/doc/doc/man.Requirements.md index dbdc65f..b4a5699 100644 --- a/doc/doc/man.Requirements.md +++ b/doc/doc/man.Requirements.md @@ -1,12 +1,22 @@ -.title Requirements - -## System Requirements - -* **GPU:** Graphic card with OpenGL 3.3 support. -* **CPU** and **memory** requirements are mostly dependent on your project characteristics. - -A faster computer for development is recommended. - -### Supported desktop OS - -* Windows 7+, OSX 10.8+, Ubuntu 14.04+ +.title Requirements + +## System Requirements + +* **GPU:** Graphic card with OpenGL 3.3, Direct3D 11 or OpenGL ES 3.1 support. +* **CPU** and **memory** requirements are mostly dependent on your project characteristics. + +A faster computer for development is recommended. + +### Supported desktop OS + +* Windows 10+ (Intel) +* Ubuntu 20.04 LTS+ (Intel) +* Aarch Linux 64 (ARM) + +### Window systems +* X11 +* Wayland + +### VR Support +* Via SteamVR (Windows only) + diff --git a/doc/doc/man.Scene.md b/doc/doc/man.Scene.md index 7980a86..3c880f3 100644 --- a/doc/doc/man.Scene.md +++ b/doc/doc/man.Scene.md @@ -1,48 +1,48 @@ -.title Working with Scene - -A scene is a 3d world populated with [Node]. - -Nodes are container objects taking meaning through the use of components. - -## Node & Components - -Calling [Scene_CreateNode] returns an empty node with no component attached. In this state, it serves little to no purpose as it will not be drawn or implement any concrete behavior. - -### Object - -In order to be drawn a node must provide two essential informations: - -- *A transformation:* This is done using [Node_SetTransform] to assign it a [Transform] component. -- *A visual representation:* This is done using [Node_SetObject] to assign it an [Object] component. - -The object component accepts a [ModelRef] to a [Model] and holds a local list of [Material] used to draw the model. Each object component may hold different material definition for the same model. - -The same components can be assigned to multiple nodes. - -### Camera - -Assign the [Camera] component to nodes to turn them into observers into the scene. - -For more information on how this integrates with drawing a scene, see [man.DrawingScene]. - -### Light - -Assign the [Light] component to nodes to turn them into light sources. - -### Instance - -Scenes can be instantiated in one another. This is useful to create multiple complex parts with their own animations or scripts from which you compose a larger world. - -To instantiate a scene use [Node_SetInstance] to assign an [Instance] component to a node. To perform explicit instantiation use [Node_SetupInstanceFromAssets] or [Node_SetupInstanceFromFile]. - -*Note:* Instances are automatically setup when loading a scene. - -After instantiation, the instance content is held in the host scene. [Node_GetInstanceSceneView] can be used to access it in isolation from the host content via the returned [SceneView] object. - -## Managing Scene Resources - -Most scene resources are returned by value as generational references (see [man.Ownership]) wrapped into helper classes such as [Node], [Camera], [Light] or [Object]. - -The scene has strong ownership of the resources it manages. - -Nodes are explicitely destroyed using [Scene_DestroyNode] and components are implicitely destroyed using [Scene_GarbageCollect]. +.title Working with Scene + +A scene is a 3d world populated with [Node]. + +Nodes are container objects taking meaning through the use of components. + +## Node & Components + +Calling [Scene_CreateNode] returns an empty node with no component attached. In this state, it serves little to no purpose as it will not be drawn or implement any concrete behavior. + +### Object + +In order to be drawn a node must provide two essential informations: + +- *A transformation:* This is done using [Node_SetTransform] to assign it a [Transform] component. +- *A visual representation:* This is done using [Node_SetObject] to assign it an [Object] component. + +The object component accepts a [ModelRef] to a [Model] and holds a local list of [Material] used to draw the model. Each object component may hold different material definition for the same model. + +The same components can be assigned to multiple nodes. + +### Camera + +Assign the [Camera] component to nodes to turn them into observers into the scene. + +For more information on how this integrates with drawing a scene, see [man.DrawingScene]. + +### Light + +Assign the [Light] component to nodes to turn them into light sources. + +### Instance + +Scenes can be instantiated in one another. This is useful to create multiple complex parts with their own animations or scripts from which you compose a larger world. + +To instantiate a scene use [Node_SetInstance] to assign an [Instance] component to a node. To perform explicit instantiation use [Node_SetupInstanceFromAssets] or [Node_SetupInstanceFromFile]. + +*Note:* Instances are automatically setup when loading a scene. + +After instantiation, the instance content is held in the host scene. [Node_GetInstanceSceneView] can be used to access it in isolation from the host content via the returned [SceneView] object. + +## Managing Scene Resources + +Most scene resources are returned by value as generational references (see [man.Ownership]) wrapped into helper classes such as [Node], [Camera], [Light] or [Object]. + +The scene has strong ownership of the resources it manages. + +Nodes are explicitely destroyed using [Scene_DestroyNode] and components are implicitely destroyed using [Scene_GarbageCollect]. diff --git a/doc/doc/man.Scripting.md b/doc/doc/man.Scripting.md index 9bbbfa0..c6e00b0 100644 --- a/doc/doc/man.Scripting.md +++ b/doc/doc/man.Scripting.md @@ -1,84 +1,84 @@ -.title Scripting - -Scripts can be used to extend the behavior of nodes and scenes. - -[TOC] - -## Host vs. Embedded VM - -When using Harfang from a scripting language it can be difficult to differentiate between parts of your program running on you main script VM and parts of your program running on one of the supported embedded VMs. - -We differentiate between those VMs by using the term *host VM* and *embedded VM*. For example, you may write a program in CPython which declares a scene extended using Lua scripts. In this case, CPython is the *host VM* and Lua is the *embedded VM*. - -## Declaring Scripts - -Create a [Script] component and assign it to a node or scene using [Node_SetScript] or [Scene_SetScript]. Set the path to the script source using [Script_SetPath]. - -## Creating & Evaluating Scripts - -Create a backend such as [SceneLuaVM] and call [SceneLuaVM_SceneCreateScriptsFromAssets] to create the script states corresponding to the scene declaration. A [Script] component can be assigned to multiple nodes in which case they will all share the same execution environment. - -You should then use the [SceneUpdateSystems] function to update both the scene and its systems. This function will update all the systems you pass to it and implement a default behavior that dispatch common events to script using a set of specific callbacks. - -## Script Environment - -The following symbols are defined when creating the environment for a script. - -Symbol | Description ------- | ----------- -G | Table shared by all script component created by the same scene. -hg | Access to the Harfang API. -scene | Scene object this component belongs to. - -### Default Events & Callbacks - -Node events reported by the default update behavior: - -- *OnAttachToNode(Node node, int slot_index)*: A script component was attached to a node slot as a result of calling [Node_SetScript]. -- *OnDetachFromNode(Node node, int slot_index)*: A script component was detached from a node slot as a result of calling [Node_RemoveScript]. -- *OnDestroy()*: A script component is about to be destroyed and its memory released. -- *OnUpdate(Node node, time_ns dt)*: Called during a scene update for each node a script component is attached to. -- *OnCollision(Node a, Node b)*: Called when two node collide. - -Scene events reported by the default update behavior: - -- *OnDestroy()*: A script component is about to be destroyed and its memory released. -- *OnAttachToScene(scene, slot_idx)*: A script component was attached to a Scene slot as a result of calling [Scene_SetScript]. -- *OnDetachFromScene(scene, slot_idx)*: A script component was detached from a Scene slot as a result of calling [Scene_RemoveScript]. -- *OnUpdate(Scene scene, time_ns dt)*: Called during a scene update. -- *OnSubmitSceneToForwardPipeline(ViewId base_view_id, Scene scene, Rect rect, ViewState view_state, ForwardPipeline pipeline, PipelineResources, FramebufferHandle fb)*: Called at the end of a scene submission to the forward pipeline. - -### Communicating with Scripts - -Depending on the host and embedded VM used you may be able to access a script component environment directly (eg. [SceneLuaVM_GetScriptEnv]). - -If this is not possible however this can be done using the backend get and set value methods from any host VM. In some cases the transfer will be automatic while in other cases you may need to explicitely marshall the outgoing/incoming values. - -Source VM/Target VM | CPython | Lua ---------------|---------|----- -CPython | N/A | `LuaObject.Pack`/`LuaObject.Unpack` -Lua | N/A | - - -Sending a value from a host CPython VM to an embedded Lua VM and back. - -```python -# from host Python to embedded Lua -lua_vm.SetScriptValue('target_node', lua_vm.MakeLuaObject().Pack(node)) - -# from embedded Lua to host Python -target_node = lua_vm.GetScriptValue('target_node').Unpack() -``` - -Sending a value from a host Lua VM to an embedded Lua VM and back. - -```lua --- from host Lua to embedded Lua -lua_vm.SetScriptValue('target_node', node) - --- from embedded Lua to host Lua -node = lua_vm.GetScriptValue('target_node') -``` - -## Keeping the System Synchronized - -Call the script system garbage collect method (eg. [SceneLuaVM_GarbageCollect]) on each update to ensure that destroyed nodes or components are properly removed. If you know that no node or component was destroyed during a particular update, not calling the garbage collector will save on performance. +.title Scripting + +Scripts can be used to extend the behavior of nodes and scenes. + +[TOC] + +## Host vs. Embedded VM + +When using Harfang from a scripting language it can be difficult to differentiate between parts of your program running on you main script VM and parts of your program running on one of the supported embedded VMs. + +We differentiate between those VMs by using the term *host VM* and *embedded VM*. For example, you may write a program in CPython which declares a scene extended using Lua scripts. In this case, CPython is the *host VM* and Lua is the *embedded VM*. + +## Declaring Scripts + +Create a [Script] component and assign it to a node or scene using [Node_SetScript] or [Scene_SetScript]. Set the path to the script source using [Script_SetPath]. + +## Creating & Evaluating Scripts + +Create a backend such as [SceneLuaVM] and call [SceneLuaVM_SceneCreateScriptsFromAssets] to create the script states corresponding to the scene declaration. A [Script] component can be assigned to multiple nodes in which case they will all share the same execution environment. + +You should then use the [SceneUpdateSystems] function to update both the scene and its systems. This function will update all the systems you pass to it and implement a default behavior that dispatch common events to script using a set of specific callbacks. + +## Script Environment + +The following symbols are defined when creating the environment for a script. + +Symbol | Description +------ | ----------- +G | Table shared by all script component created by the same scene. +hg | Access to the Harfang API. +scene | Scene object this component belongs to. + +### Default Events & Callbacks + +Node events reported by the default update behavior: + +- *OnAttachToNode(Node node, int slot_index)*: A script component was attached to a node slot as a result of calling [Node_SetScript]. +- *OnDetachFromNode(Node node, int slot_index)*: A script component was detached from a node slot as a result of calling [Node_RemoveScript]. +- *OnDestroy()*: A script component is about to be destroyed and its memory released. +- *OnUpdate(Node node, time_ns dt)*: Called during a scene update for each node a script component is attached to. +- *OnCollision(Node a, Node b)*: Called when two node collide. + +Scene events reported by the default update behavior: + +- *OnDestroy()*: A script component is about to be destroyed and its memory released. +- *OnAttachToScene(scene, slot_idx)*: A script component was attached to a Scene slot as a result of calling [Scene_SetScript]. +- *OnDetachFromScene(scene, slot_idx)*: A script component was detached from a Scene slot as a result of calling [Scene_RemoveScript]. +- *OnUpdate(Scene scene, time_ns dt)*: Called during a scene update. +- *OnSubmitSceneToForwardPipeline(ViewId base_view_id, Scene scene, Rect rect, ViewState view_state, ForwardPipeline pipeline, PipelineResources, FramebufferHandle fb)*: Called at the end of a scene submission to the forward pipeline. + +### Communicating with Scripts + +Depending on the host and embedded VM used you may be able to access a script component environment directly (eg. [SceneLuaVM_GetScriptEnv]). + +If this is not possible however this can be done using the backend get and set value methods from any host VM. In some cases the transfer will be automatic while in other cases you may need to explicitely marshall the outgoing/incoming values. + +Source VM/Target VM | CPython | Lua +--------------|---------|----- +CPython | N/A | `LuaObject.Pack`/`LuaObject.Unpack` +Lua | N/A | - + +Sending a value from a host CPython VM to an embedded Lua VM and back. + +```python +# from host Python to embedded Lua +lua_vm.SetScriptValue('target_node', lua_vm.MakeLuaObject().Pack(node)) + +# from embedded Lua to host Python +target_node = lua_vm.GetScriptValue('target_node').Unpack() +``` + +Sending a value from a host Lua VM to an embedded Lua VM and back. + +```lua +-- from host Lua to embedded Lua +lua_vm.SetScriptValue('target_node', node) + +-- from embedded Lua to host Lua +node = lua_vm.GetScriptValue('target_node') +``` + +## Keeping the System Synchronized + +Call the script system garbage collect method (eg. [SceneLuaVM_GarbageCollect]) on each update to ensure that destroyed nodes or components are properly removed. If you know that no node or component was destroyed during a particular update, not calling the garbage collector will save on performance. diff --git a/doc/doc/man.Shader.md b/doc/doc/man.Shader.md index 8b5fd2b..0166f7e 100644 --- a/doc/doc/man.Shader.md +++ b/doc/doc/man.Shader.md @@ -1,99 +1,99 @@ -.title Writing a Shader - -[TOC] - -## Shader Language Overview - -Harfang uses [bgfx](https://bkaradzic.github.io/bgfx/index.html) as its rendering system, the cross-platform shader language is based on [GLSL](https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.40.pdf) with a few key differences: - -* No `bool/int` uniforms, all uniforms must be `float`. -* Attributes and varyings can be accessed only from `main()` function. -* Must use `SAMPLER2D/3D/CUBE/etc.` macros instead of `sampler2D/3D/Cube/etc.` tokens. -* Must use `vec2/3/4_splat()` instead of `vec2/3/4()`. -* Must use `mtxFromCols/mtxFromRows` when constructing matrices in shaders. -* Must use `mul(x, y)` when multiplying vectors and matrices. -* Must use `varying.def.sc` to define input/output semantic and precision instead of using `attribute/in` and `varying/in/out`. -* `$input/$output` tokens must appear at the begining of shader. - -## Data Flow - -In a shader, vertex attributes such as position or normal are send to the vertex shader as a stream of **attributes**. The vertex shader outputs are then interpolated across the rendered primitive and passed to the fragment shader as **varyings** to compute the final pixel color. - -## Writing a Shader - -A shader is composed of 3 files: a definition file, the vertex and fragment source files: - -- `example_vs.sc`: Source for the vertex program. -- `example_fs.sc`: Source for the fragment program. -- `example_varying.def`: Shader definition file. - -Both the vertex and fragment program must declare a main function. - -```glsl -void main() { ... } -``` - -The shader definition file must list all inputs and outputs of the shader programs and associate them with standard semantics like `POSITION` or `NORMAL` (see [HLSL Semantics](https://docs.microsoft.com/fr-fr/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics?redirectedfrom=MSDN) for a complete list). - -```glsl -vec3 vNormal : NORMAL; - -vec3 a_position : POSITION; -vec3 a_normal : NORMAL; -``` - -In the vertex program use `$input` to declare an attribute and `$output` to declare a varying. `$input/$output` tokens must appear at the begining of the program. - -By convention attributes must be named one of the following: `a_position`, `a_normal`, `a_tangent`, `a_bitangent`, `a_color0`, `a_color1`, `a_color2`, `a_color3`, `a_indices`, `a_weight`, `a_texcoord0`, `a_texcoord1`, `a_texcoord2`, `a_texcoord3`, `a_texcoord4`, `a_texcoord5`, `a_texcoord6`, `a_texcoord7`, `i_data0`, `i_data1`, `i_data2`, `i_data3` or `i_data4`. - -The following vertex program declares that it takes two attributes as input and outputs to a single varying. - -```glsl -$input a_position, a_normal -$output vNormal - -#include - -void main() { - vNormal = mul(u_model[0], vec4(a_normal * 2.0 - 1.0, 0.0)).xyz; - gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); -} -``` - -**Note:** Attributes and varyings can only be accessed from the shader main function. - -Outputs from the vertex program then become inputs to the fragment program. The fragment program outputs its result to standard GLSL variables such as `gl_FragColor` (see [GLSL Language Specifications](https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.40.pdf)). - -```glsl -$input vNormal - -#include - -void main() { - vec3 normal = normalize(vNormal); - gl_FragColor = vec4(normal.x, normal.y, normal.z, 1.0); -} -``` - -## Passing Constants to a Shader - -Constant values can be passed to a shader programs by using **uniforms**. This is done using [UniformSetValue] and [UniformSetTexture]. - -### Predefined Uniforms - -The `bgfx_shader.sh` include file defines the following predefined uniforms. - -Type | Symbol | Description ----- | ------ | ----------- -vec4 | u_viewRect | View rectangle for current view, in pixels. (x, y, width, height) -vec4 | u_viewTexel | Inverse width and height. (1.0 / width, 1.0 / height, undef, undef) -mat4 | u_view | View matrix. -mat4 | u_invView | Inverse view matrix. -mat4 | u_proj | Projection matrix. -mat4 | u_invProj | Inverse projection matrix. -mat4 | u_viewProj | Concaneted view projection matrix. -mat4 | u_invViewProj | Concatenated inverted view projection matrix. -mat4 | u_model[BGFX_CONFIG_MAX_BONES] | Array of model matrices. -mat4 | u_modelView | Concatenated model view matrix, only the first model matrix from array is used. -mat4 | u_modelViewProj | Concatenated model view projection matrix. -vec4 | u_alphaRef | Alpha reference value for alpha test. +.title Writing a Shader + +[TOC] + +## Shader Language Overview + +Harfang uses [bgfx](https://bkaradzic.github.io/bgfx/index.html) as its rendering system, the cross-platform shader language is based on [GLSL](https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.40.pdf) with a few key differences: + +* No `bool/int` uniforms, all uniforms must be `float`. +* Attributes and varyings can be accessed only from `main()` function. +* Must use `SAMPLER2D/3D/CUBE/etc.` macros instead of `sampler2D/3D/Cube/etc.` tokens. +* Must use `vec2/3/4_splat()` instead of `vec2/3/4()`. +* Must use `mtxFromCols/mtxFromRows` when constructing matrices in shaders. +* Must use `mul(x, y)` when multiplying vectors and matrices. +* Must use `varying.def.sc` to define input/output semantic and precision instead of using `attribute/in` and `varying/in/out`. +* `$input/$output` tokens must appear at the begining of shader. + +## Data Flow + +In a shader, vertex attributes such as position or normal are send to the vertex shader as a stream of **attributes**. The vertex shader outputs are then interpolated across the rendered primitive and passed to the fragment shader as **varyings** to compute the final pixel color. + +## Writing a Shader + +A shader is composed of 3 files: a definition file, the vertex and fragment source files: + +- `example_vs.sc`: Source for the vertex program. +- `example_fs.sc`: Source for the fragment program. +- `example_varying.def`: Shader definition file. + +Both the vertex and fragment program must declare a main function. + +```glsl +void main() { ... } +``` + +The shader definition file must list all inputs and outputs of the shader programs and associate them with standard semantics like `POSITION` or `NORMAL` (see [HLSL Semantics](https://docs.microsoft.com/fr-fr/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics?redirectedfrom=MSDN) for a complete list). + +```glsl +vec3 vNormal : NORMAL; + +vec3 a_position : POSITION; +vec3 a_normal : NORMAL; +``` + +In the vertex program use `$input` to declare an attribute and `$output` to declare a varying. `$input/$output` tokens must appear at the begining of the program. + +By convention attributes must be named one of the following: `a_position`, `a_normal`, `a_tangent`, `a_bitangent`, `a_color0`, `a_color1`, `a_color2`, `a_color3`, `a_indices`, `a_weight`, `a_texcoord0`, `a_texcoord1`, `a_texcoord2`, `a_texcoord3`, `a_texcoord4`, `a_texcoord5`, `a_texcoord6`, `a_texcoord7`, `i_data0`, `i_data1`, `i_data2`, `i_data3` or `i_data4`. + +The following vertex program declares that it takes two attributes as input and outputs to a single varying. + +```glsl +$input a_position, a_normal +$output vNormal + +#include + +void main() { + vNormal = mul(u_model[0], vec4(a_normal * 2.0 - 1.0, 0.0)).xyz; + gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); +} +``` + +**Note:** Attributes and varyings can only be accessed from the shader main function. + +Outputs from the vertex program then become inputs to the fragment program. The fragment program outputs its result to standard GLSL variables such as `gl_FragColor` (see [GLSL Language Specifications](https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.40.pdf)). + +```glsl +$input vNormal + +#include + +void main() { + vec3 normal = normalize(vNormal); + gl_FragColor = vec4(normal.x, normal.y, normal.z, 1.0); +} +``` + +## Passing Constants to a Shader + +Constant values can be passed to a shader programs by using **uniforms**. This is done using [UniformSetValue] and [UniformSetTexture]. + +### Predefined Uniforms + +The `bgfx_shader.sh` include file defines the following predefined uniforms. + +Type | Symbol | Description +---- | ------ | ----------- +vec4 | u_viewRect | View rectangle for current view, in pixels. (x, y, width, height) +vec4 | u_viewTexel | Inverse width and height. (1.0 / width, 1.0 / height, undef, undef) +mat4 | u_view | View matrix. +mat4 | u_invView | Inverse view matrix. +mat4 | u_proj | Projection matrix. +mat4 | u_invProj | Inverse projection matrix. +mat4 | u_viewProj | Concaneted view projection matrix. +mat4 | u_invViewProj | Concatenated inverted view projection matrix. +mat4 | u_model[BGFX_CONFIG_MAX_BONES] | Array of model matrices. +mat4 | u_modelView | Concatenated model view matrix, only the first model matrix from array is used. +mat4 | u_modelViewProj | Concatenated model view projection matrix. +vec4 | u_alphaRef | Alpha reference value for alpha test. diff --git a/doc/doc/man.Toolchain.md b/doc/doc/man.Toolchain.md deleted file mode 100644 index 51e2325..0000000 --- a/doc/doc/man.Toolchain.md +++ /dev/null @@ -1 +0,0 @@ -.title Toolchain \ No newline at end of file diff --git a/doc/doc/man.TutorialFilesystemAssetsFolder.md b/doc/doc/man.TutorialFilesystemAssetsFolder.md deleted file mode 100644 index 852a5b3..0000000 --- a/doc/doc/man.TutorialFilesystemAssetsFolder.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Assets folders - -.tutorial(goal="Access to the file system through assets folders", level="Intermediate", duration=15, lang=All, group="Filesystem") - -#### Python - -```python -[import:tutorials/filesystem/1_assets_folder.py] -``` - -#### Lua - -```lua -[import:tutorials/filesystem/1_assets_folder.lua] -``` diff --git a/doc/doc/man.TutorialFilesystemRecursiveDirectoryListing.md b/doc/doc/man.TutorialFilesystemRecursiveDirectoryListing.md deleted file mode 100644 index 5f56f6a..0000000 --- a/doc/doc/man.TutorialFilesystemRecursiveDirectoryListing.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Recursive walk - -.tutorial(goal="Walk recursively though the content of a directory", level="Beginner", duration=5, lang=All, group="Filesystem") - -#### Python - -```python -[import:tutorials/filesystem/3_Recursive_directory_listing.py] -``` - -#### Lua - -```lua -[import:tutorials/filesystem/3_Recursive_directory_listing.lua] -``` diff --git a/doc/doc/man.TutorialGUIBasicImGui.md b/doc/doc/man.TutorialGUIBasicImGui.md deleted file mode 100644 index 9802c14..0000000 --- a/doc/doc/man.TutorialGUIBasicImGui.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Basic GUI - -.tutorial(goal="Create and display a basic GUI using ImGui", level="Beginner", duration=15, lang=All, group="GUI") - -#### Python - -```python -[import:tutorials/imgui/1_basic.py] -``` - -#### Lua - -```lua -[import:tutorials/imgui/1_basic.lua] -``` diff --git a/doc/doc/man.TutorialImmediateRenderingBasicLoop.md b/doc/doc/man.TutorialImmediateRenderingBasicLoop.md deleted file mode 100644 index 1ea7db0..0000000 --- a/doc/doc/man.TutorialImmediateRenderingBasicLoop.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Basic loop - -.tutorial(goal="Basic immediate rendering loop", level="Beginner", duration=10, lang=All, group="Immediate rendering") - -#### Python - -```python -[import:tutorials/immediate/1_basic_loop.py] -``` - -#### Lua - -```lua -[import:tutorials/immediate/1_basic_loop.lua] -``` diff --git a/doc/doc/man.TutorialImmediateRenderingNoPipeline.md b/doc/doc/man.TutorialImmediateRenderingNoPipeline.md deleted file mode 100644 index 43579a3..0000000 --- a/doc/doc/man.TutorialImmediateRenderingNoPipeline.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Immediate scene - -.tutorial(goal="Immediate rendering of a scene without a pipeline", level="Beginner", duration=10, lang=All, group="Immediate rendering") - -#### Python - -```python -[import:tutorials/immediate/3_scene_no_pipeline.py] -``` - -#### Lua - -```lua -[import:tutorials/immediate/3_scene_no_pipeline.lua] -``` diff --git a/doc/doc/man.TutorialInputListDevices.md b/doc/doc/man.TutorialInputListDevices.md deleted file mode 100644 index 85b0ce5..0000000 --- a/doc/doc/man.TutorialInputListDevices.md +++ /dev/null @@ -1,15 +0,0 @@ -.title List devices - -.tutorial(goal="List the input devices", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/1_list_devices.py] -``` - -#### Lua - -```lua -[import:tutorials/input/1_list_devices.lua] -``` diff --git a/doc/doc/man.TutorialInputPad.md b/doc/doc/man.TutorialInputPad.md deleted file mode 100644 index 6d6fa61..0000000 --- a/doc/doc/man.TutorialInputPad.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Input pad - -.tutorial(goal="How to read values from the joypad - Using Joypad object", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/8_read_pad.py] -``` - -#### Lua - -```lua -[import:tutorials/input/8_read_pad.lua] -``` diff --git a/doc/doc/man.TutorialInputPad2.md b/doc/doc/man.TutorialInputPad2.md deleted file mode 100644 index ce642af..0000000 --- a/doc/doc/man.TutorialInputPad2.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Input pad 2 - -.tutorial(goal="How to read values from the gamepad - Using hg.ReadGamepad", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/9_read_pad_2.py] -``` - -#### Lua - -```lua -[import:tutorials/input/9_read_pad_2.lua] -``` diff --git a/doc/doc/man.TutorialInputReadKeyboard.md b/doc/doc/man.TutorialInputReadKeyboard.md deleted file mode 100644 index e9133b2..0000000 --- a/doc/doc/man.TutorialInputReadKeyboard.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Read keyboard - -.tutorial(goal="How to read from the keyboard", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/2_read_keyboard.py] -``` - -#### Lua - -```lua -[import:tutorials/input/2_read_keyboard.lua] -``` diff --git a/doc/doc/man.TutorialInputReadKeyboard2.md b/doc/doc/man.TutorialInputReadKeyboard2.md deleted file mode 100644 index 18b2e2a..0000000 --- a/doc/doc/man.TutorialInputReadKeyboard2.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Read keyboard 2 - -.tutorial(goal="How to read from the keyboard - Using hg.ReadKeybard('default') function", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/3_read_keyboard_2.py] -``` - -#### Lua - -```lua -[import:tutorials/input/3_read_keyboard_2.lua] -``` diff --git a/doc/doc/man.TutorialInputReadKeyboard3.md b/doc/doc/man.TutorialInputReadKeyboard3.md deleted file mode 100644 index 4d48616..0000000 --- a/doc/doc/man.TutorialInputReadKeyboard3.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Read keyboard 3 - -.tutorial(goal="How to read from the keyboard - Using hg.ReadKeyboard('raw') function", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/4_read_keyboard_3.py] -``` - -#### Lua - -```lua -[import:tutorials/input/4_read_keyboard_3.lua] -``` diff --git a/doc/doc/man.TutorialInputReadMouse.md b/doc/doc/man.TutorialInputReadMouse.md deleted file mode 100644 index 9ecee3a..0000000 --- a/doc/doc/man.TutorialInputReadMouse.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Read mouse - -.tutorial(goal="How to read values from the mouse - Using Mouse object", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/5_read_mouse.py] -``` - -#### Lua - -```lua -[import:tutorials/input/5_read_mouse.lua] -``` diff --git a/doc/doc/man.TutorialInputReadMouse2.md b/doc/doc/man.TutorialInputReadMouse2.md deleted file mode 100644 index aa9e014..0000000 --- a/doc/doc/man.TutorialInputReadMouse2.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Read mouse 2 - -.tutorial(goal="How to read values from the mouse - Using hg.ReadMouse", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/6_read_mouse_2.py] -``` - -#### Lua - -```lua -[import:tutorials/input/6_read_mouse_2.lua] -``` diff --git a/doc/doc/man.TutorialInputReadMouse3.md b/doc/doc/man.TutorialInputReadMouse3.md deleted file mode 100644 index 4377194..0000000 --- a/doc/doc/man.TutorialInputReadMouse3.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Read mouse 3 - -.tutorial(goal="How to read values from the mouse - Using hg.ReadMouse('raw')", level="Beginner", duration=10, lang=All, group="Input") - -#### Python - -```python -[import:tutorials/input/7_read_mouse_3.py] -``` - -#### Lua - -```lua -[import:tutorials/input/7_read_mouse_3.lua] -``` diff --git a/doc/doc/man.TutorialPhysicsCubes.md b/doc/doc/man.TutorialPhysicsCubes.md deleted file mode 100644 index aeeff88..0000000 --- a/doc/doc/man.TutorialPhysicsCubes.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Physics cubes - -.tutorial(goal="Add and remove many physics objects", level="Intermediate", duration=10, lang=All, group="Physics") - -#### Python - -```python -[import:tutorials/physics/2_physics_cubes.py] -``` - -#### Lua - -```lua -[import:tutorials/physics/2_physics_cubes.lua] -``` diff --git a/doc/doc/man.TutorialPhysicsImpulse.md b/doc/doc/man.TutorialPhysicsImpulse.md deleted file mode 100644 index 657fc76..0000000 --- a/doc/doc/man.TutorialPhysicsImpulse.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Impulse - -.tutorial(goal="Apply an impulse to a rigid body", level="Intermediate", duration=10, lang=All, group="Physics") - -#### Python - -```python -[import:tutorials/physics/1_impulse.py] -``` - -#### Lua - -```lua -[import:tutorials/physics/1_impulse.lua] -``` diff --git a/doc/doc/man.TutorialPhysicsOverridesMatrix.md b/doc/doc/man.TutorialPhysicsOverridesMatrix.md deleted file mode 100644 index f1022aa..0000000 --- a/doc/doc/man.TutorialPhysicsOverridesMatrix.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Physics overrides matrix - -.tutorial(goal="Physics overrides Transform's matrix", level="Intermediate", duration=10, lang=All, group="Physics") - -#### Python - -```python -[import:tutorials/physics/3_test_physics_overrides_matrix.py] -``` - -#### Lua - -```lua -[import:tutorials/physics/3_test_physics_overrides_matrix.lua] -``` diff --git a/doc/doc/man.TutorialPictureLoad.md b/doc/doc/man.TutorialPictureLoad.md deleted file mode 100644 index f4041a1..0000000 --- a/doc/doc/man.TutorialPictureLoad.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Load picture - -.tutorial(goal="Load a picture", level="Beginner", duration=5, lang=All, group="Picture") - -#### Python - -```python -[import:tutorials/picture/1_load_picture.py] -``` - -#### Lua - -```python -[import:tutorials/picture/1_load_picture.py] -``` \ No newline at end of file diff --git a/doc/doc/man.TutorialPictureSave.md b/doc/doc/man.TutorialPictureSave.md deleted file mode 100644 index 9adec37..0000000 --- a/doc/doc/man.TutorialPictureSave.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Save picture - -.tutorial(goal="Save a picture", level="Beginner", duration=5, lang=All, group="Picture") - -#### Python - -```python -[import:tutorials/picture/2_save_picture.py] -``` - -#### Lua - -```python -[import:tutorials/picture/2_save_picture.py] -``` \ No newline at end of file diff --git a/doc/doc/man.TutorialSceneDynamicsObjects.md b/doc/doc/man.TutorialSceneDynamicsObjects.md deleted file mode 100644 index 15eb1bf..0000000 --- a/doc/doc/man.TutorialSceneDynamicsObjects.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Dynamics objects - -.tutorial(goal="Render and animate a large amount of objects", level="Beginner", duration=15, lang=All, group="Scene") - -#### Python - -```python -[import:tutorials/scene/2_many_dynamics_objects.py] -``` - -#### Lua - -```lua -[import:tutorials/scene/2_many_dynamics_objects.lua] -``` diff --git a/doc/doc/man.TutorialSceneKaplaShooting.md b/doc/doc/man.TutorialSceneKaplaShooting.md deleted file mode 100644 index a5df33d..0000000 --- a/doc/doc/man.TutorialSceneKaplaShooting.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Physics kapla shooting - -.tutorial(goal="Shoot boulders on a physics kapla tower", level="Intermediate", duration=30, lang=All, group="Scene") - -#### Python - -```python -[import:tutorials/scene/5_kapla_shooting.py] -``` - -#### Lua - -```lua -[import:tutorials/scene/5_kapla_shooting.lua] -``` diff --git a/doc/doc/man.TutorialSceneKaplaTower.md b/doc/doc/man.TutorialSceneKaplaTower.md deleted file mode 100644 index cd118f9..0000000 --- a/doc/doc/man.TutorialSceneKaplaTower.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Physics kapla tower - -.tutorial(goal="Physics simulation of a Kapla tower", level="Intermediate", duration=30, lang=All, group="Scene") - -#### Python - -```python -[import:tutorials/scene/3_scene_kapla_tower.py] -``` - -#### Lua - -```lua -[import:tutorials/scene/3_scene_kapla_tower.lua] -``` diff --git a/doc/doc/man.TutorialSceneLightPriority.md b/doc/doc/man.TutorialSceneLightPriority.md deleted file mode 100644 index 4633dd3..0000000 --- a/doc/doc/man.TutorialSceneLightPriority.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Light priority - -.tutorial(goal="How light priority works", level="Beginner", duration=10, lang=All, group="Scene") - -#### Python - -```python -[import:tutorials/scene/6_light_priority.py] -``` - -#### Lua - -```lua -[import:tutorials/scene/6_light_priority.lua] -``` diff --git a/doc/doc/man.TutorialSceneMultiViewport.md b/doc/doc/man.TutorialSceneMultiViewport.md deleted file mode 100644 index 7fc99f7..0000000 --- a/doc/doc/man.TutorialSceneMultiViewport.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Scene multi viewports - -.tutorial(goal="Split the display of a scene in several viewports", level="Beginner", duration=10, lang=All, group="Scene") - -#### Python - -```python -[import:tutorials/scene/4_multi_viewport.py] -``` - -#### Lua - -```lua -[import:tutorials/scene/4_multi_viewport.lua] -``` diff --git a/doc/doc/man.TutorialSceneWithPipeline.md b/doc/doc/man.TutorialSceneWithPipeline.md deleted file mode 100644 index 2c6bfcd..0000000 --- a/doc/doc/man.TutorialSceneWithPipeline.md +++ /dev/null @@ -1,15 +0,0 @@ -.title Scene - -.tutorial(goal="Setup and render a 3D scene with the forward pipeline", level="Beginner", duration=10, lang=All, group="Scene") - -#### Python - -```python -[import:tutorials/scene/1_scene.py] -``` - -#### Lua - -```lua -[import:tutorials/scene/1_scene.lua] -``` diff --git a/doc/doc/man.Tutorials.md b/doc/doc/man.Tutorials.md deleted file mode 100644 index 6fb89bd..0000000 --- a/doc/doc/man.Tutorials.md +++ /dev/null @@ -1,5 +0,0 @@ -.title Tutorials - -The following tutorials demonstrate usage of the Harfang API. A complete archive of the tutorials and their associated datas can be downloaded from [GitHub](https://github.com/harfang3d/tutorials-hg2/releases). - -%TutorialIndex% diff --git a/doc/doc/man.VR.md b/doc/doc/man.VR.md index c8f64b2..12f10c6 100644 --- a/doc/doc/man.VR.md +++ b/doc/doc/man.VR.md @@ -1,71 +1,71 @@ -.title Virtual Reality - -Virtual reality is supported through the OpenVR API on the Windows operating system. - -[TOC] - -## Prerequisites - -- A compatible VR headset (HTC Vive, Occulus Rift or Mixed Reality compatible). -- SteamVR must be installed on your computer. - -## OpenVR API - -Initialize OpenVR using [OpenVRInit], when done call [OpenVRShutdown]. You can initialize and shutdown OpenVR multiple times throughout your program lifetime. - -Create two framebuffer to render both eyes to by calling [OpenVRCreateEyeFrameBuffer]. These buffers can be kept alive between a shutdown/init cycle of OpenVR. - -### Main Loop - -During your program main loop perform the following steps: - -1. Read the current VR state using [OpenVRGetState]. This function takes the transformation of the actor body in the virtual world and returns a state object containing everything required to draw the left and right eye views. -1. Use [OpenVRStateToViewState] function to generate the left and right eye [ViewState]. -1. For each eye: - 1. Use [OpenVREyeFrameBuffer_GetHandle] to retrieve a framebuffer handle. - 2. Use [SubmitSceneToPipeline] to draw the scene to the eye framebuffer. To improve performance consider using the low-level APIs to draw the scene, see [man.DrawingScene]. -1. Kick-off rendering by calling [Frame]. -1. Submit eye framebuffers to the device using [OpenVRSubmitFrame]. - -The following Python code implements the complete render loop to efficiently display a scene in VR. - -```python -scene.Update(hg.TickClock()) - -vr_state = hg.OpenVRGetState(cam.GetTransform().GetWorld(), cam.GetCamera().GetZNear(), cam.GetCamera().GetZFar()) -left, right = hg.OpenVRStateToViewState(vr_state) -vr_eye_rect = hg.IntRect(0, 0, vr_state.width, vr_state.height) - -# common, view-independent render data -vid = 0 -vid, pass_ids = hg.PrepareSceneForwardPipelineCommonRenderData(vid, scene, render_data, pipeline, res) - -# view-dependent render data and submit -vid, pass_ids = hg.PrepareSceneForwardPipelineViewDependentRenderData(vid, left, scene, render_data, pipeline, res) -vid, pass_ids = hg.SubmitSceneToForwardPipeline(vid, scene, vr_eye_rect, left, pipeline, render_data, res, vr_left_fb.GetHandle()) - -vid, pass_ids = hg.PrepareSceneForwardPipelineViewDependentRenderData(vid, right, scene, render_data, pipeline, res) -vid, pass_ids = hg.SubmitSceneToForwardPipeline(vid, scene, vr_eye_rect, right, pipeline, render_data, res, vr_right_fb.GetHandle()) - -# start rendering -hg.Frame() - -# submit to VR device -hg.OpenVRSubmitFrame(vr_left_fb, vr_right_fb) -``` - -### Reading Input - -Reading input from the VR controllers or trackers is done using the [ReadVRController] and [ReadVRGenericTracker] functions. See [man.Input] for more information on reading inputs in general. - -## Performance Notes - -VR is extremely costly in terms of performance. Depending on the headset, the minumum required display refresh rate can vary between 90Hz and 120Hz. - -At least two different renders, one for both eyes, are required. You might occasionally need a third render to monitor the simulation from an external point of view. - -As a result of these requirements, keep in mind the following points: - -- You should minimize the amount of work required per scene render. See [man.DrawingScene] to learn how to use the low-level API to efficiently render a scene for multiple observers. -- Linear light shadow mapping is extremely costly. As they are view-dependent, PSSM splits must be drawn from scratch for each viewpoint. Consider using spot light shadow mapping which is not view-dependent and can be performed once for all viewpoints. -- Given the number of scene pass required, the triangle budget must kept under control and will usually be significantly lower than usual. +.title Virtual Reality + +Virtual reality is supported through the OpenVR API on the Windows operating system. + +[TOC] + +## Prerequisites + +- A compatible VR headset (HTC Vive, Occulus Rift or Mixed Reality compatible). +- SteamVR must be installed on your computer. + +## OpenVR API + +Initialize OpenVR using [OpenVRInit], when done call [OpenVRShutdown]. You can initialize and shutdown OpenVR multiple times throughout your program lifetime. + +Create two framebuffer to render both eyes to by calling [OpenVRCreateEyeFrameBuffer]. These buffers can be kept alive between a shutdown/init cycle of OpenVR. + +### Main Loop + +During your program main loop perform the following steps: + +1. Read the current VR state using [OpenVRGetState]. This function takes the transformation of the actor body in the virtual world and returns a state object containing everything required to draw the left and right eye views. +1. Use [OpenVRStateToViewState] function to generate the left and right eye [ViewState]. +1. For each eye: + 1. Use [OpenVREyeFrameBuffer_GetHandle] to retrieve a framebuffer handle. + 2. Use [SubmitSceneToPipeline] to draw the scene to the eye framebuffer. To improve performance consider using the low-level APIs to draw the scene, see [man.DrawingScene]. +1. Kick-off rendering by calling [Frame]. +1. Submit eye framebuffers to the device using [OpenVRSubmitFrame]. + +The following Python code implements the complete render loop to efficiently display a scene in VR. + +```python +scene.Update(hg.TickClock()) + +vr_state = hg.OpenVRGetState(cam.GetTransform().GetWorld(), cam.GetCamera().GetZNear(), cam.GetCamera().GetZFar()) +left, right = hg.OpenVRStateToViewState(vr_state) +vr_eye_rect = hg.IntRect(0, 0, vr_state.width, vr_state.height) + +# common, view-independent render data +vid = 0 +vid, pass_ids = hg.PrepareSceneForwardPipelineCommonRenderData(vid, scene, render_data, pipeline, res) + +# view-dependent render data and submit +vid, pass_ids = hg.PrepareSceneForwardPipelineViewDependentRenderData(vid, left, scene, render_data, pipeline, res) +vid, pass_ids = hg.SubmitSceneToForwardPipeline(vid, scene, vr_eye_rect, left, pipeline, render_data, res, vr_left_fb.GetHandle()) + +vid, pass_ids = hg.PrepareSceneForwardPipelineViewDependentRenderData(vid, right, scene, render_data, pipeline, res) +vid, pass_ids = hg.SubmitSceneToForwardPipeline(vid, scene, vr_eye_rect, right, pipeline, render_data, res, vr_right_fb.GetHandle()) + +# start rendering +hg.Frame() + +# submit to VR device +hg.OpenVRSubmitFrame(vr_left_fb, vr_right_fb) +``` + +### Reading Input + +Reading input from the VR controllers or trackers is done using the [ReadVRController] and [ReadVRGenericTracker] functions. See [man.Input] for more information on reading inputs in general. + +## Performance Notes + +VR is extremely costly in terms of performance. Depending on the headset, the minumum required display refresh rate can vary between 90Hz and 120Hz. + +At least two different renders, one for both eyes, are required. You might occasionally need a third render to monitor the simulation from an external point of view. + +As a result of these requirements, keep in mind the following points: + +- You should minimize the amount of work required per scene render. See [man.DrawingScene] to learn how to use the low-level API to efficiently render a scene for multiple observers. +- Linear light shadow mapping is extremely costly. As they are view-dependent, PSSM splits must be drawn from scratch for each viewpoint. Consider using spot light shadow mapping which is not view-dependent and can be performed once for all viewpoints. +- Given the number of scene pass required, the triangle budget must kept under control and will usually be significantly lower than usual. diff --git a/doc/doc/man.WritingAGraphicApplication.md b/doc/doc/man.WritingAGraphicApplication.md deleted file mode 100644 index 2268043..0000000 --- a/doc/doc/man.WritingAGraphicApplication.md +++ /dev/null @@ -1,46 +0,0 @@ -.title Writing a graphic application - -There many ways to write a graphic application using the library. Depending the needs of your application one approach might be much better suited than the others. The following options are available: - -Note: You can find many examples of graphic applications in the [man.Tutorials]. - -## Renderer-only application - -This is the most low-level application where most manual work is required. But for very specific needs it might make much more sense to directly use the renderer instead of trying to bend the [RenderSystem] or even a [Scene] to achieve your goals. - -This type of application is advised if you: - -* Need to render a lot of custom primitives or very dynamic content, -* Do not require complex rendering functionalities such as shadows, -* Do not need scene functionalities such as animation or scripts, -* Have sufficient knowledge to work at such a low-level. - -## RenderSystem application - -The [RenderSystem] without a [Scene] is mostly useful for the high-level drawing code it provides over the [Renderer] API. It comes at the cost of having to bundle the core resources package and, as with most high-level APIs, somewhat reduced performances and higher memory consumption. - -This type of application is advised if you: - -* Need to render a moderate amount of standard primitives with common attributes, -* Do not require complex rendering functionalities such as shadows, -* Do not need scene functionalities such as animation or scripts. - -## Scene application - -Applications making use of a [Scene] object can leverage the full set of functionalities the engine has to offer. Complete scene management extensible with Lua scripts ([man.ScriptComponent]), support for advanced rendering features such as hardware skinning, shadow-mapping or post-processing. - -Using a scene also automatically provides multi-threading benefits as the engine has knowledge of most your application graphical objects and can manage them more efficiently. - -This type of application is advised if you: - -* Need to display complex world efficiently with the typical feature set of real time modern 3d applications, - -## Plus application - -The [Plus] API is a high-level wrapper over the Harfang API. It simplifies the use of all previously mentionned APIs and is usually a good starting point for any project. - -It is however very limited in scope and complex requirements will usually require dealing directly with the low-level APIs. - -## Mixing application types - -Since all of the previously described approaches are build on the top of each other ([Scene] is renderer by the [RenderSystem] through [Renderer] with [Plus] wrapping all of those APIs) it is safe to mix different approaches to build an application. \ No newline at end of file diff --git a/doc/doc/tree_desc.txt b/doc/doc/tree_desc.txt index 375e3e1..12a2aa7 100644 --- a/doc/doc/tree_desc.txt +++ b/doc/doc/tree_desc.txt @@ -29,10 +29,3 @@ man.Scripting man.Navigation man.VR - -man.Classes -man.Functions -man.Constants -man.Enums - -man.Tutorials diff --git a/doc/doc_editor.py b/doc/doc_editor.py deleted file mode 100644 index 0dbf7fe..0000000 --- a/doc/doc_editor.py +++ /dev/null @@ -1,34 +0,0 @@ -from PyQt5.QtWidgets import QApplication -from doc_editor.main_window import MainWindow -import sys -import os -import argparse - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Internal documentation editor") - parser.add_argument('--api_path', type=str, help="API path", required=False) - parser.add_argument('--doc_path', type=str, help="Documentation path", required=False) - parser.add_argument('--out_path', type=str, help="Documentation output path", required=False) - args = parser.parse_args() - - app = QApplication(sys.argv) - - path = os.path.dirname(os.path.realpath(sys.argv[0])) - - main_window = MainWindow() - main_window.preview_css = os.path.join(path, "doc_editor", "markdown.css") - - if args.api_path: - main_window.load_api(args.api_path) - if args.doc_path: - main_window.load_doc(args.doc_path) - - if args.out_path: - main_window.build_documentation(args.out_path) - r = 0 - else: - main_window.showMaximized() - r = app.exec_() - - sys.exit(r) diff --git a/doc/doc_to_html.bat b/doc/doc_to_html.bat deleted file mode 100644 index 3cedda8..0000000 --- a/doc/doc_to_html.bat +++ /dev/null @@ -1 +0,0 @@ -python doc_to_html.py --api_path api.xml --doc_path doc.xml --out_path doc_html diff --git a/doc/doc_to_html.py b/doc/doc_to_html.py deleted file mode 100644 index c99319b..0000000 --- a/doc/doc_to_html.py +++ /dev/null @@ -1,502 +0,0 @@ -import doc_utils.doc_tools as doc_tools -import doc_utils.api_tools as api_tools -import doc_utils.html_tools as html_tools -import search_index -from functools import partial -import argparse -import shutil -import sys -import os -import datetime -from collections import OrderedDict - -gen_online_doc = None -version = "0.0.0" - -def get_element_output_path(output_path, uid, ext='.html'): - return os.path.join(output_path, uid + ext) - - -def html_link_formatter(output_path, uid, is_img_link=False): - if is_img_link: - if gen_online_doc: - return "/documentations/%s/img/%s" % (version, uid) - return "img/" + uid - - link, name, hint = get_element_output_path('', uid), uid, None - - if uid in doc_tools.man: - return "[%s](%s)" % (doc_tools.get_element_title(uid), link) - - if uid in api_tools.api: - tag = api_tools.api[uid][0] - parent = api_tools.api_parent_map[tag] - - def format_enum_values(tag): - values = [val.get('name') for val in tag.iter('entry')] - return doc_tools.list_to_natural_string(values, "or") - - def format_constants_values(tag): - values = [val.get('name') for val in tag.iter('entry')] - return doc_tools.list_to_natural_string(values, "and") - - if tag.get("global") == "1": # global symbol link - global_type_pages = {"function": "man.Functions.html", "enum": "man.Enums.html", "constants": "man.Constants.html"} - if tag.tag in global_type_pages: - link = "%s#%s" % (global_type_pages[tag.tag], uid) - name = tag.get("name") - - if tag.tag == "enum": - hint = format_enum_values(tag) - elif tag.tag == "constants": - hint = format_constants_values(tag) - else: - if parent.tag == "class": - parent_uid = parent.get("uid") - link = "%s.html#%s" % (parent_uid, uid) - name = tag.get("name") - hint = "%s.%s" % (parent.get("name"), name) - - if tag.tag == "enum": - hint += ' : ' + format_enum_values(tag) - if hint: - path = '%s' % (link, hint, name) - else: - path = '%s' % (link, name) - - return path - - -def get_header(uid): - if not gen_online_doc: - title = 'Documentation' - if uid in doc_tools.man: - title = doc_tools.get_element_title(uid) - return '

%s

' % title - - return "" - - -def get_footer(uid): - return '
%s %s documentation. Generated %s.\n' % (args.project_name, version, datetime.datetime.today().strftime('%c')) - - -def format_complete_html_man_page(uid, body, header=get_header, footer=get_footer): - if not gen_online_doc: - html = '\n' - html += '\n' - html += '\n' - if header: - html += header(uid) - html += body - if footer: - html += footer(uid) - html += '\n' - html += '\n' - return html - - html = '' - if header: - html += header(uid) - return html + body - - -def format_html_link(text, url): - if text in api_tools.api: - text = '%s' % text - return '%s' % (url, text) - - -def format_html_breadcrumb(breadcrumb): - # convert from uid to HTML link - return '' - - if len(breadcrumb) < 2: - if gen_online_doc: - return '

\n' - return '' # do not output empty breadcrumb - - links = [] - # for uid in reversed(breadcrumb[1:]): - for uid in reversed(breadcrumb): - title = doc_tools.get_element_title(uid) - path = get_element_output_path('', uid) - links.append(format_html_link(title, path)) - - # links.append(doc_tools.get_element_title(breadcrumb[0])) - - return '
%s
' % ' > '.join(links) # offline breadcrumb - - -def get_class_header(uid): - breadcrumb = doc_tools.build_man_page_breadcrumb(man_tree, 'man.Classes') - breadcrumb.insert(0, uid) - return get_header(uid) + format_html_breadcrumb(breadcrumb) - - -def get_enum_header(uid): - breadcrumb = doc_tools.build_man_page_breadcrumb(man_tree, 'man.ScriptReference') - breadcrumb.insert(0, uid) - return get_header(uid) + format_html_breadcrumb(breadcrumb) - - -def get_man_header(uid): - breadcrumb = doc_tools.build_man_page_breadcrumb(man_tree, uid) - return get_header(uid) + format_html_breadcrumb(breadcrumb) - - -get_header_fn = { - 'class': get_class_header, - 'enum': get_enum_header -} - - -def save_page(output_path, uid, html): - with open(get_element_output_path(output_path, uid), 'w', encoding='utf-8') as file: - file.write(html) - - -# ------------------------------------------------------------------------------ -man_tree = None - - -def build_doc_tree(): - # build the final documentation tree - tree = OrderedDict() - - def insert_in_tree(uid): - if uid not in man_tree: # insert root UID - if uid not in tree: - tree[uid] = OrderedDict() - return tree[uid] - - sub_tree = insert_in_tree(man_tree[uid]) - if uid not in sub_tree: - sub_tree[uid] = OrderedDict() - return sub_tree[uid] - - for uid in man_tree: - insert_in_tree(uid) - - if True: - # populate custom pages - def insert_in_uid_dict(uid, uids): - dict = insert_in_tree(uid) - for uid in uids: - if uid not in dict: - dict[uid] = {} - - insert_in_uid_dict("man.Classes", doc_tools.get_all_classes()) - insert_in_uid_dict("man.Functions", sorted(html_tools.get_tag_uids("function", True))) - insert_in_uid_dict("man.Enums", sorted(html_tools.get_tag_uids("enum", True))) - - return tree - - -def output_tree_html(path): - """ Obsolete function that creates the documentation tree automatically """ - tree = build_doc_tree() - - with open(path, 'w') as file: - def output_tree(tree, depth): - for uid, sub_tree in tree.items(): - title = doc_tools.get_element_title(uid) - if depth == 0: - file.write('
  • \n') - else: - file.write('
  • \n') - file.write('%s\n' % (uid + ".html", title, title)) - - if len(sub_tree) > 0: - file.write('
      \n') - output_tree(sub_tree, depth+1) - file.write('
    \n') - - file.write('
  • \n') - - file.write('
      \n') - output_tree(tree, 0) - file.write('
    \n') - - return True - - -def create_tree_html(path): - """ New function to create the documentation tree from a simple input file """ - with open(os.path.join(args.doc_path, "tree_desc.txt"), "r", encoding="utf-8") as file: - tree_lines = file.read().splitlines() - - class Node: - def __init__(self, name): - self.name = name - self.children = [] - - def get_link(self): - return self.name - - def __repr__(self): - return 'NODE %s' % self.name - - def to_html(self): - if self.name == '': - return '
    \n' - - str = '
  • ' - str += self.get_link() - - if len(self.children) > 0: - str += '
      \n' - for child in self.children: - str += child.to_html() - str += '
    \n' - - str += '
  • \n' - return str - - class Link(Node): - def __init__(self, link): - super().__init__(link) - - def get_link(self): - name = doc_tools.get_element_title(self.name) - - if gen_online_doc: - return '%s' % (self.name + '.html', name) - return '%s' % (self.name + '.html', name) - - def __repr__(self): - return 'LINK %s' % self.name - - last_node = Link('man.Pyindex') - stack = [last_node] - - for line in tree_lines: - current_depth = len(stack) - - # determine target depth - target_depth = 0 - while line[target_depth:target_depth+1] == '\t': - target_depth += 1 - target_depth += 1 - - # create the new node - decl = line[target_depth-1:] - - if decl.startswith("!"): - node = Node(decl[1:]) - else: - node = Link(decl) - - # make sure we only go down one level at a time - assert target_depth <= current_depth + 1 - - if target_depth > current_depth: - stack.append(last_node) - - elif target_depth < current_depth: - while target_depth < len(stack): - stack.pop() - - stack[-1].children.append(node) - last_node = node - - # output final tree - with open(path, "w", encoding="utf-8") as file: - if not gen_online_doc: - file.write('\n') - file.write('\n') - file.write('\n') - file.write('

    %s %s

    ' % (args.project_name, version)) - - file.write('
      ') - for child in stack[0].children: # skip root entry - file.write(child.to_html()) - file.write('
    ') - - if not gen_online_doc: - file.write('\n') - file.write('\n') - -def sanity_check(): - report = { - 'missing': {}, - 'obsolete': [], - 'missing_pages': [], - 'orphan_pages': [] - } - - ok = True - - for uid in doc_tools.doc.keys(): - if not uid in api_tools.api: - report['obsolete'].append(uid) - ok = ok and report['obsolete'] - - for uid, tag in api_tools.api.items(): - if not uid in doc_tools.doc: - typename = tag[0].tag - if not typename in report['missing']: - report['missing'][typename] = [] - report['missing'][typename].append(uid) - ok = ok and report['missing'] - - link_list = ['man.Index'] - for uid in doc_tools.man.keys(): - for link in doc_tools.gather_manual_links(uid): - filename = '{0}.md'.format(link) - link_list.append(link) - if not os.path.isfile(os.path.join(args.doc_path, filename)): - report['missing_pages'].append(link) - ok = ok and report['missing_pages'] - - with open(os.path.join(args.doc_path, "tree_desc.txt"), "r", encoding="utf-8") as file: - tree_lines = file.read().splitlines() - for line in tree_lines: - line = line.strip() - if(line.startswith('!')): - continue - link_list.append(line) - - for uid in doc_tools.man.keys(): - if not uid in link_list: - report['orphan_pages'].append(uid) - ok = ok and report['orphan_pages'] - - report['missing_pages'].sort() - report['orphan_pages'].sort() - - return (ok, report) - -def save_sanity_check_report(path, report): - with open(path, "w", encoding="utf-8") as file: - file.write('# Error report\n') - if report['missing']: - file.write(' * [missing](#missing) : undocumented symbols.\n') - for typename, items in report['missing'].items(): - if not items: - continue - file.write(' * [{0}](#{1})\n'.format(typename, typename)) - if report['obsolete']: - file.write(' * [obsolete](#obsolete) : cannot find the symbol in the api. \n') - if report['missing_pages']: - file.write(' * [missing pages](#missing-pages) : broken link\n') - if report['orphan_pages']: - file.write(' * [orphan pages](#orphan-pages) : cannot find a reference to this file.\n') - - if report['missing']: - file.write('## missing\n') - for typename, items in report['missing'].items(): - if not items: - continue - file.write('### {0}\n'.format(typename)) - for uid in items: - file.write(' * {0}\n'.format(uid)) - - if report['obsolete']: - file.write('## obsolete\n') - for uid in report['obsolete']: - file.write(' * {0}\n'.format(uid)) - - if report['missing_pages']: - file.write('## missing pages\n') - for uid in report['missing_pages']: - file.write(' * {0}\n'.format(uid)) - - if report['orphan_pages']: - file.write('## orphan pages\n') - for uid in report['orphan_pages']: - file.write(' * {0}\n'.format(uid)) - -def doc_to_html(output_path, css=None, whitelist=None, callback=None): - """Convert the complete documentation to HTML""" - os.makedirs(output_path, exist_ok = True) - - link_formatter = partial(html_link_formatter, output_path) - - # gather symbol pages - doc_pages = [] - for uid, tag in api_tools.api.items(): - if whitelist is None or uid in whitelist: - if tag[0].tag in ['class']: - doc_pages.append((uid, tag)) - - # gather manual pages - man_pages = [] - for uid, data in doc_tools.man.items(): - if whitelist is None or uid in whitelist: - man_pages.append((uid, data)) - - page_count = len(doc_pages) + len(man_pages) - - # generate manual pages hierarchy tree - tree_blacklist = [] - - global man_tree - man_tree = doc_tools.generate_manual_tree(tree_blacklist) - - create_tree_html(os.path.join(output_path, "tree.html")) - - # generate all pages - page_generated = 0 - - for uid, tag in doc_pages: - type = tag[0].tag - - if callback and not callback(uid, page_generated, page_count): - return False - data = doc_tools.get_content_always(uid) - body = html_tools.format_page_content(uid, data, link_formatter) - html = format_complete_html_man_page(uid, body, get_header_fn[type]) - save_page(output_path, uid, html) - search_index.parse(uid, version, html) - page_generated += 1 - - for uid, data in man_pages: - body = html_tools.format_page_content(uid, data, link_formatter) - html = format_complete_html_man_page(uid, body, get_man_header) - save_page(output_path, uid, html) - search_index.parse(uid, version, html) - page_generated += 1 - - # copy css - if css is not None: - shutil.copyfile(css, os.path.join(output_path, 'markdown.css')) - - search_index.save(output_path, "search_index.json") - - man_tree = None - return True - - -# ------------------------------------------------------------------------------ -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Convert the internal documentation format to HTML") - parser.add_argument('--project_name', type=str, help="Project name", required=True) - parser.add_argument('--api_path', type=str, help="API path", required=True) - parser.add_argument('--doc_path', type=str, help="Documentation path", required=True) - parser.add_argument('--out_path', type=str, help="Output folder", required=True) - parser.add_argument('--uid', type=str, help="Generate the documentation for this specific UID") - parser.add_argument('--version', type=str, help="API version") - parser.add_argument('--online', action="store_true", help="Generate the online documentation") - args = parser.parse_args() - - gen_online_doc = args.online - - html_tools.gen_online_doc = gen_online_doc - - if args.version: version = args.version - - # determine CSS location - path = os.path.dirname(os.path.realpath(sys.argv[0])) - css = os.path.join(path, "markdown.css") - - # load the API and documentation - api_tools.load_api(args.api_path) - doc_tools.load_doc_folder(args.doc_path) - - # generate documentation - whitelist = None if args.uid is None else args.uid.split(',') - doc_to_html(args.out_path, css, whitelist) - - ok, report = sanity_check() - save_sanity_check_report(os.path.join(args.out_path, 'sanity_report.md'), report) \ No newline at end of file diff --git a/doc/doc_to_hugo.py b/doc/doc_to_hugo.py index d3271e9..e952c3f 100644 --- a/doc/doc_to_hugo.py +++ b/doc/doc_to_hugo.py @@ -1,7 +1,7 @@ import os import re import argparse -import datetime +import shutil import xml.etree.ElementTree as ETree import doc_utils.doc_tools as doc_tools @@ -430,8 +430,7 @@ def convert(api, doc, out): if page == '': man_pages_spacing.append(man_pages[-1]) else: - if page not in ['man.Classes', 'man.Constants', 'man.Functions', 'man.Enums', 'man.Tutorials']: - man_pages.append(page) + man_pages.append(page) for page in man_pages: parse_man_page(page) @@ -590,6 +589,9 @@ if __name__ == '__main__': parser.add_argument('--version', type=str, help="Documentation version", required=True) args = parser.parse_args() + static_img = os.path.join(args.out, '..', 'static', 'images', 'docs', args.version) + shutil.copytree('img', static_img) + with open(args.api, "r") as file: api = ETree.fromstring(file.read()) diff --git a/doc/img/assetc.gif b/doc/img/assetc.gif index a1eb524451186a51a853f1619eea80c7bf0e96de..1c6136840c3c6a8b7f8c2f793ed6cb21c9b02bf2 100644 GIT binary patch delta 182635 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�{xE&({P5C#`qLay+X_Z6eP>U2;BMe8o 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

    O3<2rT-{DDRqYwHjGv%N;R(T591=_N87bIM zm#W`j1F#7b!kEUXGkg;Ot?@Y3Bi>fgsl#W~mj}+T&eIo%W3{k+Qft4zW!m2(g_w(w z1`*Z0{8;_PnNYrhQaF@pheqpPI$TzFl9Pk>X8~`> zVj|vM0xP>`%sHD83Mq6T>2E-ypdPHh)e(z~+?TGCoV(5CTW?v5%kUTV$anO4j1>l! zwtfq29UY@i$A_3{smNa;jI+fB`6Zqm_}egclK%q{HczM%W58P14H`vBSaHQE)M0`7 zBIr{+t}L~(|BKr(IQH;kuBiZ3|CASENt6tBz zQ6{_##q$P3c7iRk{Z7UV=@Wr%wD6?Uf1)?mC~EIK$JG43m}sQA>TEVVy5z>s^#`E8(hh>E>?OPSvQlU zrmpPS`6@pheKs*e1hHegVY6hIYA&-}-upDsB9B;Wds*1^u;z!bgb++Nx(rMlDK5=$ z78z__-dsVMJPBS5(WYF<;ym%NJh7%cMY?c#c;0iFd`+itTGD)lVvctgdD=e#^Se}r zt|&gsuP|Dp7wt6a0}m!_N%<;G`AjlUM^}4IJkFhq0`*(a{I1qdSfd=@xo5$JSLIg$ zmcA&Tc|M8r$mbIUniha@b;Vc*jA%`VsY0$<9!rBsTIQr!d6z_!CWfe;^yf)@Q-MyQ zyy)RIg({lLVP`R@lL+03SOZq`pEQ~R2~h|f1z7o&bH-I9r8Fy-2|n$D z%Zh|FTT%J~MRAEZEKW4ml{z49G^QBM7mdF$BH}V0ftH}k9siRf&YmU)7ii|(s4=n0 zOTeKi43AfV#HzWad%|^Pi!z7%lYcEF*7qy^Q~;grcL8 zR2<5)&dU>Ak{X=jMp!M-+1-Qk*udYC=rt@ksC-zuIT;sW@%p*RaJV64N*2 zoCco4>s$}5mH=mr*@>V+`7-f@vw1?;4)W)WD-HbM#d}rY7@wX2V6K!#8xKADKr+^^ zbcTcKvZSbH>F4nvaKmvb;trm65$}-~-v@@@#izwKq&;7v=*f|h*f6Iki9DfB#G{N^ zrp4tcOocuFg*Qi+`%J0Mg*rZvGMM3<4eorRmjZH6XoYN>4pOKm57Pp~=qnp+ziS_UR-E9gwT z-4gsUIDFDv_0I;Vs@SHAUfsQr^&3Im9Fp$hpa2W4^`^P}eIaMQ5hB3E;Z_zOVvhI) z;{%s|5O6nfnk6lwoyOh@DQx0Nmyc;@M{CE>ZYcgy!ySacg>rv} zlNyKY@oAgr195!zqwAddi0Zjf9MBQiViD&fFP3ORCRG_OXGh6?Ehgdrh`pGAvLepC z)6mLD2?|9TYl7KD(4{w*i)}Rir3r?=XQ>Ac#Jpsj>T_E(A=X&y`(aWlK49S1}BEG zCJf0_E{69~ez$?cz7(NfYhmee*MYa9Or}76|7-cxQ0U@)fI*nS>cyDCrQJ=orHWGk z;>sUU(3E8H(imH+Ki5J6xB3#?_~3}S)e}he({fgQYP&J1HjdpwNe2!u`l;nFhV7DD zGErJ=jUH8jO0zk3T4PEyUrbXl_```^J<>6n z!F+&?vkMVct`z`9f99v3srCi@4U#VR3}YB`dKi1ha6P&sx6CrOnQk#gDOT~|F_Bj` z<*YN_nd3_f;|q#t-X1EY12b%Wztaz| z(K>-0f)yZ9r1@jDYqlfOwpQv5t(j|c=CvyECkKT|j_0Ps3K6lEKTd(}8MtT0E<8F* z#G~v2jFXf>uP=-lr~vvTB$M!LA=MX@J%R&m8e zLeNJ`ZbzIz$V@;5BA@N?BbfALb-t~5gi_toyhQU2d@^7TOTFc+c{y4q-*+VhaCoFz zJZCAkd2$5!@A}sx8XHgY`3sQf=03k?dDH{Vn9fPfPM7nN;>HzRu#l1Fz%yzPTV$N*oTq;o} zHvvsr+$`?%NbEt6w_SXc8cce(O*%!LB@;ML;G!dnsnHCi(80c9vEwdSB{V>^QYvl^ zJf*czq`A`fpsF)!-EpHna&SmMv?4y*5*yP-qhYV=#+1^{cejD&Y(Xzw;RPy0HRh}^ zrp((VL_xse7oH8*BZ*d$w58|m1aGB}h{5o5_0V4_SPIDdx#rvhwx<~hyDAqC_-5(` z>4~M6t(IX^a>k$44C(q>isEa2Nyu1youu7Q<~7)N=R2_NK($zZX$|Em{E+c*h9V73 zZI98CZKby$^&bl!K^G6AIR_eQp&C@K1$;l{-S_rn#?_D^Dx#KFG_Z~_!|WHz8gnE< zEh9@HpExu1ho7j7K{sv2rb=MElAz*n_2b;Ih@{BlY@Ih~Pm0euS6+HThR??qU$ z(e!DymI(ZRcrtlf8JquG<3BF~20dM#7l-oT(&w7IYC!vv5+WYv+|Cn3$`i)-SQ=em z%Ui@c(i>3tt5hw~qZWG{ouZW^DyW91g~eYe)R{orL@U5|n1)uJq){1G(J*M~+J?5q zRj}+GG_mre@5tSZaU9COr=Da}e7%L#Q72|v zzn);S^oBM95npL15+7LuGp5H$teb+k5>``OP)ap_tFj~@@5W-|Y}}Dvf{kjO3BEWt zD$u5Dw~cm?K$GDP#8;%YP5Y0fjDwV+p1p08U|83qdVO}+|Jl9st_Lyrc1i;zoc*@{ zAgkze83h)`g!eUMF@F!~gQff^D`(uNMy(y_HpZw6o?^CdpZ~oLTszYCGCgxCHgb;X z9wNHkL`OswoKZwXX+23OqT{?Bq(b#`-t~6L1-G4XNdURPV&v4}I?@Mhtud_&4O(Ah z#lLW49QHgvy~6JaABc+Pt%v5`jqfmDm>uYJ4GOO-z1?*lNUAVzpzij|7gMauFy1f4 zxZJ!!fDNRhIZ9%Ivf@Z49C?^U7Szz1!tT+1A=Hhh#u0YtCyE~d-qaY@x6owuviNs2 znVtvG@^tavEsgZpaMPU_-wmQsk?h0_;_QRY@wGsUT-7TGpNp`YmDA1kG3w7nm=~o* z!7ki5<0nt1_g@ z{#9`S9LIGPbfuF?jbS~xaP4;!a%49SbOx9wEl<#LLfRV2i@k_retNA^A*#f)idM=6 zRMV@Q3Yy=SjV_KXShOe^SZMyrXhbe21bJVH4%J!l(@dPqG za%`u>Mv*#(-AnBZr-FI><>IGN_$%9&r}kmxtHJ5{{T2iZ@Ehd$_H$Mn5ZlHRXEfGo zH@=)qvvc7wk^&%SKl@x%$=5DQkM}vIX%Hzmpkg8PNrg~OvUGV-hDJK9;Y$>M?ktxq zPbT~1hgXpLpp|L zkf?ib_`T^VcjA7LZfs|RMyjFis{~@J)-U&r&RVJ&phe6es%vTI?ur9-^C0L_ zWgfLh(H|90(U;{L!CYxc({ZrDJE<%?J`{?~ZuRAV6{eC|sj<*G4vGI6DD?_GQ5O?} zyW2kOPVOWQ%Yp|nvohoLZ-776$}meEd%kYYx;X;RW!gn3zfP9XlRdvd0v>!5GVU^ReG7?t!oJ`hE#GmKIqHyvDYmA9iRrqd753 zI+a@`KSqbHQcUmYKIZ4nRbN6O`cjec?XP)gUj1}dXzm;JD5I{Cvi_-@!ON-et47R$ zMl@?e{>CpCv-v6JpSk)@6=wTBIelF84aq%N!ZEz&OPrIO*;|;(@(Goeoo64Z`F+aW z>*M2%Px$ug6N3oji0l5Kyt@{%4qJ=AQCiE%2ea-Pf2up!m}%Lv`C zLykj`q^3+KDoy2O$0FpY;;>s>^&Ka-8cTw1cdHVxnKW>_CG%CZ$|ZX1s=~29gRN;C z|8?h;mASrMuv{@Rnb>sRR#*37v9q$V=4?OR0I18|1-r8sBR_qeIA9FEd>#DbqNoK| zkuZe#Pz%{5<|HVy4kOVSyl$p98d<%GqgvORb+Ke!K5mvig&OkHWm|^y;|FMct|?eo z%-I=uU5>rRXtmos@7(z%X(OAao27NV$JKwal-pST^f2$a2aDwop3pImub%$ z3oNzH{ZQbz%9rGUH^B1O1y#7&L?UZ*!7l~JPkGyySuW~UQKUbPauV2p~K?~$gVabfr z)KsUSA+LwG)sO3HZB<%OL*h*mpI!08itg7zcW-f_kpt_z&5Fr3YrzlZ7^&F`kdPHH zTAW8qS@#)#np$AV*KUJHa6dzS^8x2*h;>Yh6;aOj&`q?%1{rBoJo5l3H7t)6Mu7~) zduweN-z@J?X;)Y2VHd8qU1G;H&ImfNV|Xx0fO7NKvcrW4*Ty5p^;5R2GIhQa!H04n zJ;vmhbLi#4&chD)olO~7O0o=)!!8F(#PdeNS8`RJ3+6^UB2{W0QdRF^YHv9svTRY3+8^A?GYnMiog?;g^4uj$H(i`{OqSLGqcUfMiII)Sf!)M{G zAkuuaIEU0GkP7m((~k3t)S!?r<>Ff%`63CRDx`=*&2i7n!SK!eu=YRXVzUEU5c$TQ zlnZD=VL?hDNp;1Z(OD0(lYQV?Axo2#l(8c{Q+DwAW3Jeg*J$*`;+<-errVMoMWaoa z6T`Ff-kOPdy6Fk%Y6+E6jB&bek9x{*8+-!%=ITz{S>#F^2B%u$`*_L?dQmu3%#WCQ z@r%0m?uHp>i92-RJd+8?7AZgr%Wyq>Y*#IdG)g~}BRv$a)X7+578-=(*a!{$Fx^gZ z3a_xHNEs|EpDIb8`&eZP=r8m-RBJjqjCjagUSC4t;*h|k*WQXl_~1gEaj!)mHtr z2{H>df;yi=Y$J)q`29e9VOTXmsMEH|;+7(jScdkQ#te)Byh_9j!Q5w=ch+zlwh|FJ zSQ5CH;PcQXwG@$-kT0c7a_h-NcUeZ4R-G1+MT=0WaWsX`jAB#{UVivbzQ84Gd`;Zs zQm;ftJ0|2->p2V5c;HBmj=$uwHmdDqSTg`?Q6>%DE`9bHh;yNW&%pZL8FsoD_U$>> zozs3~1%oD*Z8Jj8{;ai-F3w)A!w;@`tGyhtl?H|57N~c*4M}pHB6h!&VG3PW5MqZDSAYaW6_R?v{ z5>$$k|JeKF(T-q~56`d^-?-DXC++=wk$dTT>NOlty@G-?OJ9M2K~8Van;}rb1-t-ae1x*l`6ygg3sXYy8fsTusLoe8@1`21 zV@hJng??VB{v`j>QR2x1*;1cp>$}@}HX8}( zMNn2Fj;0Nj=H}>YQ;<@p=CS>m`d0tT32|)K9e|#&?!!r8C!|>RNpnSrc2s=+AAN}^ zE#)Z{>J|J7#cV2P_@L9g!_{-sEkPULVGt|Ix6L~NOFiS*&7*I;oK@7-W{vKRbssID zpwKz`rnAQzo<$)LHz`2*X!sgeK9~h$7rg$^lCYMd5=ex5bo*B~&}f-_Knt;-&}MJI zbVcND^yav5=G410NHIBhcAjf5Sg^|Dad-Ch*^nGoF)0!;PFk8V?5U2rKKU|CKYE7S zn%r@Ag+8S-vAKftdxoI~wA3pl=Q}6UU9{J9kD>rBHosDR9j2IMkX6xX^kx>&@Y>RM ziW=%3GXKiGm`Ij-(0(V@QadY%(Y(dVA&>K_D^_y%k~BFusexV3sDe?%uagt`4nteRf2@ z;PZpuX&w8lhp@-(tRUjRh4(-9@Ckg1TiO^ef|#{2jD(p{yDAX1z32p9{sr@$x1AeJ zD*A-C)sq%ovcF6|D#U3nIVSLXa$;3q z1((W4N30NgJJsqps3}Qzzzbeom{)7we3}q1+jLcVq^}QKULnhXu+RCLq32 zihO(XvKAQalHX2r8$~7NhyHD#`MmDeEDDh(stm^?7zEAksL6Bdsr5UC(K$|K*H*2+ zvjn65c^X_4PgVN*B?G=L7r!3dZcHuD(5eXba$Fz@63t1UfM}a&U7FxHF(%EKFyL)5 zKxWHiAsQ>0%w}ek2_~i^VwzUzL7(pPnR^P(+|HvN9{3)H`M;1d#Q${aKxWq^LQ(_q zr;%xk+~k-E|63#55Bgf-snB51v{~=t7zNpL(Cnwgn*WWn347k9@6#tEK6_jEFQ3e{ z_U%7DS!5Kpw+}89Vq#JfYI1aX2FMIGJ0~|Uzo4+FxTLhKyrQzI6c!A2#7<3a%7I1B zZ13po>c&wMjl)4ih8rHi0m7l8;UgdnV@_kw6MY;0hB*sZo`GA@gri(qL|EO$nqHeb zSU>8yy1pUogBrj_M5_g(Q9tLj;UPgoBkiVRY-X2)g@r<6%c(Vsg+l?M39S?XX+z?$ z2yHBYKa1Z1kZ2#v1TQsW6*1xeuoc)An;~anFEM|&UDTJ4h245w0kxi4_n~3L2;sT! z9ES$9P@i(&{Md#8a9GG3p5*M}VX;g^)P6N;RXXvAotWqV+)%YT%R)jnyL;$~bfI(v zZy5};sA@=U+K5;6P`<=xOaf3Z^JI*g3pQKevQQl@p@y2~m{C5H5)^*#i*4PZDuVwg> zmQxZ5sAeVKwzMI*@lunB)x08Nkm(VJ^cu{jBLVm}33j?A*bj4rf9eEQLY06|qOVv0 zcX5Zm3(YX=9}r@zxJyAjEaVuHhV#(qVJOIVX09COyeg2$K=tNziGN_)C$U4tz1JxzOo8> zq&9HB=}%744%VNVPjiO3e7?V&!d2uNkDYlX7gfhm&wm_*TE5 zDZv4}B?Y2ZU_5Rm*BpC0eD>nMOCzl6!I@c!$zU6;twvZ>{#Hjv8&rJ5qadN_qL6>CW8^}TC(i}?%9AQVHgDC%un zt|&+KiIG(wr_3{{_4)uPiG*$bVYTZ#+a~Q3+fcXCnJh_`1{&QEC>Df`h9`HYAz6iv zz(PuLU@7O9R*KAwpDV8c<^|cwYfx9h5M-mSlgR@%rA|^$E19AcaoR3Y{-@eRipXr3 z2HK|m_)dBU?O8a|v`YHvxhhQ+XO@`~i3R-I^KofV$vig|a;9Yu>t8VOUv(y2Gi^p3 zMN52rKlXTGFcM3Lg#;Y>^jIVTm43wtk5}A3fIEl+9qHr{*r|Qk<^3j;U0vg@aFG;J z$_;bNp)f4X4aHb->)&KD#i>If7ux8Xg8r#XSvL|GcRaU!PNTW+o>!BG0|AX%7`YFC zVzEAwCB+k8%39d>5RxSaO0iYN#Nws*LhF{_&lZIM&!b)h`~$2^_fUv-*l@?K3E1oz zU>p>j+%V#DNodOFhy`EFOU%4Mj8rOYLG$eB_CR&iuc z&)En((>317Ibe@(aYo?x}wS#6Kfi_qr6TnD9sjXlE$mYQ&q)to7 z>ujxyn4-%-6^n~Hv%sy7Ugo^lcO=G=oQo%31M4)@8fV9AL0)`TW9xy=|e z-J;}ZXEfNg%Vbrf8&a&8tDIrr=_+9ltyFDK;1{ejV7pnO<|RF-;#(qhn6ybyb%61f>*=?>qIffqXwD7 zQ1OkB4Boifbl=HRWjAB-Fia5On#8aN+`M-F`Xj-IUQ!ek3tQ zD>@xd|L|_1#cfjiHFH!>Nsuf3#G1f+Z*|4G)-K@{s>oR$x~9WcgEZJ2FllrOsp#Ej z#lx9tU3G}WxzPjb(`fqEdwiVrr^bD-F5&UzI}H%`8(j#8Ka7GNWd2sy{^{L^=&SlB z%opB83extulL9l~G3@4aqDXr&0nrhgtdGA^W_?l1^`QdU`pS8Shqk2hm1xYL;Rv6* zFjt{-t961|@zr~N@Grm@O}3hdPnS+m@lr}Qpe8%3Ekc-NOC;QG3)H}og2sGfr%1TI zeaciq6>eV>jy=^8;$6Hncj56&(2}Y1(=Ip3#gzG=)bY>W&D*dHvjZN34UXk>tU;f+LZ=ILg%+EOx-q(L7 zfXdvhJU0h~5!}9*>wl{KD7>#Z>IE2E(`%sue#vCH9m!5OwQK}CE8+#OEyK2CEG=9p z0CyLhfUg`6$oDOpfA=AzUwz&G>3eq(o*wG>nx*wTK#caSbNRe{DX4 z7KRNU?k(e|0A~<^XD$B(oZ}8-n6zS~3+AS^0^)h=o*|dXO1%vMo??KI*jhI&CNg@; zQ??ot&#E3>s)f(0ihhyOG(fd>^qMGu=?2j7h&IY-)5wp`f?_T5`|rqq*ipuI8oIuy zt7-wL)d2>4WVZM*lP6&m($O&7mZpBnK2xfJ^f8#RK3SR3CtiS>%&32-F|j$Z@!heB zf0S?+Bb=-tERQsz!$1a z1klUJ76CqJMk_oTH*_vCY2HV~KHKXrUQ)cQ8sVnuS3Rxsj*$4x1iDO!Yfr*dF9|6< z*Q7O33LR&R&Y&Zmr0i!S_M8aUNh@IbhSIBEV2pJ1Z$7n~?u56P_^XRJn21D<3gA1v z8BDe-cdV-wo{e*-U2|4oD7X2!ra5kR6un0X%Z4vmXA0b;YWkDK_Jy4!Oon)sDwlt_ zwyb5kw6$Z5rE8}ah|cXVU%Kl=20gsC+*9b-pK#7i%Prg})|wA5nI0t_T{$4OyD2|DfkEyELS_x5O43Hz{e1{*(l zKfM>%K(OcORvr{M++^AVk2m>*uK=6gT$MX$sV5B2&n*h?qhNxjco3Lt%YmPYk5Dj| ztM9jIHOvQMrSnD0&BfX*@yjWUkuwREvt~bc-?#C_i7#;D^|I*px8?UDM98+$_3e_+ z|AOZq=}b+oq!XiCw{nvw9r-ClJkP(eCrOE>R0t9}(!rCR zFN{X6bd4UQB447^6__31h49BNTqfzfBXyTLg2T>|D*L1O92W>?$h8sU_f_i&<3eAv?DW3_KTCb&@a&AEk8`lTpUqv-DSRKn}3d+f;p))4V7vwFKySaRa6jp#a+iW(5#gp>NkAnYS{5=ShZ^0Csf`Jh$YHuq>qY&U5lAKuiK?= z+P7-@Q5*437XuiDEqIgH1R!dLWQtLBM}!S*hR<(C>}xJlLwf`?K@qiJ3bs(epy39# z;ODmx_O%f2w2;2FkQ2313bs;A+cN;6YCY;#hs@{dnD3!+l+^iwr#jK6`p z$^?5WfK085`K|a4=1c@(l2>I5a9M@+;L}Ne<+^exyn2kV^u=o1^o<18POq}pl!vf5 z!c7Zv{bZQ#EVQ%sez5Cae33vNSW6(goze3}u3xvW*08p2rLv#D_tUTQU83!&G#os0 z9IQ^LHk>ZBp2+keC^EIzCt=7!O;6Pli0QEdez-XI*nK-4xao7f%g-ghGM35i{ytf( z{89&f)zh=%6-{J?_x#m~sLEc!kzvclrl;VBpau!KiU79d1>Pz-dgzFt_dIZf-l*$R zusn9llVAf#`Zs$oIFS=}XI^QY4>>uoKeeZCzcY(AM zFhcNh1ZI6?47Z`NFK2}zTZwomim^{QXl!K_7(g&eN?=U6uoG%y5(@kfbcL#M zzP-%S;!nMl^Qc7pI{XwRA3LczJ>f4`T9IW7tO7yjhStyqK`<)sjZZo=4_)|=dNPzm zC?ze%g+c|DNuhGUdbN;msYKAU zw5_ihQBx?b=KE$ClC2_EybL8xVa7J`9>#;u^FHF ziWXJSJPi5FZb6shoQs-3&FEZ(`jxw;LRIu*`J|(tNB^vtfHARs@ljBknv6S;56*!J zvEW87<;&l?tv^cMw1HwX=8XwG<_TJ@czy4&U>TzZZN_;~8Na*jY`UsSBc(xtx_Q;r zdE$nViuyss0`MClSp98gFSe6oTSFmU~OMeG}67?GTUkE=kI6BpcLG^F{+Ar)4w}k?Rx*L1OAkNGJG1 zqYc5>jH{4nMidm}_Z#&0RG}zn0bE=TC}>UtvtIMc>494UHCs~F8?nF&WGn#8yc$!` zUJDV$Rv!d**Rl{22oZ*yOpJo!{()VULbduG&{T-7%FflyvYCCy4rbc|y)QXUY+2gv zP7TBs%`245M|?AjBd;UJb;N)KpydqX*g0dgyrZXz?HCQCqa0B6YV3iOci=NN@6@*U zjJBrt_J5GH%#ujaVgLo_=aI+~J_u5sl_bk8L|V7El(F|4cK)zzJ^;}4s=s4O?2QQT ze%hZu-w)7>ntmArQuqj-*#v`Gwz`(_FB|GfN&$7jE!!jq59E8$jrDM(J0v3NgOs~B zB!_II#|3glr`o;3Y=yW;^4dBkKT}zDcdUpdk|#1PthUl{*IU$cR~qy56Ov2$OXvUn4E7 zgWhax3?g-da@*+2kBw3$pZry3Tw)p*@jqid!hI8n$~ya6JtB3ouzJfO`Z+3E1$|M- zL8}HyEh@jo_*B|#oYzTIT8UZraRq-=EX;rZJF~FU1N`CWD#HAAi!PM5%WS_d-A$p0 za;wXtHfO-GI@mj7HL-InYGCwrVX7_*Yrenl?ML$5Pif}N!#E!$lu_VY9)fEI()R&! zOo&80O)x3{!7JU!B(5w{{kkZ@zv-HfX;7=`^j&PWB51 zZ`z)4aS#~edr^($x`7ty!JyrNOi90Cl%5nJTR*rP>7%{Tj%=0jhpS$YZ7-9Wz zasQem7m=jJcgEr;qP~e1+VQqHk5Y!winzWDT#qoO3Avf^tcHuQD)O98V^`H@h;L;f zXX6G1k3~Pm>vpOdUvmqP{)jt8q^=szy?rN;otmhZrS^iV%Lt|lb&Zs(w{4xv$p zN}p<)TAYBs)p5Tx5GPf>*JPs&E&qv%QE?>$dZ+-0!^wV!8AasWT$<-Uv=>ZelTNaJ zeNLGRYZMsD_-mJ`^tYS%?+Vw*3;dhvm$wP2SFa`Yj`Y`quj~FUGZry3%|de)1gqTz z{}jQ%cUu3c1=hS_hS`B{AL++gX&bbO0@-cxa1iDg2DjtBFs*c%knW&J?n)=>uZvC?ek%scX4Kp1` zeHcsZ=VN?FM@?A}2l4R%@oNM1Ku8pF3*liZm8?~l#FT$`L)AoDn^JjO!DpdFIumjh zCqB-cume&XmDmj6H5&%4(;g<7r**mf!Jz(~xi{+4sMmXxKgt1^P-`$4{5~`NqI-AHz=J?0Q;-8NWI$-vmq+(i z{~|SBC~Wxns{h+=xl-BG+Gxlfw=3n;B$`Cf{%pS5`TInn;j_!WnWpSI z=F5;(v8*+jtV=Xc&;ER&Xbh?G>*n{(dQ)F#p%|fp zI2EEHGM0rclGmnwPEo#z3WRH>ahgFy<{Qz$r$D+WaD6L+41U`c8a1B$%;Fa~Ahbr$ z&kN4*+N7Cut8h{)GY{+n4JYae|lMgP|011fKN_KD#`4NTRo#YF;)YS zJ7oB{-K*gPBd=zV=Ipl#dU848aiP!F7Gc~S6MJhQpd>S2El-@gb9L9T)l$aWjr@n%kgl$)j zlog2?M$El9j*rjoPh4M46v}MjMkTo!Un~N8Oz}e)rF{hS+Nf+=^&n~u%0DD=naY9;Pv~?I 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%0At6&tLmMRT4V<)kqweb}!W)J(* zuIq=SuysdvNgPu^s-yXLkRr!Wtlx!Vhp~ZUGdsdEfxFk~QR<}clXi9*T&aqPk_d{ zF8NqbxG<<3d?mZQ&gytm9(!N21fcCgMn078mJm!!1}X&&z3mGQ+>oazxKqbj1&$3+ z8J^Rn1%5>ho}AstNi;g+X5M)Bam>g5h*Wx9uQB+8-K zy`u}geOjS8OI9#wqOXUW%9Rg~0y)Q4ntD4tb|=B|BAsY}Ktz-8CP;6$Wl|CdrEi3h z8aQN@L=xl}!Q@j%svuHn9bHLk2~yXj?G4>0utD>UL;e>x> z2D?#GErk3a3KvTTgk?Gb#0?g)2EWm#wGY#s5p6Nmao>Qb^y-c17<5lV+KqZuq2FcsPiMQ}SBt|6j;v&|P`2+yuY?1Bf zb;F+d@_}H*)p4-kT2`@W(XccQnJsei>tdPPwW#LF;qnOb<3tV)^EM^Np-gLeUu|dt z!HKTvY0$A_!4h1=a15LD7_CK;R6Eg8_aEd~<7x_+%9@*toB>^hwbX(lSaHSo6@F;U zNb-FueCT6EEH}*X_AfH@cM9eCd2^QA2gEtct02WR?42-?e6rvX87{p~pIDjARzvhPCgo_fB1qZ6Ntl~Vrsc^>Y=`A0(JW!45bMtJcsg9@){ z$5(IzjpY6>_5AR01Y54;-#|RCRDqH3Rv;|E+Me}IQ`a^dv0z-v?&B5ml$He@Diyv{ z2FMC-bH=wTu$CRz<7vE0q1HX{yTOh0U>iBM5+)n(hx=AbOPcs~4<1yp$&2+7d|~v| zxB0cnw|n+m0)9?$f2X8p&Rg_PX~N!CU5>7wy%Mu<3H_(9Bt;Zu{A9MZPNfy$;&w#v50s$>+GFB zi-lpA@v__fjDW!vd&GU3IJ$T+^ao*>{ex65(#6ZT> zdTm``lkjcx;d6CI5K#+MG3me^71q!uav3mywoGqav2<|{AhcwEb^M{}?dWyl|wl?o?$Ho6$KM}&Gb zITGippMNY!avSwG8RT0q=w=CB*@t!L{+6KlhkT3ZdQf6#`i~1Tg9>4U3GrtLk10=% zLNHbvd)_hGkWwm*=(;=4=@W*5TYk#e06&<2>8SDHyisgZ8xwB+`%S9+0*YkZSB#!J zi_xgU3O5%JFj(L8iQGbf3LbrLFUT0qYvY(Y>=jRzLMq=ehVYIXWRa8TMb*kuW8>^# zX*i?#C9^7P`|-pytRb|dEz?GzeYzy{zP`KdH|F-MFqbXVdB&5Y(_3Y(S_PabvCE6a zGif`jqd#h+$PG~q9aj|v8t_o7p(^?_0#$DSSYIn?FEYZ&$ECQ+RPl(^#_{Mz+CZd> zAjT{;a}UB@I;BhyzPhcv|A~#tRE9d=xcYBCO#^-)wfx5)d_-oDkJH1de>0UZZa=he zJ4FoZE-YavB9Z1u0p&K;!-qc#WU;c;IQ|sWg35@&XKBs)fl=ke(0wQ|`6&>&wVO0G zQ#3{Ws13x{qQl2s2Y^^@?G9llVcxMgj$l=vbL|sGE>QuHr3$Pf4Zo77g%+(`H0xRz zag8z;KgG}eX#P){c`F27LIx2v+I7d`%R}ga2E=RBY3`Cp@x-5SAhdWT{xK#*@Ur6= z^7Qs_M4Bomq36*~Ia8`y_;k9z)jtRU0j;{j9#L?q7%e@*hCW2a)c8%q=wPDQaCliQ zA-os7#1{URt>n;6 zD+CRI1k}U0FE#NFh~W?CZjZU3Y8)Vd`n~A`1pw`q^@f`R0-@d5zHmMN%^5$qu>YoN u035~p%y9~YgT5b~g5VVYd)yHW7xUi~3x&h@@3jgDF8lp>2!azq`2PU2ahTu$ delta 272598 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%0A-rvNJX|7#_Tf=8EmzyFQGB$V;8^^To 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 o5&6zxT94UtZ`P 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%0D^tGSm?rY)mXyj03rfY!X@e}M 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

      E##7{H5!J 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

      l5>G0qsilc znj=u!P$?`SbBhXG0xYC>qB1*Slp3o%vy9RzdqMR&s%Z;ZgL7r683YxBxqLlwyzuZ? z^IiGXW83(x>^;8J)lsHZMI#289wyzzj%lc-^-x6KvAq!`1{`N|);0C}l@fX4-u_*a z_wGQJDF=JuUe3dR6t5qQRoy-mgi-H*gDhSc7J7SVkIyh}1^@N=0%(w0yC#MjA>vl< zpR#I`&JoO5k`Sj70s%6_sIHYMiV{E794Uv$wT8W5I@1E~o=u)UvK>X5ROwQt3xY9) zO08(rSEyuCJ&85ORjpmUe(m}cY#Xs-&7M7a6YW~GYT3Spy9;4lL{ryVwF{MhtJl4B z{r&|USn%K>gbf>+dss2mvVY}&PL-^QI=_io<3eg6ioHMHz!ot5dxD;ucU;WPQ-xaMpul;|8kUoB2G zaqQ-OlB(;MajW%v8?*P}tT9P{`h=YON#7TkSn{anFF))5!Mpp?{i3u6Ey^wJ7UC(t z>x#0EsZ-`ttT_g=lB<}iIDpP5KDJ|lo`bA|sSWBdxup#|pdrzb$^>Mqzu=tH4MVK@ zGZ3p!u5vM{83UBiG8Gl9q(;9IEX*3De6)lGV^*=Ffv05Ii9r7zX>1&SEr38#$ttd4 z(#Rk!q)Qzo$WuwG8>~Za3osu<$vut+Df7!-%49JnBiuZQ&7sg(PR=`X%F7}$?ab#T z`1Fh^qB`L;h!j5teNsI=*-URy@{A!=J)qcYMNLN_?S#zq=rk`!??$~6(eUQG6bkD= zT?SJztvQpVkp4u}hn3=g1EUfvMS+ut4HrqGF)BB-_5af)mmAhsUXr2js}b#?k_bxS z`3P59s{JsAXQd?pqL!*{bXsZ0tG0_yc-7^nVrops5p`#Y2BdPwh3Q?R=DG=5Jf)$R zTZbI_W+QNqtk+-UxZ2j8e}&MO)N9%35T#Z~F_(*R<7nhyO2|2Xgv&D&?br;A$+_5F zW>>;Nj|55BMhH*#4Gkb9+@(;|YkOcB;CBFaIIck#oynwgDaDKAa9_1n*>^VPvY?_V zt*fw^W6S}|Ji;Xsm7=HCwwQ>GhPdPk=VB(5JDyZ_h)qr^d&?>L#cOSZDTe#jp-*~x zWT-*P)TV6XOQa)zmd?=|zzBUBchrK>tq_cewBD=#*ZhLoA;N|>F`FMoB$vGBAK{gw z8cqg}IPY*q6w+a(NzdHr4KKn>5~}2S$v;lBapQ)rQyU8EF6FuG@*k zxm<8}KigigF`F?xmP6%NYaxE_}J#Tr+LVKvlA0|rh>Y@5KwCSvqa-C z2EgD!Xm7dN67#GluIEv2B3o)6+Ng%J!No34s~d;$PDnU-IWSJ)3PT*iL_G=m2S!6H z+^rPGJ&Cb@hy+;L7t?SRvFum@T$5N*5;L%d{{N9fEIaCoQ}&Q8?^W!0`6CB5MkI&{ zNP-X>(@7aF0j&0EkBtQ(qZV1whXAQ%3B6kZB+7U+#AvVv5}gr>vhk-9cS`XDH7c&RKHAJmuBTDhzO%Z<5Gwpd(x+rtjTUIS=ED*xu-yR ziXY_Jr8I{Lq`g(7S+IMT0R3U88EPOsbV_`JUU2JioXfr5jCg@%V! zeM^XpjgF6yk&=^Pijc%2p2loE|00Rp22M0_(b|v}^T*$DY!-tJZBoGE*$uNBj+%1C0v7^V2 zApgVQV=&PoMvV$Xs$9vkrAryZDmFN{FX79Ze>ijMY)PzvPX_&pg&=@(r_rNGlg=CF zv*|^IK$S|JO10`GVNRV^1PV2!)vsW~ik)ME!A}W4g^DOTwyoQ@aI0V~I8$v#n{o5% z-OKmJNQ)H#oW$F=u;IgqEvVZ{ad6bckRwZOOR=&=u4@TT-psjk&c6~&IQHtfwCU3h ze=q;VeD~dn1h8Yvo}J(aY|^N6>)!2TfP&w^g9mrK5k-OH$d3DlAUpRb)e_tD=IjNeZl#rgCctk*#Q zdkHK6AAJZW7@vLjd4~dc{Ot!@fYcEne*$aQd7Sssh0SDMP-~|ejz>~}e`cab zVumCY7-EJBE%4%q9uh#(04Ij9Vgn0EfQ*O>ga{p+KSG*7iz_;S*peO?b;7GuawU)Vn0)nbZYYDklwCe)~)OPEy0Nl!`1hpCyg8x?q zKAR3vw7Rg19#}J*uf7$t9p5sl}!I{-6)S^4V>WAKlw4PX|pe(9su9r~y z1Y4!`wxcMNp$06+lA#SqVtGsUW3C1ZUr-)eLY9dtX%AB{WRngI=wSl_9Qq!K1x#6y znpH{~;*k_Eus|vr8z8b-IKrl=VkAcjfXhub@af2@&IvAX;(h=jxe)Goe=d3eoTp`I zx1K4Yth}!n$LXIg_M>mr^=cb}yaI=<2Eig4fTPRr3HY8&>`~A_#yZ3OBilL` z{NBV!HdQRFX0_e7%MUP0waZ(6sQ|57dI&bHP~x1l%?8LxE2A3Y%XH$h7I*po0giiV z`Q{R2?9k6ZJNO!&d1?#5e{n{0VENQcr-Jm;wT=k;(*|};E7DbJ-N3$9H@m5YON(uM z89|n5Hi>BAi0YEpo;u)>Cmy+k&J#Ufcg8|$FXXDv58wcbymG&6hlHCseVkCr8u7@D zhghY|tO~gCmKqB<@5~i2&Uu(Cd(qQklp&ingmk|Nng@o)p8*tLe+l|&opuD6u6i+` zTh=OoqDFMJ4&@F2EpkA&mcTgyZta6tgH7LZbi5W;A$esJ(vf`9JtFnZCkI+j;wH8p z66DWpW>Fa4h{QuwJ)kM?A=UtUhQ7)zpkvoNQN{oUzyC>KepA#{%POa_O8?c5e<>r1 z6#-B(4~i^JI~xcBe{p0wp`osUA_SQPH83?dErMTT@{^!Q#2!0>@I-3^VXZI-fNs4A zMIl0A)8O+#7djFPWJ6I2T2{mCwWo;RGor?Bb~uBXrIMHoVwo5q9kQWmiW^HGzUnps z#6{_M8*A0USgF204TzQd)7;3kB)Xf0$UUx-K>I%SEd(l1e?4#9kNCd#z=~-=Wfw}+ zlS1>M4GlAY6>63u&t{O_=0k_jnaVS#obk z>4acETqR8e~-y0aveuVtaGM0R~iqsA!!0=GeDADCY>9~307lD(wLZZ!;j7|a!EoG z4Ed%v5aJL?4y}NV;PW9XMb0b!>k|Q_6FKE+$eK%)=9B10v+phSaiqK|s!TLGW43XD z$=pvIQ_9k|K4f7kod9lFg{HVF$(eSoD-~~(jzjjZe|--#M*S>yFu)Qvuflwq!!SU_ zVv>%e8(<)R;%KJkxb?D_oy(aF@$moD5epX#XV_+zgQ10yOu! z*bT{Ze^cv5&-w_)FjdkHWv-@os9u?kuY;~LwTe+@Fev5t4FVe{IS$3PBpA#yC_A{$u_ z+tuZfm(1iC`&c7NZnBi8oCP9N`N~*+f|aw(Q=k@)v%7WtY=N@THE^8 zxX!h%cg^cw`})_w4z{p|P3&SD``E}%wz8Ma>}EUr+0c%*si#ftYU>%*KZC3=qs{GZ zd;8nq4!5|+P404=``qYGx4PFYY-_vwf8Er^Hn_Ac7qPM^o&hUmi{NWIfxWp$;abO$#;uvQ$!WpFSihKOyAP>37M^5sRgFNFX zH~7Zw({Yo#{N*r@xy)xy^O^@b*`WlTLL~@4QY>iSo9k&h@T){p(;4`_ZdT_Jb$c=VDL0+SktZwm-e>aL@DA z!=-Au3LIZ<&%56D&iB3xI_`jP^W3}B^9;B;ax}dg;unwjyS<0;&i;?zCr|mxTYl() z&-|a^%v}o{p7AsVz35p>`V+<4e@4VB0qSwIdI6Y4L)yw_U8-pSEnWWhxUc*q_10~e zA=DjkJCcxKJ;~v-TJ<0b>s4L9tpy-o0Iru(pqr1-Cq>EnU}|56Y!9X9GrvMOYW4Q@ zF;#JEKJ$YwUEvukekcRQ{UvC}Ux+1ZWc1bfSQ?+F@*gGt1t2sd*9UyQf07|2z*^jw zdkC0-YhaW-3queMp#u0+0a@ z_-Y9eB^iMwK#>mTF?2v_c!B9Kg|ha9#7BYQ zmkv6U4%4TGtae0i7-;GIglx2a7jSqN(Etb#ilR7*Ot^;Cl>ZSQSbin&e>GHiRdJ$&YN9k= z7>2Z1i%mC%-M2h3Wm5+*Lk1IQZ{rgRFkOPy6l8c*&O=c+#7@3gjCb<@ti@^`!iRYX zB~!#lwHJucM~H*ige2$_1C=p-2#MX&gW9NrLy;wtcO+H9eb)$Xm>74O*m6ZkN|BR; zqnMAOcr&1PGsL$$f7bX!>3}6PSco_{HLW%^vS^D8*^opxNqx9HN)U&rGKLHgjok;1 z8FL~O)ea3Pd=r>4$vA-%Sdr2AHrs+KhKPsYn1`!4KGR}+h$8^p=zJ~Ghkc`nDfxY# zdFUb2e6A&><$Zt)0r^Eia{#%czj5$$mQ0Q#7Zf2HVzRoFPbvWgtjeU*YG zcW8|yQ!BIRkYrhwCI^wZh$G5Kk%x0cyqA$ULXtXjL}_R?f&+)nQ-g6imq_%KCxV93 z2#z~>k-zwk)3S{QGn9nbS6dX09HTP7$N=l&YOZvRct=VD(vE3(9$(3ANm+JF$%&pP zeoEjD0x*@Ne<*tX=!E=-B^mKl#(;QGVjw8uh=~b_)X^LUXqLR$n;*x3;dexV^O(!? zm<~mmaVVDxK$j(8m!?;eowS$B8JYBlmJmodnCW_v!+g6^iO@HQGtr$B(Ve!Wowsy{ zz+#SviH-poh^|P1*?5VcS#_cLV}?hXh=-4;x&M!yf7pb_r+h0z6sR{71Zhohs3D$& z1e2s8{^y$x`k)R6oN%~<)5D05X%;Yfe95_?&Z8|36_RYJoJRzcK!K9txRTw+nNZ_; zyAplBc#b^SEuP{@@3<|s!YPZ{eLm@(uBT1N;CxfqJImRNVhNA)NpT!%!)7gJ~RF~0bK6G%RAC_~0LkVjQR5g3PET8L$eqJg$lfi{5t z>4!F2ez2*0PqI{YYE2hpiPPsR1oKQmX{C}n zsb2SlY6B23 ztBx9_i^n<8+O6LDt#2o-DL1QH*Oc{mtGSArO(H4a+OF>Ut}-XCB#z2C ze~2en%nGmm`mX@HaPo?B#foUw4FB-58`>+suZv&fg1zW9*S`47LpLMmY z`Bti{3aSwsvU~Tcx<;}j@~No`foEu{VK}iESF!AOu*GlyN~nYz+c>d?Hg73xlz3}< z>M6})YZr-yB3rb1x0cWthq(q#<;QB$e|P_V1{#PBn<2}2RsDKyQDJvgeOph zYZ!sT)_g50v=}6`>0-2M>vod4mC)#G+Jcbdxn>iUC7d#j2@$B%7<~r9F^U+7vZykgDFDZ^g)LjU@OHYj+IwaZeScY; zDOiT=RC=y9s3@W>-J&R*IZg&Ce?ojK|CU~as>gzbwNrllw;?v#naW$hF^8F=TajoP zhorZa94G?PxQEuOGzG8#fH*B1DF9V+x66mTmnXjCE4a{Be3O(8PZ@#8;ET-rpM*Fg zyW1>gk%GeW9^QL6w}K^^JEx%wEOeVmwa2%RYrsN0bLmEjA*)<(XII>KM73MnX@jU!GILwxglRVA2(y<0UkoGm!qeLU!UKzyV2 zLc?)Pz(ah;BeyMpbU4x2wjisxrk95YHIebzy9V+iR4lr*35HngZdz=2sr#kWlfdh{ zPRt7;8Ol+aYarhHJ2%-Pf0;=XysQ7eG^!|G>AmAq!?MDWqFl##{K_JCk#YG{4LHUq z@_?=Dv?KtzefxrS=_i5cf9LU-d%J@{`-P31#UlJ{xM;$Ec|>Rvm%fO}Ea|`=@$gzSO)Hb3o|?|RyUr5bkdt(^z&g-M$9cwzl+P)) zuEx-*5NkYKi48i@CLM-*s?g1f(bGn*1`BS0!)mtt(F7m?h7!`0DWfEP(mdVMP{-1y z=F%3cZx&+H4IQeKf9caoz0{Zo|I}8u%=lK+6L8ZG&D2(X)kWvjPp#1}P1H3#)klrh zVm;PBr`4lY(B|sZ7C_Zx-PUexa%L@RjIkQ5p`@t0Z&FPGZ2i`J-PaVCX5+ACcNQ3Q zUDt{V$$0(MYaP~p-Pn!|aOY6ihE1!B+76%=9MJe1nJpaSe{=)}a$L&^jhlVhq+Qyk zecGs<+N!>VHFZP(phf8EdIEaBbW_Raqr?|s;1fZmEh-|Kzf06q%W4BTXp->%^Shikay>PCAN zSN#3mA?PWERagOD;h1ngc3|KO4yg#vC>u^8BJ&}n@!*OPf|Q6TZDZjoE(j`RE*M@1 z22S8Gh2Vem;SAG98uZ`EBHtYlI7N-(6}93*o(I*$e^0=h;&c$>Dqt#12x2MN0*InP z=|SQbz%Dfn3hBAZi ziRD}F89gGDKr%!{k~cbqTR2r$TvAQMb46B?09f)+(2pHJ9sO_~Yz{K-qBO)?;(ATzbN=If zgyeTV8KdGJW)4F&uqqOx!W7JeT z?|ek>LB8veK{i2jF!M4oiWOOiRWS%JA`RamooMW3f=YzLG#?c*3Q?Jmb214fEG%P7 z86Sr-qf4%|9<{Ns79YPTF9b{2C9^Z&VouwqUgi|=0OPYQe{KQrjwVvBJOB^t2V)>4 zf0*yPlM4L)^68W70WbgXi9t3VvZZNLR5#=m!RWkiV>hhyH+a*f$usqA12}13Hcq4v z*Qhud?;Lw+4U1mRaEoFXylP8REK-Y0M%jFJk- zu=6|b@~o2d8q@PPlM#_3)%yM$QtdD2e=hd_p7~6_88Gzp%rhp=LnQhnKGM^RTsjk= zpFKp>DSRGJRo(Sllxx&4@Cmw`VW?rqp#+8;Kyrv7q@)BKKxvdNfnn&5p?e7F1_8yO zBt#GpNl{8#1r)(R4twwOzSlWFt?Ry@f8e?9&syKLj=a9;e4QJo_K3vLPz(KcQ}G+W z*lE?ZhFGH)IZEt-5mVpfr-G4i_1lwXB#|;gUwr@U-~l$0OnCZ_pXtmuI$dVh_Xtcd zr7vGiuPcLno+&?fQqas~!v37E#C2|fg9mlTI67Da}cop z8B*|B>t~lTAb}b-G3=H;te;(huvvp~Y$%$T+05q{irb2?8VFX$F_Gxh->Dh{q{o?8 z!$b&#^0&W96JCBK8=#?rco9GP$x&xDa+G&2MORgc9KazZ@9-d?;y?thI1BgCdqu!}P{|YhgA0`W{=4 zlMdh7=$O%7x?5B@GUjaJB3qoG4=nba^v$qx*rmv+Tj*r& zeiLMPAt25$os>k2ahXpQm;uZ}{l#vfNuU1rv)Pd)JK+c*rP&>&oZcuHE}bzn z?Ux0J-bq9KGW*v0Uo~0Z@nc*nkvm75#D@nj>U#Nmd z3BDpu6GG7D2bSpdzp~SK2_(a*z=!dfKB9im(&B=36X#W&_ z4NTzGWM z*kkAa27UJ0)AU^sjW2g>;J_PA1I!KeLnDRT5odvsVfrkoeE*qlIF+6aF1}rtdll<25v8|n zCVu#=LUqOH5if4$GTQ29Z(|v)USU|S28B#DLGZN}z3()qHR=#H+=xxPaGQ#L_(z?` zzKWJI1dya}$-W{)HQo62yZOGKy~?7!!_$z>`-R3yl-7&hl5~(>&yur|C-{ey6g#R( zZ5OByOC!M_>egciaiV}y=B`E9b?U@pH~(v7#88grJmO>LU-TicttW$QPJGN-LNrI+ z*=oWSuB4x(@UN}0F<^@fDO!OzMj8m8Xq%Q8ch(oy!@0!%eMS zo1r@PnYr6nmCq+@wa@eA%aX3qXldEN;(v(l3EbyJQE1c45IUgUv1nSF897Eb zUFqgq6N}dP;h&d+5+R-;y43kWXpC%Ia*)Owntk|7{;(N$X>Didg(-b~lc;|mFoxn+ ztqNpqh;$K2?=(09fiz^9AP@N=qH=ngYy@y<*pwbi`;f<&H}8(L7VjOlSa?Sqt%HTr z-){ACp*BWbvJK2xRh=&%3=Q}3+??#1-7Y~A40w|LD}YZ8~P zam36{qVbmupz`G9p}4`cz!+CQMi%xZMF_5WAcYedw-I_<9(HTT)u*1e_k225pbneK zglVgkuKpDm-zf|@aeaiN@+fR!oKo7AZZ~-!ui(RIL;-u%$F{tNIas>A;m|{VyCyx* zc3@aYef|dHYdR(=DSEKLG`hIRSHk!`x1o0>YmiWLrS3BhADwgHL(EPU6u8P!2_0cz z0pK2ddpmx}!mL-FzE|0r5u#l&8DX=0K@bn<^*>XMqB_pg^q(v_7H|MSw?y8q8h|H@sRSvct@tTgB0fBj;3?V+9^>4%Yt$vhX9syo6&{0eq zs(+t)MW$Dt%)N5WBf7z@IeF~Kql36y-SJnY25-EUyTp`z1p9*rFyL@XmeNds&&NW$ zl)toIYk)=4a8^SILx=`_C3Cu_5D|NKeoS^Rx#e4~8<_lhFBOhqdTqd_5)7r8KFX~p zeG{I-bKdkI8uWs)gG!O%YF2eEFPEyU|3UN#S!|qDzv|Q8wao|l!i!w;iK~4#IqX)W zIHj@2D-~dXIIQ#2@bDX9$TXy5i;P+bqW(-(4e)w}F$#Ja`OTLs3QQF%zZ~ro&dOr7 zYP4WWW2fq42gVWhZm!>=WVmXyE8&L00A!#xdsk`^IW91@6u>E7Toe`sl%k}7%YbGT zG!m)R=Xppk+yenA^iTB`0d@FY0Ju)IEr;P2-s+|UPaA1}imJ)BZIY`yDQ{vLW8Q~5 zBo@KB;5?;yfZe|Po&XprEmF(($f!T+p2fTnFn22rmw+#CwpV=b4N-Ji32+-(hyr}Y zqwRn(_BPwV7kdjeVL_5>Nls+iVXo0)jG4@&FgmNfumVt$BR!1g*HEw-=4Mm~uQh_J z5rlDdN+=Bs6wCAls&mxEGh&x;p7+?a)G8Xf7r|G+npsxJ0=Sz^QFAwI1*1p2EHo6{jvnWBTjk5>I*9! zO!k3)dFtvdtBqgTHMFL~#6k=}ZOqL#HUBhLm*pUYT#q7X7CPil1o0pS=0KT3qM49y zm+V11-y`Yk!4;@dAoLh(LA8DNU^b;vrT-U&4wv6Dw`t+ z;V{hyO6dUKCj%9bNQzM-eRcb8e)|;g-mc&>uAMQr#q qeq~z|7nF&9KvU(%min3 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 diff --git a/doc/search_index.py b/doc/search_index.py deleted file mode 100644 index 834a57e..0000000 --- a/doc/search_index.py +++ /dev/null @@ -1,169 +0,0 @@ -from bs4 import BeautifulSoup -import json -import os - -index = [] - -def get_soup_title_and_category(uid, soup): - if uid == 'man.Functions': - uid = uid - - title, category = None, 'other' - - tutorial_header = soup.find('table', class_='tutorial-header') - is_tutorial = tutorial_header is not None - - # - if title is None: - doc_details = soup.find('div', class_='doc-details') - - if doc_details is not None: - h1 = doc_details.find('h1') - if h1 is not None: - title = str(h1.text) - - if title is None: - breadcrumb = soup.find('div', class_='doc-breadcrumb') - - if breadcrumb is not None: - links = breadcrumb.find_all('a') - if len(links) > 0: - title = str(links[-1].string) - else: - title = None - else: - h1 = soup.find('h1') - if h1 is not None: - title = h1.string - if title is not None: - title = str(title) - - if title is None or title == 'Documentation': - title = uid # fallback to uid - - if is_tutorial: - title = "Tutorial '%s'" % title - - # determine category - if is_tutorial: - category = 'tutorial' - elif uid.startswith('man.'): - category = 'manual' - elif title.endswith('Class'): - category = 'class' - title = title[:-5].rstrip() - - return title, category - - -def filter_and_parse(uid, version, html, filter): - soup = BeautifulSoup(html, 'html.parser') - - # get title and category - title, category = get_soup_title_and_category(uid, soup) - - # apply filter - soup, body = filter(soup) - if body == '': - return - - # build excerpt candidates - excerpts = [] - - def add_excerpt(ex): - if ex is None: - return - s = str(ex).strip() - if len(s) > 0: - excerpts.append(s) - - for p in soup.find_all('p'): # raw paragraph content - add_excerpt(p.string) - - for div in soup.find_all('div'): # function documentation - if 'class' in div.attrs: - for class_ in div['class']: - if class_ in ['function_doc']: - add_excerpt(div.string) - - # finalize entry - entry = {'uid': uid, 'category': category, 'title': title, 'body': body, 'excerpts': excerpts, 'version': str(version)} - index.append(entry) - - return soup, title - - -def clean_text(t): - return t.lstrip().rstrip() - - -def extract_page_functions(uid, version, page_title, soup): - function_divs = soup.find_all('div', class_='function_div') - - for function_div in function_divs: - id = function_div.get('id') - - if id.startswith(uid): - name = id[len(uid)+1:] - title = '%s.%s' % (uid, name) - else: - name = id - title = page_title - - body = name - - function_proto = function_div.find('div', class_='function_proto') - function_doc = function_div.find('div', class_='function_doc') - - excerpt = [] - if function_doc: - excerpt.append(clean_text(function_doc.text)) - - entry = {'uid': '%s#%s' % (uid, id), 'category': 'function', 'title': title, 'body': body, 'excerpts': excerpt, 'version': str(version)} - index.append(entry) - - -def parse(uid, version, html): - def basic_stripper_filter(soup): - # strip breadcrumb - breadcrumb = soup.find('div', {'class': 'doc-breadcrumb'}) - if breadcrumb is not None: - breadcrumb.extract() - - # strip version footer - version_span = soup.find('small', {'id': 'version-footer'}) - if version_span is not None: - version_span.extract() - - code_divs = soup.find_all('div', {'class': 'codehilite'}) - for div in code_divs: - div.extract() - - # extract body - body = soup.get_text(separator=' ') - # body = ' '.join(body.split()) - - for s in ['(', '[', '{', '\n']: - body = body.replace(s + ' ', s) - for s in [')', ']', '}', ',', ':', ';', '\n']: - body = body.replace(' ' + s, s) - - for i in range(4): - body = body.replace(' ', ' ') - body = body.replace('\n\n', '\n') - - return soup, body - - soup, title = filter_and_parse(uid, version, html, basic_stripper_filter) - - # extract page functions - extract_page_functions(uid, version, title, soup) - - -def save(dir, path): - with open(os.path.join(dir, path), 'w') as file: - for entry in index: - file.write('{"index":{}}\n') - json.dump(entry, file) - file.write('\n') - file.write('{"index":{}}\n') diff --git a/extern/tiny-process-library/process_win.cpp b/extern/tiny-process-library/process_win.cpp index 7f52069..4f1544e 100644 --- a/extern/tiny-process-library/process_win.cpp +++ b/extern/tiny-process-library/process_win.cpp @@ -112,7 +112,7 @@ Process::id_type Process::open(const string_type &command, const string_type &pa #endif const std::wstring wpath=utf8_to_wstring(path); - BOOL bSuccess = CreateProcess(nullptr, process_command.empty()?nullptr:&process_command[0], nullptr, nullptr, TRUE, 0, + BOOL bSuccess = CreateProcess(nullptr, process_command.empty()?nullptr:&process_command[0], nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, wpath.empty()?nullptr:wpath.c_str(), &startup_info, &process_info); if(!bSuccess) diff --git a/harfang/CMakeLists.txt b/harfang/CMakeLists.txt index c22c761..cf457ee 100644 --- a/harfang/CMakeLists.txt +++ b/harfang/CMakeLists.txt @@ -16,9 +16,9 @@ if(HG_BUILD_DOCS) configure_file(doxyfile.tpl ${CMAKE_CURRENT_BINARY_DIR}/doxyfile @ONLY) add_custom_target(doc_cppsdk ALL ${CMAKE_COMMAND} -E make_directory ${CMAKE_INSTALL_PREFIX}/cppsdk_docs - COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doxyfile - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Generating CPPSDK documentation" - VERBATIM + COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doxyfile + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating CPPSDK documentation" + VERBATIM ) endif() diff --git a/harfang/engine/aaa_blur.cpp b/harfang/engine/aaa_blur.cpp index ce158ec..7308ab9 100644 --- a/harfang/engine/aaa_blur.cpp +++ b/harfang/engine/aaa_blur.cpp @@ -19,12 +19,20 @@ static AAABlur _CreateAAABlur(const Reader &ir, const ReadProvider &ip, const ch aaa_blur.u_sigma = bgfx::createUniform("u_sigma", bgfx::UniformType::Vec4); aaa_blur.u_input = bgfx::createUniform("u_input", bgfx::UniformType::Sampler); aaa_blur.u_attr0 = bgfx::createUniform("u_attr0", bgfx::UniformType::Sampler); + if (!IsValid(aaa_blur)) { + DestroyAAABlur(aaa_blur); + } return aaa_blur; } AAABlur CreateAAABlurFromFile(const char *path) { return _CreateAAABlur(g_file_reader, g_file_read_provider, path); } AAABlur CreateAAABlurFromAssets(const char *path) { return _CreateAAABlur(g_assets_reader, g_assets_read_provider, path); } +bool IsValid(const AAABlur &aaa_blur) { + return bgfx::isValid(aaa_blur.compute) && bgfx::isValid(aaa_blur.u_dir) && bgfx::isValid(aaa_blur.u_sigma) && bgfx::isValid(aaa_blur.u_input) && + bgfx::isValid(aaa_blur.u_attr0); +} + void DestroyAAABlur(AAABlur &aaa_blur) { bgfx_Destroy(aaa_blur.compute); bgfx_Destroy(aaa_blur.u_dir); @@ -34,6 +42,7 @@ void DestroyAAABlur(AAABlur &aaa_blur) { } void ComputeAAABlur(bgfx::ViewId &view_id, const iRect &rect, const Texture &attr0, bgfx::FrameBufferHandle fb0, bgfx::FrameBufferHandle fb1, const AAABlur &aaa_blur) { + __ASSERT__(IsValid(aaa_blur)); bgfx::TransientIndexBuffer idx; bgfx::TransientVertexBuffer vtx; CreateFullscreenQuad(idx, vtx); diff --git a/harfang/engine/aaa_blur.h b/harfang/engine/aaa_blur.h index b38f917..58d9af1 100644 --- a/harfang/engine/aaa_blur.h +++ b/harfang/engine/aaa_blur.h @@ -23,4 +23,6 @@ void DestroyAAABlur(AAABlur &aaa_blur); void ComputeAAABlur(bgfx::ViewId &view_id, const iRect &rect, const Texture &attr0, bgfx::FrameBufferHandle fb0, bgfx::FrameBufferHandle fb1, const AAABlur &aaa_blur); +bool IsValid(const AAABlur &aaa_blur); + } // namespace hg diff --git a/harfang/engine/bloom.cpp b/harfang/engine/bloom.cpp index 21049f0..87cb380 100644 --- a/harfang/engine/bloom.cpp +++ b/harfang/engine/bloom.cpp @@ -43,15 +43,28 @@ static bool LoadShaders(Bloom &bloom, const Reader &ir, const ReadProvider &ip, static const uint64_t g_bloom_tex_flags = 0 | BGFX_TEXTURE_RT | BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; +bool IsValid(const Bloom &bloom) { + return bgfx::isValid(bloom.in_fb) && bgfx::isValid(bloom.out_fb) && bgfx::isValid(bloom.u_source) && bgfx::isValid(bloom.u_input) && + bgfx::isValid(bloom.u_params) && bgfx::isValid(bloom.u_source_rect) && bgfx::isValid(bloom.prg_threshold) && bgfx::isValid(bloom.prg_downsample) && + bgfx::isValid(bloom.prg_upsample) && bgfx::isValid(bloom.prg_combine); +} + static Bloom CreateBloom(const Reader &ir, const ReadProvider &ip, const char *path, bgfx::BackbufferRatio::Enum ratio) { Bloom bloom; + if (!LoadShaders(bloom, ir, ip, path)) { + DestroyBloom(bloom); + return bloom; + } bloom.in_fb = bgfx::createFrameBuffer(ratio, bgfx::TextureFormat::RGBA16F, g_bloom_tex_flags); - bgfx::setName(bloom.in_fb, "Bloom IN FB"); bloom.out_fb = bgfx::createFrameBuffer(ratio, bgfx::TextureFormat::RGBA16F, g_bloom_tex_flags); - bgfx::setName(bloom.out_fb, "Bloom OUT FB"); + if (!(bgfx::isValid(bloom.in_fb) && bgfx::isValid(bloom.out_fb))) { + DestroyBloom(bloom); + return bloom; + } - LoadShaders(bloom, ir, ip, path); + bgfx::setName(bloom.in_fb, "Bloom IN FB"); + bgfx::setName(bloom.out_fb, "Bloom OUT FB"); return bloom; } @@ -75,6 +88,8 @@ void DestroyBloom(Bloom &bloom) { void ApplyBloom(bgfx::ViewId &view_id, const iRect &rect, const hg::Texture &input, bgfx::FrameBufferHandle output, const Bloom &bloom, float threshold, float smoothness, float intensity) { + __ASSERT__(IsValid(bloom)); + const bgfx::Caps *caps = bgfx::getCaps(); bgfx::TransientIndexBuffer idx; diff --git a/harfang/engine/bloom.h b/harfang/engine/bloom.h index 0427f50..f2e71a6 100644 --- a/harfang/engine/bloom.h +++ b/harfang/engine/bloom.h @@ -34,4 +34,6 @@ void DestroyBloom(Bloom &bloom); void ApplyBloom(bgfx::ViewId &view_id, const iRect &rect, const hg::Texture &input, bgfx::FrameBufferHandle output, const Bloom &bloom, float threshold, float smoothness, float intensity); +bool IsValid(const Bloom &bloom); + } // namespace hg diff --git a/harfang/engine/component.cpp b/harfang/engine/component.cpp index 6579292..6e1fc06 100644 --- a/harfang/engine/component.cpp +++ b/harfang/engine/component.cpp @@ -887,6 +887,12 @@ void RigidBody::SetFriction(float friction) { if (scene_ref && scene_ref->scene) scene_ref->scene->SetRigidBodyFriction(ref, friction); } +float RigidBody::GetRollingFriction() const { return scene_ref && scene_ref->scene ? scene_ref->scene->GetRigidBodyRollingFriction(ref) : 0.f; } + +void RigidBody::SetRollingFriction(float rolling_friction) { + if (scene_ref && scene_ref->scene) + scene_ref->scene->SetRigidBodyRollingFriction(ref, rolling_friction); +} // bool Collision::IsValid() const { return scene_ref && scene_ref->scene ? scene_ref->scene->IsValidCollisionRef(ref) : false; } @@ -898,6 +904,13 @@ void Collision::SetType(CollisionType type) { scene_ref->scene->SetCollisionType(ref, type); } +Mat4 Collision::GetLocalTransform() const { return scene_ref && scene_ref->scene ? scene_ref->scene->GetCollisionLocalTransform(ref) : Mat4::Identity; } + +void Collision::SetLocalTransform(Mat4 m) { + if (scene_ref && scene_ref->scene) + scene_ref->scene->SetCollisionLocalTransform(ref, m); +} + float Collision::GetMass() const { return scene_ref && scene_ref->scene ? scene_ref->scene->GetCollisionMass(ref) : 0.f; } void Collision::SetMass(float mass) { diff --git a/harfang/engine/dear_imgui.cpp b/harfang/engine/dear_imgui.cpp index 3fe8280..de0873a 100644 --- a/harfang/engine/dear_imgui.cpp +++ b/harfang/engine/dear_imgui.cpp @@ -198,7 +198,8 @@ void ImGuiCreateFontTexture(DearImguiContext &ctx) { ctx.m_texture = bgfx::createTexture2D((uint16_t)width, (uint16_t)height, false, 1, bgfx::TextureFormat::BGRA8, 0, bgfx::copy(data, width * height * 4)); - io.Fonts->ClearInputData(); + // commented out because we don't want to destroy imgui mouse cursors + // io.Fonts->ClearInputData(); io.Fonts->ClearTexData(); } diff --git a/harfang/engine/dear_imgui.h b/harfang/engine/dear_imgui.h index 2b701c2..1729c28 100644 --- a/harfang/engine/dear_imgui.h +++ b/harfang/engine/dear_imgui.h @@ -95,3 +95,8 @@ void AddImageRounded(ImDrawList *draw_list, const hg::Texture &tex, const ImVec2 float rounding, int rounding_corners = ImDrawCornerFlags_All); } // namespace ImGui + +inline ImVec2 operator+(const ImVec2 &a, const ImVec2 &b) { return {a.x + b.x, a.y + b.y}; } +inline ImVec2 operator-(const ImVec2 &a, const ImVec2 &b) { return {a.x - b.x, a.y - b.y}; } +inline ImVec2 operator*(const ImVec2 &a, const ImVec2 &b) { return {a.x * b.x, a.y * b.y}; } +inline ImVec2 operator/(const ImVec2 &a, const ImVec2 &b) { return {a.x / b.x, a.y / b.y}; } diff --git a/harfang/engine/downsample.cpp b/harfang/engine/downsample.cpp index 945d314..e508057 100644 --- a/harfang/engine/downsample.cpp +++ b/harfang/engine/downsample.cpp @@ -10,6 +10,12 @@ namespace hg { +bool IsValid(const Downsample &downsample) { + return bgfx::isValid(downsample.compute) && bgfx::isValid(downsample.u_color) && bgfx::isValid(downsample.u_attr0) && bgfx::isValid(downsample.u_depth) && + bgfx::isValid(downsample.fb) && bgfx::isValid(downsample.color.handle) && bgfx::isValid(downsample.attr0.handle) && + bgfx::isValid(downsample.depth.handle); +} + static Downsample _CreateDownsample(const Reader &ir, const ReadProvider &ip, const char *path) { Downsample down; down.compute = hg::LoadProgram(ir, ip, hg::format("%1/shader/aaa_downsample").arg(path)); @@ -21,16 +27,22 @@ static Downsample _CreateDownsample(const Reader &ir, const ReadProvider &ip, co 0 | BGFX_TEXTURE_RT | BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT | BGFX_SAMPLER_MIP_POINT | BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; down.color = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Half, false, 1, bgfx::TextureFormat::RGBA32F, flags)}; - bgfx::setName(down.color.handle, "color.downsampled"); - + down.attr0 = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Half, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(down.attr0.handle, "attr0.downsampled"); - + down.depth = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Half, false, 1, bgfx::TextureFormat::R32F, flags)}; - bgfx::setName(down.depth.handle, "depth.downsampled"); - + bgfx::TextureHandle texs[] = {down.color.handle, down.attr0.handle, down.depth.handle}; down.fb = bgfx::createFrameBuffer(3, texs, true); + + if (!IsValid(down)) { + DestroyDownsample(down); + return down; + } + + bgfx::setName(down.depth.handle, "depth.downsampled"); + bgfx::setName(down.color.handle, "color.downsampled"); + bgfx::setName(down.attr0.handle, "attr0.downsampled"); bgfx::setName(down.fb, "Downsample FB"); return down; @@ -45,18 +57,15 @@ void DestroyDownsample(Downsample &down) { bgfx_Destroy(down.u_attr0); bgfx_Destroy(down.u_depth); bgfx_Destroy(down.fb); - - down.compute = BGFX_INVALID_HANDLE; - down.u_color = BGFX_INVALID_HANDLE; - down.u_attr0 = BGFX_INVALID_HANDLE; - down.fb = BGFX_INVALID_HANDLE; + down.depth = BGFX_INVALID_HANDLE; down.color = BGFX_INVALID_HANDLE; down.attr0 = BGFX_INVALID_HANDLE; - down.depth = BGFX_INVALID_HANDLE; } // [todo] use a pyramid void ComputeDownsample(bgfx::ViewId &view_id, const iRect &rect, const Texture &color, const Texture &attr0, const Texture &depth, const Downsample &down) { + __ASSERT__(IsValid(down)); + const bgfx::Caps *caps = bgfx::getCaps(); bgfx::TransientIndexBuffer idx; diff --git a/harfang/engine/downsample.h b/harfang/engine/downsample.h index 3bd4a20..520c2ce 100644 --- a/harfang/engine/downsample.h +++ b/harfang/engine/downsample.h @@ -25,4 +25,6 @@ void DestroyDownsample(Downsample &downsample); void ComputeDownsample( bgfx::ViewId &view_id, const iRect &rect, const Texture &color, const Texture &attr0, const Texture &depth, const Downsample &downsample); +bool IsValid(const Downsample &downsample); + } // namespace hg diff --git a/harfang/engine/hiz.cpp b/harfang/engine/hiz.cpp index 4055c37..361cd4b 100644 --- a/harfang/engine/hiz.cpp +++ b/harfang/engine/hiz.cpp @@ -10,6 +10,11 @@ namespace hg { +bool IsValid(const HiZ &hiz) { + return bgfx::isValid(hiz.pyramid.handle) && bgfx::isValid(hiz.prg_copy) && bgfx::isValid(hiz.prg_compute) && bgfx::isValid(hiz.u_depth) && + bgfx::isValid(hiz.u_projection); +} + static HiZ _CreateHiZ(const Reader &ir, const ReadProvider &ip, const char *path, bgfx::BackbufferRatio::Enum ratio) { HiZ hiz; @@ -44,12 +49,17 @@ static HiZ _CreateHiZ(const Reader &ir, const ReadProvider &ip, const char *path bgfx::TextureHandle handle = bgfx::createTexture2D(hiz.pyramid_infos.width, hiz.pyramid_infos.height, hiz.pyramid_infos.numMips, hiz.pyramid_infos.numLayers, hiz.pyramid_infos.format, BGFX_TEXTURE_COMPUTE_WRITE | flags); - bgfx::setName(handle, "hiz.pyramid"); + hiz.pyramid = hg::MakeTexture(handle, BGFX_TEXTURE_COMPUTE_WRITE | flags); - hiz.prg_copy = hg::LoadComputeProgram(ir, ip, hg::format("%1/shader/hiz_copy_cs.sc").arg(path)); hiz.prg_compute = hg::LoadComputeProgram(ir, ip, hg::format("%1/shader/hiz_compute_cs.sc").arg(path)); + if (!IsValid(hiz)) { + DestroyHiZ(hiz); + return hiz; + } + + bgfx::setName(handle, "hiz.pyramid"); return hiz; } @@ -65,6 +75,8 @@ void DestroyHiZ(HiZ &hiz) { } void ComputeHiZ(bgfx::ViewId &view_id, const iRect &rect, const Mat44& proj, const Texture &depth, const HiZ &hiz) { + __ASSERT__(IsValid(hiz)); + bgfx::setUniform(hiz.u_projection, to_bgfx(proj).data()); bgfx::setViewName(view_id, "HiZ copy"); diff --git a/harfang/engine/hiz.h b/harfang/engine/hiz.h index c14d6b4..d2efac2 100644 --- a/harfang/engine/hiz.h +++ b/harfang/engine/hiz.h @@ -16,7 +16,6 @@ struct HiZ { bgfx::ProgramHandle prg_compute = BGFX_INVALID_HANDLE; bgfx::UniformHandle u_depth = BGFX_INVALID_HANDLE; bgfx::UniformHandle u_projection = BGFX_INVALID_HANDLE; - bgfx::UniformHandle u_resolution = BGFX_INVALID_HANDLE; }; HiZ CreateHiZFromFile(const char *path, bgfx::BackbufferRatio::Enum ratio); @@ -27,4 +26,6 @@ void DestroyHiZ(HiZ &hiz); /// @note input depth buffer must be in linear depth void ComputeHiZ(bgfx::ViewId &view_id, const iRect &rect, const Mat44 &proj, const Texture &attr0, const HiZ &hiz); +bool IsValid(const HiZ &hiz); + } // namespace hg diff --git a/harfang/engine/iso_surface.cpp b/harfang/engine/iso_surface.cpp index f6ada27..db010ca 100644 --- a/harfang/engine/iso_surface.cpp +++ b/harfang/engine/iso_surface.cpp @@ -384,7 +384,7 @@ IsoSurface ConvoluteIsoSurface(const IsoSurface &surface, int width, int height, } IsoSurface GaussianBlurIsoSurface(const IsoSurface &surface, int width, int height, int depth) { - static float kernel[27] = { + static const float kernel[27] = { 0.3f * 0.6f, 0.6f * 0.6f, 0.3f * 0.6f, 0.6f * 0.6f, 1.f * 0.6f, 0.6f * 0.6f, 0.3f * 0.6f, 0.6f * 0.6f, 0.3f * 0.6f, diff --git a/harfang/engine/meta.cpp b/harfang/engine/meta.cpp index 44c4c80..c2b31a9 100644 --- a/harfang/engine/meta.cpp +++ b/harfang/engine/meta.cpp @@ -84,8 +84,8 @@ json LoadJson(const Reader &ir, const Handle &h, bool *result) { return js; } -json LoadJsonFromFile(const char *path, bool *result) { return LoadJson(g_file_reader, ScopedReadHandle(g_file_read_provider, path), result); } -json LoadJsonFromAssets(const char *name, bool *result) { return LoadJson(g_assets_reader, ScopedReadHandle(g_assets_read_provider, name), result); } +json LoadJsonFromFile(const char *path, bool *result) { return LoadJson(g_file_reader, ScopedReadHandle(g_file_read_provider, path, true), result); } +json LoadJsonFromAssets(const char *name, bool *result) { return LoadJson(g_assets_reader, ScopedReadHandle(g_assets_read_provider, name, true), result); } bool SaveJsonToFile(const json &js, const char *path) { const auto js_ = js.dump(1, '\t', true); diff --git a/harfang/engine/motion_blur.cpp b/harfang/engine/motion_blur.cpp index 06e7033..152338e 100644 --- a/harfang/engine/motion_blur.cpp +++ b/harfang/engine/motion_blur.cpp @@ -10,6 +10,11 @@ namespace hg { +bool IsValid(const MotionBlur &motion_blur) { + return bgfx::isValid(motion_blur.prg_motion_blur) && bgfx::isValid(motion_blur.u_color) && bgfx::isValid(motion_blur.u_attr0) && + bgfx::isValid(motion_blur.u_attr1) && bgfx::isValid(motion_blur.u_noise); +} + static MotionBlur _CreateMotionBlur(const Reader &ir, const ReadProvider &ip, const char *path) { MotionBlur motion_blur; @@ -19,6 +24,9 @@ static MotionBlur _CreateMotionBlur(const Reader &ir, const ReadProvider &ip, co motion_blur.u_attr1 = bgfx::createUniform("u_attr1", bgfx::UniformType::Sampler); motion_blur.u_noise = bgfx::createUniform("u_noise", bgfx::UniformType::Sampler); + if (!IsValid(motion_blur)) { + DestroyMotionBlur(motion_blur); + } return motion_blur; } @@ -35,6 +43,8 @@ void DestroyMotionBlur(MotionBlur &motion_blur) { void ApplyMotionBlur(bgfx::ViewId &view_id, const iRect &rect, const Texture &color, const Texture &attr0, const Texture &attr1, const Texture &noise, bgfx::FrameBufferHandle output, const MotionBlur &motion_blur) { + __ASSERT__(IsValid(motion_blur)); + const bgfx::Caps *caps = bgfx::getCaps(); bgfx::TransientIndexBuffer idx; diff --git a/harfang/engine/motion_blur.h b/harfang/engine/motion_blur.h index fafd146..71c2a60 100644 --- a/harfang/engine/motion_blur.h +++ b/harfang/engine/motion_blur.h @@ -24,4 +24,6 @@ void DestroyMotionBlur(MotionBlur &motion_blur); void ApplyMotionBlur(bgfx::ViewId &view_id, const iRect &rect, const Texture &color, const Texture &attr0, const Texture &attr1, const Texture &noise, bgfx::FrameBufferHandle output, const MotionBlur &motion_blur); +bool IsValid(const MotionBlur &motion_blur); + } // namespace hg diff --git a/harfang/engine/node.h b/harfang/engine/node.h index 8b07a18..a7b7cc2 100644 --- a/harfang/engine/node.h +++ b/harfang/engine/node.h @@ -167,7 +167,7 @@ struct Light { // 16B on 64 bit }; // -enum RigidBodyType : uint8_t { RBT_Dynamic, RBT_Kinematic, RBT_Static}; +enum RigidBodyType : uint8_t { RBT_Dynamic, RBT_Kinematic, RBT_Static }; struct RigidBody { // 16B on 64 bit bool IsValid() const; @@ -187,6 +187,8 @@ struct RigidBody { // 16B on 64 bit void SetRestitution(float restitution); float GetFriction() const; void SetFriction(float friction); + float GetRollingFriction() const; + void SetRollingFriction(float rolling_friction); intrusive_shared_ptr_st scene_ref; ComponentRef ref; @@ -204,6 +206,8 @@ struct Collision { // 16B on 64 bit CollisionType GetType() const; void SetType(CollisionType type); + Mat4 GetLocalTransform() const; + void SetLocalTransform(Mat4 m); float GetMass() const; void SetMass(float mass); float GetRadius() const; diff --git a/harfang/engine/openvr_api.cpp b/harfang/engine/openvr_api.cpp index 01f00e7..df0e82a 100644 --- a/harfang/engine/openvr_api.cpp +++ b/harfang/engine/openvr_api.cpp @@ -36,14 +36,14 @@ static std::array openv static uint32_t rt_width = 0, rt_height = 0; static Mat44 OVRToMat44(const vr::HmdMatrix44_t &m) { - static Mat44 VR_to_gs(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1); + static const Mat44 VR_to_gs(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1); return /*VR_to_gs * */ Mat44(m.m[0][0], m.m[1][0], m.m[2][0], m.m[3][0], m.m[0][1], m.m[1][1], m.m[2][1], m.m[3][1], m.m[0][2], m.m[1][2], m.m[2][2], m.m[3][2], m.m[0][3], m.m[1][3], m.m[2][3], m.m[3][3]) * VR_to_gs; } static Mat4 OVRToMat4(const vr::HmdMatrix34_t &m) { - static Mat4 VR_to_gs(1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0); + static const Mat4 VR_to_gs(1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0); return VR_to_gs * Mat4(m.m[0][0], m.m[1][0], m.m[2][0], m.m[0][1], m.m[1][1], m.m[2][1], m.m[0][2], m.m[1][2], m.m[2][2], m.m[0][3], m.m[1][3], m.m[2][3]) * VR_to_gs; } diff --git a/harfang/engine/picture.cpp b/harfang/engine/picture.cpp index 09e8546..fe73f0d 100644 --- a/harfang/engine/picture.cpp +++ b/harfang/engine/picture.cpp @@ -13,12 +13,12 @@ namespace hg { int size_of(PictureFormat format) { - static int size_of_format[PF_Last] = {0, 3, 4, 16}; + static const int size_of_format[PF_Last] = {0, 3, 4, 16}; return size_of_format[format]; } size_t GetChannelCount(PictureFormat format) { - static size_t channel_count_format[PF_Last] = {0, 3, 4, 4}; + static const size_t channel_count_format[PF_Last] = {0, 3, 4, 4}; return channel_count_format[format]; } diff --git a/harfang/engine/recast_detour.cpp b/harfang/engine/recast_detour.cpp index e0ea34e..fdaff77 100644 --- a/harfang/engine/recast_detour.cpp +++ b/harfang/engine/recast_detour.cpp @@ -177,21 +177,21 @@ void DrawNavMesh(const dtNavMesh *mesh, bgfx::ViewId view_id, const bgfx::Vertex int tileNum = mesh->decodePolyIdTile(base); - for (int i = 0; i < tile->header->polyCount; ++i) { - const dtPoly *p = &tile->polys[i]; + for (int j = 0; j < tile->header->polyCount; ++j) { + const dtPoly *p = &tile->polys[j]; if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) // Skip off-mesh links. continue; - const dtPolyDetail *pd = &tile->detailMeshes[i]; + const dtPolyDetail *pd = &tile->detailMeshes[j]; - for (int j = 0; j < pd->triCount; ++j) { - const unsigned char *t = &tile->detailTris[(pd->triBase + j) * 4]; - for (int k = 0; k < 3; ++k) { - if (t[k] < p->vertCount) { - float *v = &tile->verts[p->verts[t[k]] * 3]; + for (int k = 0; k < pd->triCount; ++k) { + const unsigned char *t = &tile->detailTris[(pd->triBase + k) * 4]; + for (int l = 0; l < 3; ++l) { + if (t[l] < p->vertCount) { + float *v = &tile->verts[p->verts[t[l]] * 3]; vtx.Begin(vtx_count++).SetPos(Vec3(v[0], v[1], v[2])).SetColor0(col).End(); } else { - float *v = &tile->detailVerts[(pd->vertBase + t[k] - p->vertCount) * 3]; + float *v = &tile->detailVerts[(pd->vertBase + t[l] - p->vertCount) * 3]; vtx.Begin(vtx_count++).SetPos(Vec3(v[0], v[1], v[2])).SetColor0(col).End(); } } diff --git a/harfang/engine/render_pipeline.cpp b/harfang/engine/render_pipeline.cpp index de1ccd7..5e7a89b 100644 --- a/harfang/engine/render_pipeline.cpp +++ b/harfang/engine/render_pipeline.cpp @@ -694,7 +694,7 @@ std::vector LoadPipelineProgramFeaturesFromAssets(const // bool LoadPipelineProgramUniforms(const Reader &ir, const ReadProvider &ip, const char *name, std::vector &texs, std::vector &vecs, PipelineResources &resources) { - const auto json_text = LoadString(ir, ScopedReadHandle(ip, name)); + const auto json_text = LoadString(ir, ScopedReadHandle(ip, name, true)); if (json_text.empty()) return false; @@ -702,7 +702,7 @@ bool LoadPipelineProgramUniforms(const Reader &ir, const ReadProvider &ip, const const auto i = js.find("uniforms"); if (i == std::end(js)) - return false; + return true; // [EJ] if there's no uniforms declared look no further for (const auto &j : *i) { const auto uname = j["name"].get(); @@ -2223,7 +2223,8 @@ void Vertices::End(bool validate) { "Weight", "TexCoord0", "TexCoord1", "TexCoord2", "TexCoord3", "TexCoord4", "TexCoord5", "TexCoord6", "TexCoord7"}; for (int attr = 0; attr < bgfx::Attrib::Count; ++attr) - if (decl.has(bgfx::Attrib::Enum(attr)) && !(vtx_attr_flag & (1 << attr))) // attribute expected by the vertex layout but missing from vertex declaration + if (decl.has(bgfx::Attrib::Enum(attr)) && + !(vtx_attr_flag & (1 << attr))) // attribute expected by the vertex layout but missing from vertex declaration warn(format("Incomplete vertex #%1 declaration: missing %2 attribute").arg(idx).arg(attr_name[attr])); } @@ -2490,8 +2491,8 @@ void SetView2D(bgfx::ViewId id, int x, int y, int res_x, int res_y, float znear, bgfx::setViewTransform(id, _view.data(), _proj.data()); } -void SetViewPerspective(bgfx::ViewId id, int x, int y, int res_x, int res_y, const Mat4 &world, float znear, float zfar, float zoom_factor, uint16_t clear_flags, - const Color &clear_color, float depth, uint8_t stencil, const Vec2 &aspect_ratio) { +void SetViewPerspective(bgfx::ViewId id, int x, int y, int res_x, int res_y, const Mat4 &world, float znear, float zfar, float zoom_factor, + uint16_t clear_flags, const Color &clear_color, float depth, uint8_t stencil, const Vec2 &aspect_ratio) { const bgfx::Caps *caps = bgfx::getCaps(); bgfx::setViewClear(id, clear_flags, ColorToABGR32(clear_color), depth, stencil); @@ -2521,6 +2522,24 @@ void SetViewOrthographic(bgfx::ViewId id, int x, int y, int res_x, int res_y, co } // +static inline FrameBuffer CreateFrameBuffer(bgfx::TextureHandle color, bgfx::TextureHandle depth, const char *name, bool own_textures) { + bgfx::TextureHandle texs[] = {color, depth}; + + if (own_textures) { + bgfx::setName(color, format("FrameBuffer.color (%1)").arg(name).c_str()); + bgfx::setName(depth, format("FrameBuffer.depth (%1)").arg(name).c_str()); + } + + bgfx::FrameBufferHandle handle = bgfx::createFrameBuffer(2, texs, own_textures); + bgfx::setName(handle, format("FrameBuffer (%1)").arg(name).c_str()); + + return {handle}; +} + +FrameBuffer CreateFrameBuffer(const hg::Texture &color, const hg::Texture &depth, const char *name) { + return CreateFrameBuffer(color.handle, depth.handle, name, false); +} + static inline uint64_t ComputeTextureRTMSAAFlag(int aa) { switch (aa) { case 2: @@ -2536,60 +2555,31 @@ static inline uint64_t ComputeTextureRTMSAAFlag(int aa) { } } -static inline PipelineFrameBuffer CreateFrameBuffer(hg::Texture &color, hg::Texture &depth, PipelineResources &res, const char *name) { - auto color_name = format("FrameBuffer.color (%1)").arg(name); - auto depth_name = format("FrameBuffer.depth (%1)").arg(name); - - bgfx::setName(color.handle, color_name.c_str()); - bgfx::setName(depth.handle, depth_name.c_str()); - - PipelineFrameBuffer frame_buffer; - bgfx::TextureHandle texs[] = {color.handle, depth.handle}; - frame_buffer.handle = bgfx::createFrameBuffer(2, texs); - bgfx::setName(frame_buffer.handle, format("FrameBuffer (%1)").arg(name).c_str()); - - res.textures.Destroy(res.textures.Has(color_name.c_str())); - frame_buffer.color = res.textures.Add(color_name.c_str(), color); - res.textures.Destroy(res.textures.Has(depth_name.c_str())); - frame_buffer.depth = res.textures.Add(depth_name.c_str(), depth); - - return frame_buffer; -} - -PipelineFrameBuffer CreateFrameBuffer( - bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, PipelineResources &res, const char *name) { +FrameBuffer CreateFrameBuffer(bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, const char *name) { uint64_t flags = ComputeTextureRTMSAAFlag(aa); - hg::Texture color, depth; - color.flags = flags; - color.handle = bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, color_format, color.flags); + const auto color = bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, color_format, flags); + const auto depth = bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, depth_format, flags | BGFX_TEXTURE_RT_WRITE_ONLY); - depth.flags = BGFX_TEXTURE_RT_WRITE_ONLY | flags; - depth.handle = bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, depth_format, depth.flags); - - return CreateFrameBuffer(color, depth, res, name); + return CreateFrameBuffer(color, depth, name, true); } -PipelineFrameBuffer CreateFrameBuffer( - int width, int height, bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, PipelineResources &res, const char *name) { +FrameBuffer CreateFrameBuffer(int width, int height, bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, const char *name) { uint64_t flags = ComputeTextureRTMSAAFlag(aa); - hg::Texture color, depth; - color.flags = flags; - color.handle = bgfx::createTexture2D(width, height, false, 1, color_format, color.flags); + const auto color = bgfx::createTexture2D(width, height, false, 1, color_format, flags); + const auto depth = bgfx::createTexture2D(width, height, false, 1, depth_format, flags | BGFX_TEXTURE_RT_WRITE_ONLY); - depth.flags = BGFX_TEXTURE_RT_WRITE_ONLY | flags; - depth.handle = bgfx::createTexture2D(width, height, false, 1, depth_format, depth.flags); - - return CreateFrameBuffer(color, depth, res, name); + return CreateFrameBuffer(color, depth, name, true); } -void DestroyFrameBuffer(PipelineResources &res, PipelineFrameBuffer &frameBuffer) { - res.textures.Destroy(frameBuffer.color); - res.textures.Destroy(frameBuffer.depth); - bgfx::destroy(frameBuffer.handle); -} +Texture GetColorTexture(FrameBuffer &frameBuffer) { return MakeTexture(bgfx::getTexture(frameBuffer.handle, 0)); } +Texture GetDepthTexture(FrameBuffer &frameBuffer) { return MakeTexture(bgfx::getTexture(frameBuffer.handle, 1)); } + +void DestroyFrameBuffer(FrameBuffer &frameBuffer) { bgfx::destroy(frameBuffer.handle); } + +// bool CreateFullscreenQuad(bgfx::TransientIndexBuffer &idx, bgfx::TransientVertexBuffer &vtx) { bgfx::VertexLayout layout; layout.begin().add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float).add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float).end(); diff --git a/harfang/engine/render_pipeline.h b/harfang/engine/render_pipeline.h index 5abf617..76352d3 100644 --- a/harfang/engine/render_pipeline.h +++ b/harfang/engine/render_pipeline.h @@ -84,12 +84,12 @@ bool IsRenderUp(); /// Fit the backbuffer to the specified window client area dimensions, return true if resizing was carried out. bool RenderResetToWindow(Window *win, int &width, int &height, uint32_t reset_flags = 0); -void SetView2D(bgfx::ViewId id, int x, int y, int res_x, int res_y, float znear = -1.f, float zfar = 1.f, uint16_t clear_flags = BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, - const Color &clear_color = Color::Black, float depth = 1.f, uint8_t stencil = 0, bool y_up = false); -void SetViewPerspective(bgfx::ViewId id, int x, int y, int res_x, int res_y, const Mat4 &world, float znear = 0.01f, float zfar = 1000.f, - float zoom_factor = 1.8, +void SetView2D(bgfx::ViewId id, int x, int y, int res_x, int res_y, float znear = -1.f, float zfar = 1.f, uint16_t clear_flags = BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, const Color &clear_color = Color::Black, float depth = 1.f, uint8_t stencil = 0, - const Vec2 &aspect_ratio = {}); + bool y_up = false); +void SetViewPerspective(bgfx::ViewId id, int x, int y, int res_x, int res_y, const Mat4 &world, float znear = 0.01f, float zfar = 1000.f, + float zoom_factor = 1.8, uint16_t clear_flags = BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, const Color &clear_color = Color::Black, float depth = 1.f, + uint8_t stencil = 0, const Vec2 &aspect_ratio = {}); void SetViewOrthographic(bgfx::ViewId id, int x, int y, int res_x, int res_y, const Mat4 &world, float znear = 0.01f, float zfar = 1000.f, float size = 1.f, uint16_t clear_flags = BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, const Color &clear_color = Color::Black, float depth = 1.f, uint8_t stencil = 0, const Vec2 &aspect_ratio = {}); @@ -618,18 +618,18 @@ struct Pipeline { void DestroyPipeline(Pipeline &pipeline); // -struct PipelineFrameBuffer { +struct FrameBuffer { bgfx::FrameBufferHandle handle = BGFX_INVALID_HANDLE; - TextureRef color, depth; }; -PipelineFrameBuffer CreateFrameBuffer( - bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, PipelineResources &res, const char *name); +FrameBuffer CreateFrameBuffer(const hg::Texture &color, const hg::Texture &depth, const char *name); +FrameBuffer CreateFrameBuffer(bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, const char *name); +FrameBuffer CreateFrameBuffer(int width, int height, bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, const char *name); -PipelineFrameBuffer CreateFrameBuffer( - int width, int height, bgfx::TextureFormat::Enum color_format, bgfx::TextureFormat::Enum depth_format, int aa, PipelineResources &res, const char *name); +Texture GetColorTexture(FrameBuffer &frameBuffer); +Texture GetDepthTexture(FrameBuffer &frameBuffer); -void DestroyFrameBuffer(PipelineResources &res, PipelineFrameBuffer &frameBuffer); +void DestroyFrameBuffer(FrameBuffer &frameBuffer); bool CreateFullscreenQuad(bgfx::TransientIndexBuffer &idx, bgfx::TransientVertexBuffer &vtx); diff --git a/harfang/engine/sao.cpp b/harfang/engine/sao.cpp index df0fee5..3001d2d 100644 --- a/harfang/engine/sao.cpp +++ b/harfang/engine/sao.cpp @@ -11,6 +11,12 @@ namespace hg { +bool IsValid(const SAO &sao) { + return bgfx::isValid(sao.compute_fb) && bgfx::isValid(sao.blur_fb) && bgfx::isValid(sao.prg_compute) && bgfx::isValid(sao.prg_blur) && + bgfx::isValid(sao.u_attr0) && bgfx::isValid(sao.u_attr1) && bgfx::isValid(sao.u_noise) && bgfx::isValid(sao.u_input) && + bgfx::isValid(sao.u_params) && bgfx::isValid(sao.u_projection_infos) && bgfx::isValid(sao.u_sample_count); +} + static void CreateSAOCommon(SAO &sao, const Reader &ir, const ReadProvider &ip, const char *path) { sao.u_attr0 = bgfx::createUniform("u_attr0", bgfx::UniformType::Sampler); sao.u_attr1 = bgfx::createUniform("u_attr1", bgfx::UniformType::Sampler); @@ -30,14 +36,17 @@ static SAO _CreateSAO(const Reader &ir, const ReadProvider &ip, const char *path const uint64_t flags = 0 | BGFX_TEXTURE_RT | BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; sao.compute_fb = bgfx::createFrameBuffer(ratio, bgfx::TextureFormat::R8, flags); - bgfx::setName(sao.compute_fb, "SAO.compute_fb"); - sao.blur_fb = bgfx::createFrameBuffer(ratio, bgfx::TextureFormat::R8, flags); - bgfx::setName(sao.blur_fb, "SAO.blur_fb"); CreateSAOCommon(sao, ir, ip, path); - // [todo] check for errors... + if (!IsValid(sao)) { + DestroySAO(sao); + return sao; + } + + bgfx::setName(sao.compute_fb, "SAO.compute_fb"); + bgfx::setName(sao.blur_fb, "SAO.blur_fb"); return sao; } @@ -65,6 +74,8 @@ void DestroySAO(SAO &sao) { void ComputeSAO(bgfx::ViewId &view_id, const iRect &rect, const Texture &attr0, const Texture &attr1, const Texture &noise, bgfx::FrameBufferHandle output, const SAO &sao, const Mat44 &projection, float bias, float radius, int sample_count, float sharpness) { + __ASSERT__(IsValid(sao)); + const bgfx::Caps *caps = bgfx::getCaps(); bgfx::TransientIndexBuffer idx; diff --git a/harfang/engine/sao.h b/harfang/engine/sao.h index 015059a..59d9a98 100644 --- a/harfang/engine/sao.h +++ b/harfang/engine/sao.h @@ -36,4 +36,6 @@ void DestroySAO(SAO &sao); void ComputeSAO(bgfx::ViewId &view_id, const iRect &rect, const Texture &attr0, const Texture &attr1, const Texture &noise, bgfx::FrameBufferHandle output, const SAO &sao, const Mat44 &projection, float bias, float radius, int sample_count, float sharpness); +bool IsValid(const SAO &sao); + } // namespace hg diff --git a/harfang/engine/scene.cpp b/harfang/engine/scene.cpp index 80a772d..3bf1d32 100644 --- a/harfang/engine/scene.cpp +++ b/harfang/engine/scene.cpp @@ -865,6 +865,17 @@ void Scene::SetRigidBodyFriction(ComponentRef ref, float friction) { rb->friction = friction; } +float Scene::GetRigidBodyRollingFriction(ComponentRef ref) const { + if (const auto rb = GetComponent_(rigid_bodies, ref)) + return rb->rolling_friction; + return 0.f; +} + +void Scene::SetRigidBodyRollingFriction(ComponentRef ref, float rolling_friction) { + if (auto rb = GetComponent_(rigid_bodies, ref)) + rb->rolling_friction = rolling_friction; +} + // Collision Scene::CreateCollision() { return {scene_ref, collisions.add_ref({})}; } void Scene::DestroyCollision(ComponentRef ref) { collisions.remove_ref(ref); } @@ -880,6 +891,17 @@ CollisionType Scene::GetCollisionType(ComponentRef ref) const { return CT_Sphere; } +void Scene::SetCollisionLocalTransform(ComponentRef ref, Mat4 m) { + if (auto col = GetComponent_(collisions, ref)) + col->m = m; +} + +Mat4 Scene::GetCollisionLocalTransform(ComponentRef ref) const { + if (const auto col = GetComponent_(collisions, ref)) + return col->m; + return Mat4::Identity; +} + void Scene::SetCollisionMass(ComponentRef ref, float mass) { if (auto col = GetComponent_(collisions, ref)) col->mass = mass; @@ -1222,23 +1244,23 @@ Node CreateLinearLight(Scene &scene, const Mat4 &mtx, const Color &diffuse, floa } Node CreateInstance(Scene &scene, const Mat4 &mtx, const std::string &name, const Reader &ir, const ReadProvider &ip, PipelineResources &resources, - const PipelineInfo &pipeline, uint32_t flags) { + const PipelineInfo &pipeline, bool &success, uint32_t flags) { Node node = scene.CreateNode(); node.SetName(name); node.SetTransform(scene.CreateTransform(mtx)); node.SetInstance(scene.CreateInstance(name)); - node.SetupInstance(ir, ip, resources, pipeline, flags); + success = node.SetupInstance(ir, ip, resources, pipeline, flags); return node; } Node CreateInstanceFromFile( - Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, uint32_t flags) { - return CreateInstance(scene, mtx, name, g_file_reader, g_file_read_provider, resources, pipeline, flags); + Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, bool &success, uint32_t flags) { + return CreateInstance(scene, mtx, name, g_file_reader, g_file_read_provider, resources, pipeline, success, flags); } Node CreateInstanceFromAssets( - Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, uint32_t flags) { - return CreateInstance(scene, mtx, name, g_assets_reader, g_assets_read_provider, resources, pipeline, flags); + Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, bool &success, uint32_t flags) { + return CreateInstance(scene, mtx, name, g_assets_reader, g_assets_read_provider, resources, pipeline, success, flags); } // @@ -1661,6 +1683,8 @@ Node SceneView::GetNode(const Scene &scene, const std::string &name) const { for (const auto &ref : nodes) if (scene.GetNodeName(ref) == name) return scene.GetNode(ref); + + debug(format("Node '%1' not found in scene").arg(name)); return {}; } @@ -1810,6 +1834,7 @@ BoundToNodeAnim Scene::BindNodeAnim(NodeRef ref, AnimRef anim_ref) const { bound_anim.color_track[NCAT_LightSpecular] = int8_t(i); } + bound_anim.bound_to_node_instance_anim = {0, nullptr}; if (!anim.instance_anim_track.keys.empty()) bound_anim.bound_to_node_instance_anim.kf = -1; diff --git a/harfang/engine/scene.h b/harfang/engine/scene.h index 60cd377..a77004c 100644 --- a/harfang/engine/scene.h +++ b/harfang/engine/scene.h @@ -414,6 +414,8 @@ public: void SetRigidBodyRestitution(ComponentRef ref, float restitution); float GetRigidBodyFriction(ComponentRef ref) const; void SetRigidBodyFriction(ComponentRef ref, float friction); + float GetRigidBodyRollingFriction(ComponentRef ref) const; + void SetRigidBodyRollingFriction(ComponentRef ref, float rolling_friction); // collision component Collision CreateCollision(); @@ -422,6 +424,8 @@ public: void SetCollisionType(ComponentRef ref, CollisionType type); CollisionType GetCollisionType(ComponentRef ref) const; + void SetCollisionLocalTransform(ComponentRef ref, Mat4 m); + Mat4 GetCollisionLocalTransform(ComponentRef ref) const; void SetCollisionMass(ComponentRef ref, float mass); float GetCollisionMass(ComponentRef ref) const; void SetCollisionSize(ComponentRef ref, const Vec3 &size); @@ -729,7 +733,7 @@ private: uint8_t linear_damping{pack_float(1.f)}; std::array angular_damping = {pack_float(1.f), pack_float(1.f), pack_float(1.f)}; - float restitution{0.f}, friction{0.5f}; + float restitution{0.f}, friction{0.5f}, rolling_friction{0.f}; }; generational_vector_list transforms; @@ -744,6 +748,8 @@ private: float mass; Vec3 size; std::string resource_path; + Mat4 m; + Collision_() : m(Mat4::Identity) {} }; generational_vector_list collisions; @@ -884,10 +890,10 @@ Node CreateLinearLight(Scene &scene, const Mat4 &mtx, const Color &diffuse, cons Node CreateObject(Scene &scene, const Mat4 &mtx, const ModelRef &model, std::vector materials = {}); Node CreateInstance(Scene &scene, const Mat4 &mtx, const std::string &name, const Reader &ir, const ReadProvider &ip, PipelineResources &resources, - const PipelineInfo &pipeline, uint32_t flags = LSSF_Nodes | LSSF_Anims); -Node CreateInstanceFromFile(Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, + const PipelineInfo &pipeline, bool &success, uint32_t flags = LSSF_Nodes | LSSF_Anims); +Node CreateInstanceFromFile(Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, bool &success, uint32_t flags = LSSF_Nodes | LSSF_Anims); -Node CreateInstanceFromAssets(Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, +Node CreateInstanceFromAssets(Scene &scene, const Mat4 &mtx, const std::string &name, PipelineResources &resources, const PipelineInfo &pipeline, bool &success, uint32_t flags = LSSF_Nodes | LSSF_Anims); Node CreateScript(Scene &scene); diff --git a/harfang/engine/scene_bullet3_physics.cpp b/harfang/engine/scene_bullet3_physics.cpp index cc73d58..f7ed65d 100644 --- a/harfang/engine/scene_bullet3_physics.cpp +++ b/harfang/engine/scene_bullet3_physics.cpp @@ -181,12 +181,14 @@ Vec3 from_btVector3(const btVector3 &v) { return Vec3(v.x(), v.y(), v.z()); } btVector3 to_btVector3(const Vec3 &v) { return btVector3(v.x, v.y, v.z); } // +#if 0 struct PhysicSystemMotionState : public btMotionState { PhysicSystemMotionState(Node node_, const btTransform &com_) : com(com_), node(node_) {} void getWorldTransform(btTransform &com_world_trs) const override { if (auto trs = node.GetTransform()) { - const auto world = TransformationMat4(trs.GetPos(), trs.GetRot(), Vec3::One); + auto w = trs.GetWorld(); + const auto world = TransformationMat4(GetT(w), GetR(w), Vec3::One); com_world_trs = to_btTransform(world) * com.inverse(); } } @@ -199,6 +201,7 @@ struct PhysicSystemMotionState : public btMotionState { btTransform com; Node node; }; +#endif // static void __DeleteRigidBody(const btRigidBody *body) { @@ -253,7 +256,6 @@ void SceneBullet3Physics::NodeCreatePhysics(const Node &node, const Reader &ir, } auto trs = node.GetTransform(); - btTransform bt_trs = btTransform::getIdentity(); if (trs) bt_trs = to_btTransform(TransformationMat4(trs.GetPos(), trs.GetRot(), Vec3::One)); // most up to date transform @@ -265,8 +267,10 @@ void SceneBullet3Physics::NodeCreatePhysics(const Node &node, const Reader &ir, root_shape = *shapes.begin(); } else { auto compound = new btCompoundShape; - for (auto shape : shapes) - compound->addChildShape(btTransform::getIdentity(), shape); + for (size_t idx = 0; idx < node.GetCollisionCount(); ++idx) { + const auto col = node.GetCollision(idx); + compound->addChildShape(to_btTransform(col.GetLocalTransform()), shapes[idx]); + } root_shape = compound; } @@ -276,11 +280,12 @@ void SceneBullet3Physics::NodeCreatePhysics(const Node &node, const Reader &ir, btVector3 local_inertia; root_shape->calculateLocalInertia(total_mass, local_inertia); - btMotionState *motion_state = new PhysicSystemMotionState(node, btTransform::getIdentity()); - btRigidBody::btRigidBodyConstructionInfo rb_info(total_mass, motion_state, root_shape, local_inertia); + btRigidBody::btRigidBodyConstructionInfo rb_info(total_mass, nullptr, root_shape, local_inertia); rb_info.m_restitution = rb.GetRestitution(); rb_info.m_friction = rb.GetFriction(); + rb_info.m_rollingFriction = rb.GetRollingFriction(); + rb_info.m_startWorldTransform = bt_trs; _node.body = new btRigidBody(rb_info); @@ -401,10 +406,10 @@ void SceneBullet3Physics::CollectCollisionEvents(const Scene &scene, NodeNodeCon if (i != std::end(node_collision_event_tracking_modes)) { auto &contacts = node_node_contacts[node_a_ref][node_b_ref]; - for (int i = 0; i < manifold_contact_count; ++i) { - const auto &contact = manifold->getContactPoint(i); + for (int j = 0; j < manifold_contact_count; ++j) { + const auto &contact = manifold->getContactPoint(j); - contacts.push_back({from_btVector3(contact.m_positionWorldOnB), from_btVector3(contact.m_normalWorldOnB), numeric_cast(i), + contacts.push_back({from_btVector3(contact.m_positionWorldOnB), from_btVector3(contact.m_normalWorldOnB), numeric_cast(j), contact.getDistance()}); } } @@ -413,30 +418,22 @@ void SceneBullet3Physics::CollectCollisionEvents(const Scene &scene, NodeNodeCon } // -void SceneBullet3Physics::SyncKinematicBodiesFromScene(const Scene &scene) { - float mtx[16]; +void SceneBullet3Physics::SyncBodiesFromScene(const Scene &scene) { + for (const auto &i : nodes) + if (const auto node = scene.GetNode(i.first)) + if (auto trs = node.GetTransform()) { + const auto body = i.second.body; + const auto flags = body->getCollisionFlags(); - bool had_sync = false; - - for (auto &i : nodes) { - auto body = i.second.body; - /* - if (NewtonBodyGetType(body) == NEWTON_KINEMATIC_BODY) { - if (auto node = scene.GetNode(i.first)) { - const auto world = node.GetTransform().GetWorld(); - - const auto m = TransformationMat4(GetT(world), GetR(world)); - Mat4ToFloat16Transposed(m, mtx); - - NewtonBodySetMatrix(body, mtx); - had_sync = true; - } + if (flags == btRigidBody::CF_KINEMATIC_OBJECT) { + const auto world = hg::Orthonormalize(trs.GetWorld()); + // const auto world = TransformationMat4(GetT(w), GetR(w), Vec3::One); + body->setWorldTransform(to_btTransform(world)); + } else if (flags == btRigidBody::CF_DYNAMIC_OBJECT) { + const auto world = body->getInterpolationWorldTransform(); + trs.SetWorld(from_btTransform(world)); } - */ - } - - if (had_sync) - ; // NewtonInvalidateCache(world); + } } // @@ -445,6 +442,29 @@ void SceneBullet3Physics::NodeWake(NodeRef ref) const { body->activate(); } +// +void SceneBullet3Physics::NodeSetDeactivation(NodeRef ref, bool enable) const { + if (auto body = GetNodeBody(ref)) + body->setActivationState(enable ? ACTIVE_TAG : DISABLE_DEACTIVATION); +} + +bool SceneBullet3Physics::NodeGetDeactivation(NodeRef ref) const { + if (auto body = GetNodeBody(ref)) + return body->getActivationState() == ACTIVE_TAG ? true : false; + return true; +} + +// +void SceneBullet3Physics::NodeResetWorld(NodeRef ref, const Mat4 &world) const { + if (auto body = GetNodeBody(ref)) { + body->setWorldTransform(to_btTransform(world)); + body->setLinearVelocity(btVector3(0, 0, 0)); + body->setAngularVelocity(btVector3(0, 0, 0)); + body->clearForces(); + body->activate(); + } +} + // static btVector3 __bt_WorldToLocalPos(btRigidBody *body, const btVector3 &pos) { btTransform world_transform; @@ -500,12 +520,40 @@ void SceneBullet3Physics::NodeSetAngularVelocity(NodeRef ref, const Vec3 &W) { body->setAngularVelocity(to_btVector3(W)); } +void SceneBullet3Physics::NodeGetLinearLockAxes(NodeRef ref, bool &X, bool &Y, bool &Z) const { + if (auto body = GetNodeBody(ref)) { + const auto &v = body->getAngularVelocity(); + X = v.x() == 1.f ? false : true; + X = v.y() == 1.f ? false : true; + X = v.z() == 1.f ? false : true; + } +} + +void SceneBullet3Physics::NodeSetLinearLockAxes(NodeRef ref, bool X, bool Y, bool Z) { + if (auto body = GetNodeBody(ref)) + body->setLinearFactor(btVector3(X ? 0.f : 1.f, Y ? 0.f : 1.f, Z ? 0.f : 1.f)); +} + +void SceneBullet3Physics::NodeGetAngularLockAxes(NodeRef ref, bool &X, bool &Y, bool &Z) const { + if (auto body = GetNodeBody(ref)) { + const auto &v = body->getAngularVelocity(); + X = v.x() == 1.f ? false : true; + X = v.y() == 1.f ? false : true; + X = v.z() == 1.f ? false : true; + } +} + +void SceneBullet3Physics::NodeSetAngularLockAxes(NodeRef ref, bool X, bool Y, bool Z) { + if (auto body = GetNodeBody(ref)) + body->setAngularFactor(btVector3(X ? 0.f : 1.f, Y ? 0.f : 1.f, Z ? 0.f : 1.f)); +} + // struct NodeCollideWorldCallback : btCollisionWorld::ContactResultCallback { NodeCollideWorldCallback(const Scene &_scene, NodeRef _node_ref) : scene(_scene), node_ref(_node_ref) {} - virtual btScalar addSingleResult(btManifoldPoint &cp, const btCollisionObjectWrapper *col_obj_0_wrap, int part_id_0, int index_0, - const btCollisionObjectWrapper *col_obj_1_wrap, int part_id_1, int index_1) { + btScalar addSingleResult(btManifoldPoint &cp, const btCollisionObjectWrapper *col_obj_0_wrap, int part_id_0, int index_0, + const btCollisionObjectWrapper *col_obj_1_wrap, int part_id_1, int index_1) override { const auto node_0_ref = scene.GetNodeRef(uint32_t(col_obj_0_wrap->getCollisionObject()->getUserIndex())); const auto node_1_ref = scene.GetNodeRef(uint32_t(col_obj_1_wrap->getCollisionObject()->getUserIndex())); @@ -590,6 +638,57 @@ RaycastOut SceneBullet3Physics::RaycastFirstHit(const Scene &scene, const Vec3 & return out; } +std::vector SceneBullet3Physics::RaycastAllHits(const Scene &scene, const Vec3 &world_p0, const Vec3 &world_p1) const { + struct AllRayResultWithTriangleIndexCallback : public btCollisionWorld::AllHitsRayResultCallback { + AllRayResultWithTriangleIndexCallback(const btVector3 &rayFromWorld, const btVector3 &rayToWorld) + : AllHitsRayResultCallback(rayFromWorld, rayToWorld), m_TriangleIndex(-1), m_shapePart(-1) {} + + int m_TriangleIndex; + int m_shapePart; + + virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult &rayResult, bool normalInWorldSpace) { + if (rayResult.m_localShapeInfo) { + m_TriangleIndex = rayResult.m_localShapeInfo->m_triangleIndex; + m_shapePart = rayResult.m_localShapeInfo->m_shapePart; + } else { + m_TriangleIndex = -1; + m_shapePart = -1; + } + return AllHitsRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); + } + }; + + btVector3 from = to_btVector3(world_p0), to = to_btVector3(world_p1); + + AllRayResultWithTriangleIndexCallback trace(from, to); + trace.m_collisionFilterGroup = btBroadphaseProxy::AllFilter; + trace.m_collisionFilterMask = ~0; // FIXME + + world->rayTest(from, to, trace); + + if (!trace.hasHit()) + return {}; + + std::vector outs; + outs.resize(trace.m_hitNormalWorld.size()); + + for (int i = 0; i < trace.m_hitNormalWorld.size(); ++i) { + outs[i].N = from_btVector3(trace.m_hitNormalWorld[i]); + outs[i].node = scene.GetNode(trace.m_collisionObjects[i]->getUserIndex()); + outs[i].P = from_btVector3(trace.m_hitPointWorld[i]); + outs[i].t = Dot(outs[i].P - world_p0, Normalize(world_p1 - world_p0)); + } + + /* FIXME + if (RigidBody *rb = (RigidBody *)hit.i->rigid_body) + if ((trace.m_shapePart >= 0) && ((uint)trace.m_shapePart < rb->shapes.size())) + if (Mesh *mesh = rb->shapes[trace.m_shapePart].mesh.c_ptr()) + if (uint(trace.m_TriangleIndex) < mesh->bt_id_mat.size()) + hit.m = mesh->bt_mat[mesh->bt_id_mat[trace.m_TriangleIndex]]; + */ + return outs; +} + // struct DeserializeHandle { Reader ir; diff --git a/harfang/engine/scene_bullet3_physics.h b/harfang/engine/scene_bullet3_physics.h index da5ac41..f79f782 100644 --- a/harfang/engine/scene_bullet3_physics.h +++ b/harfang/engine/scene_bullet3_physics.h @@ -51,14 +51,13 @@ public: bool NodeHasBody(NodeRef ref) const { return nodes.find(ref) != std::end(nodes); } bool NodeHasBody(const Node &node) const { return NodeHasBody(node.ref); } - - void StoreInterpolationRefs(time_ns clock); + /// Step physics world void StepSimulation(time_ns dt, time_ns step = time_from_ms(16), int max_step = 8); void CollectCollisionEvents(const Scene &scene, NodeNodeContacts &node_node_contacts); - void SyncKinematicBodiesFromScene(const Scene &scene); + void SyncBodiesFromScene(const Scene &scene); // size_t GarbageCollect(const Scene &scene); @@ -71,6 +70,14 @@ public: void NodeWake(NodeRef ref) const; void NodeWake(const Node &node) const { NodeWake(node.ref); } + void NodeSetDeactivation(NodeRef ref, bool enable) const; + void NodeSetDeactivation(const Node &node, bool enable) const { NodeSetDeactivation(node.ref, enable); } + bool NodeGetDeactivation(NodeRef ref) const; + bool NodeGetDeactivation(const Node &node) const { return NodeGetDeactivation(node.ref); } + + void NodeResetWorld(NodeRef ref, const Mat4 &world) const; + void NodeResetWorld(const Node &node, const Mat4 &world) { NodeResetWorld(node.ref, world); } + void NodeAddForce(NodeRef ref, const Vec3 &F); void NodeAddForce(NodeRef ref, const Vec3 &F, const Vec3 &world_pos); void NodeAddImpulse(NodeRef ref, const Vec3 &dt_velocity); @@ -94,6 +101,16 @@ public: Vec3 NodeGetAngularVelocity(const Node &node) const { return NodeGetAngularVelocity(node.ref); } void NodeSetAngularVelocity(const Node &node, const Vec3 &W) { NodeSetAngularVelocity(node.ref, W); } + void NodeGetLinearLockAxes(NodeRef ref, bool &X, bool &Y, bool &Z) const; + void NodeSetLinearLockAxes(NodeRef ref, bool X, bool Y, bool Z); + void NodeGetAngularLockAxes(NodeRef ref, bool &X, bool &Y, bool &Z) const; + void NodeSetAngularLockAxes(NodeRef ref, bool X, bool Y, bool Z); + + void NodeGetLinearLockAxes(const Node &node, bool &X, bool &Y, bool &Z) const { return NodeGetLinearLockAxes(node.ref, X, Y, Z); } + void NodeSetLinearLockAxes(const Node &node, bool X, bool Y, bool Z) { NodeSetLinearLockAxes(node.ref, X, Y, Z); } + void NodeGetAngularLockAxes(const Node &node, bool &X, bool &Y, bool &Z) const { return NodeGetAngularLockAxes(node.ref, X, Y, Z); } + void NodeSetAngularLockAxes(const Node &node, bool X, bool Y, bool Z) { NodeSetAngularLockAxes(node.ref, X, Y, Z); } + // btCollisionShape *LoadCollisionTree(const Reader &ir, const ReadProvider &ip, const char *name, int shape_id = 0); @@ -105,6 +122,7 @@ public: NodeContacts NodeCollideWorld(const Node &node, const Mat4 &world, int max_contact = 1) const; RaycastOut RaycastFirstHit(const Scene &scene, const Vec3 &world_p0, const Vec3 &world_p1) const; + std::vector RaycastAllHits(const Scene &scene, const Vec3 &world_p0, const Vec3 &world_p1) const; // void RenderCollision(bgfx::ViewId view_id, const bgfx::VertexLayout &vtx_decl, bgfx::ProgramHandle program, RenderState state, uint32_t depth); diff --git a/harfang/engine/scene_forward_pipeline.cpp b/harfang/engine/scene_forward_pipeline.cpp index 98af4e4..8c6d572 100644 --- a/harfang/engine/scene_forward_pipeline.cpp +++ b/harfang/engine/scene_forward_pipeline.cpp @@ -22,6 +22,56 @@ void ForwardPipelineAAA::Flip(const ViewState &view_state) { } // +bool IsValid(const ForwardPipelineAAA &aaa) { + for (auto &tex : aaa.noise) { + if (!bgfx::isValid(tex.handle)) { + return false; + } + } + if (!(bgfx::isValid(aaa.depth.handle) && bgfx::isValid(aaa.attr0.handle) && bgfx::isValid(aaa.attr1.handle))) { + return false; + } + if (!bgfx::isValid(aaa.attributes_fb)) { + return false; + } + + if (!(IsValid(aaa.downsample) && IsValid(aaa.upsample) && IsValid(aaa.ssgi) && IsValid(aaa.ssr) && IsValid(aaa.temporal_acc) && IsValid(aaa.hiz) && + IsValid(aaa.taa) && IsValid(aaa.blur) && IsValid(aaa.motion_blur) && IsValid(aaa.bloom))) { + return false; + } + for (int i = 0; i < 2; i++) { + if (!(bgfx::isValid(aaa.ssgi_history[i].handle) && bgfx::isValid(aaa.ssgi_history_fb[i]) && bgfx::isValid(aaa.ssr_history[i].handle) && + bgfx::isValid(aaa.ssr_history_fb[i]))) { + return false; + } + } + + if (aaa.ssgi_ratio != bgfx::BackbufferRatio::Equal) { + if (!(bgfx::isValid(aaa.ssgi_output.handle) && bgfx::isValid(aaa.ssgi_output_fb))) { + return false; + } + } + + if (aaa.ssr_ratio != bgfx::BackbufferRatio::Equal) { + if (!(bgfx::isValid(aaa.ssr_output.handle) && bgfx::isValid(aaa.ssr_output_fb))) { + return false; + } + } + + if (!(bgfx::isValid(aaa.work[0].handle) && bgfx::isValid(aaa.work_fb[0]))) { + return false; + } + if (aaa.ssgi_ratio != aaa.ssr_ratio) { + if (!(bgfx::isValid(aaa.work[1].handle) && bgfx::isValid(aaa.work_fb[1]))) { + return false; + } + } + + return bgfx::isValid(aaa.frame_hdr.handle) && bgfx::isValid(aaa.frame_hdr_fb) && bgfx::isValid(aaa.work_frame_hdr_fb) && + bgfx::isValid(aaa.prv_frame_hdr_fb) && bgfx::isValid(aaa.next_frame_hdr_fb) && bgfx::isValid(aaa.u_color) && bgfx::isValid(aaa.u_depth) && + bgfx::isValid(aaa.compositing_prg); +} + static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const ReadProvider &ip, const char *path, const ForwardPipelineAAAConfig &config, bgfx::BackbufferRatio::Enum ssgi_ratio, bgfx::BackbufferRatio::Enum ssr_ratio) { ForwardPipelineAAA aaa; @@ -34,29 +84,21 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read { // depth texture aaa.depth = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::D24, flags)}; - bgfx::setName(aaa.depth.handle, "aaa.depth"); // w: linear depth, xyz: view normal aaa.attr0 = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.attr0.handle, "aaa.attr0"); // yz: velocity aaa.attr1 = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.attr1.handle, "aaa.attr1"); - + bgfx::TextureHandle texs[] = {aaa.depth.handle, aaa.attr0.handle, aaa.attr1.handle}; aaa.attributes_fb = bgfx::createFrameBuffer(3, texs, true); - bgfx::setName(aaa.attributes_fb, "Attributes FB"); } { aaa.work[0] = {flags, bgfx::createTexture2D(ssgi_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.work[0].handle, "aaa.work[0]"); aaa.work_fb[0] = bgfx::createFrameBuffer(1, &aaa.work[0].handle, true); - bgfx::setName(aaa.work_fb[0], "Work FB #0"); if(ssgi_ratio != ssr_ratio) { aaa.work[1] = {flags, bgfx::createTexture2D(ssr_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.work[1].handle, "aaa.work[1]"); aaa.work_fb[1] = bgfx::createFrameBuffer(1, &aaa.work[1].handle, true); - bgfx::setName(aaa.work_fb[1], "Work FB #1"); } else { aaa.work[1] = aaa.work[0]; aaa.work_fb[1] = aaa.work_fb[0]; @@ -67,7 +109,6 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read aaa.ssgi_ratio = ssgi_ratio; if (ssgi_ratio != bgfx::BackbufferRatio::Equal) { aaa.ssgi_output = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.ssgi_output.handle, "aaa.ssgi_output"); bgfx::TextureHandle texs[] = {aaa.ssgi_output.handle}; aaa.ssgi_output_fb = bgfx::createFrameBuffer(1, texs, true); } else { @@ -77,11 +118,8 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read aaa.ssgi = CreateSSGIFromAssets(path); aaa.ssgi_history[0] = {flags, bgfx::createTexture2D(aaa.ssgi_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.ssgi_history[0].handle, "aaa.ssgi_history_0"); - aaa.ssgi_history[1] = {flags, bgfx::createTexture2D(aaa.ssgi_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.ssgi_history[1].handle, "aaa.ssgi_history_1"); - + aaa.ssgi_history_fb[0] = bgfx::createFrameBuffer(1, &aaa.ssgi_history[0].handle, true); aaa.ssgi_history_fb[1] = bgfx::createFrameBuffer(1, &aaa.ssgi_history[1].handle, true); } @@ -90,7 +128,6 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read aaa.ssr_ratio = ssr_ratio; if (ssr_ratio != bgfx::BackbufferRatio::Equal) { aaa.ssr_output = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Equal, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.ssr_output.handle, "aaa.ssr_output"); bgfx::TextureHandle texs[] = {aaa.ssr_output.handle}; aaa.ssr_output_fb = bgfx::createFrameBuffer(1, texs, true); } else { @@ -100,11 +137,9 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read aaa.ssr = CreateSSRFromAssets(path); aaa.ssr_history[0] = {flags, bgfx::createTexture2D(aaa.ssr_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.ssr_history[0].handle, "aaa.ssr_history_0"); - + aaa.ssr_history[1] = {flags, bgfx::createTexture2D(aaa.ssr_ratio, false, 1, bgfx::TextureFormat::RGBA16F, flags)}; - bgfx::setName(aaa.ssr_history[1].handle, "aaa.ssr_history_1"); - + aaa.ssr_history_fb[0] = bgfx::createFrameBuffer(1, &aaa.ssr_history[0].handle, true); aaa.ssr_history_fb[1] = bgfx::createFrameBuffer(1, &aaa.ssr_history[1].handle, true); } @@ -119,18 +154,6 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read { aaa.temporal_acc = CreateTemporalAccumulationFromAssets(path); } -#if 0 - { - aaa.sao_output = {flags, bgfx::createTexture2D(bgfx::BackbufferRatio::Half, false, 1, bgfx::TextureFormat::R8, flags)}; - - bgfx::TextureHandle texs[] = {aaa.sao_output.handle}; - aaa.sao_output_fb = bgfx::createFrameBuffer(1, texs, true); - bgfx::setName(aaa.sao_output_fb, "SAO output FB"); - - aaa.sao = CreateSAOFromAssets(path, bgfx::BackbufferRatio::Half); - } -#endif - { aaa.motion_blur = CreateMotionBlurFromAssets(path); } { @@ -139,20 +162,15 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read bgfx::TextureHandle texs[] = {aaa.depth.handle, aaa.frame_hdr.handle}; aaa.frame_hdr_fb = bgfx::createFrameBuffer(2, texs, false); - bgfx::setName(aaa.frame_hdr_fb, "Frame HDR FB"); // current frame without SSR/SSGI - + aaa.work_frame_hdr_fb = bgfx::createFrameBuffer(bgfx::BackbufferRatio::Equal, bgfx::TextureFormat::RGBA16F, flags); - bgfx::setName(aaa.work_frame_hdr_fb, "Work HDR frame FB"); - + aaa.prv_frame_hdr_fb = bgfx::createFrameBuffer(bgfx::BackbufferRatio::Equal, bgfx::TextureFormat::RGBA16F, flags); // (reprojected) input to SSR/SSGI - bgfx::setName(aaa.prv_frame_hdr_fb, "Previous HDR frame FB"); aaa.next_frame_hdr_fb = bgfx::createFrameBuffer(bgfx::BackbufferRatio::Equal, bgfx::TextureFormat::RGBA16F, flags); // current frame with SSR/SSGI - bgfx::setName(aaa.next_frame_hdr_fb, "Next HDR frame FB"); } { aaa.compositing_prg = hg::LoadProgram(ir, ip, hg::format("%1/shader/compositing").arg(path).c_str()); - aaa.u_color = bgfx::createUniform("u_color", bgfx::UniformType::Sampler); aaa.u_depth = bgfx::createUniform("u_depth", bgfx::UniformType::Sampler); } @@ -160,6 +178,35 @@ static ForwardPipelineAAA _CreateForwardPipelineAAA(const Reader &ir, const Read aaa.taa = CreateTAAFromAssets(path); aaa.bloom = CreateBloomFromAssets(hg::format("%1/shader").arg(path), bgfx::BackbufferRatio::Equal); + if (!IsValid(aaa)) { + DestroyForwardPipelineAAA(aaa); + return aaa; + } + + bgfx::setName(aaa.depth.handle, "aaa.depth"); + bgfx::setName(aaa.attr0.handle, "aaa.attr0"); + bgfx::setName(aaa.attr1.handle, "aaa.attr1"); + bgfx::setName(aaa.attributes_fb, "Attributes FB"); + bgfx::setName(aaa.work[0].handle, "aaa.work[0]"); + bgfx::setName(aaa.work_fb[0], "Work FB #0"); + bgfx::setName(aaa.ssgi_history[0].handle, "aaa.ssgi_history_0"); + bgfx::setName(aaa.ssgi_history[1].handle, "aaa.ssgi_history_1"); + bgfx::setName(aaa.ssr_history[0].handle, "aaa.ssr_history_0"); + bgfx::setName(aaa.ssr_history[1].handle, "aaa.ssr_history_1"); + bgfx::setName(aaa.frame_hdr_fb, "Frame HDR FB"); + bgfx::setName(aaa.work_frame_hdr_fb, "Work HDR frame FB"); + bgfx::setName(aaa.prv_frame_hdr_fb, "Previous HDR frame FB"); + bgfx::setName(aaa.next_frame_hdr_fb, "Next HDR frame FB"); + if (ssgi_ratio != bgfx::BackbufferRatio::Equal) { + bgfx::setName(aaa.ssgi_output.handle, "aaa.ssgi_output"); + } + if (ssr_ratio != bgfx::BackbufferRatio::Equal) { + bgfx::setName(aaa.ssr_output.handle, "aaa.ssr_output"); + } + if (ssgi_ratio != ssr_ratio) { + bgfx::setName(aaa.work[1].handle, "aaa.work[1]"); + bgfx::setName(aaa.work_fb[1], "Work FB #1"); + } return aaa; } @@ -259,14 +306,21 @@ void GetSceneForwardPipelineLights(const Scene &scene, std::vectormass; js["size"] = data_->size; js["path"] = data_->resource_path; + js["m"] = data_->m; } // @@ -231,6 +232,7 @@ void LoadComponent(Scene::Collision_ *data_, const json &js) { data_->mass = js["mass"]; data_->size = js["size"]; data_->resource_path = js["path"].get(); + data_->m = js["m"]; } // diff --git a/harfang/engine/scene_systems.cpp b/harfang/engine/scene_systems.cpp index be92c91..2c6b66c 100644 --- a/harfang/engine/scene_systems.cpp +++ b/harfang/engine/scene_systems.cpp @@ -185,7 +185,7 @@ static void SceneUpdateSystemsImpl(Scene &scene, SceneClocks &clocks, time_ns dt #if HG_ENABLE_BULLET3_SCENE_PHYSICS if (bullet3_physics) - bullet3_physics->SyncKinematicBodiesFromScene(scene); + bullet3_physics->SyncBodiesFromScene(scene); #endif } diff --git a/harfang/engine/ssgi.cpp b/harfang/engine/ssgi.cpp index dee4c05..4026998 100644 --- a/harfang/engine/ssgi.cpp +++ b/harfang/engine/ssgi.cpp @@ -10,6 +10,11 @@ namespace hg { +bool IsValid(const SSGI &ssgi) { + return bgfx::isValid(ssgi.compute) && bgfx::isValid(ssgi.u_color) && bgfx::isValid(ssgi.u_attr0) && bgfx::isValid(ssgi.u_attr1) && + bgfx::isValid(ssgi.u_noise) && bgfx::isValid(ssgi.u_probe) && bgfx::isValid(ssgi.u_depthTex) && bgfx::isValid(ssgi.u_depthTexInfos); +} + static SSGI _CreateSSGI(const Reader &ir, const ReadProvider &ip, const char *path) { SSGI ssgi; ssgi.compute = hg::LoadProgram(ir, ip, hg::format("%1/shader/ssgi").arg(path)); @@ -20,6 +25,10 @@ static SSGI _CreateSSGI(const Reader &ir, const ReadProvider &ip, const char *pa ssgi.u_probe = bgfx::createUniform("u_probe", bgfx::UniformType::Sampler); ssgi.u_depthTex = bgfx::createUniform("u_depthTex", bgfx::UniformType::Sampler); ssgi.u_depthTexInfos = bgfx::createUniform("u_depthTexInfos", bgfx::UniformType::Vec4); + + if (!IsValid(ssgi)) { + DestroySSGI(ssgi); + } return ssgi; } @@ -39,7 +48,7 @@ void DestroySSGI(SSGI &ssgi) { void ComputeSSGI(bgfx::ViewId &view_id, const iRect &rect, bgfx::BackbufferRatio::Enum ratio, const Texture &color, const Texture &attr0, const Texture &attr1, const Texture &probe, const Texture &noise, const HiZ &hiz, bgfx::FrameBufferHandle output, const SSGI &ssgi) { - const bgfx::Caps *caps = bgfx::getCaps(); + __ASSERT__(IsValid(ssgi)); bgfx::TransientIndexBuffer idx; bgfx::TransientVertexBuffer vtx; diff --git a/harfang/engine/ssgi.h b/harfang/engine/ssgi.h index 75f25c3..fe5e568 100644 --- a/harfang/engine/ssgi.h +++ b/harfang/engine/ssgi.h @@ -28,4 +28,6 @@ void DestroySSGI(SSGI &ssgi); void ComputeSSGI(bgfx::ViewId &view_id, const iRect &rect, bgfx::BackbufferRatio::Enum ratio, const Texture &color, const Texture &attr0, const Texture &attr1, const Texture &probe, const Texture &noise, const HiZ &hiz, bgfx::FrameBufferHandle output, const SSGI &ssgi); +bool IsValid(const SSGI &ssgi); + } // namespace hg diff --git a/harfang/engine/ssr.cpp b/harfang/engine/ssr.cpp index f6c022d..b90fb85 100644 --- a/harfang/engine/ssr.cpp +++ b/harfang/engine/ssr.cpp @@ -10,6 +10,11 @@ namespace hg { +bool IsValid(const SSR &ssr) { + return bgfx::isValid(ssr.prg_ssr) && bgfx::isValid(ssr.u_color) && bgfx::isValid(ssr.u_attr0) && bgfx::isValid(ssr.u_attr1) && bgfx::isValid(ssr.u_noise) && + bgfx::isValid(ssr.u_probe) && bgfx::isValid(ssr.u_hiz) && bgfx::isValid(ssr.u_depthTexInfos); +} + static SSR _CreateSSR(const Reader &ir, const ReadProvider &ip, const char *path) { SSR ssr; ssr.prg_ssr = hg::LoadProgram(ir, ip, hg::format("%1/shader/ssr").arg(path)); @@ -20,6 +25,10 @@ static SSR _CreateSSR(const Reader &ir, const ReadProvider &ip, const char *path ssr.u_probe = bgfx::createUniform("u_probe", bgfx::UniformType::Sampler); ssr.u_hiz = bgfx::createUniform("u_hiz", bgfx::UniformType::Sampler); ssr.u_depthTexInfos = bgfx::createUniform("u_depthTexInfos", bgfx::UniformType::Vec4); + + if (!IsValid(ssr)) { + DestroySSR(ssr); + } return ssr; } @@ -39,7 +48,7 @@ void DestroySSR(SSR &ssr) { void ComputeSSR(bgfx::ViewId &view_id, const iRect &rect, bgfx::BackbufferRatio::Enum ratio, const Texture &color, const Texture &attr0, const Texture &attr1, const Texture &probe, const Texture &noise, const HiZ &hiz, bgfx::FrameBufferHandle output, const SSR &ssr) { - const bgfx::Caps *caps = bgfx::getCaps(); + __ASSERT__(IsValid(ssr)); bgfx::TransientIndexBuffer idx; bgfx::TransientVertexBuffer vtx; diff --git a/harfang/engine/ssr.h b/harfang/engine/ssr.h index 0c667e7..a0d7b9e 100644 --- a/harfang/engine/ssr.h +++ b/harfang/engine/ssr.h @@ -28,4 +28,6 @@ void DestroySSR(SSR &ssr); void ComputeSSR(bgfx::ViewId &view_id, const iRect &rect, bgfx::BackbufferRatio::Enum ratio, const Texture &color, const Texture &attr0, const Texture &attr1, const Texture &probe, const Texture &noise, const HiZ &hiz, bgfx::FrameBufferHandle output, const SSR &ssr); +bool IsValid(const SSR &ssr); + } // namespace hg diff --git a/harfang/engine/taa.cpp b/harfang/engine/taa.cpp index 5eaa1cd..6c81298 100644 --- a/harfang/engine/taa.cpp +++ b/harfang/engine/taa.cpp @@ -10,6 +10,8 @@ namespace hg { +bool IsValid(const TAA &taa) { return bgfx::isValid(taa.u_color) && bgfx::isValid(taa.u_prv_color) && bgfx::isValid(taa.u_attr0) && bgfx::isValid(taa.u_attr1); } + static TAA _CreateTAA(const Reader &ir, const ReadProvider &ip, const char *path) { TAA taa; @@ -19,6 +21,9 @@ static TAA _CreateTAA(const Reader &ir, const ReadProvider &ip, const char *path taa.u_attr0 = bgfx::createUniform("u_attr0", bgfx::UniformType::Sampler); taa.u_attr1 = bgfx::createUniform("u_attr1", bgfx::UniformType::Sampler); + if (!IsValid(taa)) { + DestroyTAA(taa); + } return taa; } @@ -35,7 +40,7 @@ void DestroyTAA(TAA &taa) { void ApplyTAA(bgfx::ViewId &view_id, const iRect &rect, const Texture &color, const Texture &prv_color, const Texture &attr0, const Texture &attr1, bgfx::FrameBufferHandle output, const TAA &taa) { - const bgfx::Caps *caps = bgfx::getCaps(); + __ASSERT__(IsValid(taa)); bgfx::TransientIndexBuffer idx; bgfx::TransientVertexBuffer vtx; diff --git a/harfang/engine/taa.h b/harfang/engine/taa.h index 2be2cf1..0d32cad 100644 --- a/harfang/engine/taa.h +++ b/harfang/engine/taa.h @@ -27,4 +27,6 @@ void ApplyTAA(bgfx::ViewId &view_id, const iRect &rect, const Texture &color, co Vec2 TAAProjectionJitter8(int frame); Vec2 TAAProjectionJitter16(int frame); +bool IsValid(const TAA &taa); + } // namespace hg diff --git a/harfang/engine/temporal_accumulation.cpp b/harfang/engine/temporal_accumulation.cpp index 5e55b88..a08581e 100644 --- a/harfang/engine/temporal_accumulation.cpp +++ b/harfang/engine/temporal_accumulation.cpp @@ -10,12 +10,20 @@ namespace hg { +bool IsValid(const TemporalAccumulation &temporal_acc) { + return bgfx::isValid(temporal_acc.compute) && bgfx::isValid(temporal_acc.u_previous) && bgfx::isValid(temporal_acc.u_current) && + bgfx::isValid(temporal_acc.u_attr1); +} + static TemporalAccumulation _CreateTemporalAccumulation(const Reader &ir, const ReadProvider &ip, const char *path) { TemporalAccumulation temporal_acc; temporal_acc.compute = hg::LoadProgram(ir, ip, hg::format("%1/shader/temporal_accumulation").arg(path)); temporal_acc.u_current = bgfx::createUniform("u_current", bgfx::UniformType::Sampler); temporal_acc.u_previous = bgfx::createUniform("u_previous", bgfx::UniformType::Sampler); temporal_acc.u_attr1 = bgfx::createUniform("u_attr1", bgfx::UniformType::Sampler); + if (!IsValid(temporal_acc)) { + DestroyTemporalAccumulation(temporal_acc); + } return temporal_acc; } @@ -30,6 +38,8 @@ void DestroyTemporalAccumulation(TemporalAccumulation &temporal_acc) { } void ComputeTemporalAccumulation(bgfx::ViewId &view_id, const iRect &rect, const Texture ¤t, const Texture &previous, const Texture &attr1, bgfx::FrameBufferHandle output, const TemporalAccumulation &temporal_acc) { + __ASSERT__(IsValid(temporal_acc)); + const bgfx::Caps *caps = bgfx::getCaps(); bgfx::TransientIndexBuffer idx; diff --git a/harfang/engine/temporal_accumulation.h b/harfang/engine/temporal_accumulation.h index ddf855e..470b08e 100644 --- a/harfang/engine/temporal_accumulation.h +++ b/harfang/engine/temporal_accumulation.h @@ -21,4 +21,6 @@ void DestroyTemporalAccumulation(TemporalAccumulation &temporal_acc); void ComputeTemporalAccumulation(bgfx::ViewId &view_id, const iRect &rect, const Texture ¤t, const Texture &previous, const Texture &attr1, bgfx::FrameBufferHandle output, const TemporalAccumulation &temporal_acc); +bool IsValid(const TemporalAccumulation &temporal_acc); + } // namespace hg diff --git a/harfang/engine/upsample.cpp b/harfang/engine/upsample.cpp index 0d2af2a..474fa71 100644 --- a/harfang/engine/upsample.cpp +++ b/harfang/engine/upsample.cpp @@ -10,13 +10,20 @@ namespace hg { +bool IsValid(const Upsample &upsample) { + return bgfx::isValid(upsample.compute) && bgfx::isValid(upsample.u_input) && bgfx::isValid(upsample.u_attr_lo) && bgfx::isValid(upsample.u_attr_hi); +} + static Upsample _CreateUpsample(const Reader &ir, const ReadProvider &ip, const char *path) { Upsample up; up.compute = hg::LoadProgram(ir, ip, hg::format("%1/shader/aaa_upsample").arg(path)); up.u_input = bgfx::createUniform("u_input", bgfx::UniformType::Sampler); up.u_attr_lo = bgfx::createUniform("u_attr_lo", bgfx::UniformType::Sampler); up.u_attr_hi = bgfx::createUniform("u_attr_hi", bgfx::UniformType::Sampler); - return up; + if (!IsValid(up)) { + DestroyUpsample(up); + } + return up; } Upsample CreateUpsampleFromFile(const char *path) { return _CreateUpsample(g_file_reader, g_file_read_provider, path); } @@ -30,7 +37,7 @@ void DestroyUpsample(Upsample &up) { } void ComputeUpsample(bgfx::ViewId &view_id, const iRect &rect, const Texture &input, const Texture &attr_lo, const Texture &attr_hi, bgfx::FrameBufferHandle output, const Upsample &up) { - const bgfx::Caps *caps = bgfx::getCaps(); + __ASSERT__(IsValid(up)); bgfx::TransientIndexBuffer idx; bgfx::TransientVertexBuffer vtx; diff --git a/harfang/engine/upsample.h b/harfang/engine/upsample.h index ce6edfb..fee9f9c 100644 --- a/harfang/engine/upsample.h +++ b/harfang/engine/upsample.h @@ -21,4 +21,6 @@ void DestroyUpsample(Upsample &upsample); void ComputeUpsample(bgfx::ViewId &view_id, const iRect &rect, const Texture &input, const Texture &attr0_lo, const Texture &attr0_hi, bgfx::FrameBufferHandle output, const Upsample &upsample); +bool IsValid(const Upsample &upsample); + } // namespace hg diff --git a/harfang/foundation/CMakeLists.txt b/harfang/foundation/CMakeLists.txt index f77e0ec..4741538 100644 --- a/harfang/foundation/CMakeLists.txt +++ b/harfang/foundation/CMakeLists.txt @@ -41,6 +41,7 @@ set(HDRS intrusive_shared_ptr_st.h kv_store.h log.h + log_file.h math.h matrix3.h matrix4.h @@ -107,6 +108,7 @@ set(SRCS intersection.cpp kv_store.cpp log.cpp + log_file.cpp math.cpp matrix3.cpp matrix4.cpp diff --git a/harfang/foundation/color.cpp b/harfang/foundation/color.cpp index 6a42052..7ee7c52 100644 --- a/harfang/foundation/color.cpp +++ b/harfang/foundation/color.cpp @@ -22,42 +22,30 @@ Color operator/(const Color &a, const float v) { return {a.r / v, a.g / v, a.b / float ColorToGrayscale(const Color &c) { return 0.3f * c.r + 0.59f * c.g + 0.11f * c.b; } uint32_t ColorToRGBA32(const Color &c) { - uint32_t value; - const auto pl = reinterpret_cast(&value); - pl[0] = static_cast(Clamp(c.r, 0.f, 1.f) * 255.f); - pl[1] = static_cast(Clamp(c.g, 0.f, 1.f) * 255.f); - pl[2] = static_cast(Clamp(c.b, 0.f, 1.f) * 255.f); - pl[3] = static_cast(Clamp(c.a, 0.f, 1.f) * 255.f); - return value; + return static_cast(Clamp(c.r, 0.f, 1.f) * 255.f) | (static_cast(Clamp(c.g, 0.f, 1.f) * 255.f) << 8) | + (static_cast(Clamp(c.b, 0.f, 1.f) * 255.f) << 16) | (static_cast(Clamp(c.a, 0.f, 1.f) * 255.f) << 24); } uint32_t ColorToABGR32(const Color &c) { - uint32_t value; - const auto pl = reinterpret_cast(&value); - pl[0] = static_cast(Clamp(c.a, 0.f, 1.f) * 255.f); - pl[1] = static_cast(Clamp(c.b, 0.f, 1.f) * 255.f); - pl[2] = static_cast(Clamp(c.g, 0.f, 1.f) * 255.f); - pl[3] = static_cast(Clamp(c.r, 0.f, 1.f) * 255.f); - return value; + return static_cast(Clamp(c.a, 0.f, 1.f) * 255.f) | (static_cast(Clamp(c.b, 0.f, 1.f) * 255.f) << 8) | + (static_cast(Clamp(c.g, 0.f, 1.f) * 255.f) << 16) | (static_cast(Clamp(c.r, 0.f, 1.f) * 255.f) << 24); } Color ColorFromRGBA32(unsigned int value) { Color c; - const auto i = reinterpret_cast(&value); - c.r = float(i[0]) / 255.f; - c.g = float(i[1]) / 255.f; - c.b = float(i[2]) / 255.f; - c.a = float(i[3]) / 255.f; + c.r = float((value)&0xff) / 255.f; + c.g = float((value >> 8) & 0xff) / 255.f; + c.b = float((value >> 16) & 0xff) / 255.f; + c.a = float((value >> 24) & 0xff) / 255.f; return c; } Color ColorFromABGR32(unsigned int value) { Color c; - const auto i = reinterpret_cast(&value); - c.a = float(i[0]) / 255.f; - c.b = float(i[1]) / 255.f; - c.g = float(i[2]) / 255.f; - c.r = float(i[3]) / 255.f; + c.a = float((value)&0xff) / 255.f; + c.b = float((value >> 8) & 0xff) / 255.f; + c.g = float((value >> 16) & 0xff) / 255.f; + c.r = float((value >> 24) & 0xff) / 255.f; return c; } @@ -112,4 +100,111 @@ Color ClampLen(const Color &c, float min, float max) { Color ColorFromVector3(const Vec3 &v) { return {v.x, v.y, v.z, 1}; } Color ColorFromVector4(const Vec4 &v) { return {v.x, v.y, v.z, v.w}; } +// +Color ToHLS(const Color &c) { + const float min = Min(c.r, c.g, c.b); + const float max = Max(c.r, c.g, c.b); + + const double diff = max - min; + const float l = (max + min) / 2; + + float h = 0, s = 0; + + if (diff > 0.f) { + if (l <= 0.5) + s = diff / (max + min); + else + s = diff / (2 - max - min); + + const double r_dist = (max - c.r) / diff; + const double g_dist = (max - c.g) / diff; + const double b_dist = (max - c.b) / diff; + + if (c.r == max) + h = b_dist - g_dist; + else if (c.g == max) + h = 2.f + r_dist - b_dist; + else + h = 4.f + g_dist - r_dist; + + h = h * 60.f; + if (h < 0.f) + h += 360.f; + } + + return {h, l, s, c.a}; +} + +static float QqhToRgb(float q1, float q2, float hue) { + if (hue > 360.f) + hue -= 360.f; + else if (hue < 0.f) + hue += 360.f; + + if (hue < 60.f) + return q1 + (q2 - q1) * hue / 60.f; + if (hue < 180.f) + return q2; + if (hue < 240.f) + return q1 + (q2 - q1) * (240.f - hue) / 60.f; + + return q1; +} + +Color FromHLS(const Color &c) { + const float p2 = c.g <= 0.5f ? c.g * (1.f + c.b) : c.g + c.b - c.g * c.b; + const float p1 = 2.f * c.g - p2; + + float r, g, b; + + if (c.b == 0.f) { + r = c.g; + g = c.g; + b = c.g; + } else { + r = QqhToRgb(p1, p2, c.r + 120.f); + g = QqhToRgb(p1, p2, c.r); + b = QqhToRgb(p1, p2, c.r - 120.f); + } + + return {r, g, b, c.a}; +} + +// +Color SetHue(const Color &c, float h) { + auto hls = ToHLS(c); + hls.r = h; + return FromHLS(hls); +} + +Color SetSaturation(const Color &c, float s) { + auto hls = ToHLS(c); + hls.b = s; + return FromHLS(hls); +} + +Color SetLuminance(const Color &c, float l) { + auto hls = ToHLS(c); + hls.g = l; + return FromHLS(hls); +} + +Color ScaleHue(const Color &c, float k) { + auto hls = ToHLS(c); + hls.r *= k; + return FromHLS(hls); +} + +Color ScaleSaturation(const Color &c, float k) { + auto hls = ToHLS(c); + hls.b *= k; + return FromHLS(hls); +} + +Color ScaleLuminance(const Color &c, float k) { + auto hls = ToHLS(c); + hls.g *= k; + return FromHLS(hls); +} + } // namespace hg diff --git a/harfang/foundation/color.h b/harfang/foundation/color.h index 3aeb900..afe4601 100644 --- a/harfang/foundation/color.h +++ b/harfang/foundation/color.h @@ -155,4 +155,15 @@ Color ColorFromVector4(const Vec4 &); inline Color ColorI(int r, int g, int b, int a = 255) { return {float(r) / 255.f, float(g) / 255.f, float(b) / 255.f, float(a) / 255.f}; } +// +Color ToHLS(const Color &); +Color FromHLS(const Color &); + +Color SetHue(const Color &c, float h); +Color SetSaturation(const Color &c, float s); +Color SetLuminance(const Color &c, float l); +Color ScaleHue(const Color &c, float k); +Color ScaleSaturation(const Color &c, float k); +Color ScaleLuminance(const Color &c, float k); + } // namespace hg diff --git a/harfang/foundation/file.cpp b/harfang/foundation/file.cpp index 9e3fc55..92e203d 100644 --- a/harfang/foundation/file.cpp +++ b/harfang/foundation/file.cpp @@ -89,6 +89,17 @@ File OpenWriteText(const char *path) { return {invalid_gen_ref}; } +File OpenAppendText(const char *path) { + auto *f = _Open(path, "a"); + + if (f) { + std::lock_guard lock(files_mutex); + return {files.add_ref(f)}; + } + + return {invalid_gen_ref}; +} + File OpenTemp(const char *tmplt) { size_t len = strlen(tmplt); File out{invalid_gen_ref}; diff --git a/harfang/foundation/file.h b/harfang/foundation/file.h index 5f6501a..93306ca 100644 --- a/harfang/foundation/file.h +++ b/harfang/foundation/file.h @@ -20,6 +20,7 @@ File Open(const char *path, bool silent = false); File OpenText(const char *path, bool silent = false); File OpenWrite(const char *path); File OpenWriteText(const char *path); +File OpenAppendText(const char *path); File OpenTemp(const char *tmplt); bool Close(File file); diff --git a/harfang/foundation/log_file.cpp b/harfang/foundation/log_file.cpp new file mode 100644 index 0000000..81b2dec --- /dev/null +++ b/harfang/foundation/log_file.cpp @@ -0,0 +1,38 @@ +// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details. + +#include "foundation/file.h" + +#include +#include + +namespace hg { + +static std::map id_to_path; +static std::mutex mutex; + +bool DeclareLogFile(const std::string &id, const std::string &path) { + std::lock_guard lock(mutex); + id_to_path[id] = path; + + const auto file = OpenWriteText(path.c_str()); + bool result = IsValid(file); + Close(file); + + return result; +} + +bool LogToFile(const std::string &id, const std::string &msg) { + std::lock_guard lock(mutex); + + const auto i = id_to_path.find(id); + if (i == std::end(id_to_path)) + return false; + + const auto file = OpenAppendText(i->second.c_str()); + const auto result = WriteStringAsText(file, msg + "\n"); + Close(file); + + return result; +} + +} // namespace hg diff --git a/harfang/foundation/log_file.h b/harfang/foundation/log_file.h new file mode 100644 index 0000000..6d6c6a4 --- /dev/null +++ b/harfang/foundation/log_file.h @@ -0,0 +1,12 @@ +// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details. + +#pragma once + +#include + +namespace hg { + +bool DeclareLogFile(const std::string &id, const std::string &path); +bool LogToFile(const std::string &id, const std::string &msg); + +} // namespace hg diff --git a/harfang/foundation/math.h b/harfang/foundation/math.h index b67da66..1deb559 100644 --- a/harfang/foundation/math.h +++ b/harfang/foundation/math.h @@ -14,7 +14,9 @@ static const float TwoPi = Pi * 2.f; template constexpr T inline Abs(const T &v) { return v < 0 ? -v : v; } template constexpr T inline Min(const T &a, const T &b) { return a < b ? a : b; } +template constexpr T inline Min(const T &a, const T &b, const T &c) { return Min(Min(a, b), c); } template constexpr T inline Max(const T &a, const T &b) { return a > b ? a : b; } +template constexpr T inline Max(const T &a, const T &b, const T &c) { return Max(Max(a, b), c); } template constexpr T inline Clamp(const T &v, const T &min, const T &max) { return v < min ? min : (v > max ? max : v); } template constexpr T inline Lerp(const T &a, const T &b, float k) { return T(a * (1.f - k) + b * k); } @@ -83,7 +85,7 @@ template T HermiteInterpolate(T y0, T y1, T y2, T y3, float t, floa const auto a1 = t3 - 2.f * t2 + t; const auto a2 = t3 - t2; const auto a3 = -2.f * t3 + 3.f * t2; - return y1 * a0 + t0 * a1 + t1 * a2 + y2 * a3; + return y1 * a0 + t0 * a1 + t1 * a2 + y2 * a3; } template T LinearInterpolateArray(uint32_t count, const T *values, float t) { diff --git a/harfang/foundation/minmax.h b/harfang/foundation/minmax.h index a5d7b2d..50f5b7b 100644 --- a/harfang/foundation/minmax.h +++ b/harfang/foundation/minmax.h @@ -58,7 +58,7 @@ bool IntersectRay(const MinMax &mm, const Vec3 &o, const Vec3 &d); /// Returns whether a line intersect with the MinMax. bool ClassifyLine(const MinMax &mm, const Vec3 &p, const Vec3 &d, Vec3 &i, Vec3 *n = nullptr); /// Returns whether a segment intersect with the MinMax. -bool ClassifySegment(const MinMax &mm, const Vec3 &p0, const Vec3 &p1, Vec3 &i, Vec3 *n = nullptr); +bool ClassifySegment(const MinMax &mm, const Vec3 &p1, const Vec3 &p2, Vec3 &itr, Vec3 *n = nullptr); /// Set from position and size. inline MinMax MinMaxFromPositionSize(const Vec3 &p, const Vec3 &s) { return {p - s * 0.5f, p + s * 0.5f}; } diff --git a/harfang/foundation/obb.cpp b/harfang/foundation/obb.cpp index 3517398..8d62d59 100644 --- a/harfang/foundation/obb.cpp +++ b/harfang/foundation/obb.cpp @@ -11,7 +11,7 @@ OBB OBBFromMinMax(const MinMax &minmax) { return {(minmax.mn + minmax.mx) * 0.5f MinMax MinMaxFromOBB(const OBB &obb) { Vec3 xtd = obb.scl * 0.5f; - Vec3 smt[4] = { + const Vec3 smt[4] = { {xtd.x, xtd.y, xtd.z}, {-xtd.x, xtd.y, xtd.z}, {xtd.x, -xtd.y, xtd.z}, diff --git a/harfang/foundation/profiler.cpp b/harfang/foundation/profiler.cpp index d836f40..e684965 100644 --- a/harfang/foundation/profiler.cpp +++ b/harfang/foundation/profiler.cpp @@ -40,8 +40,8 @@ static size_t GetTaskInBucket(size_t bucket_index, const std::string &name) { return i; // create missing event - auto &event = *tasks.emplace(tasks.end()); - event.name = name; + auto event = tasks.emplace(tasks.end()); + event->name = name; bucket.push_back(tasks.size() - 1); return tasks.size() - 1; @@ -144,10 +144,10 @@ ProfilerSectionIndex BeginProfilerSection(const std::string &name, const std::st auto task_idx = GetTaskInBucket(bucket_idx, name); auto &task = tasks[task_idx]; - auto §ion = *sections.emplace(sections.end()); - section.thread_id = std::this_thread::get_id(); - section.details = section_details; - section.start = time_now(); + auto section = sections.emplace(sections.end()); + section->thread_id = std::this_thread::get_id(); + section->details = section_details; + section->start = time_now(); auto section_idx = sections.size() - 1; task.section_indexes.push_back(section_idx); diff --git a/harfang/foundation/qmc.h b/harfang/foundation/qmc.h index 367ab74..2031a3e 100644 --- a/harfang/foundation/qmc.h +++ b/harfang/foundation/qmc.h @@ -26,7 +26,7 @@ typename std::enable_if::value, T>::type vdC(int index /// Computes generalized halton sequence. template void halton(T *out, int dimension, int index, int n) { - static int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; + static const int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; __ASSERT__(n >= 0); diff --git a/harfang/foundation/string.cpp b/harfang/foundation/string.cpp index a8205b7..83d2f15 100644 --- a/harfang/foundation/string.cpp +++ b/harfang/foundation/string.cpp @@ -247,30 +247,26 @@ std::string strip_suffix(const std::string &str, const std::string &suffix) { re // std::string utf16_to_utf8(const std::u16string &str) { - auto utf16string = (char16_t *)str.data(); std::vector utf8string; - utf8::utf16to8(utf16string, utf16string + str.size(), std::back_inserter(utf8string)); + utf8::utf16to8(str.begin(), str.end(), std::back_inserter(utf8string)); return std::string((char *)utf8string.data(), utf8string.size()); } std::u16string utf8_to_utf16(const std::string &str) { - auto utf8string = (char *)str.data(); std::vector utf16string; - utf8::utf8to16(utf8string, utf8string + str.size(), std::back_inserter(utf16string)); + utf8::utf8to16(str.begin(), str.end(), std::back_inserter(utf16string)); return std::u16string(reinterpret_cast(utf16string.data()), utf16string.size()); } std::string utf32_to_utf8(const std::u32string &str) { - auto utf32string = (char32_t *)str.data(); std::vector utf8string; - utf8::utf32to8(utf32string, utf32string + str.size(), std::back_inserter(utf8string)); + utf8::utf32to8(str.begin(), str.end(), std::back_inserter(utf8string)); return std::string((char *)utf8string.data(), utf8string.size()); } std::u32string utf8_to_utf32(const std::string &str) { - auto utf8string = (char *)str.data(); std::vector utf32string; - utf8::utf8to32(utf8string, utf8string + str.size(), std::back_inserter(utf32string)); + utf8::utf8to32(str.begin(), str.end(), std::back_inserter(utf32string)); return std::u32string(utf32string.data(), utf32string.size()); } diff --git a/harfang/foundation/string.h b/harfang/foundation/string.h index 086ac4f..0281a5f 100644 --- a/harfang/foundation/string.h +++ b/harfang/foundation/string.h @@ -103,8 +103,7 @@ template std::string join(T begin_it, T end_it, const std::string & auto e = std::prev(end_it); std::string out; - if (count > 1) - out = join(begin_it, e, separator) + last_separator; + out = join(begin_it, e, separator) + last_separator; out += *e; return out; diff --git a/harfang/foundation/time.h b/harfang/foundation/time.h index 890bf72..d4a2f30 100644 --- a/harfang/foundation/time.h +++ b/harfang/foundation/time.h @@ -22,6 +22,10 @@ constexpr int64_t time_to_ms(time_ns t) { return t / 1000000LL; } constexpr int64_t time_to_us(time_ns t) { return t / 1000LL; } constexpr int64_t time_to_ns(time_ns t) { return t; } +constexpr time_ns time_from_sec_d(double sec) { return time_ns(double(sec) * 1000000000.0); } +constexpr time_ns time_from_ms_d(double ms) { return time_ns(double(ms) * 1000000.0); } +constexpr time_ns time_from_us_d(double us) { return time_ns(double(us) * 1000.0); } + constexpr time_ns time_from_sec_f(float sec) { return time_ns(double(sec) * 1000000000.0); } constexpr time_ns time_from_ms_f(float ms) { return time_ns(double(ms) * 1000000.0); } constexpr time_ns time_from_us_f(float us) { return time_ns(double(us) * 1000.0); } diff --git a/harfang/foundation/time_to_string.cpp b/harfang/foundation/time_to_string.cpp index 6aa7c73..423a62d 100644 --- a/harfang/foundation/time_to_string.cpp +++ b/harfang/foundation/time_to_string.cpp @@ -32,7 +32,7 @@ bool time_from_string(const std::string &v, time_ns &out) { switch (vals.size()) { case 1: { - sec = strtof(vals[0].c_str(), &eon); + sec = strtod(vals[0].c_str(), &eon); if (eon == vals[0].c_str() || sec > 59) return false; } break; @@ -42,7 +42,7 @@ bool time_from_string(const std::string &v, time_ns &out) { if (eon == vals[0].c_str() || min > 59) return false; - sec = strtof(vals[1].c_str(), &eon); + sec = strtod(vals[1].c_str(), &eon); if (eon == vals[1].c_str() || sec > 59) return false; } break; @@ -56,13 +56,13 @@ bool time_from_string(const std::string &v, time_ns &out) { if (eon == vals[1].c_str() || min > 59) return false; - sec = strtof(vals[2].c_str(), &eon); + sec = strtod(vals[2].c_str(), &eon); if (eon == vals[2].c_str() || sec > 59) return false; } break; } - out = time_from_sec_f(sec); + out = time_from_sec_d(sec); out += time_from_min(min); out += time_from_hour(hour); return true; diff --git a/harfang/foundation/vector2.cpp b/harfang/foundation/vector2.cpp index 242ffa1..e075f65 100644 --- a/harfang/foundation/vector2.cpp +++ b/harfang/foundation/vector2.cpp @@ -1,7 +1,8 @@ // HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details. -#include "foundation/vector2.h" #include "foundation/math.h" + +#include "foundation/vector2.h" #include "foundation/matrix3.h" #include "foundation/vector3.h" #include "foundation/vector4.h" diff --git a/harfang/foundation/vector_list.h b/harfang/foundation/vector_list.h index b6ca799..75700ed 100644 --- a/harfang/foundation/vector_list.h +++ b/harfang/foundation/vector_list.h @@ -183,9 +183,11 @@ public: uint32_t remove(uint32_t i) { const auto n = next(i); // next entry in use - const uint32_t idx = idx_[i]; - __ASSERT__(!is_free_idx(idx)); // assert entry is in use - reinterpret_cast(storage_)[idx].~T(); // destroy object + { + const uint32_t idx = idx_[i]; + __ASSERT__(!is_free_idx(idx)); // assert entry is in use + reinterpret_cast(storage_)[idx].~T(); // destroy object + } uint32_t free_skip = 1; if ((i + 1) < idx_.size()) { diff --git a/harfang/foundation/version.cpp b/harfang/foundation/version.cpp index 6f2c761..e201cc0 100644 --- a/harfang/foundation/version.cpp +++ b/harfang/foundation/version.cpp @@ -2,6 +2,7 @@ #include "foundation/version.h" #include "foundation/string.h" + #include #include diff --git a/harfang/platform/filesystem_watcher.cpp b/harfang/platform/filesystem_watcher.cpp index 6f48d96..615a15a 100644 --- a/harfang/platform/filesystem_watcher.cpp +++ b/harfang/platform/filesystem_watcher.cpp @@ -69,9 +69,9 @@ void DirectoryWatchPull::Update(const std::string &root, const std::string &path auto new_entries = ListDir(PathJoin({root, path}).c_str()); - const auto &i = path_entries.find(path); + const auto ¤t_entry = path_entries.find(path); - if (i == std::end(path_entries)) { + if (current_entry == std::end(path_entries)) { #if ENABLE_HASH_CONFIRMATION { auto &hashes = path_hashes[path]; @@ -85,7 +85,7 @@ void DirectoryWatchPull::Update(const std::string &root, const std::string &path #endif path_entries[path] = std::move(new_entries); } else { - const auto &old_entries = i->second; + const auto &old_entries = current_entry->second; // std::map old_; @@ -136,11 +136,11 @@ void DirectoryWatchPull::Update(const std::string &root, const std::string &path } // update state - i->second = std::move(new_entries); + current_entry->second = std::move(new_entries); // if (recursive) - for (auto &j : i->second) + for (auto &j : current_entry->second) if (j.type == DE_Dir) Update(root, PathJoin({path, j.name}), recursive); } diff --git a/harfang/platform/glfw/input_system_glfw.cpp b/harfang/platform/glfw/input_system_glfw.cpp index 5f00091..ffa4c44 100644 --- a/harfang/platform/glfw/input_system_glfw.cpp +++ b/harfang/platform/glfw/input_system_glfw.cpp @@ -443,7 +443,7 @@ static KeyboardState ReadRawKeyboard() { // #if ((GLFW_VERSION_MAJOR >= 3) && (GLFW_VERSION_MINOR >= 3)) -static int GamepadSlotToGLFWJoystick(const std::string name) { +static int GamepadSlotToGLFWJoystick(const std::string& name) { if (name == "gamepad_slot_0") return GLFW_JOYSTICK_1; else if (name == "gamepad_slot_1") @@ -511,7 +511,7 @@ template GamepadState ReadGamepad() { return state; } -static int JoystickSlotToGLFWJoystick(const std::string name) { +static int JoystickSlotToGLFWJoystick(const std::string& name) { if (name == "joystick_slot_0") return GLFW_JOYSTICK_1; else if (name == "joystick_slot_1") @@ -554,7 +554,7 @@ template JoystickState ReadJoystick() { int buttons_count = 0; auto buttons = glfwGetJoystickButtons(ID, &buttons_count); - JoystickState state{axis_count, buttons_count}; + JoystickState state{static_cast(axis_count), static_cast(buttons_count)}; state.connected = axis_count && buttons_count; for (int i = 0; i < axis_count; ++i) diff --git a/harfang/platform/glfw/window_system.cpp b/harfang/platform/glfw/window_system.cpp index 543bb7c..1bc9706 100644 --- a/harfang/platform/glfw/window_system.cpp +++ b/harfang/platform/glfw/window_system.cpp @@ -149,6 +149,7 @@ bool GetMonitorModes(const Monitor *monitor, std::vector &out) { //-- Window static std::map window_drop_cb; +static std::map window_refresh_cb; struct Window { uintptr_t unused; @@ -438,6 +439,18 @@ void SetWindowDropCallback(const Window *w, void (*cb)(const Window *w, int coun glfwSetDropCallback(glfw_w, GLFW_window_drop_cb_proxy); } +static void GLFW_window_refresh_cb_proxy(GLFWwindow *w) { + const auto i = window_refresh_cb.find((Window *)w); + if (i != std::end(window_refresh_cb)) + i->second((Window *)w); +} + +void SetWindowRefreshCallback(const Window *w, void (*cb)(const Window *window)) { + window_refresh_cb[w] = cb; + if (auto glfw_w = GetGLFWWindow(w)) + glfwSetWindowRefreshCallback(glfw_w, GLFW_window_refresh_cb_proxy); +} + // void SetWindowIcon(const Window *w, int count, const Icon *icons) { if (auto glfw_w = GetGLFWWindow(w)) { @@ -450,4 +463,18 @@ void SetWindowIcon(const Window *w, int count, const Icon *icons) { } } +void CenterWindow(Window *w) { + if (auto glfw_w = GetGLFWWindow(w)) + if (const auto monitor = glfwGetPrimaryMonitor()) { + int xpos, ypos; + glfwGetMonitorPos(monitor, &xpos, &ypos); + + int w, h; + glfwGetWindowSize(glfw_w, &w, &h); + + if (const auto mode = glfwGetVideoMode(monitor)) + glfwSetWindowPos(glfw_w, (mode->width - w) / 2 + xpos, (mode->height - h) / 2 + ypos); + } +} + } // namespace hg diff --git a/harfang/platform/input_system.h b/harfang/platform/input_system.h index f694f94..f424a58 100644 --- a/harfang/platform/input_system.h +++ b/harfang/platform/input_system.h @@ -240,8 +240,8 @@ enum GamepadButton { struct GamepadState { bool connected = false; - float axes[GA_Count]; std::bitset button; + float axes[GA_Count]; }; using GamepadReader = GamepadState (*)(); @@ -274,8 +274,8 @@ struct Gamepad { const GamepadState &GetOldState() const { return old_state; } private: - std::string name; GamepadState state{}, old_state{}; + std::string name; }; // diff --git a/harfang/platform/window_system.h b/harfang/platform/window_system.h index a6d4818..8c203cc 100644 --- a/harfang/platform/window_system.h +++ b/harfang/platform/window_system.h @@ -112,6 +112,8 @@ Window *GetWindowInFocus(); iVec2 GetWindowPos(const Window *window); /// Set the window position bool SetWindowPos(Window *window, const iVec2 &position); +/// Center a window on the primary monitor. +void CenterWindow(Window *window); /// Return true if the window is open. bool IsWindowOpen(const Window *window); @@ -132,6 +134,8 @@ void HideCursor(); /// Enable drop support on a specific window void SetWindowDropCallback(const Window *window, void (*cb)(const Window *window, int count, const char **paths)); +/// Called when a window needs to be refreshed. +void SetWindowRefreshCallback(const Window *window, void (*cb)(const Window *window)); /// \} diff --git a/harfang/tests/t_dir.cpp b/harfang/tests/t_dir.cpp index 6f74a82..5d09bad 100644 --- a/harfang/tests/t_dir.cpp +++ b/harfang/tests/t_dir.cpp @@ -13,7 +13,7 @@ TEST(Dir, ListDirRecursive) { std::vector entries = hg::ListDirRecursive(GetResPath("").c_str()); EXPECT_FALSE(entries.empty()); - std::string expected = hg::PathJoin({"gpu", "texture", "mire512.png"}); + std::string expected = hg::PathJoin({"pic", "owl.jpg"}); auto i = std::find_if(entries.begin(), entries.end(), [&](const hg::DirEntry &e) { return e.name == expected; }); EXPECT_NE(i, entries.end()); } diff --git a/harfang/version.txt b/harfang/version.txt index 4a36342..fd2a018 100644 --- a/harfang/version.txt +++ b/harfang/version.txt @@ -1 +1 @@ -3.0.0 +3.1.0 diff --git a/languages/hg_python/bdist_wheel/DESCRIPTION.rst b/languages/hg_python/bdist_wheel/DESCRIPTION.rst index 6079893..1f1d6b6 100644 --- a/languages/hg_python/bdist_wheel/DESCRIPTION.rst +++ b/languages/hg_python/bdist_wheel/DESCRIPTION.rst @@ -9,11 +9,12 @@ Harfang is a 3D real time visualization framework for the industry, the educatio See https://www.harfang3d.com/license for licensing terms. +| | **Quickstart** -1. Download the tutorials from Github here and unzip them to your computer (eg. *d:/tutorials-hg2*). -2. Download assetc for your platform to compile the tutorial resources. -3. Drag and drop the tutorial resources folder on the assetc executable -OR- execute assetc passing it the path to the tutorial resources folder (eg. *assetc d:/tutorials-hg2/resources*). +1. Download the tutorials https://github.com/harfang3d/tutorials-hg2 and unzip them to your computer (eg. *d:/tutorials-hg2*). +2. To compile the tutorial resources, download **assetc** for your platform: https://www.harfang3d.com/releases/3.0.0/ +3. Drag and drop the tutorial resources folder on the **`assetc** executable -OR- execute **assetc** passing it the path to the tutorial resources folder (eg. *assetc d:/tutorials-hg2/resources*). .. image:: https://raw.githubusercontent.com/harfang3d/image-storage/main/tutorials/assetc.gif @@ -23,6 +24,7 @@ After the compilation process finishes, you should see a ``resources_compiled`` Alternatively you can open the tutorial folder and run the provided debug targets using `Visual Studio Code `_ +| | **Screenshots** The following screenshots were captured on a 1080GTX in 1080P running at 60FPS, GI is performed using screen space raytracing and does not require RTX capable hardware. @@ -43,6 +45,7 @@ The following screenshots were captured on a 1080GTX in 1080P running at 60FPS, *(Bistro, courtesy of the Open Research Content Archive (ORCA))* +| | **Features** | Scene API diff --git a/readme.md b/readme.md index fe6ab4b..9162bea 100644 --- a/readme.md +++ b/readme.md @@ -95,6 +95,7 @@ https://www.harfang3d.com/download 1. Clone the `Harfang 3D` repository including its submodules. ``` git clone --recursive -j8 https://github.com/harfang3d/harfang3d.git + cd harfang3d ``` 1. Create build directory. Note that the described directory layout is not mandatory. @@ -116,7 +117,7 @@ https://www.harfang3d.com/download cd cmake ``` -1. Generate build system using CMake. For example, on Windows it wil be: +1. Generate the build system using CMake. For example, on Windows it will be: ``` cmake ../.. -G "Visual Studio 16 2019" -A x64 \ -DCMAKE_INSTALL_PREFIX:PATH="D:/harfang/build/install" \ diff --git a/release-notes.md b/release-notes.md new file mode 100644 index 0000000..f2d130a --- /dev/null +++ b/release-notes.md @@ -0,0 +1,110 @@ +# [3.1.0] - 2021-12-13 + +This minor release brings several improvements and fixes, mainly in the Bullet Physics API. + +### Engine + +- Added new HLS colorspace functions. +- Improved `CreateInstanceFromFile`/`CreateInstanceFromAssets` by returning a boolean to inform the caller that the hosted scene could be instantiated. +- Fixed various bindings (`CreateInstanceFromFile`, `CreateInstanceFromAssets`, `GetColorTexture`, `GetDepthTexture`, `GetTextures`) +- Added a function to center a window (GLFW). +- Added the support for Windows refresh callback (GLFW). +- Bind read back and blit destination texture flags. +- Simplify the frame buffer API store less content in Harfang objects. +- Added fog to render data. +- Validate AAA forward pipeline and associated post processes. +- Fixed an issue where setting up the imgui font destroyed the cursors. +- Added `time_from_xxx_d` functions and fixed a precision issue with `time_from_string` + +### Physics + +- Fixed the initial transform setup code of Bullet rigid bodies. +- Changed the sync kinematic and dynamic mechanisme. +- Added `ResetWorld`, `DisableDeactivation`, add a way to lock axes. +- Added a getter/setter for rolling friction. +- Added a raycast functions to return all collisions found between two points. +- Fixed a raycast issue with geometry hierarchy. +- Added the support for multi collision shapes with matrices + +### Toolchain + +- Assetc cleanup error messages, do not output unless truly reporting on an error. +- Assetc will now delete the outputs for deleted inputs. A flag was added to opt-out from this mechanism (`-no_clean_removed_inputs`). +- Assetc will now delete the previous compiled lua scripts if a compilation error arises. +- Assimp importer: fixed a mention to the wrong converter. +- Assimp importer: added a `-merge-mesh` option. +- Assimp importer: updated to Assimp v5.1.2. +- GLTF importer: fixed the import in case of many instances. +- Fixed the toolchain integrity check. + +### Documentation + +- Documentation improvements and cleanup. +- Fixed a typo in `GetTextures` documentation filename. +- Fixed the reference to `SceneBullet3Physics` in the man Physics page. +- Added a man.Requirements page. +- Improved the Pypi wheel description ('Quickstart' was lacking a download URL). + +### Misc + +- Updated the license. +- Fixed some cppcheck issues. + +# [3.0.0] - 2021-10-12 + +This major release mainly replaces Newton Dynamics with Bullet Dynamics for physics and improves interoperability between supported languages. + +### Engine + +- Add diffuse and specular intensity to lights. +- Performance and filtering improvements of the AAA rendering pipeline. +- Replace Newton Dynamics with Bullet 3 Dynamics, SceneBullet3Physics uses the same API as SceneNewtonPhysics. +- Add SceneBullet3Physics.NodeWake to wake a sleeping body. +- Implement restitution and friction in rigid bodies. +- Provide support for value transfer between Lua VMs when calling Get/Set/Call on SceneLuaVM. +- Implement full value transfer between Python and Lua VMs. +- Implement Call on SceneLuaVM. +- Light default specular value is now (1.0, 1.0, 1.0). +- Support for specular color in core light models (point, spot, linear) in the PBR shader. + This is not compliant with a strict definition of a PBR Pipeline, but this approach tends to be a consensus in the industry (Blender, Redshift, ...) and is artist-friendly. +- Add Joystick support (Joystick, JoystickState, ReadJoystick, GetJoystickNames, GetJoystickDeviceNames). +- Improve OpenAsset and LoadResourceMeta performance. +- Improve Vertices.End performance by removing systematic default validation. +- Rename x_aspect_ratio parameter to fov_axis_is_horizontal in SceneSubmitToForwardRenderPipeline. +- SetView2D and SetViewPerspective now takes x,y,w,h instead of w,h. +- Fix redundant loads of texture meta JSON when loading materials. +- Fix ImGui Enter/Return key distinction. +- Fix broken linear texture filtering. +- Fix GLFW window system not sending window signals and breaking IsWindowOpen as a consequence. +- Fix spurious error message when opening asset with several asset folders registered. + +### Toolchain + +- New AssImp converter. +- GLTF Importer: Add occlusion texture if no metal/roughness texture is declared in a source material. +- FBX, GLTF importer: Fix on the importation of animations in some specific rotations situations. + +### Documentation + +- Quickstart page improvements. +- Index class members in the search index. + +### Misc + +- Update STB Truetype. +- Update STB Vorbis. +- Update STB Image. +- Update STB Image Write. +- Update bgfx. +- Update bimg. +- Update tiny process. + +# [2.0.111] - 2021-06-21 + +This release is a rewrite of the Harfang API toward a data-oriented approach. +As a developer, you will find that many of the old classes are now gone and replaced by function calls that you string together in order to implement complex functionalities. The improved API provides greater flexibility and delivers higher performance than the Harfang 1.x API. +Of major interest in this first release of Harfang 2.x are the new resource/asset pipeline (see Resources & Assets) as well as the new AAA renderer beta enabling screen-space global illumation with correct approximation of both the radiance and irradiance terms of the PBR equation. + +**NOTE:** + +The AAA rendering pipeline is currently a **WORK IN PROGRESS** and has important issues to address. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1e75fb5..5e782ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -pycurl -pygit2 -wheel -pypeg2 -markdown \ No newline at end of file +pycurl +pygit2 +wheel +pypeg2 +markdown +beautifulsoup4 diff --git a/tools/assetc/assetc.cpp b/tools/assetc/assetc.cpp index 5eae00d..a82bc33 100644 --- a/tools/assetc/assetc.cpp +++ b/tools/assetc/assetc.cpp @@ -133,8 +133,7 @@ static void SetToolchain(const std::string &path) { toolchain = {PathJoin({path, "shaderc.exe"}), PathJoin({path, "texturec.exe"}), PathJoin({path, "luac.exe"}), PathJoin({path, "cmft.exe"}), PathJoin({path, "recastc.exe"}), PathJoin({path, "texconv.exe"})}; #else - toolchain = { PathJoin({path, "shaderc"}), PathJoin({path, "texturec"}), PathJoin({path, "luac"}), PathJoin({path, "cmft"}), - PathJoin({path, "texconv"})}; + toolchain = {PathJoin({path, "shaderc"}), PathJoin({path, "texturec"}), PathJoin({path, "luac"}), PathJoin({path, "cmft"}), PathJoin({path, "texconv"})}; #endif } @@ -172,8 +171,10 @@ static std::string api = HG_GRAPHIC_API; // DX11/DX12/GL static std::string platform = ASSSETC_DEFAULT_PLATFORM; +#if 0 +// CWE 561: The function 'SetAPI' is never used. void SetAPI(const char *api_) { api = api_; } - +#endif // static std::string input_dir; static std::string output_dir; @@ -181,11 +182,8 @@ static std::string output_dir; static std::string FullInputPath(const std::string &path) { return PathJoin({input_dir, path}); } static std::string FullOutputPath(const std::string &path) { return PathJoin({output_dir, path}); } -static std::string ReadyFullOutputPath(const std::string &path) { - const auto full_path = FullOutputPath(path); - MkTree(CutFileName(full_path).c_str()); - return full_path; -} +static std::string FullOutputDir(const std::string &path) { return CutFileName(FullOutputPath(path)); } +static void MkOutputTree(const std::string &path) { MkTree(FullOutputDir(path).c_str()); } #if HASH_METHOD == 0 using Hash = SHA1Hash; @@ -314,6 +312,13 @@ static bool NeedsCompilation( return need_refresh; } +static void CleanOutputs(const std::set &outputs) { + ProfilerPerfSection perf("Manage/CleanOutputs"); + + for (const auto &output : outputs) + Unlink(FullOutputPath(output).c_str()); +} + // void Reset() { compilation_db.source_hashes.clear(); @@ -345,7 +350,7 @@ static void RunProcess(const std::string &name, const std::string &cmd, const st const auto cmd_elms = hg::split(cmd, " "); ProfilerPerfSection perf(hg::format("Command/RunProcess/%1").arg(cmd_elms[0])); - log(format(" Spawning compile process for %1").arg(name)); + hg::log(format(" Spawning compile process for %1").arg(name)); TinyProcessLib::Process process( cmd, cwd, @@ -442,7 +447,7 @@ static void RunTaskQueue() { const auto t_now = time_now(); if (t_now - t_ref >= time_from_sec(5)) { - log(format(" %1 tasks running, %2 queued...").arg(task_running.size()).arg(task_queue.size())); + hg::log(format(" %1 tasks running, %2 queued...").arg(task_running.size()).arg(task_queue.size())); t_ref = t_now; } } @@ -456,20 +461,18 @@ static void RunTaskQueue() { // static const uint16_t CAB1 = 0xCAB1; -bool LoadCompilationDB(const char *path) { +bool LoadCompilationDB(const char *filename) { ProfilerPerfSection perf("Manage/LoadCompilationDB"); Reset(); - if (!Exists(path)) + if (!Exists(filename)) return false; - ScopedFile file(Open(path)); + ScopedFile file(Open(filename)); if (!file) return false; - const auto size = GetSize(file); - // if (Read(file) != CAB1) return false; @@ -480,8 +483,8 @@ bool LoadCompilationDB(const char *path) { // if (version >= 2) { - const auto build_sha = ReadString(file); - const auto version_string = ReadString(file); + (void)ReadString(file); // build_sha + (void)ReadString(file); // version_string } // @@ -500,7 +503,7 @@ bool LoadCompilationDB(const char *path) { const auto name = ReadString(file); const auto src_count = Read(file); - for (uint32_t i = 0; i < src_count; ++i) { + for (uint32_t j = 0; j < src_count; ++j) { const auto path = ReadString(file); compilation_db.output_to_inputs[name].insert(path); } @@ -611,10 +614,13 @@ static void Copy(std::map &hashes, const std::string &path) { __ASSERT__(!IsDir(path.c_str())); - log(format(" Copy '%1'").arg(path)); + hg::log(format(" Copy '%1'").arg(path)); if (NeedsCompilation(hashes, {path}, {path}, {})) { - const auto src = FullInputPath(path), dst = ReadyFullOutputPath(path); + const auto src = FullInputPath(path), dst = FullOutputPath(path); + + MkOutputTree(path); + CleanOutputs({path}); if (!CopyFile(src.c_str(), dst.c_str())) { ReportFailedInput(src); @@ -655,14 +661,18 @@ static void ProcessScene(const std::string &src, const std::string &dst) { void Scene(std::map &hashes, const std::string &path) { ProfilerPerfSection perf("Command/Scene"); - log(format(" Scene '%1'").arg(path)); + hg::log(format(" Scene '%1'").arg(path)); Data build_ctx; Write(build_ctx, std::string(get_version_string())); Write(build_ctx, GetSceneBinaryFormatVersion()); if (NeedsCompilation(hashes, {path}, {path}, build_ctx)) { - const auto src = FullInputPath(path), dst = ReadyFullOutputPath(path); + const auto src = FullInputPath(path), dst = FullOutputPath(path); + + MkOutputTree(path); + CleanOutputs({path}); + task_queue.emplace_back(task{path, [=]() { return std::async(std::launch::async, ProcessScene, src, dst); }}); } else { debug(" [O] Scene up to date"); @@ -834,7 +844,7 @@ static bool PreprocessTexture(const json &i_preprocess_texture, std::map &hashes, std::string path) { ProfilerPerfSection perf("Command/Texture"); - log(format(" Texture '%1'").arg(path)); + hg::log(format(" Texture '%1'").arg(path)); std::string in_path = path; // may be changed by a construct directive @@ -907,7 +917,7 @@ void Texture(std::map &hashes, std::string path) { if (type == "Copy") { Copy(hashes, in_path); } else if (type != "Ignore") { - const auto dst = ReadyFullOutputPath(path); + const auto dst = FullOutputPath(path); Data build_ctx; Write(build_ctx, max_size); @@ -921,9 +931,12 @@ void Texture(std::map &hashes, std::string path) { Write(build_ctx, "texconv"); if (toolchain.texturec.empty()) { - log(" Skipping, no compiler found for texture resource"); + hg::warn(" Skipping, no compiler found for texture resource"); } else { if (NeedsCompilation(hashes, {in_path}, {path}, build_ctx)) { + MkOutputTree(path); + CleanOutputs({path}); + std::string cmd; if (use_texconv) { // favor the much faster texconv over texturec @@ -955,16 +968,19 @@ void Texture(std::map &hashes, std::string path) { } if (generate_probe) { - const auto dst = ReadyFullOutputPath(path + ".radiance"); + const auto dst = FullOutputPath(path + ".radiance"); Data build_ctx; Write(build_ctx, max_probe_size); Write(build_ctx, radiance_edge_fixup); if (toolchain.cmft.empty()) { - log(" Skipping, no compiler found for radiance probe resource"); + hg::warn(" Skipping, no compiler found for radiance probe resource"); } else { if (NeedsCompilation(hashes, {in_path}, {path + ".radiance"}, build_ctx)) { + MkOutputTree(path + ".radiance"); + CleanOutputs({path + ".radiance"}); + const auto cmd = format("%1 --input \"%2\" --output0 \"%3\" --output0params dds,rgba16f,cubemap --useOpenCL false --filter radiance " "--srcFaceSize %4 --edgeFixup %5") .arg(toolchain.cmft) @@ -982,15 +998,18 @@ void Texture(std::map &hashes, std::string path) { } if (generate_probe) { - const auto dst = ReadyFullOutputPath(path + ".irradiance"); + const auto dst = FullOutputPath(path + ".irradiance"); Data build_ctx; Write(build_ctx, max_probe_size); if (toolchain.cmft.empty()) { - log(" Skipping, no compiler found for irradiance probe resource"); + hg::warn(" Skipping, no compiler found for irradiance probe resource"); } else { if (NeedsCompilation(hashes, {in_path}, {path + ".irradiance"}, build_ctx)) { + MkOutputTree(path + ".irradiance"); + CleanOutputs({path + ".irradiance"}); + const auto cmd = format("%1 --input \"%2\" --output0 \"%3\" --output0params dds,rgba16f,cubemap --useOpenCL false --filter irradiance --srcFaceSize %4") .arg(toolchain.cmft) @@ -1025,7 +1044,7 @@ static void ProcessGeometry(const std::string &src, const std::string &dst, Mode void Geometry(std::map &hashes, const std::string &path) { ProfilerPerfSection perf("Command/Geometry"); - log(format(" Geometry '%1'").arg(path)); + hg::log(format(" Geometry '%1'").arg(path)); const auto meta_db = LoadMeta(path); @@ -1041,7 +1060,11 @@ void Geometry(std::map &hashes, const std::string &path) { Write(build_ctx, optimisation_level); if (NeedsCompilation(hashes, {path}, {path}, build_ctx)) { - const auto src = FullInputPath(path), dst = ReadyFullOutputPath(path); + const auto src = FullInputPath(path), dst = FullOutputPath(path); + + MkOutputTree(path); + CleanOutputs({path}); + task_queue.emplace_back(task{path, [=]() { return std::async(std::launch::async, ProcessGeometry, src, dst, optimisation_level); }}); } else { debug(" [O] Geometry up to date"); @@ -1078,13 +1101,17 @@ static void BuildComputeShader(std::map &hashes, const std::s Write(cs_build_ctx, vs_defines); if (toolchain.shaderc.empty()) { - log(" Skipping, no compiler found for compute resource"); + hg::warn(" Skipping, no compiler found for compute resource"); } else { if (NeedsCompilation(hashes, {cs_path}, {cs_path}, cs_build_ctx)) { - if (!cs_profile.empty()) { // GLES profile must be empty... + if (!cs_profile.empty()) // GLES profile must be empty... cs_profile = "-p " + cs_profile; - } - const auto cs_out = ReadyFullOutputPath(cs_path); + + const auto cs_out = FullOutputPath(cs_path); + + MkOutputTree(cs_path); + CleanOutputs({cs_path}); + const auto cs_cmd = format("%1 -f \"%2\" -o \"%3\" --platform %4 %5 %6 --type compute --define \"%7\"") .arg(toolchain.shaderc) .arg(FullInputPath(cs_path)) @@ -1102,7 +1129,7 @@ static void BuildComputeShader(std::map &hashes, const std::s } static void ComputeShader(std::map &hashes, const std::string &cs_path) { - log(format(" Compute Shader '%1'").arg(cs_path)); + hg::log(format(" Compute Shader '%1'").arg(cs_path)); BuildComputeShader(hashes, cs_path, global_shader_defines); } @@ -1140,13 +1167,17 @@ static void BuildShader(std::map &hashes, const std::string & const auto vs_name = format("%1.vsb").arg(name); if (toolchain.shaderc.empty()) { - log(" Skipping, no compiler found for shader resource"); + hg::warn(" Skipping, no compiler found for shader resource"); } else { if (NeedsCompilation(hashes, {vs_path, varying_path}, {vs_name}, vs_build_ctx)) { - if (!vs_profile.empty()) { // GLES profile must be empty... + if (!vs_profile.empty()) // GLES profile must be empty... vs_profile = "-p " + vs_profile; - } - const auto vs_out = ReadyFullOutputPath(vs_name); + + const auto vs_out = FullOutputPath(vs_name); + + MkOutputTree(vs_name); + CleanOutputs({vs_name}); + const auto vs_cmd = format("%1 -f \"%2\" -o \"%3\" --varyingdef \"%4\" --type v --platform %5 %6 %7 --define \"%8\"") .arg(toolchain.shaderc) .arg(FullInputPath(vs_path)) @@ -1174,13 +1205,17 @@ static void BuildShader(std::map &hashes, const std::string & const auto fs_name = format("%1.fsb").arg(name); if (toolchain.shaderc.empty()) { - log(" Skipping, no compiler found for shader resource"); + hg::warn(" Skipping, no compiler found for shader resource"); } else { if (NeedsCompilation(hashes, {fs_path, varying_path}, {fs_name}, fs_build_ctx)) { - if (!fs_profile.empty()) { + if (!fs_profile.empty()) fs_profile = "-p " + fs_profile; - } - const auto fs_out = ReadyFullOutputPath(fs_name); + + const auto fs_out = FullOutputPath(fs_name); + + MkOutputTree(fs_name); + CleanOutputs({fs_name}); + const auto fs_cmd = format("%1 -f \"%2\" -o \"%3\" --varyingdef \"%4\" --type f --platform %5 %6 %7 --define \"%8\"") .arg(toolchain.shaderc) .arg(FullInputPath(fs_path)) @@ -1200,7 +1235,7 @@ static void BuildShader(std::map &hashes, const std::string & static void Shader(std::map &hashes, const std::string &vs_path, const std::string &fs_path, const std::string &varying_path) { const auto name = slice(vs_path, 0, -6); - log(format(" Shader '%1'").arg(name)); + hg::log(format(" Shader '%1'").arg(name)); BuildShader(hashes, name, vs_path, fs_path, varying_path, global_shader_defines); } @@ -1211,7 +1246,7 @@ static void BuildPipelineShaderVariant(std::map &hashes, cons size_t stage = 0; for (auto &variant : pipeline.configs) { - log(format(" Pipeline shader variant '%1' for pipeline config %2").arg(name).arg(stage)); + hg::log(format(" Pipeline shader variant '%1' for pipeline config %2").arg(name).arg(stage)); const auto variant_name = format("%1_pipe-%2-cfg-%3").arg(name).arg(pipeline.name).arg(stage++); const auto variant_defines = join(std::begin(variant), std::end(variant), ";") + ";" + defines; BuildShader(hashes, variant_name, vs_path, fs_path, varying_path, global_shader_defines + variant_defines); @@ -1281,7 +1316,7 @@ static void PipelineShader( std::map &hashes, const std::string &hps_path, const std::string &vs_path, const std::string &fs_path, const std::string &varying_path) { ProfilerPerfSection perf("Command/PipelineShader"); - log(format(" Pipeline shader '%1' for pipeline '%2'").arg(hps_path).arg(pipeline.name)); + hg::log(format(" Pipeline shader '%1' for pipeline '%2'").arg(hps_path).arg(pipeline.name)); Copy(hashes, hps_path); @@ -1305,13 +1340,17 @@ static void PipelineShader( static void LuaScript(std::map &hashes, const std::string &path) { ProfilerPerfSection perf("Command/LuaScript"); - log(format(" Lua script '%1'").arg(path)); + hg::log(format(" Lua script '%1'").arg(path)); if (toolchain.luac.empty()) { - log(" Skipping, no compiler found for Lua script resource"); + hg::warn(" Skipping, no compiler found for Lua script resource"); } else { if (NeedsCompilation(hashes, {path}, {path}, {})) { - const auto src = FullInputPath(path), dst = ReadyFullOutputPath(path); + const auto src = FullInputPath(path), dst = FullOutputPath(path); + + MkOutputTree(path); + CleanOutputs({path}); + const auto cmd = format("%1 -o %3 -s %2").arg(toolchain.luac).arg(src).arg(dst); PushAsyncProcessTask(path, cmd, cwd); } else { @@ -1324,7 +1363,7 @@ static void LuaScript(std::map &hashes, const std::string &pa static void Physics(std::map &hashes, const std::string &path) { ProfilerPerfSection perf("Command/Physics"); - log(format(" Physics resource '%1'").arg(path)); + hg::log(format(" Physics resource '%1'").arg(path)); // [todo] debug(" Skipping, no compiler found for physics resource"); } @@ -1332,16 +1371,19 @@ static void Physics(std::map &hashes, const std::string &path static void PathFinding(std::map &hashes, const std::string &path) { ProfilerPerfSection perf("Command/PathFinding"); - log(format(" Pathfinding resource '%1'").arg(path)); + hg::log(format(" Pathfinding resource '%1'").arg(path)); if (toolchain.recastc.empty()) { debug(" Skipping, no compiler found for pathfinding resource"); } else { if (NeedsCompilation(hashes, {path}, {path}, {})) { - const auto cwd = GetCurrentWorkingDirectory(); - const auto src = FullInputPath(path), dst = ReadyFullOutputPath(path); + const auto src = FullInputPath(path), dst = FullOutputPath(path); + + MkOutputTree(path); + CleanOutputs({path}); + const auto cmd = format("%1 %2 %3 -root %4").arg(toolchain.recastc).arg(src).arg(dst).arg(input_dir); - PushAsyncProcessTask(path, cmd, cwd); + PushAsyncProcessTask(path, cmd, GetCurrentWorkingDirectory()); } else { debug(" [O] Pathfinding resource up to date"); } @@ -1351,19 +1393,22 @@ static void PathFinding(std::map &hashes, const std::string & // enum class AssetType { Unprocessed, Ignore, Broken, Scene, Texture, Geometry, Lua, Shader, PipelineShader, Physics, PathFinding, ComputeShader, Count }; -static std::string AssetTypeToString(AssetType type) { - static std::string types[] = { - "Unprocessed", "Ignore", "Broken", "Scene", "Texture", "Geometry", "Lua", "Shader", "PipelineShader", "Physics", "PathFinding", "ComputeShader"}; +#if 0 +// CWE 561: The function 'AssetTypeToString' is never used. +static const std::string& AssetTypeToString(AssetType type) { + static const std::string types[static_cast(AssetType::Count) + 1] = { + "Unprocessed", "Ignore", "Broken", "Scene", "Texture", "Geometry", "Lua", "Shader", "PipelineShader", "Physics", "PathFinding", "ComputeShader", "Undefined"}; return types[int(type)]; } +#endif struct AssetConfig { AssetType type; }; static AssetType GetAssetFileType(std::string path, const std::vector &all_files, std::vector &out_files) { - static std::set texture_exts = {"bmp", "exr", "gif", "jpg", "hdr", "png", "psd", "tga"}; - static std::set ignored_exts = {"tmp"}; + static const std::set texture_exts = {"bmp", "exr", "gif", "jpg", "hdr", "png", "psd", "tga"}; + static const std::set ignored_exts = {"tmp"}; const auto ext = tolower(GetFileExtension(path)); @@ -1544,8 +1589,6 @@ static bool CompileClassifiedInputs(const std::map, Ass std::cout << "-> Progress: " << j * compile_progress_weight / input_count + classify_progress_weight << "% (" << names[0] << ")" << std::endl; ++j; - std::vector outputs; - if (config.type == AssetType::Unprocessed) Copy(updated_hashes, names[0]); else if (config.type == AssetType::Scene) @@ -1586,9 +1629,33 @@ static bool CompileClassifiedInputs(const std::map, Ass return true; } +// +static bool clean_outputs_for_removed_inputs = true; + +static void CleanOutputsForRemovedInputs() { + ProfilerPerfSection perf("Manage/CleanOutputsForRemovedInputs"); + + std::map> db_input_to_outputs; + for (const auto &i : compilation_db.output_to_inputs) + for (const auto &input : i.second) + db_input_to_outputs[input].insert(i.first); + + const auto input_files = GetInputDirFiles(); // available inputs + + size_t removed = 0; + for (const auto &i : db_input_to_outputs) + if (std::find(std::begin(input_files), std::end(input_files), i.first) == std::end(input_files)) { + for (const auto &output : i.second) // DB input is missing from input files, remove its associated outputs + if (hg::Unlink(FullOutputPath(output).c_str())) + ++removed; + } + + std::cout << "Removed " << removed << " outputs due to missing input" << std::endl; +} + // static void DaemonMode() { - log("Entering daemon mode, press Ctrl+C to close\n"); + hg::log("Entering daemon mode, press Ctrl+C to close\n"); WatchDirectory(input_dir, true); @@ -1612,13 +1679,16 @@ static void DaemonMode() { } if (!modified_files_.empty()) { - log(format("File system changes detected (%1):").arg(modified_files_.size())); + hg::log(format("File system changes detected (%1):").arg(modified_files_.size())); for (const auto &f : modified_files_) - log((std::string(" - ") + f).c_str()); + hg::log((std::string(" - ") + f).c_str()); + + if (assetc::clean_outputs_for_removed_inputs) + assetc::CleanOutputsForRemovedInputs(); if (!CompileClassifiedInputs(ClassifyInputs(GetInputDirFiles(), {std::begin(modified_files_), std::end(modified_files_)}))) break; - log("Press Ctrl+C to close\n"); + hg::log("Press Ctrl+C to close\n"); } } @@ -1629,16 +1699,16 @@ static void DaemonMode() { static void OutputPerfReport() { EndProfilerFrame(); - const auto profile = GetLastFrameProfile(); + const auto last_profile = GetLastFrameProfile(); time_ns total = 0; - for (auto &task : profile.tasks) + for (auto &task : last_profile.tasks) if (task.name == "Total") total = task.duration; if (total) { std::cout << std::endl << "Performance report" << std::endl; - for (auto &task : profile.tasks) + for (auto &task : last_profile.tasks) std::cout << " - " << task.name << ": " << "(" << (task.duration * 100 / total) << "%) " << FormatTime(task.duration) << " (" << task.section_indexes.size() << " call)" << std::endl; @@ -1673,6 +1743,7 @@ int main(int narg, const char **args) { {"-quiet", "Disable all build information but errors"}, {"-verbose", "Output additional information about the compilation process"}, {"-fast_check", "Perform modification detection using input file timestamp"}, + {"-no_clean_removed_inputs", "Do not remove outputs for removed input files"}, }, { {"-job", "Maximum number of parallel job (0 - automatic)", true}, @@ -1696,6 +1767,7 @@ int main(int narg, const char **args) { {"-v", "-verbose"}, {"-D", "-defines"}, {"-f", "-fast_check"}, + {"-n", "-no_clean_removed_inputs"}, }, }; @@ -1737,6 +1809,7 @@ int main(int narg, const char **args) { assetc::fast_check = GetCmdLineFlagValue(cmd_content, "-fast_check"); assetc::poll_process_id = GetCmdLineSingleValue(cmd_content, "-poll_pid", 0); + assetc::clean_outputs_for_removed_inputs = !GetCmdLineFlagValue(cmd_content, "-no_clean_removed_inputs"); assetc::SetRenderPipeline(GetForwardPipelineInfo()); @@ -1774,27 +1847,31 @@ int main(int narg, const char **args) { // auto exe_path_or_nothing = [](const std::string &path) { return path.empty() ? "-" : path; }; - log(format("> Input dir: %1").arg(assetc::input_dir)); - log(format("> Output dir: %1").arg(assetc::output_dir)); - log(""); - log(format("> Target platform: %1").arg(assetc::platform)); - log(format("> Target graphics API: %1").arg(assetc::api)); - log(format("> Target pipeline: %1").arg(assetc::pipeline.name)); - log(""); - log(format("> Using %1 parallel job").arg(assetc::max_async_jobs)); - log(format("> Toolchain compilers (%1):").arg(toolchain_path)); - log(format(" - Shader %1").arg(exe_path_or_nothing(assetc::toolchain.shaderc))); - log(format(" - Texture %1").arg(exe_path_or_nothing(assetc::toolchain.texturec))); - log(format(" - Probe %1").arg(exe_path_or_nothing(assetc::toolchain.cmft))); - log(format(" - Lua %1").arg(exe_path_or_nothing(assetc::toolchain.luac))); - log(format(" - Pathfinding %1").arg(exe_path_or_nothing(assetc::toolchain.recastc))); - log(""); + hg::log(format("> Input dir: %1").arg(assetc::input_dir)); + hg::log(format("> Output dir: %1").arg(assetc::output_dir)); + hg::log(""); + hg::log(format("> Target platform: %1").arg(assetc::platform)); + hg::log(format("> Target graphics API: %1").arg(assetc::api)); + hg::log(format("> Target pipeline: %1").arg(assetc::pipeline.name)); + hg::log(""); + hg::log(format("> Using %1 parallel job").arg(assetc::max_async_jobs)); + hg::log(format("> Toolchain compilers (%1):").arg(toolchain_path)); + hg::log(format(" - Shader %1").arg(exe_path_or_nothing(assetc::toolchain.shaderc))); + hg::log(format(" - Texture %1").arg(exe_path_or_nothing(assetc::toolchain.texturec))); + hg::log(format(" - Probe %1").arg(exe_path_or_nothing(assetc::toolchain.cmft))); + hg::log(format(" - Lua %1").arg(exe_path_or_nothing(assetc::toolchain.luac))); + hg::log(format(" - Pathfinding %1").arg(exe_path_or_nothing(assetc::toolchain.recastc))); + hg::log(""); // initial run over input directory (process all files) { ProfilerPerfSection perf("Total"); assetc::LoadCompilationDB(); + + if (assetc::clean_outputs_for_removed_inputs) + assetc::CleanOutputsForRemovedInputs(); + if (assetc::CompileClassifiedInputs(assetc::ClassifyInputDir())) { // enter daemon mode, process input dir files as they change if (GetCmdLineFlagValue(cmd_content, "-daemon")) diff --git a/tools/assimp_converter/assimp_converter.cpp b/tools/assimp_converter/assimp_converter.cpp index 5155b34..9bbb925 100644 --- a/tools/assimp_converter/assimp_converter.cpp +++ b/tools/assimp_converter/assimp_converter.cpp @@ -64,6 +64,7 @@ struct Config { bool calculate_normal_if_missing{false}, calculate_tangent_if_missing{false}; //bool detect_geometry_instances{false}; //bool anim_to_file{false}; + bool merge_meshes{false}; std::string finalizer_script; }; @@ -191,7 +192,7 @@ static std::string MakeRelativeResourceName(const std::string &name, const std:: } static void ExportMotions(const aiScene *ai_scene, hg::Scene &scene, ExportMap &export_map, const Config &config, hg::PipelineResources &resources) { - if (!config.import_animation) + if (!config.import_animation || config.merge_meshes) return; @@ -1456,20 +1457,22 @@ static const aiScene* LoadAssimpScene(Assimp::Importer* importer, const Config & importer->SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_LINE | aiPrimitiveType_POINT); importer->SetPropertyBool(AI_CONFIG_IMPORT_COLLADA_IGNORE_UP_DIRECTION, true); - importer->SetPropertyBool(AI_CONFIG_PP_PTV_KEEP_HIERARCHY, true); + + if (!config.merge_meshes) + importer->SetPropertyBool(AI_CONFIG_PP_PTV_KEEP_HIERARCHY, true); // importer->SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // see https://gamedev.stackexchange.com/questions/175044/assimp-skeletal-animation-with-some-fbx-files-has-issues-weird-node-added importer->SetPropertyFloat(AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY, config.scene_scale); auto scene = importer->ReadFile(hg::ansi_to_utf8(ai_path), - // aiProcess_OptimizeGraph | // this removes almost all nodes, which we may want to keep in some cases aiProcess_PopulateArmatureData | aiProcess_ConvertToLeftHanded | aiProcess_CalcTangentSpace | aiProcess_GlobalScale | aiProcess_FindInstances | aiProcess_OptimizeMeshes | aiProcess_JoinIdenticalVertices | - aiProcess_ImproveCacheLocality | aiProcess_SortByPType + aiProcess_ImproveCacheLocality | aiProcess_SortByPType | + (config.merge_meshes ? aiProcess_PreTransformVertices : 0) // aiProcess_Triangulate ); @@ -1546,6 +1549,10 @@ static bool ImportAssimpScene(const std::string &path, const Config &config) { export_map.all_nodes = ListAllNodes(ai_scene); } + if (config.merge_meshes) { + ai_scene = importer.ApplyPostProcessing(aiProcess_OptimizeGraph); // can't combine it with aiProcess_PreTransformVertices so we use a 2nd pass + } + // FBX/gltf seem to be in ms. // see https://github.com/assimp/assimp/issues/3462 export_map.fps_workaround = hg::ends_with(path, ".fbx", hg::insensitive) || hg::ends_with(path, ".gltf", hg::insensitive) || hg::ends_with(path, ".glb", hg::insensitive); @@ -1620,7 +1627,7 @@ int main(int argc, const char **argv) { nullptr); hg::set_log_level(hg::LL_All); - std::cout << hg::format("FBX->GS Converter %1 (%2)").arg(hg::get_version_string()).arg(hg::get_build_sha()).str() << std::endl; + std::cout << hg::format("Assimp Converter %1 (%2)").arg(hg::get_version_string()).arg(hg::get_build_sha()).str() << std::endl; hg::CmdLineFormat cmd_format = { { @@ -1631,6 +1638,7 @@ int main(int argc, const char **argv) { //{"-detect-geometry-instances", "Detect and optimize geometry instances"}, {"-import-animation", "Detect and optimize geometry instances"}, //{"-anim-to-file", "Scene animations will be exported to separate files and not embedded in scene", true}, // not supported for now + {"-merge-meshes", "Merge meshes if possible"}, {"-quiet", "Quiet log, only log errors"}, }, { @@ -1699,8 +1707,8 @@ int main(int argc, const char **argv) { config.max_smoothing_angle = hg::GetCmdLineSingleValue(cmd_content, "-max-smoothing-angle", 0.7f); - config.calculate_tangent_if_missing = hg::GetCmdLineFlagValue(cmd_content, "-calculate-tangent-if-missing"); //config.detect_geometry_instances = hg::GetCmdLineFlagValue(cmd_content, "-detect-geometry-instances"); + config.merge_meshes = hg::GetCmdLineFlagValue(cmd_content, "-merge-meshes"); config.finalizer_script = hg::GetCmdLineSingleValue(cmd_content, "-finalizer-script", ""); diff --git a/tools/gltf_converter/CMakeLists.txt b/tools/gltf_converter/CMakeLists.txt index 8adf4e4..86c27f7 100644 --- a/tools/gltf_converter/CMakeLists.txt +++ b/tools/gltf_converter/CMakeLists.txt @@ -1,38 +1,38 @@ -file(WRITE ${CMAKE_BINARY_DIR}/tiny_gltf.cpp "#define TINYGLTF_IMPLEMENTATION\n#include \"${CMAKE_CURRENT_SOURCE_DIR}/tiny_gltf.h\"") -add_library(tiny_gltf OBJECT ${CMAKE_BINARY_DIR}/tiny_gltf.cpp tiny_gltf.h) -target_include_directories(tiny_gltf - PUBLIC - $ - $ -) -set_target_properties(tiny_gltf PROPERTIES FOLDER "harfang/3rdparty") - -function(gltf_build_tool name) - add_executable(${name} ${name}.cpp) - target_include_directories(${name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../extern/lua/src) - target_link_libraries(${name} PUBLIC engine tiny_gltf) - set_target_properties(${name} PROPERTIES FOLDER "harfang/tools") - - add_dependencies(${name} bind_hg_lua) - - install(TARGETS ${name} libluadll RUNTIME DESTINATION ${name} LIBRARY DESTINATION ${name} COMPONENT ${name}) - - install(FILES $ DESTINATION ${name} COMPONENT ${name}) - - if(HG_ENABLE_OPENVR_API) - install(FILES ${OPENVR_DLL} DESTINATION ${name} COMPONENT ${name}) - endif() - - if(HG_ENABLE_SRANIPAL_API) - install(FILES ${SRANIPAL_DLL} DESTINATION ${name} COMPONENT ${name}) - endif() -endfunction() - - -if(HG_BUILD_GLTF_EXPORTER) - gltf_build_tool(gltf_exporter) -endif() - -if(HG_BUILD_GLTF_IMPORTER) - gltf_build_tool(gltf_importer) -endif() +file(WRITE ${CMAKE_BINARY_DIR}/tiny_gltf.cpp "#define TINYGLTF_IMPLEMENTATION\n#include \"${CMAKE_CURRENT_SOURCE_DIR}/tiny_gltf.h\"") +add_library(tiny_gltf OBJECT ${CMAKE_BINARY_DIR}/tiny_gltf.cpp tiny_gltf.h) +target_include_directories(tiny_gltf + PUBLIC + $ + $ +) +set_target_properties(tiny_gltf PROPERTIES FOLDER "harfang/3rdparty") + +function(gltf_build_tool name) + add_executable(${name} ${name}.cpp) + target_include_directories(${name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../extern/lua/src) + target_link_libraries(${name} PUBLIC engine tiny_gltf) + set_target_properties(${name} PROPERTIES FOLDER "harfang/tools") + + add_dependencies(${name} bind_hg_lua) + + install(TARGETS ${name} libluadll RUNTIME DESTINATION ${name} LIBRARY DESTINATION ${name} COMPONENT ${name}) + + install(FILES $ DESTINATION ${name} COMPONENT ${name}) + + if(HG_ENABLE_OPENVR_API) + install(FILES ${OPENVR_DLL} DESTINATION ${name} COMPONENT ${name}) + endif() + + if(HG_ENABLE_SRANIPAL_API) + install(FILES ${SRANIPAL_DLL} DESTINATION ${name} COMPONENT ${name}) + endif() +endfunction() + + +if(HG_BUILD_GLTF_EXPORTER) + gltf_build_tool(gltf_exporter) +endif() + +if(HG_BUILD_GLTF_IMPORTER) + gltf_build_tool(gltf_importer) +endif() diff --git a/tools/gltf_converter/gltf_exporter.cpp b/tools/gltf_converter/gltf_exporter.cpp index 0496afe..3cc9e37 100644 --- a/tools/gltf_converter/gltf_exporter.cpp +++ b/tools/gltf_converter/gltf_exporter.cpp @@ -57,6 +57,8 @@ static inline const hg::Material::Value *GetMaterialValue(const hg::Material &ma std::map NodeRef_to_IdNode; +#if 0 +// CWE 561: The function 'Indent' is never used. static std::string Indent(const int indent) { std::string s; for (int i = 0; i < indent; i++) { @@ -64,6 +66,7 @@ static std::string Indent(const int indent) { } return s; } +#endif /// Adapts an array of bytes to an array of T. Will advace of byte_stride each /// elements. @@ -109,7 +112,7 @@ struct floatArrayBase { template struct intArray : public intArrayBase { arrayAdapter adapter; - intArray(const arrayAdapter &a) : adapter(a) {} + explicit intArray(const arrayAdapter &a) : adapter(a) {} unsigned int operator[](size_t position) const override { return static_cast(adapter[position]); } size_t size() const override { return adapter.elemCount; } @@ -118,7 +121,7 @@ template struct intArray : public intArrayBase { template struct floatArray : public floatArrayBase { arrayAdapter adapter; - floatArray(const arrayAdapter &a) : adapter(a) {} + explicit floatArray(const arrayAdapter &a) : adapter(a) {} float operator[](size_t position) const override { return static_cast(adapter[position]); } size_t size() const override { return adapter.elemCount; } @@ -126,7 +129,7 @@ template struct floatArray : public floatArrayBase { struct v2fArray { arrayAdapter adapter; - v2fArray(const arrayAdapter &a) : adapter(a) {} + explicit v2fArray(const arrayAdapter &a) : adapter(a) {} hg::Vec2 operator[](size_t position) const { return adapter[position]; } size_t size() const { return adapter.elemCount; } @@ -134,7 +137,7 @@ struct v2fArray { struct v3fArray { arrayAdapter adapter; - v3fArray(const arrayAdapter &a) : adapter(a) {} + explicit v3fArray(const arrayAdapter &a) : adapter(a) {} hg::Vec3 operator[](size_t position) const { return adapter[position]; } size_t size() const { return adapter.elemCount; } @@ -142,7 +145,7 @@ struct v3fArray { struct v4fArray { arrayAdapter adapter; - v4fArray(const arrayAdapter &a) : adapter(a) {} + explicit v4fArray(const arrayAdapter &a) : adapter(a) {} hg::Vec4 operator[](size_t position) const { return adapter[position]; } size_t size() const { return adapter.elemCount; } @@ -167,7 +170,8 @@ static bool GetOutputPath(std::string &path, const std::string &base, const std: return true; } -// +#if 0 +// CWE 561: The function 'MakeRelativeResourceName' is never used. static std::string MakeRelativeResourceName(const std::string &name, const std::string &base_path, const std::string &prefix) { if (hg::starts_with(name, base_path, hg::case_sensitivity::insensitive)) { const auto stripped_name = hg::lstrip(hg::slice(name, base_path.length()), "/"); @@ -175,6 +179,7 @@ static std::string MakeRelativeResourceName(const std::string &name, const std:: } return name; } +#endif #define for_with_index(V, ...) \ for (size_t V = 0, _brk_ = 0, _i_ = 1; _i_; _i_ = 0) \ @@ -869,6 +874,7 @@ static void ExportGeometry(Model &model, Mesh &mesh, const int &nb_materials, co } std::map> geo_to_primitives; +static const size_t EmptyMeshID = static_cast(-1); static const size_t ExportObject(Model &model, const hg::Object &object, const Config &config, hg::PipelineResources &resources) { Mesh mesh; @@ -890,12 +896,12 @@ static const size_t ExportObject(Model &model, const hg::Object &object, const C ExportGeometry(model, mesh, object.GetMaterialCount(), geo, config); geo_to_primitives[geo_name] = mesh.primitives; } else - return -1; // no geo, no mesh + return EmptyMeshID; // no geo, no mesh } else { mesh.primitives = geo_to_primitives[geo_name]; } } else - return -1; // no geo, no mesh + return EmptyMeshID; // no geo, no mesh // export materials for (int i = 0; i < object.GetMaterialCount(); ++i) { @@ -950,7 +956,7 @@ static const size_t ExportNode(Model &model, const hg::NodeRef nodeRef, const st // if (node.HasObject()) { auto id_mesh = ExportObject(model, node.GetObject(), config, resources); - if (id_mesh != -1) + if (id_mesh != EmptyMeshID) n.mesh = id_mesh; } @@ -1022,20 +1028,19 @@ static bool ExportGltfScene(const std::string &path, const Config &config) { model.scenes.push_back(scn); std::string out_path; - if (config.binary) { - if (GetOutputPath(out_path, config.base_output_path, config.name.empty() ? hg::GetFileName(path) : config.name, {}, "glb")) { - bool embedImages = true; - bool embedBuffers = true; - bool prettyPrint = true; - bool writeBinary = true; - bool ret = saver.WriteGltfSceneToFile(&model, out_path, embedImages, embedBuffers, prettyPrint, writeBinary); - } - } else if (GetOutputPath(out_path, config.base_output_path, config.name.empty() ? hg::GetFileName(path) : config.name, {}, "gltf")) { - bool embedImages = true; - bool embedBuffers = true; - bool prettyPrint = true; - bool writeBinary = false; - bool ret = saver.WriteGltfSceneToFile(&model, out_path, embedImages, embedBuffers, prettyPrint, writeBinary); + bool embedImages = true; + bool embedBuffers = true; + bool prettyPrint = true; + bool writeBinary = config.binary; + if (!GetOutputPath(out_path, config.base_output_path, config.name.empty() ? hg::GetFileName(path) : config.name, {}, writeBinary ? "glb" : "gltf")) { + hg::error("failed to compute output path"); + return false; + } + + bool ret = saver.WriteGltfSceneToFile(&model, out_path, embedImages, embedBuffers, prettyPrint, writeBinary); + if(!ret) { + hg::error(hg::format("failed to write scene to %1").arg(out_path.c_str())); + return false; } hg::log(hg::format("Export complete, took %1 ms").arg(hg::time_to_ms(hg::time_now() - t_start))); diff --git a/tools/gltf_converter/gltf_importer.cpp b/tools/gltf_converter/gltf_importer.cpp index f7527f0..050de52 100644 --- a/tools/gltf_converter/gltf_importer.cpp +++ b/tools/gltf_converter/gltf_importer.cpp @@ -20,16 +20,16 @@ #include #include #include +#include #include #include #include -#include -#include "tiny_gltf.h" #include "json.hpp" +#include "tiny_gltf.h" -#include #include +#include #include #undef CopyFile @@ -83,7 +83,6 @@ template struct arrayAdapter { } }; -/// Interface of any adapted array that returns integer data /// Interface of any adapted array that returns byte data struct byteArrayBase { virtual ~byteArrayBase() = default; @@ -109,7 +108,7 @@ struct floatArrayBase { template struct byteArray : public byteArrayBase { arrayAdapter adapter; - byteArray(const arrayAdapter &a) : adapter(a) {} + explicit byteArray(const arrayAdapter &a) : adapter(a) {} unsigned char operator[](size_t position) const override { return static_cast(adapter[position]); } size_t size() const override { return adapter.elemCount; } @@ -119,7 +118,7 @@ template struct byteArray : public byteArrayBase { template struct intArray : public intArrayBase { arrayAdapter adapter; - intArray(const arrayAdapter &a) : adapter(a) {} + explicit intArray(const arrayAdapter &a) : adapter(a) {} unsigned int operator[](size_t position) const override { return static_cast(adapter[position]); } size_t size() const override { return adapter.elemCount; } @@ -128,7 +127,7 @@ template struct intArray : public intArrayBase { template struct floatArray : public floatArrayBase { arrayAdapter adapter; - floatArray(const arrayAdapter &a) : adapter(a) {} + explicit floatArray(const arrayAdapter &a) : adapter(a) {} float operator[](size_t position) const override { return static_cast(adapter[position]); } size_t size() const override { return adapter.elemCount; } @@ -136,7 +135,7 @@ template struct floatArray : public floatArrayBase { struct v2fArray { arrayAdapter adapter; - v2fArray(const arrayAdapter &a) : adapter(a) {} + explicit v2fArray(const arrayAdapter &a) : adapter(a) {} hg::Vec2 operator[](size_t position) const { return adapter[position]; } size_t size() const { return adapter.elemCount; } @@ -144,7 +143,7 @@ struct v2fArray { struct v3fArray { arrayAdapter adapter; - v3fArray(const arrayAdapter &a) : adapter(a) {} + explicit v3fArray(const arrayAdapter &a) : adapter(a) {} hg::Vec3 operator[](size_t position) const { return adapter[position]; } size_t size() const { return adapter.elemCount; } @@ -152,7 +151,7 @@ struct v3fArray { struct v4fArray { arrayAdapter adapter; - v4fArray(const arrayAdapter &a) : adapter(a) {} + explicit v4fArray(const arrayAdapter &a) : adapter(a) {} hg::Vec4 operator[](size_t position) const { return adapter[position]; } size_t size() const { return adapter.elemCount; } @@ -160,7 +159,7 @@ struct v4fArray { struct m44fArray { arrayAdapter adapter; - m44fArray(const arrayAdapter &a) : adapter(a) {} + explicit m44fArray(const arrayAdapter &a) : adapter(a) {} hg::Mat44 operator[](size_t position) const { return adapter[position]; } size_t size() const { return adapter.elemCount; } @@ -609,21 +608,26 @@ static hg::TextureRef ExportTexture(const Model &model, const int &textureIndex, // static hg::Material ExportMaterial(const Model &model, const Material &gltf_mat, const Config &config, hg::PipelineResources &resources) { +#if 0 + // CWE 563: Variable is assigned a value that is never used hg::Color diffuse = {0.5f, 0.5f, 0.5f, 1.f}, emissive = {0, 0, 0, 1}, specular = {0.5f, 0.5f, 0.5f, 1.f}, ambient = {0, 0, 0, 1}; +#endif float glossiness{1.f}; float reflection{1.f}; hg::debug(hg::format("Exporting material '%1'").arg(gltf_mat.name)); - - std::string meta_RAW_text("{\"profiles\": {\"default\": {\"compression\": \"RAW\"}}}"); - std::string meta_BC1_text("{\"profiles\": {\"default\": {\"compression\": \"BC1\"}}}"); - std::string meta_BC3_text("{\"profiles\": {\"default\": {\"compression\": \"BC3\"}}}"); - std::string meta_BC4_text("{\"profiles\": {\"default\": {\"compression\": \"BC4\"}}}"); - std::string meta_BC5_text("{\"profiles\": {\"default\": {\"compression\": \"BC5\"}}}"); - std::string meta_BC6_text("{\"profiles\": {\"default\": {\"compression\": \"BC6\"}}}"); - std::string meta_BC7_text("{\"profiles\": {\"default\": {\"compression\": \"BC7\"}}}"); - std::string meta_BC7_srgb_text("{\"profiles\": {\"default\": {\"compression\": \"BC7\", \"srgb\": 1}}}"); +#if 0 + // CWE 563: Variable is assigned a value that is never used + static const std::string meta_RAW_text("{\"profiles\": {\"default\": {\"compression\": \"RAW\"}}}"); + static const std::string meta_BC1_text("{\"profiles\": {\"default\": {\"compression\": \"BC1\"}}}"); + static const std::string meta_BC3_text("{\"profiles\": {\"default\": {\"compression\": \"BC3\"}}}"); + static const std::string meta_BC4_text("{\"profiles\": {\"default\": {\"compression\": \"BC4\"}}}"); + static const std::string meta_BC6_text("{\"profiles\": {\"default\": {\"compression\": \"BC6\"}}}"); + static const std::string meta_BC7_text("{\"profiles\": {\"default\": {\"compression\": \"BC7\"}}}"); +#endif + static const std::string meta_BC5_text("{\"profiles\": {\"default\": {\"compression\": \"BC5\"}}}"); + static const std::string meta_BC7_srgb_text("{\"profiles\": {\"default\": {\"compression\": \"BC7\", \"srgb\": 1}}}"); // std::string dst_path; @@ -706,7 +710,6 @@ static hg::Material ExportMaterial(const Model &model, const Material &gltf_mat, if (metallicRoughnessTexture != hg::InvalidTextureRef) { hg::debug(hg::format(" - uOcclusionRoughnessMetalnessMap: %1").arg(resources.textures.GetName(metallicRoughnessTexture))); - std::string dst_path; if (GetOutputPath(dst_path, config.prj_path, resources.textures.GetName(metallicRoughnessTexture), {}, "meta", config.import_policy_texture)) { if (std::FILE *f = std::fopen(dst_path.c_str(), "w")) { std::fwrite(meta_occlusionTexture.data(), sizeof meta_occlusionTexture[0], meta_occlusionTexture.size(), f); @@ -721,8 +724,7 @@ static hg::Material ExportMaterial(const Model &model, const Material &gltf_mat, if (normalTexture != hg::InvalidTextureRef) { hg::debug(hg::format(" - uNormalMap: %1").arg(resources.textures.GetName(normalTexture))); - if (GetOutputPath( - dst_path, config.prj_path, resources.textures.GetName(normalTexture), {}, "meta", config.import_policy_texture)) { + if (GetOutputPath(dst_path, config.prj_path, resources.textures.GetName(normalTexture), {}, "meta", config.import_policy_texture)) { if (std::FILE *f = std::fopen(dst_path.c_str(), "w")) { std::fwrite(meta_BC5_text.data(), sizeof meta_BC5_text[0], meta_BC5_text.size(), f); std::fclose(f); @@ -829,13 +831,13 @@ static void ExportGeometry(const Model &model, const Primitive &meshPrimitive, c break; } } - const auto &indices = *indicesArrayPtr; size_t start_id_pol = geo.pol.size(); size_t start_id_binding = geo.binding.size(); size_t start_id_vtx = geo.vtx.size(); if (indicesArrayPtr) { + const auto &indices = *indicesArrayPtr; // hg::debug("indices: "); // they are all triangles geo.pol.resize(start_id_pol + indices.size() / 3); @@ -914,39 +916,42 @@ static void ExportGeometry(const Model &model, const Primitive &meshPrimitive, c if (attribute.first == "POSITION") { writter = &w_position; max_components = 3; - } - else if (attribute.first == "NORMAL") { + } else if (attribute.first == "NORMAL") { writter = &w_normal; max_components = 3; - } - else if (attribute.first == "TEXCOORD_0") { + } else if (attribute.first == "TEXCOORD_0") { writter = &w_texcoord0; max_components = 2; - } - else if (attribute.first == "TEXCOORD_1") { + } else if (attribute.first == "TEXCOORD_1") { writter = &w_texcoord1; max_components = 2; - } - else if (attribute.first == "TANGENT") { + } else if (attribute.first == "TANGENT") { writter = &w_tangent; max_components = 4; - } - else if (attribute.first == "JOINTS_0") { + } else if (attribute.first == "JOINTS_0") { writter = &w_joints0; max_components = 4; - } - else if (attribute.first == "WEIGHTS_0") { + } else if (attribute.first == "WEIGHTS_0") { writter = &w_weights0; max_components = 4; } - if (!writter) continue; + if (!writter) + continue; switch (attribAccessor.type) { - case TINYGLTF_TYPE_SCALAR: max_components = std::min(max_components, 1u); break; - case TINYGLTF_TYPE_VEC2: max_components = std::min(max_components, 2u); break; - case TINYGLTF_TYPE_VEC3: max_components = std::min(max_components, 3u); break; - case TINYGLTF_TYPE_VEC4: max_components = std::min(max_components, 4u); break; + case TINYGLTF_TYPE_SCALAR: + max_components = std::min(max_components, 1u); + break; + case TINYGLTF_TYPE_VEC2: + max_components = std::min(max_components, 2u); + break; + case TINYGLTF_TYPE_VEC3: + max_components = std::min(max_components, 3u); + break; + case TINYGLTF_TYPE_VEC4: + max_components = std::min(max_components, 4u); + break; } switch (attribAccessor.componentType) { @@ -1203,6 +1208,7 @@ static void ExportObject(const Model &model, const Node &gltf_node, hg::Node &no path += hg::format("%1").arg( already_saved_geo_itr->second.ids.size()).str(); already_saved_geo_itr->second.ids.push_back(ids); + already_saved_geo_with_primitives_ids[path] = {object, {ids}}; } else { int index = it - already_saved_geo_itr->second.ids.begin(); if (index) @@ -1241,8 +1247,7 @@ static void ExportObject(const Model &model, const Node &gltf_node, hg::Node &no if (recalculate_normal) { hg::debug(" - Recalculate normals"); geo.normal = vtx_normal; - } - else + } else vtx_normal = geo.normal; // recalculate tangent frame @@ -1281,8 +1286,9 @@ static void ExportObject(const Model &model, const Node &gltf_node, hg::Node &no floatArray value(arrayAdapter(dataPtr, count*16, sizeof(float))); for (size_t k{ 0 }; k < count; ++k) { - hg::Mat4 m_InverseBindMatrices(value[k*16], value[k*16 + 1], value[k*16 + 2], value[k*16 + 4], value[k*16 + 5], value[k*16 + 6], - value[k*16 + 8], value[k*16 + 9], value[k*16 + 10], value[k*16 + 12], value[k*16 + 13], value[k*16 + 14]); + hg::Mat4 m_InverseBindMatrices(value[k * 16], value[k * 16 + 1], value[k * 16 + 2], value[k * 16 + 4], value[k * 16 + 5], + value[k * 16 + 6], value[k * 16 + 8], value[k * 16 + 9], value[k * 16 + 10], value[k * 16 + 12], value[k * 16 + 13], + value[k * 16 + 14]); m_InverseBindMatrices = hg::InverseFast(m_InverseBindMatrices); @@ -1409,8 +1415,8 @@ static hg::Node ExportNode(const Model &model, const int &gltf_id_node, hg::Scen return node; } -bool LoadImageDataEx(Image *image, const int image_idx, std::string *err, std::string *warn, int req_width, int req_height, const unsigned char *bytes, int size, - void *user_data) { +bool LoadImageDataEx(Image *image, const int image_idx, std::string *err, std::string *warn, int req_width, int req_height, const unsigned char *bytes, + int size, void *user_data) { (void)user_data; (void)warn; @@ -1458,6 +1464,9 @@ bool LoadImageDataEx(Image *image, const int image_idx, std::string *err, std::s static bool ImportGltfScene(const std::string &path, const Config &config) { const auto t_start = hg::time_now(); + if (config.base_output_path.empty()) + return false; + // create output directory if missing if (hg::Exists(config.base_output_path.c_str())) { if (!hg::IsDir(config.base_output_path.c_str())) @@ -1474,13 +1483,20 @@ static bool ImportGltfScene(const std::string &path, const Config &config) { std::string warn; // set our own save picture - loader.SetImageLoader(LoadImageDataEx, (void*)&config); - + loader.SetImageLoader(LoadImageDataEx, const_cast(&config)); bool ret; if (hg::tolower(hg::GetFileExtension(path)) == "gltf") ret = loader.LoadASCIIFromFile(&model, &err, &warn, path); else ret = loader.LoadBinaryFromFile(&model, &err, &warn, path); // for binary glTF(.glb) + if (!ret) { + hg::error(hg::format("failed to load %1: %2").arg(path.c_str()).arg(err.c_str())); + return false; + } + + if (!warn.empty()) { + hg::log(hg::format("warning %1: %2").arg(path.c_str()).arg(warn.c_str())); + } hg::log("loaded glTF file has:"); hg::log(hg::format("%1 accessors").arg(model.accessors.size()).c_str()); @@ -1498,9 +1514,6 @@ static bool ImportGltfScene(const std::string &path, const Config &config) { hg::log(hg::format("%1 scenes").arg(model.scenes.size()).c_str()); hg::log(hg::format("%1 lights").arg(model.lights.size()).c_str()); - if (config.base_output_path.empty()) - return false; - if (!config.finalizer_script.empty()) if (!LoadFinalizerScript(config.finalizer_script)) return false; @@ -1669,5 +1682,5 @@ int main(int argc, const char **argv) { const auto msg = std::string("[ImportScene") + std::string(res ? ": OK]" : ": KO]"); hg::log(msg.c_str()); - return res ? 0 : 1; + return res ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/tools/gltf_converter/tiny_gltf.h b/tools/gltf_converter/tiny_gltf.h index 3acf359..9b581aa 100644 --- a/tools/gltf_converter/tiny_gltf.h +++ b/tools/gltf_converter/tiny_gltf.h @@ -1,7639 +1,7639 @@ -// -// Header-only tiny glTF 2.0 loader and serializer. -// -// -// The MIT License (MIT) -// -// Copyright (c) 2015 - 2020 Syoyo Fujita, Aurélien Chatelain and many -// contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// Version: -// - v2.4.2 Decode percent-encoded URI. -// - v2.4.1 Fix some glTF object class does not have `extensions` and/or -// `extras` property. -// - v2.4.0 Experimental RapidJSON and C++14 support(Thanks to @jrkoone). -// - v2.3.1 Set default value of minFilter and magFilter in Sampler to -1. -// - v2.3.0 Modified Material representation according to glTF 2.0 schema -// (and introduced TextureInfo class) -// Change the behavior of `Value::IsNumber`. It return true either the -// value is int or real. -// - v2.2.0 Add loading 16bit PNG support. Add Sparse accessor support(Thanks -// to @Ybalrid) -// - v2.1.0 Add draco compression. -// - v2.0.1 Add comparsion feature(Thanks to @Selmar). -// - v2.0.0 glTF 2.0!. -// -// Tiny glTF loader is using following third party libraries: -// -// - jsonhpp: C++ JSON library. -// - base64: base64 decode/encode library. -// - stb_image: Image loading library. -// -#ifndef TINY_GLTF_H_ -#define TINY_GLTF_H_ - -#include -#include -#include // std::fabs -#include -#include -#include -#include -#include -#include -#include - -#ifndef TINYGLTF_USE_CPP14 -#include -#endif - -#ifdef __ANDROID__ -#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS -#include -#endif -#endif - -#ifdef __GNUC__ -#if (__GNUC__ < 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ <= 8)) -#define TINYGLTF_NOEXCEPT -#else -#define TINYGLTF_NOEXCEPT noexcept -#endif -#else -#define TINYGLTF_NOEXCEPT noexcept -#endif - -#define DEFAULT_METHODS(x) \ - ~x() = default; \ - x(const x &) = default; \ - x(x &&) TINYGLTF_NOEXCEPT = default; \ - x &operator=(const x &) = default; \ - x &operator=(x &&) TINYGLTF_NOEXCEPT = default; - -namespace tinygltf { - -#define TINYGLTF_MODE_POINTS (0) -#define TINYGLTF_MODE_LINE (1) -#define TINYGLTF_MODE_LINE_LOOP (2) -#define TINYGLTF_MODE_LINE_STRIP (3) -#define TINYGLTF_MODE_TRIANGLES (4) -#define TINYGLTF_MODE_TRIANGLE_STRIP (5) -#define TINYGLTF_MODE_TRIANGLE_FAN (6) - -#define TINYGLTF_COMPONENT_TYPE_BYTE (5120) -#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE (5121) -#define TINYGLTF_COMPONENT_TYPE_SHORT (5122) -#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT (5123) -#define TINYGLTF_COMPONENT_TYPE_INT (5124) -#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT (5125) -#define TINYGLTF_COMPONENT_TYPE_FLOAT (5126) -#define TINYGLTF_COMPONENT_TYPE_DOUBLE (5130) - -#define TINYGLTF_TEXTURE_FILTER_NEAREST (9728) -#define TINYGLTF_TEXTURE_FILTER_LINEAR (9729) -#define TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST (9984) -#define TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST (9985) -#define TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR (9986) -#define TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR (9987) - -#define TINYGLTF_TEXTURE_WRAP_REPEAT (10497) -#define TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE (33071) -#define TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT (33648) - - // Redeclarations of the above for technique.parameters. -#define TINYGLTF_PARAMETER_TYPE_BYTE (5120) -#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE (5121) -#define TINYGLTF_PARAMETER_TYPE_SHORT (5122) -#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_SHORT (5123) -#define TINYGLTF_PARAMETER_TYPE_INT (5124) -#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_INT (5125) -#define TINYGLTF_PARAMETER_TYPE_FLOAT (5126) - -#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC2 (35664) -#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC3 (35665) -#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC4 (35666) - -#define TINYGLTF_PARAMETER_TYPE_INT_VEC2 (35667) -#define TINYGLTF_PARAMETER_TYPE_INT_VEC3 (35668) -#define TINYGLTF_PARAMETER_TYPE_INT_VEC4 (35669) - -#define TINYGLTF_PARAMETER_TYPE_BOOL (35670) -#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC2 (35671) -#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC3 (35672) -#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC4 (35673) - -#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT2 (35674) -#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT3 (35675) -#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT4 (35676) - -#define TINYGLTF_PARAMETER_TYPE_SAMPLER_2D (35678) - - // End parameter types - -#define TINYGLTF_TYPE_VEC2 (2) -#define TINYGLTF_TYPE_VEC3 (3) -#define TINYGLTF_TYPE_VEC4 (4) -#define TINYGLTF_TYPE_MAT2 (32 + 2) -#define TINYGLTF_TYPE_MAT3 (32 + 3) -#define TINYGLTF_TYPE_MAT4 (32 + 4) -#define TINYGLTF_TYPE_SCALAR (64 + 1) -#define TINYGLTF_TYPE_VECTOR (64 + 4) -#define TINYGLTF_TYPE_MATRIX (64 + 16) - -#define TINYGLTF_IMAGE_FORMAT_JPEG (0) -#define TINYGLTF_IMAGE_FORMAT_PNG (1) -#define TINYGLTF_IMAGE_FORMAT_BMP (2) -#define TINYGLTF_IMAGE_FORMAT_GIF (3) - -#define TINYGLTF_TEXTURE_FORMAT_ALPHA (6406) -#define TINYGLTF_TEXTURE_FORMAT_RGB (6407) -#define TINYGLTF_TEXTURE_FORMAT_RGBA (6408) -#define TINYGLTF_TEXTURE_FORMAT_LUMINANCE (6409) -#define TINYGLTF_TEXTURE_FORMAT_LUMINANCE_ALPHA (6410) - -#define TINYGLTF_TEXTURE_TARGET_TEXTURE2D (3553) -#define TINYGLTF_TEXTURE_TYPE_UNSIGNED_BYTE (5121) - -#define TINYGLTF_TARGET_ARRAY_BUFFER (34962) -#define TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER (34963) - -#define TINYGLTF_SHADER_TYPE_VERTEX_SHADER (35633) -#define TINYGLTF_SHADER_TYPE_FRAGMENT_SHADER (35632) - -#define TINYGLTF_DOUBLE_EPS (1.e-12) -#define TINYGLTF_DOUBLE_EQUAL(a, b) (std::fabs((b) - (a)) < TINYGLTF_DOUBLE_EPS) - -#ifdef __ANDROID__ -#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS - AAssetManager *asset_manager = nullptr; -#endif -#endif - - typedef enum { - NULL_TYPE = 0, - REAL_TYPE = 1, - INT_TYPE = 2, - BOOL_TYPE = 3, - STRING_TYPE = 4, - ARRAY_TYPE = 5, - BINARY_TYPE = 6, - OBJECT_TYPE = 7 - } Type; - - static inline int32_t GetComponentSizeInBytes(uint32_t componentType) { - if (componentType == TINYGLTF_COMPONENT_TYPE_BYTE) { - return 1; - } else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { - return 1; - } else if (componentType == TINYGLTF_COMPONENT_TYPE_SHORT) { - return 2; - } else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - return 2; - } else if (componentType == TINYGLTF_COMPONENT_TYPE_INT) { - return 4; - } else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - return 4; - } else if (componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { - return 4; - } else if (componentType == TINYGLTF_COMPONENT_TYPE_DOUBLE) { - return 8; - } else { - // Unknown componenty type - return -1; - } - } - - static inline int32_t GetNumComponentsInType(uint32_t ty) { - if (ty == TINYGLTF_TYPE_SCALAR) { - return 1; - } else if (ty == TINYGLTF_TYPE_VEC2) { - return 2; - } else if (ty == TINYGLTF_TYPE_VEC3) { - return 3; - } else if (ty == TINYGLTF_TYPE_VEC4) { - return 4; - } else if (ty == TINYGLTF_TYPE_MAT2) { - return 4; - } else if (ty == TINYGLTF_TYPE_MAT3) { - return 9; - } else if (ty == TINYGLTF_TYPE_MAT4) { - return 16; - } else { - // Unknown componenty type - return -1; - } - } - - // TODO(syoyo): Move these functions to TinyGLTF class - bool IsDataURI(const std::string &in); - bool DecodeDataURI(std::vector *out, std::string &mime_type, - const std::string &in, size_t reqBytes, bool checkSize); - -#ifdef __clang__ -#pragma clang diagnostic push - // Suppress warning for : static Value null_value - // https://stackoverflow.com/questions/15708411/how-to-deal-with-global-constructor-warning-in-clang -#pragma clang diagnostic ignored "-Wexit-time-destructors" -#pragma clang diagnostic ignored "-Wpadded" -#endif - - // Simple class to represent JSON object - class Value { - public: - typedef std::vector Array; - typedef std::map Object; - - Value() - : type_(NULL_TYPE), - int_value_(0), - real_value_(0.0), - boolean_value_(false) {} - - explicit Value(bool b) : type_(BOOL_TYPE) { boolean_value_ = b; } - explicit Value(int i) : type_(INT_TYPE) { - int_value_ = i; - real_value_ = i; - } - explicit Value(double n) : type_(REAL_TYPE) { real_value_ = n; } - explicit Value(const std::string &s) : type_(STRING_TYPE) { - string_value_ = s; - } - explicit Value(std::string &&s) - : type_(STRING_TYPE), string_value_(std::move(s)) {} - explicit Value(const unsigned char *p, size_t n) : type_(BINARY_TYPE) { - binary_value_.resize(n); - memcpy(binary_value_.data(), p, n); - } - explicit Value(std::vector &&v) noexcept - : type_(BINARY_TYPE), - binary_value_(std::move(v)) {} - explicit Value(const Array &a) : type_(ARRAY_TYPE) { array_value_ = a; } - explicit Value(Array &&a) noexcept : type_(ARRAY_TYPE), - array_value_(std::move(a)) {} - - explicit Value(const Object &o) : type_(OBJECT_TYPE) { object_value_ = o; } - explicit Value(Object &&o) noexcept : type_(OBJECT_TYPE), - object_value_(std::move(o)) {} - - DEFAULT_METHODS(Value) - - char Type() const { return static_cast(type_); } - - bool IsBool() const { return (type_ == BOOL_TYPE); } - - bool IsInt() const { return (type_ == INT_TYPE); } - - bool IsNumber() const { return (type_ == REAL_TYPE) || (type_ == INT_TYPE); } - - bool IsReal() const { return (type_ == REAL_TYPE); } - - bool IsString() const { return (type_ == STRING_TYPE); } - - bool IsBinary() const { return (type_ == BINARY_TYPE); } - - bool IsArray() const { return (type_ == ARRAY_TYPE); } - - bool IsObject() const { return (type_ == OBJECT_TYPE); } - - // Use this function if you want to have number value as double. - double GetNumberAsDouble() const { - if (type_ == INT_TYPE) { - return double(int_value_); - } else { - return real_value_; - } - } - - // Use this function if you want to have number value as int. - // TODO(syoyo): Support int value larger than 32 bits - int GetNumberAsInt() const { - if (type_ == REAL_TYPE) { - return int(real_value_); - } else { - return int_value_; - } - } - - // Accessor - template - const T &Get() const; - template - T &Get(); - - // Lookup value from an array - const Value &Get(int idx) const { - static Value null_value; - assert(IsArray()); - assert(idx >= 0); - return (static_cast(idx) < array_value_.size()) - ? array_value_[static_cast(idx)] - : null_value; - } - - // Lookup value from a key-value pair - const Value &Get(const std::string &key) const { - static Value null_value; - assert(IsObject()); - Object::const_iterator it = object_value_.find(key); - return (it != object_value_.end()) ? it->second : null_value; - } - - size_t ArrayLen() const { - if (!IsArray()) return 0; - return array_value_.size(); - } - - // Valid only for object type. - bool Has(const std::string &key) const { - if (!IsObject()) return false; - Object::const_iterator it = object_value_.find(key); - return (it != object_value_.end()) ? true : false; - } - - // List keys - std::vector Keys() const { - std::vector keys; - if (!IsObject()) return keys; // empty - - for (Object::const_iterator it = object_value_.begin(); - it != object_value_.end(); ++it) { - keys.push_back(it->first); - } - - return keys; - } - - size_t Size() const { return (IsArray() ? ArrayLen() : Keys().size()); } - - bool operator==(const tinygltf::Value &other) const; - - protected: - int type_ = NULL_TYPE; - - int int_value_ = 0; - double real_value_ = 0.0; - std::string string_value_; - std::vector binary_value_; - Array array_value_; - Object object_value_; - bool boolean_value_ = false; - }; - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -#define TINYGLTF_VALUE_GET(ctype, var) \ - template <> \ - inline const ctype &Value::Get() const { \ - return var; \ - } \ - template <> \ - inline ctype &Value::Get() { \ - return var; \ - } - TINYGLTF_VALUE_GET(bool, boolean_value_) - TINYGLTF_VALUE_GET(double, real_value_) - TINYGLTF_VALUE_GET(int, int_value_) - TINYGLTF_VALUE_GET(std::string, string_value_) - TINYGLTF_VALUE_GET(std::vector, binary_value_) - TINYGLTF_VALUE_GET(Value::Array, array_value_) - TINYGLTF_VALUE_GET(Value::Object, object_value_) -#undef TINYGLTF_VALUE_GET - -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wc++98-compat" -#pragma clang diagnostic ignored "-Wpadded" -#endif - - /// Agregate object for representing a color - using ColorValue = std::array; - - // === legacy interface ==== - // TODO(syoyo): Deprecate `Parameter` class. - struct Parameter { - bool bool_value = false; - bool has_number_value = false; - std::string string_value; - std::vector number_array; - std::map json_double_value; - double number_value = 0.0; - - // context sensitive methods. depending the type of the Parameter you are - // accessing, these are either valid or not - // If this parameter represent a texture map in a material, will return the - // texture index - - /// Return the index of a texture if this Parameter is a texture map. - /// Returned value is only valid if the parameter represent a texture from a - /// material - int TextureIndex() const { - const auto it = json_double_value.find("index"); - if (it != std::end(json_double_value)) { - return int(it->second); - } - return -1; - } - - /// Return the index of a texture coordinate set if this Parameter is a - /// texture map. Returned value is only valid if the parameter represent a - /// texture from a material - int TextureTexCoord() const { - const auto it = json_double_value.find("texCoord"); - if (it != std::end(json_double_value)) { - return int(it->second); - } - // As per the spec, if texCoord is ommited, this parameter is 0 - return 0; - } - - /// Return the scale of a texture if this Parameter is a normal texture map. - /// Returned value is only valid if the parameter represent a normal texture - /// from a material - double TextureScale() const { - const auto it = json_double_value.find("scale"); - if (it != std::end(json_double_value)) { - return it->second; - } - // As per the spec, if scale is ommited, this paramter is 1 - return 1; - } - - /// Return the strength of a texture if this Parameter is a an occlusion map. - /// Returned value is only valid if the parameter represent an occlusion map - /// from a material - double TextureStrength() const { - const auto it = json_double_value.find("strength"); - if (it != std::end(json_double_value)) { - return it->second; - } - // As per the spec, if strenghth is ommited, this parameter is 1 - return 1; - } - - /// Material factor, like the roughness or metalness of a material - /// Returned value is only valid if the parameter represent a texture from a - /// material - double Factor() const { return number_value; } - - /// Return the color of a material - /// Returned value is only valid if the parameter represent a texture from a - /// material - ColorValue ColorFactor() const { - return { - {// this agregate intialize the std::array object, and uses C++11 RVO. - number_array[0], number_array[1], number_array[2], - (number_array.size() > 3 ? number_array[3] : 1.0)}}; - } - - Parameter() = default; - DEFAULT_METHODS(Parameter) - bool operator==(const Parameter &) const; - }; - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpadded" -#endif - - typedef std::map ParameterMap; - typedef std::map ExtensionMap; - - struct AnimationChannel { - int sampler; // required - int target_node; // required (index of the node to target) - std::string target_path; // required in ["translation", "rotation", "scale", - // "weights"] - Value extras; - ExtensionMap extensions; - ExtensionMap target_extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - std::string target_extensions_json_string; - - AnimationChannel() : sampler(-1), target_node(-1) {} - DEFAULT_METHODS(AnimationChannel) - bool operator==(const AnimationChannel &) const; - }; - - struct AnimationSampler { - int input; // required - int output; // required - std::string interpolation; // "LINEAR", "STEP","CUBICSPLINE" or user defined - // string. default "LINEAR" - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - AnimationSampler() : input(-1), output(-1), interpolation("LINEAR") {} - DEFAULT_METHODS(AnimationSampler) - bool operator==(const AnimationSampler &) const; - }; - - struct Animation { - std::string name; - std::vector channels; - std::vector samplers; - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Animation() = default; - DEFAULT_METHODS(Animation) - bool operator==(const Animation &) const; - }; - - struct Skin { - std::string name; - int inverseBindMatrices; // required here but not in the spec - int skeleton; // The index of the node used as a skeleton root - std::vector joints; // Indices of skeleton nodes - - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Skin() { - inverseBindMatrices = -1; - skeleton = -1; - } - DEFAULT_METHODS(Skin) - bool operator==(const Skin &) const; - }; - - struct Sampler { - std::string name; - // glTF 2.0 spec does not define default value for `minFilter` and - // `magFilter`. Set -1 in TinyGLTF(issue #186) - int minFilter = - -1; // optional. -1 = no filter defined. ["NEAREST", "LINEAR", - // "NEAREST_MIPMAP_LINEAR", "LINEAR_MIPMAP_NEAREST", - // "NEAREST_MIPMAP_LINEAR", "LINEAR_MIPMAP_LINEAR"] - int magFilter = - -1; // optional. -1 = no filter defined. ["NEAREST", "LINEAR"] - int wrapS = - TINYGLTF_TEXTURE_WRAP_REPEAT; // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT", - // "REPEAT"], default "REPEAT" - int wrapT = - TINYGLTF_TEXTURE_WRAP_REPEAT; // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT", - // "REPEAT"], default "REPEAT" - int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT; // TinyGLTF extension - - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Sampler() - : minFilter(-1), - magFilter(-1), - wrapS(TINYGLTF_TEXTURE_WRAP_REPEAT), - wrapT(TINYGLTF_TEXTURE_WRAP_REPEAT), - wrapR(TINYGLTF_TEXTURE_WRAP_REPEAT) {} - DEFAULT_METHODS(Sampler) - bool operator==(const Sampler &) const; - }; - - struct Image { - std::string name; - int width; - int height; - int component; - int bits; // bit depth per channel. 8(byte), 16 or 32. - int pixel_type; // pixel type(TINYGLTF_COMPONENT_TYPE_***). usually - // UBYTE(bits = 8) or USHORT(bits = 16) - std::vector image; - int bufferView; // (required if no uri) - std::string mimeType; // (required if no uri) ["image/jpeg", "image/png", - // "image/bmp", "image/gif"] - std::string uri; // (required if no mimeType) uri is not decoded(e.g. - // whitespace may be represented as %20) - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - // When this flag is true, data is stored to `image` in as-is format(e.g. jpeg - // compressed for "image/jpeg" mime) This feature is good if you use custom - // image loader function. (e.g. delayed decoding of images for faster glTF - // parsing) Default parser for Image does not provide as-is loading feature at - // the moment. (You can manipulate this by providing your own LoadImageData - // function) - bool as_is; - - Image() : as_is(false) { - bufferView = -1; - width = -1; - height = -1; - component = -1; - bits = -1; - pixel_type = -1; - } - DEFAULT_METHODS(Image) - - bool operator==(const Image &) const; - }; - - struct Texture { - std::string name; - - int sampler; - int source; - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Texture() : sampler(-1), source(-1) {} - DEFAULT_METHODS(Texture) - - bool operator==(const Texture &) const; - }; - - struct TextureInfo { - int index = -1; // required. - int texCoord; // The set index of texture's TEXCOORD attribute used for - // texture coordinate mapping. - - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - TextureInfo() : index(-1), texCoord(0) {} - DEFAULT_METHODS(TextureInfo) - bool operator==(const TextureInfo &) const; - }; - - struct NormalTextureInfo { - int index = -1; // required - int texCoord; // The set index of texture's TEXCOORD attribute used for - // texture coordinate mapping. - double scale; // scaledNormal = normalize(( - // * 2.0 - 1.0) * vec3(, , 1.0)) - - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - NormalTextureInfo() : index(-1), texCoord(0), scale(1.0) {} - DEFAULT_METHODS(NormalTextureInfo) - bool operator==(const NormalTextureInfo &) const; - }; - - struct OcclusionTextureInfo { - int index = -1; // required - int texCoord; // The set index of texture's TEXCOORD attribute used for - // texture coordinate mapping. - double strength; // occludedColor = lerp(color, color * , ) - - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - OcclusionTextureInfo() : index(-1), texCoord(0), strength(1.0) {} - DEFAULT_METHODS(OcclusionTextureInfo) - bool operator==(const OcclusionTextureInfo &) const; - }; - - // pbrMetallicRoughness class defined in glTF 2.0 spec. - struct PbrMetallicRoughness { - std::vector baseColorFactor; // len = 4. default [1,1,1,1] - TextureInfo baseColorTexture; - double metallicFactor; // default 1 - double roughnessFactor; // default 1 - TextureInfo metallicRoughnessTexture; - - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - PbrMetallicRoughness() - : baseColorFactor(std::vector{1.0, 1.0, 1.0, 1.0}), - metallicFactor(1.0), - roughnessFactor(1.0) {} - DEFAULT_METHODS(PbrMetallicRoughness) - bool operator==(const PbrMetallicRoughness &) const; - }; - - // Each extension should be stored in a ParameterMap. - // members not in the values could be included in the ParameterMap - // to keep a single material model - struct Material { - std::string name; - - std::vector emissiveFactor{ 0.0, 0.0, 0.0 }; // length 3. default [0, 0, 0] - std::string alphaMode; // default "OPAQUE" - double alphaCutoff; // default 0.5 - bool doubleSided; // default false; - - PbrMetallicRoughness pbrMetallicRoughness; - - NormalTextureInfo normalTexture; - OcclusionTextureInfo occlusionTexture; - TextureInfo emissiveTexture; - - // For backward compatibility - // TODO(syoyo): Remove `values` and `additionalValues` in the next release. - ParameterMap values; - ParameterMap additionalValues; - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Material() : alphaMode("OPAQUE"), alphaCutoff(0.5), doubleSided(false) {} - DEFAULT_METHODS(Material) - - bool operator==(const Material &) const; - }; - - struct BufferView { - std::string name; - int buffer{-1}; // Required - size_t byteOffset{0}; // minimum 0, default 0 - size_t byteLength{0}; // required, minimum 1. 0 = invalid - size_t byteStride{0}; // minimum 4, maximum 252 (multiple of 4), default 0 = - // understood to be tightly packed - int target{0}; // ["ARRAY_BUFFER", "ELEMENT_ARRAY_BUFFER"] for vertex indices - // or atttribs. Could be 0 for other data - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - bool dracoDecoded{false}; // Flag indicating this has been draco decoded - - BufferView() - : buffer(-1), - byteOffset(0), - byteLength(0), - byteStride(0), - target(0), - dracoDecoded(false) {} - DEFAULT_METHODS(BufferView) - bool operator==(const BufferView &) const; - }; - - struct Accessor { - int bufferView; // optional in spec but required here since sparse accessor - // are not supported - std::string name; - size_t byteOffset; - bool normalized; // optional. - int componentType; // (required) One of TINYGLTF_COMPONENT_TYPE_*** - size_t count; // required - int type; // (required) One of TINYGLTF_TYPE_*** .. - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - std::vector minValues; // optional - std::vector maxValues; // optional - - struct { - int count; - bool isSparse; - struct { - int byteOffset; - int bufferView; - int componentType; // a TINYGLTF_COMPONENT_TYPE_ value - } indices; - struct { - int bufferView; - int byteOffset; - } values; - } sparse; - - /// - /// Utility function to compute byteStride for a given bufferView object. - /// Returns -1 upon invalid glTF value or parameter configuration. - /// - int ByteStride(const BufferView &bufferViewObject) const { - if (bufferViewObject.byteStride == 0) { - // Assume data is tightly packed. - int componentSizeInBytes = - GetComponentSizeInBytes(static_cast(componentType)); - if (componentSizeInBytes <= 0) { - return -1; - } - - int numComponents = GetNumComponentsInType(static_cast(type)); - if (numComponents <= 0) { - return -1; - } - - return componentSizeInBytes * numComponents; - } else { - // Check if byteStride is a mulple of the size of the accessor's component - // type. - int componentSizeInBytes = - GetComponentSizeInBytes(static_cast(componentType)); - if (componentSizeInBytes <= 0) { - return -1; - } - - if ((bufferViewObject.byteStride % uint32_t(componentSizeInBytes)) != 0) { - return -1; - } - return static_cast(bufferViewObject.byteStride); - } - - // unreachable return 0; - } - - Accessor() - : bufferView(-1), - byteOffset(0), - normalized(false), - componentType(-1), - count(0), - type(-1) { - sparse.isSparse = false; - } - DEFAULT_METHODS(Accessor) - bool operator==(const tinygltf::Accessor &) const; - }; - - struct PerspectiveCamera { - double aspectRatio; // min > 0 - double yfov; // required. min > 0 - double zfar; // min > 0 - double znear; // required. min > 0 - - PerspectiveCamera() - : aspectRatio(0.0), - yfov(0.0), - zfar(0.0) // 0 = use infinite projecton matrix - , - znear(0.0) {} - DEFAULT_METHODS(PerspectiveCamera) - bool operator==(const PerspectiveCamera &) const; - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - }; - - struct OrthographicCamera { - double xmag; // required. must not be zero. - double ymag; // required. must not be zero. - double zfar; // required. `zfar` must be greater than `znear`. - double znear; // required - - OrthographicCamera() : xmag(0.0), ymag(0.0), zfar(0.0), znear(0.0) {} - DEFAULT_METHODS(OrthographicCamera) - bool operator==(const OrthographicCamera &) const; - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - }; - - struct Camera { - std::string type; // required. "perspective" or "orthographic" - std::string name; - - PerspectiveCamera perspective; - OrthographicCamera orthographic; - - Camera() {} - DEFAULT_METHODS(Camera) - bool operator==(const Camera &) const; - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - }; - - struct Primitive { - std::map attributes; // (required) A dictionary object of - // integer, where each integer - // is the index of the accessor - // containing an attribute. - int material; // The index of the material to apply to this primitive - // when rendering. - int indices; // The index of the accessor that contains the indices. - int mode; // one of TINYGLTF_MODE_*** - std::vector > targets; // array of morph targets, - // where each target is a dict with attribues in ["POSITION, "NORMAL", - // "TANGENT"] pointing - // to their corresponding accessors - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Primitive() { - material = -1; - indices = -1; - mode = -1; - } - DEFAULT_METHODS(Primitive) - bool operator==(const Primitive &) const; - }; - - struct Mesh { - std::string name; - std::vector primitives; - std::vector weights; // weights to be applied to the Morph Targets - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Mesh() = default; - DEFAULT_METHODS(Mesh) - bool operator==(const Mesh &) const; - }; - - class Node { - public: - Node() : camera(-1), skin(-1), mesh(-1) {} - - DEFAULT_METHODS(Node) - - bool operator==(const Node &) const; - - int camera; // the index of the camera referenced by this node - - std::string name; - int skin; - int mesh; - std::vector children; - std::vector rotation; // length must be 0 or 4 - std::vector scale; // length must be 0 or 3 - std::vector translation; // length must be 0 or 3 - std::vector matrix; // length must be 0 or 16 - std::vector weights; // The weights of the instantiated Morph Target - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - }; - - struct Buffer { - std::string name; - std::vector data; - std::string - uri; // considered as required here but not in the spec (need to clarify) - // uri is not decoded(e.g. whitespace may be represented as %20) - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Buffer() = default; - DEFAULT_METHODS(Buffer) - bool operator==(const Buffer &) const; - }; - - struct Asset { - std::string version; // required - std::string generator; - std::string minVersion; - std::string copyright; - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Asset() = default; - DEFAULT_METHODS(Asset) - bool operator==(const Asset &) const; - }; - - struct Scene { - std::string name; - std::vector nodes; - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - - Scene() = default; - DEFAULT_METHODS(Scene) - bool operator==(const Scene &) const; - }; - - struct SpotLight { - double innerConeAngle; - double outerConeAngle; - - SpotLight() : innerConeAngle(0.0), outerConeAngle(0.7853981634) {} - DEFAULT_METHODS(SpotLight) - bool operator==(const SpotLight &) const; - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - }; - - struct Light { - std::string name; - std::vector color; - double intensity{1.0}; - std::string type; - double range{0.0}; // 0.0 = inifinite - SpotLight spot; - - Light() : intensity(1.0), range(0.0) {} - DEFAULT_METHODS(Light) - - bool operator==(const Light &) const; - - ExtensionMap extensions; - Value extras; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - }; - - class Model { - public: - Model() = default; - DEFAULT_METHODS(Model) - - bool operator==(const Model &) const; - - std::vector accessors; - std::vector animations; - std::vector buffers; - std::vector bufferViews; - std::vector materials; - std::vector meshes; - std::vector nodes; - std::vector textures; - std::vector images; - std::vector skins; - std::vector samplers; - std::vector cameras; - std::vector scenes; - std::vector lights; - - int defaultScene = -1; - std::vector extensionsUsed; - std::vector extensionsRequired; - - Asset asset; - - Value extras; - ExtensionMap extensions; - - // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. - std::string extras_json_string; - std::string extensions_json_string; - }; - - enum SectionCheck { - NO_REQUIRE = 0x00, - REQUIRE_VERSION = 0x01, - REQUIRE_SCENE = 0x02, - REQUIRE_SCENES = 0x04, - REQUIRE_NODES = 0x08, - REQUIRE_ACCESSORS = 0x10, - REQUIRE_BUFFERS = 0x20, - REQUIRE_BUFFER_VIEWS = 0x40, - REQUIRE_ALL = 0x7f - }; - - /// - /// LoadImageDataFunction type. Signature for custom image loading callbacks. - /// - typedef bool (*LoadImageDataFunction)(Image *, const int, std::string *, - std::string *, int, int, - const unsigned char *, int, void *); - - /// - /// WriteImageDataFunction type. Signature for custom image writing callbacks. - /// - typedef bool (*WriteImageDataFunction)(const std::string *, const std::string *, - Image *, bool, void *); - -#ifndef TINYGLTF_NO_STB_IMAGE - // Declaration of default image loader callback - bool LoadImageData(Image *image, const int image_idx, std::string *err, - std::string *warn, int req_width, int req_height, - const unsigned char *bytes, int size, void *); -#endif - -#ifndef TINYGLTF_NO_STB_IMAGE_WRITE - // Declaration of default image writer callback - bool WriteImageData(const std::string *basepath, const std::string *filename, - Image *image, bool embedImages, void *); -#endif - - /// - /// FilExistsFunction type. Signature for custom filesystem callbacks. - /// - typedef bool (*FileExistsFunction)(const std::string &abs_filename, void *); - - /// - /// ExpandFilePathFunction type. Signature for custom filesystem callbacks. - /// - typedef std::string (*ExpandFilePathFunction)(const std::string &, void *); - - /// - /// ReadWholeFileFunction type. Signature for custom filesystem callbacks. - /// - typedef bool (*ReadWholeFileFunction)(std::vector *, - std::string *, const std::string &, - void *); - - /// - /// WriteWholeFileFunction type. Signature for custom filesystem callbacks. - /// - typedef bool (*WriteWholeFileFunction)(std::string *, const std::string &, - const std::vector &, - void *); - - /// - /// A structure containing all required filesystem callbacks and a pointer to - /// their user data. - /// - struct FsCallbacks { - FileExistsFunction FileExists; - ExpandFilePathFunction ExpandFilePath; - ReadWholeFileFunction ReadWholeFile; - WriteWholeFileFunction WriteWholeFile; - - void *user_data; // An argument that is passed to all fs callbacks - }; - -#ifndef TINYGLTF_NO_FS - // Declaration of default filesystem callbacks - - bool FileExists(const std::string &abs_filename, void *); - - /// - /// Expand file path(e.g. `~` to home directory on posix, `%APPDATA%` to - /// `C:\Users\tinygltf\AppData`) - /// - /// @param[in] filepath File path string. Assume UTF-8 - /// @param[in] userdata User data. Set to `nullptr` if you don't need it. - /// - std::string ExpandFilePath(const std::string &filepath, void *userdata); - - bool ReadWholeFile(std::vector *out, std::string *err, - const std::string &filepath, void *); - - bool WriteWholeFile(std::string *err, const std::string &filepath, - const std::vector &contents, void *); -#endif - - /// - /// glTF Parser/Serialier context. - /// - class TinyGLTF { - public: -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wc++98-compat" -#endif - - TinyGLTF() : bin_data_(nullptr), bin_size_(0), is_binary_(false) {} - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - - ~TinyGLTF() {} - - /// - /// Loads glTF ASCII asset from a file. - /// Set warning message to `warn` for example it fails to load asserts. - /// Returns false and set error string to `err` if there's an error. - /// - bool LoadASCIIFromFile(Model *model, std::string *err, std::string *warn, - const std::string &filename, - unsigned int check_sections = REQUIRE_VERSION); - - /// - /// Loads glTF ASCII asset from string(memory). - /// `length` = strlen(str); - /// Set warning message to `warn` for example it fails to load asserts. - /// Returns false and set error string to `err` if there's an error. - /// - bool LoadASCIIFromString(Model *model, std::string *err, std::string *warn, - const char *str, const unsigned int length, - const std::string &base_dir, - unsigned int check_sections = REQUIRE_VERSION); - - /// - /// Loads glTF binary asset from a file. - /// Set warning message to `warn` for example it fails to load asserts. - /// Returns false and set error string to `err` if there's an error. - /// - bool LoadBinaryFromFile(Model *model, std::string *err, std::string *warn, - const std::string &filename, - unsigned int check_sections = REQUIRE_VERSION); - - /// - /// Loads glTF binary asset from memory. - /// `length` = strlen(str); - /// Set warning message to `warn` for example it fails to load asserts. - /// Returns false and set error string to `err` if there's an error. - /// - bool LoadBinaryFromMemory(Model *model, std::string *err, std::string *warn, - const unsigned char *bytes, - const unsigned int length, - const std::string &base_dir = "", - unsigned int check_sections = REQUIRE_VERSION); - - /// - /// Write glTF to stream, buffers and images will be embeded - /// - bool WriteGltfSceneToStream(Model *model, std::ostream &stream, - bool prettyPrint, bool writeBinary); - - /// - /// Write glTF to file. - /// - bool WriteGltfSceneToFile(Model *model, const std::string &filename, - bool embedImages, bool embedBuffers, - bool prettyPrint, bool writeBinary); - - /// - /// Set callback to use for loading image data - /// - void SetImageLoader(LoadImageDataFunction LoadImageData, void *user_data); - - /// - /// Set callback to use for writing image data - /// - void SetImageWriter(WriteImageDataFunction WriteImageData, void *user_data); - - /// - /// Set callbacks to use for filesystem (fs) access and their user data - /// - void SetFsCallbacks(FsCallbacks callbacks); - - /// - /// Set serializing default values(default = false). - /// When true, default values are force serialized to .glTF. - /// This may be helpfull if you want to serialize a full description of glTF - /// data. - /// - /// TODO(LTE): Supply parsing option as function arguments to - /// `LoadASCIIFromFile()` and others, not by a class method - /// - void SetSerializeDefaultValues(const bool enabled) { - serialize_default_values_ = enabled; - } - - bool GetSerializeDefaultValues() const { return serialize_default_values_; } - - /// - /// Store original JSON string for `extras` and `extensions`. - /// This feature will be useful when the user want to reconstruct custom data - /// structure from JSON string. - /// - void SetStoreOriginalJSONForExtrasAndExtensions(const bool enabled) { - store_original_json_for_extras_and_extensions_ = enabled; - } - - bool GetStoreOriginalJSONForExtrasAndExtensions() const { - return store_original_json_for_extras_and_extensions_; - } - - private: - /// - /// Loads glTF asset from string(memory). - /// `length` = strlen(str); - /// Set warning message to `warn` for example it fails to load asserts - /// Returns false and set error string to `err` if there's an error. - /// - bool LoadFromString(Model *model, std::string *err, std::string *warn, - const char *str, const unsigned int length, - const std::string &base_dir, unsigned int check_sections); - - const unsigned char *bin_data_ = nullptr; - size_t bin_size_ = 0; - bool is_binary_ = false; - - bool serialize_default_values_ = false; ///< Serialize default values? - - bool store_original_json_for_extras_and_extensions_ = false; - - FsCallbacks fs = { -#ifndef TINYGLTF_NO_FS - &tinygltf::FileExists, &tinygltf::ExpandFilePath, - &tinygltf::ReadWholeFile, &tinygltf::WriteWholeFile, - - nullptr // Fs callback user data -#else - nullptr, nullptr, nullptr, nullptr, - - nullptr // Fs callback user data -#endif - }; - - LoadImageDataFunction LoadImageData = -#ifndef TINYGLTF_NO_STB_IMAGE - &tinygltf::LoadImageData; -#else - nullptr; -#endif - void *load_image_user_data_ = reinterpret_cast(&fs); - - WriteImageDataFunction WriteImageData = -#ifndef TINYGLTF_NO_STB_IMAGE_WRITE - &tinygltf::WriteImageData; -#else - nullptr; -#endif - void *write_image_user_data_ = reinterpret_cast(&fs); - }; - -#ifdef __clang__ -#pragma clang diagnostic pop // -Wpadded -#endif - -} // namespace tinygltf - -#endif // TINY_GLTF_H_ - -#if defined(TINYGLTF_IMPLEMENTATION) || defined(__INTELLISENSE__) -#include - //#include -#ifndef TINYGLTF_NO_FS -#include -#include -#endif -#include - -#ifdef __clang__ - // Disable some warnings for external files. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wfloat-equal" -#pragma clang diagnostic ignored "-Wexit-time-destructors" -#pragma clang diagnostic ignored "-Wconversion" -#pragma clang diagnostic ignored "-Wold-style-cast" -#pragma clang diagnostic ignored "-Wglobal-constructors" -#if __has_warning("-Wreserved-id-macro") -#pragma clang diagnostic ignored "-Wreserved-id-macro" -#endif -#pragma clang diagnostic ignored "-Wdisabled-macro-expansion" -#pragma clang diagnostic ignored "-Wpadded" -#pragma clang diagnostic ignored "-Wc++98-compat" -#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" -#pragma clang diagnostic ignored "-Wdocumentation-unknown-command" -#pragma clang diagnostic ignored "-Wswitch-enum" -#pragma clang diagnostic ignored "-Wimplicit-fallthrough" -#pragma clang diagnostic ignored "-Wweak-vtables" -#pragma clang diagnostic ignored "-Wcovered-switch-default" -#if __has_warning("-Wdouble-promotion") -#pragma clang diagnostic ignored "-Wdouble-promotion" -#endif -#if __has_warning("-Wcomma") -#pragma clang diagnostic ignored "-Wcomma" -#endif -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" -#endif -#if __has_warning("-Wcast-qual") -#pragma clang diagnostic ignored "-Wcast-qual" -#endif -#if __has_warning("-Wmissing-variable-declarations") -#pragma clang diagnostic ignored "-Wmissing-variable-declarations" -#endif -#if __has_warning("-Wmissing-prototypes") -#pragma clang diagnostic ignored "-Wmissing-prototypes" -#endif -#if __has_warning("-Wcast-align") -#pragma clang diagnostic ignored "-Wcast-align" -#endif -#if __has_warning("-Wnewline-eof") -#pragma clang diagnostic ignored "-Wnewline-eof" -#endif -#if __has_warning("-Wunused-parameter") -#pragma clang diagnostic ignored "-Wunused-parameter" -#endif -#if __has_warning("-Wmismatched-tags") -#pragma clang diagnostic ignored "-Wmismatched-tags" -#endif -#if __has_warning("-Wextra-semi-stmt") -#pragma clang diagnostic ignored "-Wextra-semi-stmt" -#endif -#endif - - // Disable GCC warnigs -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wtype-limits" -#endif // __GNUC__ - -#ifndef TINYGLTF_NO_INCLUDE_JSON -#ifndef TINYGLTF_USE_RAPIDJSON -#include "json.hpp" -#else -#include "document.h" -#include "prettywriter.h" -#include "rapidjson.h" -#include "stringbuffer.h" -#include "writer.h" -#endif -#endif - -#ifdef TINYGLTF_ENABLE_DRACO -#include "draco/compression/decode.h" -#include "draco/core/decoder_buffer.h" -#endif - -#ifndef TINYGLTF_NO_STB_IMAGE -#ifndef TINYGLTF_NO_INCLUDE_STB_IMAGE -#include "stb_image.h" -#endif -#endif - -#ifndef TINYGLTF_NO_STB_IMAGE_WRITE -#ifndef TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE -#include "stb_image_write.h" -#endif -#endif - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - -#ifdef _WIN32 - - // issue 143. - // Define NOMINMAX to avoid min/max defines, - // but undef it after included windows.h -#ifndef NOMINMAX -#define TINYGLTF_INTERNAL_NOMINMAX -#define NOMINMAX -#endif - -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#define TINYGLTF_INTERNAL_WIN32_LEAN_AND_MEAN -#endif -#include // include API for expanding a file path - -#ifdef TINYGLTF_INTERNAL_WIN32_LEAN_AND_MEAN -#undef WIN32_LEAN_AND_MEAN -#endif - -#if defined(TINYGLTF_INTERNAL_NOMINMAX) -#undef NOMINMAX -#endif - -#if defined(__GLIBCXX__) // mingw - -#include // _O_RDONLY - -#include // fstream (all sorts of IO stuff) + stdio_filebuf (=streambuf) - -#endif - -#elif !defined(__ANDROID__) -#include -#endif - -#if defined(__sparcv9) - // Big endian -#else -#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || MINIZ_X86_OR_X64_CPU -#define TINYGLTF_LITTLE_ENDIAN 1 -#endif -#endif - -namespace { -#ifdef TINYGLTF_USE_RAPIDJSON - -#ifdef TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR - // This uses the RapidJSON CRTAllocator. It is thread safe and multiple - // documents may be active at once. - using json = - rapidjson::GenericValue, rapidjson::CrtAllocator>; - using json_const_iterator = json::ConstMemberIterator; - using json_const_array_iterator = json const *; - using JsonDocument = - rapidjson::GenericDocument, rapidjson::CrtAllocator>; - rapidjson::CrtAllocator s_CrtAllocator; // stateless and thread safe - rapidjson::CrtAllocator &GetAllocator() { return s_CrtAllocator; } -#else - // This uses the default RapidJSON MemoryPoolAllocator. It is very fast, but - // not thread safe. Only a single JsonDocument may be active at any one time, - // meaning only a single gltf load/save can be active any one time. - using json = rapidjson::Value; - using json_const_iterator = json::ConstMemberIterator; - using json_const_array_iterator = json const *; - rapidjson::Document *s_pActiveDocument = nullptr; - rapidjson::Document::AllocatorType &GetAllocator() { - assert(s_pActiveDocument); // Root json node must be JsonDocument type - return s_pActiveDocument->GetAllocator(); - } - -#ifdef __clang__ -#pragma clang diagnostic push - // Suppress JsonDocument(JsonDocument &&rhs) noexcept -#pragma clang diagnostic ignored "-Wunused-member-function" -#endif - - struct JsonDocument : public rapidjson::Document { - JsonDocument() { - assert(s_pActiveDocument == - nullptr); // When using default allocator, only one document can be - // active at a time, if you need multiple active at once, - // define TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR - s_pActiveDocument = this; - } - JsonDocument(const JsonDocument &) = delete; - JsonDocument(JsonDocument &&rhs) noexcept - : rapidjson::Document(std::move(rhs)) { - s_pActiveDocument = this; - rhs.isNil = true; - } - ~JsonDocument() { - if (!isNil) { - s_pActiveDocument = nullptr; - } - } - - private: - bool isNil = false; - }; - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -#endif // TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR - -#else - using nlohmann::json; - using json_const_iterator = json::const_iterator; - using json_const_array_iterator = json_const_iterator; - using JsonDocument = json; -#endif - - void JsonParse(JsonDocument &doc, const char *str, size_t length, - bool throwExc = false) { -#ifdef TINYGLTF_USE_RAPIDJSON - (void)throwExc; - doc.Parse(str, length); -#else - doc = json::parse(str, str + length, nullptr, throwExc); -#endif - } -} // namespace - -#ifdef __APPLE__ -#include "TargetConditionals.h" -#endif - -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wc++98-compat" -#endif - -namespace tinygltf { - - // Equals function for Value, for recursivity - static bool Equals(const tinygltf::Value &one, const tinygltf::Value &other) { - if (one.Type() != other.Type()) return false; - - switch (one.Type()) { - case NULL_TYPE: - return true; - case BOOL_TYPE: - return one.Get() == other.Get(); - case REAL_TYPE: - return TINYGLTF_DOUBLE_EQUAL(one.Get(), other.Get()); - case INT_TYPE: - return one.Get() == other.Get(); - case OBJECT_TYPE: { - auto oneObj = one.Get(); - auto otherObj = other.Get(); - if (oneObj.size() != otherObj.size()) return false; - for (auto &it : oneObj) { - auto otherIt = otherObj.find(it.first); - if (otherIt == otherObj.end()) return false; - - if (!Equals(it.second, otherIt->second)) return false; - } - return true; - } - case ARRAY_TYPE: { - if (one.Size() != other.Size()) return false; - for (int i = 0; i < int(one.Size()); ++i) - if (!Equals(one.Get(i), other.Get(i))) return false; - return true; - } - case STRING_TYPE: - return one.Get() == other.Get(); - case BINARY_TYPE: - return one.Get >() == - other.Get >(); - default: { - // unhandled type - return false; - } - } - } - - // Equals function for std::vector using TINYGLTF_DOUBLE_EPSILON - static bool Equals(const std::vector &one, - const std::vector &other) { - if (one.size() != other.size()) return false; - for (int i = 0; i < int(one.size()); ++i) { - if (!TINYGLTF_DOUBLE_EQUAL(one[size_t(i)], other[size_t(i)])) return false; - } - return true; - } - - bool Accessor::operator==(const Accessor &other) const { - return this->bufferView == other.bufferView && - this->byteOffset == other.byteOffset && - this->componentType == other.componentType && - this->count == other.count && this->extensions == other.extensions && - this->extras == other.extras && - Equals(this->maxValues, other.maxValues) && - Equals(this->minValues, other.minValues) && this->name == other.name && - this->normalized == other.normalized && this->type == other.type; - } - bool Animation::operator==(const Animation &other) const { - return this->channels == other.channels && - this->extensions == other.extensions && this->extras == other.extras && - this->name == other.name && this->samplers == other.samplers; - } - bool AnimationChannel::operator==(const AnimationChannel &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->target_node == other.target_node && - this->target_path == other.target_path && - this->sampler == other.sampler; - } - bool AnimationSampler::operator==(const AnimationSampler &other) const { - return this->extras == other.extras && this->extensions == other.extensions && - this->input == other.input && - this->interpolation == other.interpolation && - this->output == other.output; - } - bool Asset::operator==(const Asset &other) const { - return this->copyright == other.copyright && - this->extensions == other.extensions && this->extras == other.extras && - this->generator == other.generator && - this->minVersion == other.minVersion && this->version == other.version; - } - bool Buffer::operator==(const Buffer &other) const { - return this->data == other.data && this->extensions == other.extensions && - this->extras == other.extras && this->name == other.name && - this->uri == other.uri; - } - bool BufferView::operator==(const BufferView &other) const { - return this->buffer == other.buffer && this->byteLength == other.byteLength && - this->byteOffset == other.byteOffset && - this->byteStride == other.byteStride && this->name == other.name && - this->target == other.target && this->extensions == other.extensions && - this->extras == other.extras && - this->dracoDecoded == other.dracoDecoded; - } - bool Camera::operator==(const Camera &other) const { - return this->name == other.name && this->extensions == other.extensions && - this->extras == other.extras && - this->orthographic == other.orthographic && - this->perspective == other.perspective && this->type == other.type; - } - bool Image::operator==(const Image &other) const { - return this->bufferView == other.bufferView && - this->component == other.component && - this->extensions == other.extensions && this->extras == other.extras && - this->height == other.height && this->image == other.image && - this->mimeType == other.mimeType && this->name == other.name && - this->uri == other.uri && this->width == other.width; - } - bool Light::operator==(const Light &other) const { - return Equals(this->color, other.color) && this->name == other.name && - this->type == other.type; - } - bool Material::operator==(const Material &other) const { - return (this->pbrMetallicRoughness == other.pbrMetallicRoughness) && - (this->normalTexture == other.normalTexture) && - (this->occlusionTexture == other.occlusionTexture) && - (this->emissiveTexture == other.emissiveTexture) && - Equals(this->emissiveFactor, other.emissiveFactor) && - (this->alphaMode == other.alphaMode) && - TINYGLTF_DOUBLE_EQUAL(this->alphaCutoff, other.alphaCutoff) && - (this->doubleSided == other.doubleSided) && - (this->extensions == other.extensions) && - (this->extras == other.extras) && (this->values == other.values) && - (this->additionalValues == other.additionalValues) && - (this->name == other.name); - } - bool Mesh::operator==(const Mesh &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->name == other.name && Equals(this->weights, other.weights) && - this->primitives == other.primitives; - } - bool Model::operator==(const Model &other) const { - return this->accessors == other.accessors && - this->animations == other.animations && this->asset == other.asset && - this->buffers == other.buffers && - this->bufferViews == other.bufferViews && - this->cameras == other.cameras && - this->defaultScene == other.defaultScene && - this->extensions == other.extensions && - this->extensionsRequired == other.extensionsRequired && - this->extensionsUsed == other.extensionsUsed && - this->extras == other.extras && this->images == other.images && - this->lights == other.lights && this->materials == other.materials && - this->meshes == other.meshes && this->nodes == other.nodes && - this->samplers == other.samplers && this->scenes == other.scenes && - this->skins == other.skins && this->textures == other.textures; - } - bool Node::operator==(const Node &other) const { - return this->camera == other.camera && this->children == other.children && - this->extensions == other.extensions && this->extras == other.extras && - Equals(this->matrix, other.matrix) && this->mesh == other.mesh && - this->name == other.name && Equals(this->rotation, other.rotation) && - Equals(this->scale, other.scale) && this->skin == other.skin && - Equals(this->translation, other.translation) && - Equals(this->weights, other.weights); - } - bool SpotLight::operator==(const SpotLight &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - TINYGLTF_DOUBLE_EQUAL(this->innerConeAngle, other.innerConeAngle) && - TINYGLTF_DOUBLE_EQUAL(this->outerConeAngle, other.outerConeAngle); - } - bool OrthographicCamera::operator==(const OrthographicCamera &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - TINYGLTF_DOUBLE_EQUAL(this->xmag, other.xmag) && - TINYGLTF_DOUBLE_EQUAL(this->ymag, other.ymag) && - TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && - TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); - } - bool Parameter::operator==(const Parameter &other) const { - if (this->bool_value != other.bool_value || - this->has_number_value != other.has_number_value) - return false; - - if (!TINYGLTF_DOUBLE_EQUAL(this->number_value, other.number_value)) - return false; - - if (this->json_double_value.size() != other.json_double_value.size()) - return false; - for (auto &it : this->json_double_value) { - auto otherIt = other.json_double_value.find(it.first); - if (otherIt == other.json_double_value.end()) return false; - - if (!TINYGLTF_DOUBLE_EQUAL(it.second, otherIt->second)) return false; - } - - if (!Equals(this->number_array, other.number_array)) return false; - - if (this->string_value != other.string_value) return false; - - return true; - } - bool PerspectiveCamera::operator==(const PerspectiveCamera &other) const { - return TINYGLTF_DOUBLE_EQUAL(this->aspectRatio, other.aspectRatio) && - this->extensions == other.extensions && this->extras == other.extras && - TINYGLTF_DOUBLE_EQUAL(this->yfov, other.yfov) && - TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && - TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); - } - bool Primitive::operator==(const Primitive &other) const { - return this->attributes == other.attributes && this->extras == other.extras && - this->indices == other.indices && this->material == other.material && - this->mode == other.mode && this->targets == other.targets; - } - bool Sampler::operator==(const Sampler &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->magFilter == other.magFilter && - this->minFilter == other.minFilter && this->name == other.name && - this->wrapR == other.wrapR && this->wrapS == other.wrapS && - this->wrapT == other.wrapT; - } - bool Scene::operator==(const Scene &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->name == other.name && this->nodes == other.nodes; - } - bool Skin::operator==(const Skin &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->inverseBindMatrices == other.inverseBindMatrices && - this->joints == other.joints && this->name == other.name && - this->skeleton == other.skeleton; - } - bool Texture::operator==(const Texture &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->name == other.name && this->sampler == other.sampler && - this->source == other.source; - } - bool TextureInfo::operator==(const TextureInfo &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->index == other.index && this->texCoord == other.texCoord; - } - bool NormalTextureInfo::operator==(const NormalTextureInfo &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->index == other.index && this->texCoord == other.texCoord && - TINYGLTF_DOUBLE_EQUAL(this->scale, other.scale); - } - bool OcclusionTextureInfo::operator==(const OcclusionTextureInfo &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - this->index == other.index && this->texCoord == other.texCoord && - TINYGLTF_DOUBLE_EQUAL(this->strength, other.strength); - } - bool PbrMetallicRoughness::operator==(const PbrMetallicRoughness &other) const { - return this->extensions == other.extensions && this->extras == other.extras && - (this->baseColorTexture == other.baseColorTexture) && - (this->metallicRoughnessTexture == other.metallicRoughnessTexture) && - Equals(this->baseColorFactor, other.baseColorFactor) && - TINYGLTF_DOUBLE_EQUAL(this->metallicFactor, other.metallicFactor) && - TINYGLTF_DOUBLE_EQUAL(this->roughnessFactor, other.roughnessFactor); - } - bool Value::operator==(const Value &other) const { - return Equals(*this, other); - } - - static void swap4(unsigned int *val) { -#ifdef TINYGLTF_LITTLE_ENDIAN - (void)val; -#else - unsigned int tmp = *val; - unsigned char *dst = reinterpret_cast(val); - unsigned char *src = reinterpret_cast(&tmp); - - dst[0] = src[3]; - dst[1] = src[2]; - dst[2] = src[1]; - dst[3] = src[0]; -#endif - } - - static std::string JoinPath(const std::string &path0, - const std::string &path1) { - if (path0.empty()) { - return path1; - } else { - // check '/' - char lastChar = *path0.rbegin(); - if (lastChar != '/') { - return path0 + std::string("/") + path1; - } else { - return path0 + path1; - } - } - } - - static std::string FindFile(const std::vector &paths, - const std::string &filepath, FsCallbacks *fs) { - if (fs == nullptr || fs->ExpandFilePath == nullptr || - fs->FileExists == nullptr) { - // Error, fs callback[s] missing - return std::string(); - } - - for (size_t i = 0; i < paths.size(); i++) { - std::string absPath = - fs->ExpandFilePath(JoinPath(paths[i], filepath), fs->user_data); - if (fs->FileExists(absPath, fs->user_data)) { - return absPath; - } - } - - return std::string(); - } - - static std::string GetFilePathExtension(const std::string &FileName) { - if (FileName.find_last_of(".") != std::string::npos) - return FileName.substr(FileName.find_last_of(".") + 1); - return ""; - } - - static std::string GetBaseDir(const std::string &filepath) { - if (filepath.find_last_of("/\\") != std::string::npos) - return filepath.substr(0, filepath.find_last_of("/\\")); - return ""; - } - - // https://stackoverflow.com/questions/8520560/get-a-file-name-from-a-path - static std::string GetBaseFilename(const std::string &filepath) { - return filepath.substr(filepath.find_last_of("/\\") + 1); - } - - std::string base64_encode(unsigned char const *, unsigned int len); - std::string base64_decode(std::string const &s); - - /* - base64.cpp and base64.h - - Copyright (C) 2004-2008 René Nyffenegger - - This source code is provided 'as-is', without any express or implied - warranty. In no event will the author be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this source code must not be misrepresented; you must not - claim that you wrote the original source code. If you use this source code - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original source code. - - 3. This notice may not be removed or altered from any source distribution. - - René Nyffenegger rene.nyffenegger@adp-gmbh.ch - - */ - -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wsign-conversion" -#pragma clang diagnostic ignored "-Wconversion" -#endif - - static inline bool is_base64(unsigned char c) { - return (isalnum(c) || (c == '+') || (c == '/')); - } - - std::string base64_encode(unsigned char const *bytes_to_encode, - unsigned int in_len) { - std::string ret; - int i = 0; - int j = 0; - unsigned char char_array_3[3]; - unsigned char char_array_4[4]; - - const char *base64_chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; - - while (in_len--) { - char_array_3[i++] = *(bytes_to_encode++); - if (i == 3) { - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = - ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = - ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (i = 0; (i < 4); i++) ret += base64_chars[char_array_4[i]]; - i = 0; - } - } - - if (i) { - for (j = i; j < 3; j++) char_array_3[j] = '\0'; - - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = - ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = - ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - - for (j = 0; (j < i + 1); j++) ret += base64_chars[char_array_4[j]]; - - while ((i++ < 3)) ret += '='; - } - - return ret; - } - - std::string base64_decode(std::string const &encoded_string) { - int in_len = static_cast(encoded_string.size()); - int i = 0; - int j = 0; - int in_ = 0; - unsigned char char_array_4[4], char_array_3[3]; - std::string ret; - - const std::string base64_chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; - - while (in_len-- && (encoded_string[in_] != '=') && - is_base64(encoded_string[in_])) { - char_array_4[i++] = encoded_string[in_]; - in_++; - if (i == 4) { - for (i = 0; i < 4; i++) - char_array_4[i] = - static_cast(base64_chars.find(char_array_4[i])); - - char_array_3[0] = - (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = - ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (i = 0; (i < 3); i++) ret += char_array_3[i]; - i = 0; - } - } - - if (i) { - for (j = i; j < 4; j++) char_array_4[j] = 0; - - for (j = 0; j < 4; j++) - char_array_4[j] = - static_cast(base64_chars.find(char_array_4[j])); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = - ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; - } - - return ret; - } -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - - // https://github.com/syoyo/tinygltf/issues/228 - // TODO(syoyo): Use uriparser https://uriparser.github.io/ for stricter Uri - // decoding? - // - // https://stackoverflow.com/questions/18307429/encode-decode-url-in-c - // http://dlib.net/dlib/server/server_http.cpp.html - - // --- dlib beign ------------------------------------------------------------ - // Copyright (C) 2003 Davis E. King (davis@dlib.net) - // License: Boost Software License See LICENSE.txt for the full license. - - namespace dlib { - -#if 0 - inline unsigned char to_hex( unsigned char x ) - { - return x + (x > 9 ? ('A'-10) : '0'); - } - - const std::string urlencode( const std::string& s ) - { - std::ostringstream os; - - for ( std::string::const_iterator ci = s.begin(); ci != s.end(); ++ci ) - { - if ( (*ci >= 'a' && *ci <= 'z') || - (*ci >= 'A' && *ci <= 'Z') || - (*ci >= '0' && *ci <= '9') ) - { // allowed - os << *ci; - } - else if ( *ci == ' ') - { - os << '+'; - } - else - { - os << '%' << to_hex(static_cast(*ci >> 4)) << to_hex(static_cast(*ci % 16)); - } - } - - return os.str(); - } -#endif - - inline unsigned char from_hex(unsigned char ch) { - if (ch <= '9' && ch >= '0') - ch -= '0'; - else if (ch <= 'f' && ch >= 'a') - ch -= 'a' - 10; - else if (ch <= 'F' && ch >= 'A') - ch -= 'A' - 10; - else - ch = 0; - return ch; - } - - static const std::string urldecode(const std::string &str) { - using namespace std; - string result; - string::size_type i; - for (i = 0; i < str.size(); ++i) { - if (str[i] == '+') { - result += ' '; - } else if (str[i] == '%' && str.size() > i + 2) { - const unsigned char ch1 = - from_hex(static_cast(str[i + 1])); - const unsigned char ch2 = - from_hex(static_cast(str[i + 2])); - const unsigned char ch = static_cast((ch1 << 4) | ch2); - result += static_cast(ch); - i += 2; - } else { - result += str[i]; - } - } - return result; - } - - } // namespace dlib - // --- dlib end -------------------------------------------------------------- - - static bool LoadExternalFile(std::vector *out, std::string *err, - std::string *warn, const std::string &filename, - const std::string &basedir, bool required, - size_t reqBytes, bool checkSize, FsCallbacks *fs) { - if (fs == nullptr || fs->FileExists == nullptr || - fs->ExpandFilePath == nullptr || fs->ReadWholeFile == nullptr) { - // This is a developer error, assert() ? - if (err) { - (*err) += "FS callback[s] not set\n"; - } - return false; - } - - std::string *failMsgOut = required ? err : warn; - - out->clear(); - - std::vector paths; - paths.push_back(basedir); - paths.push_back("."); - - std::string filepath = FindFile(paths, filename, fs); - if (filepath.empty() || filename.empty()) { - if (failMsgOut) { - (*failMsgOut) += "File not found : " + filename + "\n"; - } - return false; - } - - std::vector buf; - std::string fileReadErr; - bool fileRead = - fs->ReadWholeFile(&buf, &fileReadErr, filepath, fs->user_data); - if (!fileRead) { - if (failMsgOut) { - (*failMsgOut) += - "File read error : " + filepath + " : " + fileReadErr + "\n"; - } - return false; - } - - size_t sz = buf.size(); - if (sz == 0) { - if (failMsgOut) { - (*failMsgOut) += "File is empty : " + filepath + "\n"; - } - return false; - } - - if (checkSize) { - if (reqBytes == sz) { - out->swap(buf); - return true; - } else { - std::stringstream ss; - ss << "File size mismatch : " << filepath << ", requestedBytes " - << reqBytes << ", but got " << sz << std::endl; - if (failMsgOut) { - (*failMsgOut) += ss.str(); - } - return false; - } - } - - out->swap(buf); - return true; - } - - void TinyGLTF::SetImageLoader(LoadImageDataFunction func, void *user_data) { - LoadImageData = func; - load_image_user_data_ = user_data; - } - -#ifndef TINYGLTF_NO_STB_IMAGE - bool LoadImageData(Image *image, const int image_idx, std::string *err, - std::string *warn, int req_width, int req_height, - const unsigned char *bytes, int size, void *user_data) { - (void)user_data; - (void)warn; - - int w = 0, h = 0, comp = 0, req_comp = 0; - - unsigned char *data = nullptr; - - // force 32-bit textures for common Vulkan compatibility. It appears that - // some GPU drivers do not support 24-bit images for Vulkan - req_comp = 4; - int bits = 8; - int pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; - - // It is possible that the image we want to load is a 16bit per channel image - // We are going to attempt to load it as 16bit per channel, and if it worked, - // set the image data accodingly. We are casting the returned pointer into - // unsigned char, because we are representing "bytes". But we are updating - // the Image metadata to signal that this image uses 2 bytes (16bits) per - // channel: - if (stbi_is_16_bit_from_memory(bytes, size)) { - data = reinterpret_cast( - stbi_load_16_from_memory(bytes, size, &w, &h, &comp, req_comp)); - if (data) { - bits = 16; - pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT; - } - } - - // at this point, if data is still NULL, it means that the image wasn't - // 16bit per channel, we are going to load it as a normal 8bit per channel - // mage as we used to do: - // if image cannot be decoded, ignore parsing and keep it by its path - // don't break in this case - // FIXME we should only enter this function if the image is embedded. If - // image->uri references - // an image file, it should be left as it is. Image loading should not be - // mandatory (to support other formats) - if (!data) data = stbi_load_from_memory(bytes, size, &w, &h, &comp, req_comp); - if (!data) { - // NOTE: you can use `warn` instead of `err` - if (err) { - (*err) += - "Unknown image format. STB cannot decode image data for image[" + - std::to_string(image_idx) + "] name = \"" + image->name + "\".\n"; - } - return false; - } - - if ((w < 1) || (h < 1)) { - stbi_image_free(data); - if (err) { - (*err) += "Invalid image data for image[" + std::to_string(image_idx) + - "] name = \"" + image->name + "\"\n"; - } - return false; - } - - if (req_width > 0) { - if (req_width != w) { - stbi_image_free(data); - if (err) { - (*err) += "Image width mismatch for image[" + - std::to_string(image_idx) + "] name = \"" + image->name + - "\"\n"; - } - return false; - } - } - - if (req_height > 0) { - if (req_height != h) { - stbi_image_free(data); - if (err) { - (*err) += "Image height mismatch. for image[" + - std::to_string(image_idx) + "] name = \"" + image->name + - "\"\n"; - } - return false; - } - } - - image->width = w; - image->height = h; - image->component = req_comp; - image->bits = bits; - image->pixel_type = pixel_type; - image->image.resize(static_cast(w * h * req_comp) * size_t(bits / 8)); - std::copy(data, data + w * h * req_comp * (bits / 8), image->image.begin()); - stbi_image_free(data); - - return true; - } -#endif - - void TinyGLTF::SetImageWriter(WriteImageDataFunction func, void *user_data) { - WriteImageData = func; - write_image_user_data_ = user_data; - } - -#ifndef TINYGLTF_NO_STB_IMAGE_WRITE - static void WriteToMemory_stbi(void *context, void *data, int size) { - std::vector *buffer = - reinterpret_cast *>(context); - - unsigned char *pData = reinterpret_cast(data); - - buffer->insert(buffer->end(), pData, pData + size); - } - - bool WriteImageData(const std::string *basepath, const std::string *filename, - Image *image, bool embedImages, void *fsPtr) { - const std::string ext = GetFilePathExtension(*filename); - - // Write image to temporary buffer - std::string header; - std::vector data; - - if (ext == "png") { - if ((image->bits != 8) || - (image->pixel_type != TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE)) { - // Unsupported pixel format - return false; - } - - if (!stbi_write_png_to_func(WriteToMemory_stbi, &data, image->width, - image->height, image->component, - &image->image[0], 0)) { - return false; - } - header = "data:image/png;base64,"; - } else if (ext == "jpg") { - if (!stbi_write_jpg_to_func(WriteToMemory_stbi, &data, image->width, - image->height, image->component, - &image->image[0], 100)) { - return false; - } - header = "data:image/jpeg;base64,"; - } else if (ext == "bmp") { - if (!stbi_write_bmp_to_func(WriteToMemory_stbi, &data, image->width, - image->height, image->component, - &image->image[0])) { - return false; - } - header = "data:image/bmp;base64,"; - } else if (!embedImages) { - // Error: can't output requested format to file - return false; - } - - if (embedImages) { - // Embed base64-encoded image into URI - if (data.size()) { - image->uri = - header + - base64_encode(&data[0], static_cast(data.size())); - } else { - // Throw error? - } - } else { - // Write image to disc - FsCallbacks *fs = reinterpret_cast(fsPtr); - if ((fs != nullptr) && (fs->WriteWholeFile != nullptr)) { - const std::string imagefilepath = JoinPath(*basepath, *filename); - std::string writeError; - if (!fs->WriteWholeFile(&writeError, imagefilepath, data, - fs->user_data)) { - // Could not write image file to disc; Throw error ? - return false; - } - } else { - // Throw error? - } - image->uri = *filename; - } - - return true; - } -#endif - - void TinyGLTF::SetFsCallbacks(FsCallbacks callbacks) { fs = callbacks; } - -#ifdef _WIN32 - static inline std::wstring UTF8ToWchar(const std::string &str) { - int wstr_size = - MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0); - std::wstring wstr(wstr_size, 0); - MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &wstr[0], - (int)wstr.size()); - return wstr; - } - - static inline std::string WcharToUTF8(const std::wstring &wstr) { - int str_size = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), - nullptr, 0, NULL, NULL); - std::string str(str_size, 0); - WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), &str[0], - (int)str.size(), NULL, NULL); - return str; - } -#endif - -#ifndef TINYGLTF_NO_FS - // Default implementations of filesystem functions - - bool FileExists(const std::string &abs_filename, void *) { - bool ret; -#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS - if (asset_manager) { - AAsset *asset = AAssetManager_open(asset_manager, abs_filename.c_str(), - AASSET_MODE_STREAMING); - if (!asset) { - return false; - } - AAsset_close(asset); - ret = true; - } else { - return false; - } -#else -#ifdef _WIN32 -#if defined(_MSC_VER) || defined(__GLIBCXX__) - FILE *fp = nullptr; - errno_t err = _wfopen_s(&fp, UTF8ToWchar(abs_filename).c_str(), L"rb"); - if (err != 0) { - return false; - } -#else - FILE *fp = nullptr; - errno_t err = fopen_s(&fp, abs_filename.c_str(), "rb"); - if (err != 0) { - return false; - } -#endif - -#else - FILE *fp = fopen(abs_filename.c_str(), "rb"); -#endif - if (fp) { - ret = true; - fclose(fp); - } else { - ret = false; - } -#endif - - return ret; - } - - std::string ExpandFilePath(const std::string &filepath, void *) { -#ifdef _WIN32 - // Assume input `filepath` is encoded in UTF-8 - std::wstring wfilepath = UTF8ToWchar(filepath); - DWORD wlen = ExpandEnvironmentStringsW(wfilepath.c_str(), nullptr, 0); - wchar_t *wstr = new wchar_t[wlen]; - ExpandEnvironmentStringsW(wfilepath.c_str(), wstr, wlen); - - std::wstring ws(wstr); - delete[] wstr; - return WcharToUTF8(ws); - -#else - -#if defined(TARGET_OS_IPHONE) || defined(TARGET_IPHONE_SIMULATOR) || \ - defined(__ANDROID__) || defined(__EMSCRIPTEN__) - // no expansion - std::string s = filepath; -#else - std::string s; - wordexp_t p; - - if (filepath.empty()) { - return ""; - } - - // Quote the string to keep any spaces in filepath intact. - std::string quoted_path = "\"" + filepath + "\""; - // char** w; - int ret = wordexp(quoted_path.c_str(), &p, 0); - if (ret) { - // err - s = filepath; - return s; - } - - // Use first element only. - if (p.we_wordv) { - s = std::string(p.we_wordv[0]); - wordfree(&p); - } else { - s = filepath; - } - -#endif - - return s; -#endif - } - - bool ReadWholeFile(std::vector *out, std::string *err, - const std::string &filepath, void *) { -#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS - if (asset_manager) { - AAsset *asset = AAssetManager_open(asset_manager, filepath.c_str(), - AASSET_MODE_STREAMING); - if (!asset) { - if (err) { - (*err) += "File open error : " + filepath + "\n"; - } - return false; - } - size_t size = AAsset_getLength(asset); - if (size == 0) { - if (err) { - (*err) += "Invalid file size : " + filepath + - " (does the path point to a directory?)"; - } - return false; - } - out->resize(size); - AAsset_read(asset, reinterpret_cast(&out->at(0)), size); - AAsset_close(asset); - return true; - } else { - if (err) { - (*err) += "No asset manager specified : " + filepath + "\n"; - } - return false; - } -#else -#ifdef _WIN32 -#if defined(__GLIBCXX__) // mingw - int file_descriptor = - _wopen(UTF8ToWchar(filepath).c_str(), _O_RDONLY | _O_BINARY); - __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, std::ios_base::in); - std::istream f(&wfile_buf); -#elif defined(_MSC_VER) || defined(_LIBCPP_VERSION) - // For libcxx, assume _LIBCPP_HAS_OPEN_WITH_WCHAR is defined to accept - // `wchar_t *` - std::ifstream f(UTF8ToWchar(filepath).c_str(), std::ifstream::binary); -#else - // Unknown compiler/runtime - std::ifstream f(filepath.c_str(), std::ifstream::binary); -#endif -#else - std::ifstream f(filepath.c_str(), std::ifstream::binary); -#endif - if (!f) { - if (err) { - (*err) += "File open error : " + filepath + "\n"; - } - return false; - } - - f.seekg(0, f.end); - size_t sz = static_cast(f.tellg()); - f.seekg(0, f.beg); - - if (int64_t(sz) < 0) { - if (err) { - (*err) += "Invalid file size : " + filepath + - " (does the path point to a directory?)"; - } - return false; - } else if (sz == 0) { - if (err) { - (*err) += "File is empty : " + filepath + "\n"; - } - return false; - } - - out->resize(sz); - f.read(reinterpret_cast(&out->at(0)), - static_cast(sz)); - - return true; -#endif - } - - bool WriteWholeFile(std::string *err, const std::string &filepath, - const std::vector &contents, void *) { -#ifdef _WIN32 -#if defined(__GLIBCXX__) // mingw - int file_descriptor = _wopen(UTF8ToWchar(filepath).c_str(), - _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); - __gnu_cxx::stdio_filebuf wfile_buf( - file_descriptor, std::ios_base::out | std::ios_base::binary); - std::ostream f(&wfile_buf); -#elif defined(_MSC_VER) - std::ofstream f(UTF8ToWchar(filepath).c_str(), std::ofstream::binary); -#else // clang? - std::ofstream f(filepath.c_str(), std::ofstream::binary); -#endif -#else - std::ofstream f(filepath.c_str(), std::ofstream::binary); -#endif - if (!f) { - if (err) { - (*err) += "File open error for writing : " + filepath + "\n"; - } - return false; - } - - f.write(reinterpret_cast(&contents.at(0)), - static_cast(contents.size())); - if (!f) { - if (err) { - (*err) += "File write error: " + filepath + "\n"; - } - return false; - } - - return true; - } - -#endif // TINYGLTF_NO_FS - - static std::string MimeToExt(const std::string &mimeType) { - if (mimeType == "image/jpeg") { - return "jpg"; - } else if (mimeType == "image/png") { - return "png"; - } else if (mimeType == "image/bmp") { - return "bmp"; - } else if (mimeType == "image/gif") { - return "gif"; - } - - return ""; - } - - static void UpdateImageObject(Image &image, std::string &baseDir, int index, - bool embedImages, - WriteImageDataFunction *WriteImageData = nullptr, - void *user_data = nullptr) { - std::string filename; - std::string ext; - // If image has uri, use it it as a filename - if (image.uri.size()) { - filename = GetBaseFilename(image.uri); - ext = GetFilePathExtension(filename); - } else if (image.bufferView != -1) { - // If there's no URI and the data exists in a buffer, - // don't change properties or write images - } else if (image.name.size()) { - ext = MimeToExt(image.mimeType); - // Otherwise use name as filename - filename = image.name + "." + ext; - } else { - ext = MimeToExt(image.mimeType); - // Fallback to index of image as filename - filename = std::to_string(index) + "." + ext; - } - - // If callback is set, modify image data object - if (*WriteImageData != nullptr && !filename.empty()) { - std::string uri; - (*WriteImageData)(&baseDir, &filename, &image, embedImages, user_data); - } - } - - bool IsDataURI(const std::string &in) { - std::string header = "data:application/octet-stream;base64,"; - if (in.find(header) == 0) { - return true; - } - - header = "data:image/jpeg;base64,"; - if (in.find(header) == 0) { - return true; - } - - header = "data:image/png;base64,"; - if (in.find(header) == 0) { - return true; - } - - header = "data:image/bmp;base64,"; - if (in.find(header) == 0) { - return true; - } - - header = "data:image/gif;base64,"; - if (in.find(header) == 0) { - return true; - } - - header = "data:text/plain;base64,"; - if (in.find(header) == 0) { - return true; - } - - header = "data:application/gltf-buffer;base64,"; - if (in.find(header) == 0) { - return true; - } - - return false; - } - - bool DecodeDataURI(std::vector *out, std::string &mime_type, - const std::string &in, size_t reqBytes, bool checkSize) { - std::string header = "data:application/octet-stream;base64,"; - std::string data; - if (in.find(header) == 0) { - data = base64_decode(in.substr(header.size())); // cut mime string. - } - - if (data.empty()) { - header = "data:image/jpeg;base64,"; - if (in.find(header) == 0) { - mime_type = "image/jpeg"; - data = base64_decode(in.substr(header.size())); // cut mime string. - } - } - - if (data.empty()) { - header = "data:image/png;base64,"; - if (in.find(header) == 0) { - mime_type = "image/png"; - data = base64_decode(in.substr(header.size())); // cut mime string. - } - } - - if (data.empty()) { - header = "data:image/bmp;base64,"; - if (in.find(header) == 0) { - mime_type = "image/bmp"; - data = base64_decode(in.substr(header.size())); // cut mime string. - } - } - - if (data.empty()) { - header = "data:image/gif;base64,"; - if (in.find(header) == 0) { - mime_type = "image/gif"; - data = base64_decode(in.substr(header.size())); // cut mime string. - } - } - - if (data.empty()) { - header = "data:text/plain;base64,"; - if (in.find(header) == 0) { - mime_type = "text/plain"; - data = base64_decode(in.substr(header.size())); - } - } - - if (data.empty()) { - header = "data:application/gltf-buffer;base64,"; - if (in.find(header) == 0) { - data = base64_decode(in.substr(header.size())); - } - } - - // TODO(syoyo): Allow empty buffer? #229 - if (data.empty()) { - return false; - } - - if (checkSize) { - if (data.size() != reqBytes) { - return false; - } - out->resize(reqBytes); - } else { - out->resize(data.size()); - } - std::copy(data.begin(), data.end(), out->begin()); - return true; - } - - namespace { - bool GetInt(const json &o, int &val) { -#ifdef TINYGLTF_USE_RAPIDJSON - if (!o.IsDouble()) { - if (o.IsInt()) { - val = o.GetInt(); - return true; - } else if (o.IsUint()) { - val = static_cast(o.GetUint()); - return true; - } else if (o.IsInt64()) { - val = static_cast(o.GetInt64()); - return true; - } else if (o.IsUint64()) { - val = static_cast(o.GetUint64()); - return true; - } - } - - return false; -#else - auto type = o.type(); - - if ((type == json::value_t::number_integer) || - (type == json::value_t::number_unsigned)) { - val = static_cast(o.get()); - return true; - } - - return false; -#endif - } - -#ifdef TINYGLTF_USE_RAPIDJSON - bool GetDouble(const json &o, double &val) { - if (o.IsDouble()) { - val = o.GetDouble(); - return true; - } - - return false; - } -#endif - - bool GetNumber(const json &o, double &val) { -#ifdef TINYGLTF_USE_RAPIDJSON - if (o.IsNumber()) { - val = o.GetDouble(); - return true; - } - - return false; -#else - if (o.is_number()) { - val = o.get(); - return true; - } - - return false; -#endif - } - - bool GetString(const json &o, std::string &val) { -#ifdef TINYGLTF_USE_RAPIDJSON - if (o.IsString()) { - val = o.GetString(); - return true; - } - - return false; -#else - if (o.type() == json::value_t::string) { - val = o.get(); - return true; - } - - return false; -#endif - } - - bool IsArray(const json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - return o.IsArray(); -#else - return o.is_array(); -#endif - } - - json_const_array_iterator ArrayBegin(const json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - return o.Begin(); -#else - return o.begin(); -#endif - } - - json_const_array_iterator ArrayEnd(const json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - return o.End(); -#else - return o.end(); -#endif - } - - bool IsObject(const json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - return o.IsObject(); -#else - return o.is_object(); -#endif - } - - json_const_iterator ObjectBegin(const json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - return o.MemberBegin(); -#else - return o.begin(); -#endif - } - - json_const_iterator ObjectEnd(const json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - return o.MemberEnd(); -#else - return o.end(); -#endif - } - - // Making this a const char* results in a pointer to a temporary when - // TINYGLTF_USE_RAPIDJSON is off. - std::string GetKey(json_const_iterator &it) { -#ifdef TINYGLTF_USE_RAPIDJSON - return it->name.GetString(); -#else - return it.key().c_str(); -#endif - } - - bool FindMember(const json &o, const char *member, json_const_iterator &it) { -#ifdef TINYGLTF_USE_RAPIDJSON - if (!o.IsObject()) { - return false; - } - it = o.FindMember(member); - return it != o.MemberEnd(); -#else - it = o.find(member); - return it != o.end(); -#endif - } - - const json &GetValue(json_const_iterator &it) { -#ifdef TINYGLTF_USE_RAPIDJSON - return it->value; -#else - return it.value(); -#endif - } - - std::string JsonToString(const json &o, int spacing = -1) { -#ifdef TINYGLTF_USE_RAPIDJSON - using namespace rapidjson; - StringBuffer buffer; - if (spacing == -1) { - Writer writer(buffer); - o.Accept(writer); - } else { - PrettyWriter writer(buffer); - writer.SetIndent(' ', uint32_t(spacing)); - o.Accept(writer); - } - return buffer.GetString(); -#else - return o.dump(spacing); -#endif - } - - } // namespace - - static bool ParseJsonAsValue(Value *ret, const json &o) { - Value val{}; -#ifdef TINYGLTF_USE_RAPIDJSON - using rapidjson::Type; - switch (o.GetType()) { - case Type::kObjectType: { - Value::Object value_object; - for (auto it = o.MemberBegin(); it != o.MemberEnd(); ++it) { - Value entry; - ParseJsonAsValue(&entry, it->value); - if (entry.Type() != NULL_TYPE) - value_object.emplace(GetKey(it), std::move(entry)); - } - if (value_object.size() > 0) val = Value(std::move(value_object)); - } break; - case Type::kArrayType: { - Value::Array value_array; - value_array.reserve(o.Size()); - for (auto it = o.Begin(); it != o.End(); ++it) { - Value entry; - ParseJsonAsValue(&entry, *it); - if (entry.Type() != NULL_TYPE) - value_array.emplace_back(std::move(entry)); - } - if (value_array.size() > 0) val = Value(std::move(value_array)); - } break; - case Type::kStringType: - val = Value(std::string(o.GetString())); - break; - case Type::kFalseType: - case Type::kTrueType: - val = Value(o.GetBool()); - break; - case Type::kNumberType: - if (!o.IsDouble()) { - int i = 0; - GetInt(o, i); - val = Value(i); - } else { - double d = 0.0; - GetDouble(o, d); - val = Value(d); - } - break; - case Type::kNullType: - break; - // all types are covered, so no `case default` - } -#else - switch (o.type()) { - case json::value_t::object: { - Value::Object value_object; - for (auto it = o.begin(); it != o.end(); it++) { - Value entry; - ParseJsonAsValue(&entry, it.value()); - if (entry.Type() != NULL_TYPE) - value_object.emplace(it.key(), std::move(entry)); - } - if (value_object.size() > 0) val = Value(std::move(value_object)); - } break; - case json::value_t::array: { - Value::Array value_array; - value_array.reserve(o.size()); - for (auto it = o.begin(); it != o.end(); it++) { - Value entry; - ParseJsonAsValue(&entry, it.value()); - if (entry.Type() != NULL_TYPE) - value_array.emplace_back(std::move(entry)); - } - if (value_array.size() > 0) val = Value(std::move(value_array)); - } break; - case json::value_t::string: - val = Value(o.get()); - break; - case json::value_t::boolean: - val = Value(o.get()); - break; - case json::value_t::number_integer: - case json::value_t::number_unsigned: - val = Value(static_cast(o.get())); - break; - case json::value_t::number_float: - val = Value(o.get()); - break; - case json::value_t::null: - case json::value_t::discarded: - // default: - break; - } -#endif - if (ret) *ret = std::move(val); - - return val.Type() != NULL_TYPE; - } - - static bool ParseExtrasProperty(Value *ret, const json &o) { - json_const_iterator it; - if (!FindMember(o, "extras", it)) { - return false; - } - - return ParseJsonAsValue(ret, GetValue(it)); - } - - static bool ParseBooleanProperty(bool *ret, std::string *err, const json &o, - const std::string &property, - const bool required, - const std::string &parent_node = "") { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - auto &value = GetValue(it); - - bool isBoolean; - bool boolValue = false; -#ifdef TINYGLTF_USE_RAPIDJSON - isBoolean = value.IsBool(); - if (isBoolean) { - boolValue = value.GetBool(); - } -#else - isBoolean = value.is_boolean(); - if (isBoolean) { - boolValue = value.get(); - } -#endif - if (!isBoolean) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not a bool type.\n"; - } - } - return false; - } - - if (ret) { - (*ret) = boolValue; - } - - return true; - } - - static bool ParseIntegerProperty(int *ret, std::string *err, const json &o, - const std::string &property, - const bool required, - const std::string &parent_node = "") { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - int intValue; - bool isInt = GetInt(GetValue(it), intValue); - if (!isInt) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not an integer type.\n"; - } - } - return false; - } - - if (ret) { - (*ret) = intValue; - } - - return true; - } - - static bool ParseUnsignedProperty(size_t *ret, std::string *err, const json &o, - const std::string &property, - const bool required, - const std::string &parent_node = "") { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - auto &value = GetValue(it); - - size_t uValue = 0; - bool isUValue; -#ifdef TINYGLTF_USE_RAPIDJSON - isUValue = false; - if (value.IsUint()) { - uValue = value.GetUint(); - isUValue = true; - } else if (value.IsUint64()) { - uValue = value.GetUint64(); - isUValue = true; - } -#else - isUValue = value.is_number_unsigned(); - if (isUValue) { - uValue = value.get(); - } -#endif - if (!isUValue) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not a positive integer.\n"; - } - } - return false; - } - - if (ret) { - (*ret) = uValue; - } - - return true; - } - - static bool ParseNumberProperty(double *ret, std::string *err, const json &o, - const std::string &property, - const bool required, - const std::string &parent_node = "") { - json_const_iterator it; - - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - double numberValue; - bool isNumber = GetNumber(GetValue(it), numberValue); - - if (!isNumber) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not a number type.\n"; - } - } - return false; - } - - if (ret) { - (*ret) = numberValue; - } - - return true; - } - - static bool ParseNumberArrayProperty(std::vector *ret, std::string *err, - const json &o, const std::string &property, - bool required, - const std::string &parent_node = "") { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - if (!IsArray(GetValue(it))) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not an array"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - ret->clear(); - auto end = ArrayEnd(GetValue(it)); - for (auto i = ArrayBegin(GetValue(it)); i != end; ++i) { - double numberValue; - const bool isNumber = GetNumber(*i, numberValue); - if (!isNumber) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not a number.\n"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - ret->push_back(numberValue); - } - - return true; - } - - static bool ParseIntegerArrayProperty(std::vector *ret, std::string *err, - const json &o, - const std::string &property, - bool required, - const std::string &parent_node = "") { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - if (!IsArray(GetValue(it))) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not an array"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - - ret->clear(); - auto end = ArrayEnd(GetValue(it)); - for (auto i = ArrayBegin(GetValue(it)); i != end; ++i) { - int numberValue; - bool isNumber = GetInt(*i, numberValue); - if (!isNumber) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not an integer type.\n"; - if (!parent_node.empty()) { - (*err) += " in " + parent_node; - } - (*err) += ".\n"; - } - } - return false; - } - ret->push_back(numberValue); - } - - return true; - } - - static bool ParseStringProperty( - std::string *ret, std::string *err, const json &o, - const std::string &property, bool required, - const std::string &parent_node = std::string()) { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing"; - if (parent_node.empty()) { - (*err) += ".\n"; - } else { - (*err) += " in `" + parent_node + "'.\n"; - } - } - } - return false; - } - - std::string strValue; - if (!GetString(GetValue(it), strValue)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not a string type.\n"; - } - } - return false; - } - - if (ret) { - (*ret) = std::move(strValue); - } - - return true; - } - - static bool ParseStringIntegerProperty(std::map *ret, - std::string *err, const json &o, - const std::string &property, - bool required, - const std::string &parent = "") { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - if (!parent.empty()) { - (*err) += - "'" + property + "' property is missing in " + parent + ".\n"; - } else { - (*err) += "'" + property + "' property is missing.\n"; - } - } - } - return false; - } - - const json &dict = GetValue(it); - - // Make sure we are dealing with an object / dictionary. - if (!IsObject(dict)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not an object.\n"; - } - } - return false; - } - - ret->clear(); - - json_const_iterator dictIt(ObjectBegin(dict)); - json_const_iterator dictItEnd(ObjectEnd(dict)); - - for (; dictIt != dictItEnd; ++dictIt) { - int intVal; - if (!GetInt(GetValue(dictIt), intVal)) { - if (required) { - if (err) { - (*err) += "'" + property + "' value is not an integer type.\n"; - } - } - return false; - } - - // Insert into the list. - (*ret)[GetKey(dictIt)] = intVal; - } - return true; - } - - static bool ParseJSONProperty(std::map *ret, - std::string *err, const json &o, - const std::string &property, bool required) { - json_const_iterator it; - if (!FindMember(o, property.c_str(), it)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is missing. \n'"; - } - } - return false; - } - - const json &obj = GetValue(it); - - if (!IsObject(obj)) { - if (required) { - if (err) { - (*err) += "'" + property + "' property is not a JSON object.\n"; - } - } - return false; - } - - ret->clear(); - - json_const_iterator it2(ObjectBegin(obj)); - json_const_iterator itEnd(ObjectEnd(obj)); - for (; it2 != itEnd; ++it2) { - double numVal; - if (GetNumber(GetValue(it2), numVal)) - ret->emplace(std::string(GetKey(it2)), numVal); - } - - return true; - } - - static bool ParseParameterProperty(Parameter *param, std::string *err, - const json &o, const std::string &prop, - bool required) { - // A parameter value can either be a string or an array of either a boolean or - // a number. Booleans of any kind aren't supported here. Granted, it - // complicates the Parameter structure and breaks it semantically in the sense - // that the client probably works off the assumption that if the string is - // empty the vector is used, etc. Would a tagged union work? - if (ParseStringProperty(¶m->string_value, err, o, prop, false)) { - // Found string property. - return true; - } else if (ParseNumberArrayProperty(¶m->number_array, err, o, prop, - false)) { - // Found a number array. - return true; - } else if (ParseNumberProperty(¶m->number_value, err, o, prop, false)) { - return param->has_number_value = true; - } else if (ParseJSONProperty(¶m->json_double_value, err, o, prop, - false)) { - return true; - } else if (ParseBooleanProperty(¶m->bool_value, err, o, prop, false)) { - return true; - } else { - if (required) { - if (err) { - (*err) += "parameter must be a string or number / number array.\n"; - } - } - return false; - } - } - - static bool ParseExtensionsProperty(ExtensionMap *ret, std::string *err, - const json &o) { - (void)err; - - json_const_iterator it; - if (!FindMember(o, "extensions", it)) { - return false; - } - - auto &obj = GetValue(it); - if (!IsObject(obj)) { - return false; - } - ExtensionMap extensions; - json_const_iterator extIt = ObjectBegin(obj); // it.value().begin(); - json_const_iterator extEnd = ObjectEnd(obj); - for (; extIt != extEnd; ++extIt) { - auto &itObj = GetValue(extIt); - if (!IsObject(itObj)) continue; - std::string key(GetKey(extIt)); - if (!ParseJsonAsValue(&extensions[key], itObj)) { - if (!key.empty()) { - // create empty object so that an extension object is still of type - // object - extensions[key] = Value{Value::Object{}}; - } - } - } - if (ret) { - (*ret) = std::move(extensions); - } - return true; - } - - static bool ParseAsset(Asset *asset, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - ParseStringProperty(&asset->version, err, o, "version", true, "Asset"); - ParseStringProperty(&asset->generator, err, o, "generator", false, "Asset"); - ParseStringProperty(&asset->minVersion, err, o, "minVersion", false, "Asset"); - ParseStringProperty(&asset->copyright, err, o, "copyright", false, "Asset"); - - ParseExtensionsProperty(&asset->extensions, err, o); - - // Unity exporter version is added as extra here - ParseExtrasProperty(&(asset->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - asset->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - asset->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseImage(Image *image, const int image_idx, std::string *err, - std::string *warn, const json &o, - bool store_original_json_for_extras_and_extensions, - const std::string &basedir, FsCallbacks *fs, - LoadImageDataFunction *LoadImageData = nullptr, - void *load_image_user_data = nullptr) { - // A glTF image must either reference a bufferView or an image uri - - // schema says oneOf [`bufferView`, `uri`] - // TODO(syoyo): Check the type of each parameters. - json_const_iterator it; - bool hasBufferView = FindMember(o, "bufferView", it); - bool hasURI = FindMember(o, "uri", it); - - ParseStringProperty(&image->name, err, o, "name", false); - - if (hasBufferView && hasURI) { - // Should not both defined. - if (err) { - (*err) += - "Only one of `bufferView` or `uri` should be defined, but both are " - "defined for image[" + - std::to_string(image_idx) + "] name = \"" + image->name + "\"\n"; - } - return false; - } - - if (!hasBufferView && !hasURI) { - if (err) { - (*err) += "Neither required `bufferView` nor `uri` defined for image[" + - std::to_string(image_idx) + "] name = \"" + image->name + - "\"\n"; - } - return false; - } - - ParseExtensionsProperty(&image->extensions, err, o); - ParseExtrasProperty(&image->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator eit; - if (FindMember(o, "extensions", eit)) { - image->extensions_json_string = JsonToString(GetValue(eit)); - } - } - { - json_const_iterator eit; - if (FindMember(o, "extras", eit)) { - image->extras_json_string = JsonToString(GetValue(eit)); - } - } - } - - if (hasBufferView) { - int bufferView = -1; - if (!ParseIntegerProperty(&bufferView, err, o, "bufferView", true)) { - if (err) { - (*err) += "Failed to parse `bufferView` for image[" + - std::to_string(image_idx) + "] name = \"" + image->name + - "\"\n"; - } - return false; - } - - std::string mime_type; - ParseStringProperty(&mime_type, err, o, "mimeType", false); - - int width = 0; - ParseIntegerProperty(&width, err, o, "width", false); - - int height = 0; - ParseIntegerProperty(&height, err, o, "height", false); - - // Just only save some information here. Loading actual image data from - // bufferView is done after this `ParseImage` function. - image->bufferView = bufferView; - image->mimeType = mime_type; - image->width = width; - image->height = height; - - return true; - } - - // Parse URI & Load image data. - - std::string uri; - std::string tmp_err; - if (!ParseStringProperty(&uri, &tmp_err, o, "uri", true)) { - if (err) { - (*err) += "Failed to parse `uri` for image[" + std::to_string(image_idx) + - "] name = \"" + image->name + "\".\n"; - } - return false; - } - - std::vector img; - - if (IsDataURI(uri)) { - if (!DecodeDataURI(&img, image->mimeType, uri, 0, false)) { - if (err) { - (*err) += "Failed to decode 'uri' for image[" + - std::to_string(image_idx) + "] name = [" + image->name + - "]\n"; - } - return false; - } - } else { - // Assume external file - // Keep texture path (for textures that cannot be decoded) - image->uri = uri; -#ifdef TINYGLTF_NO_EXTERNAL_IMAGE - return true; -#endif - std::string decoded_uri = dlib::urldecode(uri); - if (!LoadExternalFile(&img, err, warn, decoded_uri, basedir, - /* required */ false, /* required bytes */ 0, - /* checksize */ false, fs)) { - if (warn) { - (*warn) += "Failed to load external 'uri' for image[" + - std::to_string(image_idx) + "] name = [" + image->name + - "]\n"; - } - // If the image cannot be loaded, keep uri as image->uri. - return true; - } - - if (img.empty()) { - if (warn) { - (*warn) += "Image data is empty for image[" + - std::to_string(image_idx) + "] name = [" + image->name + - "] \n"; - } - return false; - } - } - - if (*LoadImageData == nullptr) { - if (err) { - (*err) += "No LoadImageData callback specified.\n"; - } - return false; - } - return (*LoadImageData)(image, image_idx, err, warn, 0, 0, &img.at(0), - static_cast(img.size()), load_image_user_data); - } - - static bool ParseTexture(Texture *texture, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions, - const std::string &basedir) { - (void)basedir; - int sampler = -1; - int source = -1; - ParseIntegerProperty(&sampler, err, o, "sampler", false); - - ParseIntegerProperty(&source, err, o, "source", false); - - texture->sampler = sampler; - texture->source = source; - - ParseExtensionsProperty(&texture->extensions, err, o); - ParseExtrasProperty(&texture->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - texture->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - texture->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - ParseStringProperty(&texture->name, err, o, "name", false); - - return true; - } - - static bool ParseTextureInfo( - TextureInfo *texinfo, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - if (texinfo == nullptr) { - return false; - } - - if (!ParseIntegerProperty(&texinfo->index, err, o, "index", - /* required */ true, "TextureInfo")) { - return false; - } - - ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); - - ParseExtensionsProperty(&texinfo->extensions, err, o); - ParseExtrasProperty(&texinfo->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - texinfo->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - texinfo->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseNormalTextureInfo( - NormalTextureInfo *texinfo, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - if (texinfo == nullptr) { - return false; - } - - if (!ParseIntegerProperty(&texinfo->index, err, o, "index", - /* required */ true, "NormalTextureInfo")) { - return false; - } - - ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); - ParseNumberProperty(&texinfo->scale, err, o, "scale", false); - - ParseExtensionsProperty(&texinfo->extensions, err, o); - ParseExtrasProperty(&texinfo->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - texinfo->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - texinfo->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseOcclusionTextureInfo( - OcclusionTextureInfo *texinfo, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - if (texinfo == nullptr) { - return false; - } - - if (!ParseIntegerProperty(&texinfo->index, err, o, "index", - /* required */ true, "NormalTextureInfo")) { - return false; - } - - ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); - ParseNumberProperty(&texinfo->strength, err, o, "strength", false); - - ParseExtensionsProperty(&texinfo->extensions, err, o); - ParseExtrasProperty(&texinfo->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - texinfo->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - texinfo->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseBuffer(Buffer *buffer, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions, - FsCallbacks *fs, const std::string &basedir, - bool is_binary = false, - const unsigned char *bin_data = nullptr, - size_t bin_size = 0) { - size_t byteLength; - if (!ParseUnsignedProperty(&byteLength, err, o, "byteLength", true, - "Buffer")) { - return false; - } - - // In glTF 2.0, uri is not mandatory anymore - buffer->uri.clear(); - ParseStringProperty(&buffer->uri, err, o, "uri", false, "Buffer"); - - // having an empty uri for a non embedded image should not be valid - if (!is_binary && buffer->uri.empty()) { - if (err) { - (*err) += "'uri' is missing from non binary glTF file buffer.\n"; - } - } - - json_const_iterator type; - if (FindMember(o, "type", type)) { - std::string typeStr; - if (GetString(GetValue(type), typeStr)) { - if (typeStr.compare("arraybuffer") == 0) { - // buffer.type = "arraybuffer"; - } - } - } - - if (is_binary) { - // Still binary glTF accepts external dataURI. - if (!buffer->uri.empty()) { - // First try embedded data URI. - if (IsDataURI(buffer->uri)) { - std::string mime_type; - if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, byteLength, - true)) { - if (err) { - (*err) += - "Failed to decode 'uri' : " + buffer->uri + " in Buffer\n"; - } - return false; - } - } else { - // External .bin file. - std::string decoded_uri = dlib::urldecode(buffer->uri); - if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, - decoded_uri, basedir, /* required */ true, - byteLength, /* checkSize */ true, fs)) { - return false; - } - } - } else { - // load data from (embedded) binary data - - if ((bin_size == 0) || (bin_data == nullptr)) { - if (err) { - (*err) += "Invalid binary data in `Buffer'.\n"; - } - return false; - } - - if (byteLength > bin_size) { - if (err) { - std::stringstream ss; - ss << "Invalid `byteLength'. Must be equal or less than binary size: " - "`byteLength' = " - << byteLength << ", binary size = " << bin_size << std::endl; - (*err) += ss.str(); - } - return false; - } - - // Read buffer data - buffer->data.resize(static_cast(byteLength)); - memcpy(&(buffer->data.at(0)), bin_data, static_cast(byteLength)); - } - - } else { - if (IsDataURI(buffer->uri)) { - std::string mime_type; - if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, byteLength, - true)) { - if (err) { - (*err) += "Failed to decode 'uri' : " + buffer->uri + " in Buffer\n"; - } - return false; - } - } else { - // Assume external .bin file. - std::string decoded_uri = dlib::urldecode(buffer->uri); - if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, decoded_uri, - basedir, /* required */ true, byteLength, - /* checkSize */ true, fs)) { - return false; - } - } - } - - ParseStringProperty(&buffer->name, err, o, "name", false); - - ParseExtensionsProperty(&buffer->extensions, err, o); - ParseExtrasProperty(&buffer->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - buffer->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - buffer->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseBufferView( - BufferView *bufferView, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - int buffer = -1; - if (!ParseIntegerProperty(&buffer, err, o, "buffer", true, "BufferView")) { - return false; - } - - size_t byteOffset = 0; - ParseUnsignedProperty(&byteOffset, err, o, "byteOffset", false); - - size_t byteLength = 1; - if (!ParseUnsignedProperty(&byteLength, err, o, "byteLength", true, - "BufferView")) { - return false; - } - - size_t byteStride = 0; - if (!ParseUnsignedProperty(&byteStride, err, o, "byteStride", false)) { - // Spec says: When byteStride of referenced bufferView is not defined, it - // means that accessor elements are tightly packed, i.e., effective stride - // equals the size of the element. - // We cannot determine the actual byteStride until Accessor are parsed, thus - // set 0(= tightly packed) here(as done in OpenGL's VertexAttribPoiner) - byteStride = 0; - } - - if ((byteStride > 252) || ((byteStride % 4) != 0)) { - if (err) { - std::stringstream ss; - ss << "Invalid `byteStride' value. `byteStride' must be the multiple of " - "4 : " - << byteStride << std::endl; - - (*err) += ss.str(); - } - return false; - } - - int target = 0; - ParseIntegerProperty(&target, err, o, "target", false); - if ((target == TINYGLTF_TARGET_ARRAY_BUFFER) || - (target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER)) { - // OK - } else { - target = 0; - } - bufferView->target = target; - - ParseStringProperty(&bufferView->name, err, o, "name", false); - - ParseExtensionsProperty(&bufferView->extensions, err, o); - ParseExtrasProperty(&bufferView->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - bufferView->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - bufferView->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - bufferView->buffer = buffer; - bufferView->byteOffset = byteOffset; - bufferView->byteLength = byteLength; - bufferView->byteStride = byteStride; - return true; - } - - static bool ParseSparseAccessor(Accessor *accessor, std::string *err, - const json &o) { - accessor->sparse.isSparse = true; - - int count = 0; - ParseIntegerProperty(&count, err, o, "count", true); - - json_const_iterator indices_iterator; - json_const_iterator values_iterator; - if (!FindMember(o, "indices", indices_iterator)) { - (*err) = "the sparse object of this accessor doesn't have indices"; - return false; - } - - if (!FindMember(o, "values", values_iterator)) { - (*err) = "the sparse object ob ths accessor doesn't have values"; - return false; - } - - const json &indices_obj = GetValue(indices_iterator); - const json &values_obj = GetValue(values_iterator); - - int indices_buffer_view = 0, indices_byte_offset = 0, component_type = 0; - ParseIntegerProperty(&indices_buffer_view, err, indices_obj, "bufferView", - true); - ParseIntegerProperty(&indices_byte_offset, err, indices_obj, "byteOffset", - true); - ParseIntegerProperty(&component_type, err, indices_obj, "componentType", - true); - - int values_buffer_view = 0, values_byte_offset = 0; - ParseIntegerProperty(&values_buffer_view, err, values_obj, "bufferView", - true); - ParseIntegerProperty(&values_byte_offset, err, values_obj, "byteOffset", - true); - - accessor->sparse.count = count; - accessor->sparse.indices.bufferView = indices_buffer_view; - accessor->sparse.indices.byteOffset = indices_byte_offset; - accessor->sparse.indices.componentType = component_type; - accessor->sparse.values.bufferView = values_buffer_view; - accessor->sparse.values.byteOffset = values_byte_offset; - - // todo check theses values - - return true; - } - - static bool ParseAccessor(Accessor *accessor, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - int bufferView = -1; - ParseIntegerProperty(&bufferView, err, o, "bufferView", false, "Accessor"); - - size_t byteOffset = 0; - ParseUnsignedProperty(&byteOffset, err, o, "byteOffset", false, "Accessor"); - - bool normalized = false; - ParseBooleanProperty(&normalized, err, o, "normalized", false, "Accessor"); - - size_t componentType = 0; - if (!ParseUnsignedProperty(&componentType, err, o, "componentType", true, - "Accessor")) { - return false; - } - - size_t count = 0; - if (!ParseUnsignedProperty(&count, err, o, "count", true, "Accessor")) { - return false; - } - - std::string type; - if (!ParseStringProperty(&type, err, o, "type", true, "Accessor")) { - return false; - } - - if (type.compare("SCALAR") == 0) { - accessor->type = TINYGLTF_TYPE_SCALAR; - } else if (type.compare("VEC2") == 0) { - accessor->type = TINYGLTF_TYPE_VEC2; - } else if (type.compare("VEC3") == 0) { - accessor->type = TINYGLTF_TYPE_VEC3; - } else if (type.compare("VEC4") == 0) { - accessor->type = TINYGLTF_TYPE_VEC4; - } else if (type.compare("MAT2") == 0) { - accessor->type = TINYGLTF_TYPE_MAT2; - } else if (type.compare("MAT3") == 0) { - accessor->type = TINYGLTF_TYPE_MAT3; - } else if (type.compare("MAT4") == 0) { - accessor->type = TINYGLTF_TYPE_MAT4; - } else { - std::stringstream ss; - ss << "Unsupported `type` for accessor object. Got \"" << type << "\"\n"; - if (err) { - (*err) += ss.str(); - } - return false; - } - - ParseStringProperty(&accessor->name, err, o, "name", false); - - accessor->minValues.clear(); - accessor->maxValues.clear(); - ParseNumberArrayProperty(&accessor->minValues, err, o, "min", false, - "Accessor"); - - ParseNumberArrayProperty(&accessor->maxValues, err, o, "max", false, - "Accessor"); - - accessor->count = count; - accessor->bufferView = bufferView; - accessor->byteOffset = byteOffset; - accessor->normalized = normalized; - { - if (componentType >= TINYGLTF_COMPONENT_TYPE_BYTE && - componentType <= TINYGLTF_COMPONENT_TYPE_DOUBLE) { - // OK - accessor->componentType = int(componentType); - } else { - std::stringstream ss; - ss << "Invalid `componentType` in accessor. Got " << componentType - << "\n"; - if (err) { - (*err) += ss.str(); - } - return false; - } - } - - ParseExtensionsProperty(&(accessor->extensions), err, o); - ParseExtrasProperty(&(accessor->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - accessor->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - accessor->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - // check if accessor has a "sparse" object: - json_const_iterator iterator; - if (FindMember(o, "sparse", iterator)) { - // here this accessor has a "sparse" subobject - return ParseSparseAccessor(accessor, err, GetValue(iterator)); - } - - return true; - } - -#ifdef TINYGLTF_ENABLE_DRACO - - static void DecodeIndexBuffer(draco::Mesh *mesh, size_t componentSize, - std::vector &outBuffer) { - if (componentSize == 4) { - assert(sizeof(mesh->face(draco::FaceIndex(0))[0]) == componentSize); - memcpy(outBuffer.data(), &mesh->face(draco::FaceIndex(0))[0], - outBuffer.size()); - } else { - size_t faceStride = componentSize * 3; - for (draco::FaceIndex f(0); f < mesh->num_faces(); ++f) { - const draco::Mesh::Face &face = mesh->face(f); - if (componentSize == 2) { - uint16_t indices[3] = {(uint16_t)face[0].value(), - (uint16_t)face[1].value(), - (uint16_t)face[2].value()}; - memcpy(outBuffer.data() + f.value() * faceStride, &indices[0], - faceStride); - } else { - uint8_t indices[3] = {(uint8_t)face[0].value(), - (uint8_t)face[1].value(), - (uint8_t)face[2].value()}; - memcpy(outBuffer.data() + f.value() * faceStride, &indices[0], - faceStride); - } - } - } - } - - template - static bool GetAttributeForAllPoints(draco::Mesh *mesh, - const draco::PointAttribute *pAttribute, - std::vector &outBuffer) { - size_t byteOffset = 0; - T values[4] = {0, 0, 0, 0}; - for (draco::PointIndex i(0); i < mesh->num_points(); ++i) { - const draco::AttributeValueIndex val_index = pAttribute->mapped_index(i); - if (!pAttribute->ConvertValue(val_index, pAttribute->num_components(), - values)) - return false; - - memcpy(outBuffer.data() + byteOffset, &values[0], - sizeof(T) * pAttribute->num_components()); - byteOffset += sizeof(T) * pAttribute->num_components(); - } - - return true; - } - - static bool GetAttributeForAllPoints(uint32_t componentType, draco::Mesh *mesh, - const draco::PointAttribute *pAttribute, - std::vector &outBuffer) { - bool decodeResult = false; - switch (componentType) { - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - case TINYGLTF_COMPONENT_TYPE_BYTE: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - case TINYGLTF_COMPONENT_TYPE_SHORT: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - case TINYGLTF_COMPONENT_TYPE_INT: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - case TINYGLTF_COMPONENT_TYPE_FLOAT: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - case TINYGLTF_COMPONENT_TYPE_DOUBLE: - decodeResult = - GetAttributeForAllPoints(mesh, pAttribute, outBuffer); - break; - default: - return false; - } - - return decodeResult; - } - - static bool ParseDracoExtension(Primitive *primitive, Model *model, - std::string *err, - const Value &dracoExtensionValue) { - auto bufferViewValue = dracoExtensionValue.Get("bufferView"); - if (!bufferViewValue.IsInt()) return false; - auto attributesValue = dracoExtensionValue.Get("attributes"); - if (!attributesValue.IsObject()) return false; - - auto attributesObject = attributesValue.Get(); - int bufferView = bufferViewValue.Get(); - - BufferView &view = model->bufferViews[bufferView]; - Buffer &buffer = model->buffers[view.buffer]; - // BufferView has already been decoded - if (view.dracoDecoded) return true; - view.dracoDecoded = true; - - const char *bufferViewData = - reinterpret_cast(buffer.data.data() + view.byteOffset); - size_t bufferViewSize = view.byteLength; - - // decode draco - draco::DecoderBuffer decoderBuffer; - decoderBuffer.Init(bufferViewData, bufferViewSize); - draco::Decoder decoder; - auto decodeResult = decoder.DecodeMeshFromBuffer(&decoderBuffer); - if (!decodeResult.ok()) { - return false; - } - const std::unique_ptr &mesh = decodeResult.value(); - - // create new bufferView for indices - if (primitive->indices >= 0) { - int32_t componentSize = GetComponentSizeInBytes( - model->accessors[primitive->indices].componentType); - Buffer decodedIndexBuffer; - decodedIndexBuffer.data.resize(mesh->num_faces() * 3 * componentSize); - - DecodeIndexBuffer(mesh.get(), componentSize, decodedIndexBuffer.data); - - model->buffers.emplace_back(std::move(decodedIndexBuffer)); - - BufferView decodedIndexBufferView; - decodedIndexBufferView.buffer = int(model->buffers.size() - 1); - decodedIndexBufferView.byteLength = - int(mesh->num_faces() * 3 * componentSize); - decodedIndexBufferView.byteOffset = 0; - decodedIndexBufferView.byteStride = 0; - decodedIndexBufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER; - model->bufferViews.emplace_back(std::move(decodedIndexBufferView)); - - model->accessors[primitive->indices].bufferView = - int(model->bufferViews.size() - 1); - model->accessors[primitive->indices].count = int(mesh->num_faces() * 3); - } - - for (const auto &attribute : attributesObject) { - if (!attribute.second.IsInt()) return false; - auto primitiveAttribute = primitive->attributes.find(attribute.first); - if (primitiveAttribute == primitive->attributes.end()) return false; - - int dracoAttributeIndex = attribute.second.Get(); - const auto pAttribute = mesh->GetAttributeByUniqueId(dracoAttributeIndex); - const auto pBuffer = pAttribute->buffer(); - const auto componentType = - model->accessors[primitiveAttribute->second].componentType; - - // Create a new buffer for this decoded buffer - Buffer decodedBuffer; - size_t bufferSize = mesh->num_points() * pAttribute->num_components() * - GetComponentSizeInBytes(componentType); - decodedBuffer.data.resize(bufferSize); - - if (!GetAttributeForAllPoints(componentType, mesh.get(), pAttribute, - decodedBuffer.data)) - return false; - - model->buffers.emplace_back(std::move(decodedBuffer)); - - BufferView decodedBufferView; - decodedBufferView.buffer = int(model->buffers.size() - 1); - decodedBufferView.byteLength = bufferSize; - decodedBufferView.byteOffset = pAttribute->byte_offset(); - decodedBufferView.byteStride = pAttribute->byte_stride(); - decodedBufferView.target = primitive->indices >= 0 - ? TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER - : TINYGLTF_TARGET_ARRAY_BUFFER; - model->bufferViews.emplace_back(std::move(decodedBufferView)); - - model->accessors[primitiveAttribute->second].bufferView = - int(model->bufferViews.size() - 1); - model->accessors[primitiveAttribute->second].count = - int(mesh->num_points()); - } - - return true; - } -#endif - - static bool ParsePrimitive(Primitive *primitive, Model *model, std::string *err, - const json &o, - bool store_original_json_for_extras_and_extensions) { - int material = -1; - ParseIntegerProperty(&material, err, o, "material", false); - primitive->material = material; - - int mode = TINYGLTF_MODE_TRIANGLES; - ParseIntegerProperty(&mode, err, o, "mode", false); - primitive->mode = mode; // Why only triangled were supported ? - - int indices = -1; - ParseIntegerProperty(&indices, err, o, "indices", false); - primitive->indices = indices; - if (!ParseStringIntegerProperty(&primitive->attributes, err, o, "attributes", - true, "Primitive")) { - return false; - } - - // Look for morph targets - json_const_iterator targetsObject; - if (FindMember(o, "targets", targetsObject) && - IsArray(GetValue(targetsObject))) { - auto targetsObjectEnd = ArrayEnd(GetValue(targetsObject)); - for (json_const_array_iterator i = ArrayBegin(GetValue(targetsObject)); - i != targetsObjectEnd; ++i) { - std::map targetAttribues; - - const json &dict = *i; - if (IsObject(dict)) { - json_const_iterator dictIt(ObjectBegin(dict)); - json_const_iterator dictItEnd(ObjectEnd(dict)); - - for (; dictIt != dictItEnd; ++dictIt) { - int iVal; - if (GetInt(GetValue(dictIt), iVal)) - targetAttribues[GetKey(dictIt)] = iVal; - } - primitive->targets.emplace_back(std::move(targetAttribues)); - } - } - } - - ParseExtrasProperty(&(primitive->extras), o); - ParseExtensionsProperty(&primitive->extensions, err, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - primitive->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - primitive->extras_json_string = JsonToString(GetValue(it)); - } - } - } - -#ifdef TINYGLTF_ENABLE_DRACO - auto dracoExtension = - primitive->extensions.find("KHR_draco_mesh_compression"); - if (dracoExtension != primitive->extensions.end()) { - ParseDracoExtension(primitive, model, err, dracoExtension->second); - } -#else - (void)model; -#endif - - return true; - } - - static bool ParseMesh(Mesh *mesh, Model *model, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - ParseStringProperty(&mesh->name, err, o, "name", false); - - mesh->primitives.clear(); - json_const_iterator primObject; - if (FindMember(o, "primitives", primObject) && - IsArray(GetValue(primObject))) { - json_const_array_iterator primEnd = ArrayEnd(GetValue(primObject)); - for (json_const_array_iterator i = ArrayBegin(GetValue(primObject)); - i != primEnd; ++i) { - Primitive primitive; - if (ParsePrimitive(&primitive, model, err, *i, - store_original_json_for_extras_and_extensions)) { - // Only add the primitive if the parsing succeeds. - mesh->primitives.emplace_back(std::move(primitive)); - } - } - } - - // Should probably check if has targets and if dimensions fit - ParseNumberArrayProperty(&mesh->weights, err, o, "weights", false); - - ParseExtensionsProperty(&mesh->extensions, err, o); - ParseExtrasProperty(&(mesh->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - mesh->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - mesh->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseNode(Node *node, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - ParseStringProperty(&node->name, err, o, "name", false); - - int skin = -1; - ParseIntegerProperty(&skin, err, o, "skin", false); - node->skin = skin; - - // Matrix and T/R/S are exclusive - if (!ParseNumberArrayProperty(&node->matrix, err, o, "matrix", false)) { - ParseNumberArrayProperty(&node->rotation, err, o, "rotation", false); - ParseNumberArrayProperty(&node->scale, err, o, "scale", false); - ParseNumberArrayProperty(&node->translation, err, o, "translation", false); - } - - int camera = -1; - ParseIntegerProperty(&camera, err, o, "camera", false); - node->camera = camera; - - int mesh = -1; - ParseIntegerProperty(&mesh, err, o, "mesh", false); - node->mesh = mesh; - - node->children.clear(); - ParseIntegerArrayProperty(&node->children, err, o, "children", false); - - ParseNumberArrayProperty(&node->weights, err, o, "weights", false); - - ParseExtensionsProperty(&node->extensions, err, o); - ParseExtrasProperty(&(node->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - node->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - node->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParsePbrMetallicRoughness( - PbrMetallicRoughness *pbr, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - if (pbr == nullptr) { - return false; - } - - std::vector baseColorFactor; - if (ParseNumberArrayProperty(&baseColorFactor, err, o, "baseColorFactor", - /* required */ false)) { - if (baseColorFactor.size() != 4) { - if (err) { - (*err) += - "Array length of `baseColorFactor` parameter in " - "pbrMetallicRoughness must be 4, but got " + - std::to_string(baseColorFactor.size()) + "\n"; - } - return false; - } - pbr->baseColorFactor = baseColorFactor; - } - - { - json_const_iterator it; - if (FindMember(o, "baseColorTexture", it)) { - ParseTextureInfo(&pbr->baseColorTexture, err, GetValue(it), - store_original_json_for_extras_and_extensions); - } - } - - { - json_const_iterator it; - if (FindMember(o, "metallicRoughnessTexture", it)) { - ParseTextureInfo(&pbr->metallicRoughnessTexture, err, GetValue(it), - store_original_json_for_extras_and_extensions); - } - } - - ParseNumberProperty(&pbr->metallicFactor, err, o, "metallicFactor", false); - ParseNumberProperty(&pbr->roughnessFactor, err, o, "roughnessFactor", false); - - ParseExtensionsProperty(&pbr->extensions, err, o); - ParseExtrasProperty(&pbr->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - pbr->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - pbr->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseMaterial(Material *material, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - ParseStringProperty(&material->name, err, o, "name", /* required */ false); - - if (ParseNumberArrayProperty(&material->emissiveFactor, err, o, - "emissiveFactor", - /* required */ false)) { - if (material->emissiveFactor.size() != 3) { - if (err) { - (*err) += - "Array length of `emissiveFactor` parameter in " - "material must be 3, but got " + - std::to_string(material->emissiveFactor.size()) + "\n"; - } - return false; - } - } else { - // fill with default values - material->emissiveFactor = {0.0, 0.0, 0.0}; - } - - ParseStringProperty(&material->alphaMode, err, o, "alphaMode", - /* required */ false); - ParseNumberProperty(&material->alphaCutoff, err, o, "alphaCutoff", - /* required */ false); - ParseBooleanProperty(&material->doubleSided, err, o, "doubleSided", - /* required */ false); - - { - json_const_iterator it; - if (FindMember(o, "pbrMetallicRoughness", it)) { - ParsePbrMetallicRoughness(&material->pbrMetallicRoughness, err, - GetValue(it), - store_original_json_for_extras_and_extensions); - } - } - - { - json_const_iterator it; - if (FindMember(o, "normalTexture", it)) { - ParseNormalTextureInfo(&material->normalTexture, err, GetValue(it), - store_original_json_for_extras_and_extensions); - } - } - - { - json_const_iterator it; - if (FindMember(o, "occlusionTexture", it)) { - ParseOcclusionTextureInfo(&material->occlusionTexture, err, GetValue(it), - store_original_json_for_extras_and_extensions); - } - } - - { - json_const_iterator it; - if (FindMember(o, "emissiveTexture", it)) { - ParseTextureInfo(&material->emissiveTexture, err, GetValue(it), - store_original_json_for_extras_and_extensions); - } - } - - // Old code path. For backward compatibility, we still store material values - // as Parameter. This will create duplicated information for - // example(pbrMetallicRoughness), but should be neglible in terms of memory - // consumption. - // TODO(syoyo): Remove in the next major release. - material->values.clear(); - material->additionalValues.clear(); - - json_const_iterator it(ObjectBegin(o)); - json_const_iterator itEnd(ObjectEnd(o)); - - for (; it != itEnd; ++it) { - std::string key(GetKey(it)); - if (key == "pbrMetallicRoughness") { - if (IsObject(GetValue(it))) { - const json &values_object = GetValue(it); - - json_const_iterator itVal(ObjectBegin(values_object)); - json_const_iterator itValEnd(ObjectEnd(values_object)); - - for (; itVal != itValEnd; ++itVal) { - Parameter param; - if (ParseParameterProperty(¶m, err, values_object, GetKey(itVal), - false)) { - material->values.emplace(GetKey(itVal), std::move(param)); - } - } - } - } else if (key == "extensions" || key == "extras") { - // done later, skip, otherwise poorly parsed contents will be saved in the - // parametermap and serialized again later - } else { - Parameter param; - if (ParseParameterProperty(¶m, err, o, key, false)) { - // names of materials have already been parsed. Putting it in this map - // doesn't correctly reflext the glTF specification - if (key != "name") - material->additionalValues.emplace(std::move(key), std::move(param)); - } - } - } - - material->extensions.clear(); - ParseExtensionsProperty(&material->extensions, err, o); - ParseExtrasProperty(&(material->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator eit; - if (FindMember(o, "extensions", eit)) { - material->extensions_json_string = JsonToString(GetValue(eit)); - } - } - { - json_const_iterator eit; - if (FindMember(o, "extras", eit)) { - material->extras_json_string = JsonToString(GetValue(eit)); - } - } - } - - return true; - } - - static bool ParseAnimationChannel( - AnimationChannel *channel, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - int samplerIndex = -1; - int targetIndex = -1; - if (!ParseIntegerProperty(&samplerIndex, err, o, "sampler", true, - "AnimationChannel")) { - if (err) { - (*err) += "`sampler` field is missing in animation channels\n"; - } - return false; - } - - json_const_iterator targetIt; - if (FindMember(o, "target", targetIt) && IsObject(GetValue(targetIt))) { - const json &target_object = GetValue(targetIt); - - if (!ParseIntegerProperty(&targetIndex, err, target_object, "node", true)) { - if (err) { - (*err) += "`node` field is missing in animation.channels.target\n"; - } - return false; - } - - if (!ParseStringProperty(&channel->target_path, err, target_object, "path", - true)) { - if (err) { - (*err) += "`path` field is missing in animation.channels.target\n"; - } - return false; - } - ParseExtensionsProperty(&channel->target_extensions, err, target_object); - if (store_original_json_for_extras_and_extensions) { - json_const_iterator it; - if (FindMember(target_object, "extensions", it)) { - channel->target_extensions_json_string = JsonToString(GetValue(it)); - } - } - } - - channel->sampler = samplerIndex; - channel->target_node = targetIndex; - - ParseExtensionsProperty(&channel->extensions, err, o); - ParseExtrasProperty(&(channel->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - channel->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - channel->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseAnimation(Animation *animation, std::string *err, - const json &o, - bool store_original_json_for_extras_and_extensions) { - { - json_const_iterator channelsIt; - if (FindMember(o, "channels", channelsIt) && - IsArray(GetValue(channelsIt))) { - json_const_array_iterator channelEnd = ArrayEnd(GetValue(channelsIt)); - for (json_const_array_iterator i = ArrayBegin(GetValue(channelsIt)); - i != channelEnd; ++i) { - AnimationChannel channel; - if (ParseAnimationChannel( - &channel, err, *i, - store_original_json_for_extras_and_extensions)) { - // Only add the channel if the parsing succeeds. - animation->channels.emplace_back(std::move(channel)); - } - } - } - } - - { - json_const_iterator samplerIt; - if (FindMember(o, "samplers", samplerIt) && IsArray(GetValue(samplerIt))) { - const json &sampler_array = GetValue(samplerIt); - - json_const_array_iterator it = ArrayBegin(sampler_array); - json_const_array_iterator itEnd = ArrayEnd(sampler_array); - - for (; it != itEnd; ++it) { - const json &s = *it; - - AnimationSampler sampler; - int inputIndex = -1; - int outputIndex = -1; - if (!ParseIntegerProperty(&inputIndex, err, s, "input", true)) { - if (err) { - (*err) += "`input` field is missing in animation.sampler\n"; - } - return false; - } - ParseStringProperty(&sampler.interpolation, err, s, "interpolation", - false); - if (!ParseIntegerProperty(&outputIndex, err, s, "output", true)) { - if (err) { - (*err) += "`output` field is missing in animation.sampler\n"; - } - return false; - } - sampler.input = inputIndex; - sampler.output = outputIndex; - ParseExtensionsProperty(&(sampler.extensions), err, o); - ParseExtrasProperty(&(sampler.extras), s); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator eit; - if (FindMember(o, "extensions", eit)) { - sampler.extensions_json_string = JsonToString(GetValue(eit)); - } - } - { - json_const_iterator eit; - if (FindMember(o, "extras", eit)) { - sampler.extras_json_string = JsonToString(GetValue(eit)); - } - } - } - - animation->samplers.emplace_back(std::move(sampler)); - } - } - } - - ParseStringProperty(&animation->name, err, o, "name", false); - - ParseExtensionsProperty(&animation->extensions, err, o); - ParseExtrasProperty(&(animation->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - animation->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - animation->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseSampler(Sampler *sampler, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - ParseStringProperty(&sampler->name, err, o, "name", false); - - int minFilter = -1; - int magFilter = -1; - int wrapS = TINYGLTF_TEXTURE_WRAP_REPEAT; - int wrapT = TINYGLTF_TEXTURE_WRAP_REPEAT; - int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT; - ParseIntegerProperty(&minFilter, err, o, "minFilter", false); - ParseIntegerProperty(&magFilter, err, o, "magFilter", false); - ParseIntegerProperty(&wrapS, err, o, "wrapS", false); - ParseIntegerProperty(&wrapT, err, o, "wrapT", false); - ParseIntegerProperty(&wrapR, err, o, "wrapR", false); // tinygltf extension - - // TODO(syoyo): Check the value is alloed one. - // (e.g. we allow 9728(NEAREST), but don't allow 9727) - - sampler->minFilter = minFilter; - sampler->magFilter = magFilter; - sampler->wrapS = wrapS; - sampler->wrapT = wrapT; - sampler->wrapR = wrapR; - - ParseExtensionsProperty(&(sampler->extensions), err, o); - ParseExtrasProperty(&(sampler->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - sampler->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - sampler->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseSkin(Skin *skin, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - ParseStringProperty(&skin->name, err, o, "name", false, "Skin"); - - std::vector joints; - if (!ParseIntegerArrayProperty(&joints, err, o, "joints", false, "Skin")) { - return false; - } - skin->joints = std::move(joints); - - int skeleton = -1; - ParseIntegerProperty(&skeleton, err, o, "skeleton", false, "Skin"); - skin->skeleton = skeleton; - - int invBind = -1; - ParseIntegerProperty(&invBind, err, o, "inverseBindMatrices", true, "Skin"); - skin->inverseBindMatrices = invBind; - - ParseExtensionsProperty(&(skin->extensions), err, o); - ParseExtrasProperty(&(skin->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - skin->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - skin->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParsePerspectiveCamera( - PerspectiveCamera *camera, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - double yfov = 0.0; - if (!ParseNumberProperty(&yfov, err, o, "yfov", true, "OrthographicCamera")) { - return false; - } - - double znear = 0.0; - if (!ParseNumberProperty(&znear, err, o, "znear", true, - "PerspectiveCamera")) { - return false; - } - - double aspectRatio = 0.0; // = invalid - ParseNumberProperty(&aspectRatio, err, o, "aspectRatio", false, - "PerspectiveCamera"); - - double zfar = 0.0; // = invalid - ParseNumberProperty(&zfar, err, o, "zfar", false, "PerspectiveCamera"); - - camera->aspectRatio = aspectRatio; - camera->zfar = zfar; - camera->yfov = yfov; - camera->znear = znear; - - ParseExtensionsProperty(&camera->extensions, err, o); - ParseExtrasProperty(&(camera->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - camera->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - camera->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - // TODO(syoyo): Validate parameter values. - - return true; - } - - static bool ParseSpotLight(SpotLight *light, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - ParseNumberProperty(&light->innerConeAngle, err, o, "innerConeAngle", false); - ParseNumberProperty(&light->outerConeAngle, err, o, "outerConeAngle", false); - - ParseExtensionsProperty(&light->extensions, err, o); - ParseExtrasProperty(&light->extras, o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - light->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - light->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - // TODO(syoyo): Validate parameter values. - - return true; - } - - static bool ParseOrthographicCamera( - OrthographicCamera *camera, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - double xmag = 0.0; - if (!ParseNumberProperty(&xmag, err, o, "xmag", true, "OrthographicCamera")) { - return false; - } - - double ymag = 0.0; - if (!ParseNumberProperty(&ymag, err, o, "ymag", true, "OrthographicCamera")) { - return false; - } - - double zfar = 0.0; - if (!ParseNumberProperty(&zfar, err, o, "zfar", true, "OrthographicCamera")) { - return false; - } - - double znear = 0.0; - if (!ParseNumberProperty(&znear, err, o, "znear", true, - "OrthographicCamera")) { - return false; - } - - ParseExtensionsProperty(&camera->extensions, err, o); - ParseExtrasProperty(&(camera->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - camera->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - camera->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - camera->xmag = xmag; - camera->ymag = ymag; - camera->zfar = zfar; - camera->znear = znear; - - // TODO(syoyo): Validate parameter values. - - return true; - } - - static bool ParseCamera(Camera *camera, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - if (!ParseStringProperty(&camera->type, err, o, "type", true, "Camera")) { - return false; - } - - if (camera->type.compare("orthographic") == 0) { - json_const_iterator orthoIt; - if (!FindMember(o, "orthographic", orthoIt)) { - if (err) { - std::stringstream ss; - ss << "Orhographic camera description not found." << std::endl; - (*err) += ss.str(); - } - return false; - } - - const json &v = GetValue(orthoIt); - if (!IsObject(v)) { - if (err) { - std::stringstream ss; - ss << "\"orthographic\" is not a JSON object." << std::endl; - (*err) += ss.str(); - } - return false; - } - - if (!ParseOrthographicCamera( - &camera->orthographic, err, v, - store_original_json_for_extras_and_extensions)) { - return false; - } - } else if (camera->type.compare("perspective") == 0) { - json_const_iterator perspIt; - if (!FindMember(o, "perspective", perspIt)) { - if (err) { - std::stringstream ss; - ss << "Perspective camera description not found." << std::endl; - (*err) += ss.str(); - } - return false; - } - - const json &v = GetValue(perspIt); - if (!IsObject(v)) { - if (err) { - std::stringstream ss; - ss << "\"perspective\" is not a JSON object." << std::endl; - (*err) += ss.str(); - } - return false; - } - - if (!ParsePerspectiveCamera( - &camera->perspective, err, v, - store_original_json_for_extras_and_extensions)) { - return false; - } - } else { - if (err) { - std::stringstream ss; - ss << "Invalid camera type: \"" << camera->type - << "\". Must be \"perspective\" or \"orthographic\"" << std::endl; - (*err) += ss.str(); - } - return false; - } - - ParseStringProperty(&camera->name, err, o, "name", false); - - ParseExtensionsProperty(&camera->extensions, err, o); - ParseExtrasProperty(&(camera->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - camera->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - camera->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - static bool ParseLight(Light *light, std::string *err, const json &o, - bool store_original_json_for_extras_and_extensions) { - if (!ParseStringProperty(&light->type, err, o, "type", true)) { - return false; - } - - if (light->type == "spot") { - json_const_iterator spotIt; - if (!FindMember(o, "spot", spotIt)) { - if (err) { - std::stringstream ss; - ss << "Spot light description not found." << std::endl; - (*err) += ss.str(); - } - return false; - } - - const json &v = GetValue(spotIt); - if (!IsObject(v)) { - if (err) { - std::stringstream ss; - ss << "\"spot\" is not a JSON object." << std::endl; - (*err) += ss.str(); - } - return false; - } - - if (!ParseSpotLight(&light->spot, err, v, - store_original_json_for_extras_and_extensions)) { - return false; - } - } - - ParseStringProperty(&light->name, err, o, "name", false); - ParseNumberArrayProperty(&light->color, err, o, "color", false); - ParseNumberProperty(&light->range, err, o, "range", false); - ParseNumberProperty(&light->intensity, err, o, "intensity", false); - ParseExtensionsProperty(&light->extensions, err, o); - ParseExtrasProperty(&(light->extras), o); - - if (store_original_json_for_extras_and_extensions) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - light->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - light->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - return true; - } - - bool TinyGLTF::LoadFromString(Model *model, std::string *err, std::string *warn, - const char *json_str, - unsigned int json_str_length, - const std::string &base_dir, - unsigned int check_sections) { - if (json_str_length < 4) { - if (err) { - (*err) = "JSON string too short.\n"; - } - return false; - } - - JsonDocument v; - -#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || \ - defined(_CPPUNWIND)) && \ - !defined(TINYGLTF_NOEXCEPTION) - try { - JsonParse(v, json_str, json_str_length, true); - - } catch (const std::exception &e) { - if (err) { - (*err) = e.what(); - } - return false; - } -#else - { - JsonParse(v, json_str, json_str_length); - - if (!IsObject(v)) { - // Assume parsing was failed. - if (err) { - (*err) = "Failed to parse JSON object\n"; - } - return false; - } - } -#endif - - if (!IsObject(v)) { - // root is not an object. - if (err) { - (*err) = "Root element is not a JSON object\n"; - } - return false; - } - - { - bool version_found = false; - json_const_iterator it; - if (FindMember(v, "asset", it) && IsObject(GetValue(it))) { - auto &itObj = GetValue(it); - json_const_iterator version_it; - std::string versionStr; - if (FindMember(itObj, "version", version_it) && - GetString(GetValue(version_it), versionStr)) { - version_found = true; - } - } - if (version_found) { - // OK - } else if (check_sections & REQUIRE_VERSION) { - if (err) { - (*err) += "\"asset\" object not found in .gltf or not an object type\n"; - } - return false; - } - } - - // scene is not mandatory. - // FIXME Maybe a better way to handle it than removing the code - - auto IsArrayMemberPresent = [](const json &_v, const char *name) -> bool { - json_const_iterator it; - return FindMember(_v, name, it) && IsArray(GetValue(it)); - }; - - { - if ((check_sections & REQUIRE_SCENES) && - !IsArrayMemberPresent(v, "scenes")) { - if (err) { - (*err) += "\"scenes\" object not found in .gltf or not an array type\n"; - } - return false; - } - } - - { - if ((check_sections & REQUIRE_NODES) && !IsArrayMemberPresent(v, "nodes")) { - if (err) { - (*err) += "\"nodes\" object not found in .gltf\n"; - } - return false; - } - } - - { - if ((check_sections & REQUIRE_ACCESSORS) && - !IsArrayMemberPresent(v, "accessors")) { - if (err) { - (*err) += "\"accessors\" object not found in .gltf\n"; - } - return false; - } - } - - { - if ((check_sections & REQUIRE_BUFFERS) && - !IsArrayMemberPresent(v, "buffers")) { - if (err) { - (*err) += "\"buffers\" object not found in .gltf\n"; - } - return false; - } - } - - { - if ((check_sections & REQUIRE_BUFFER_VIEWS) && - !IsArrayMemberPresent(v, "bufferViews")) { - if (err) { - (*err) += "\"bufferViews\" object not found in .gltf\n"; - } - return false; - } - } - - model->buffers.clear(); - model->bufferViews.clear(); - model->accessors.clear(); - model->meshes.clear(); - model->cameras.clear(); - model->nodes.clear(); - model->extensionsUsed.clear(); - model->extensionsRequired.clear(); - model->extensions.clear(); - model->defaultScene = -1; - - // 1. Parse Asset - { - json_const_iterator it; - if (FindMember(v, "asset", it) && IsObject(GetValue(it))) { - const json &root = GetValue(it); - - ParseAsset(&model->asset, err, root, - store_original_json_for_extras_and_extensions_); - } - } - -#ifdef TINYGLTF_USE_CPP14 - auto ForEachInArray = [](const json &_v, const char *member, - const auto &cb) -> bool -#else - // The std::function<> implementation can be less efficient because it will - // allocate heap when the size of the captured lambda is above 16 bytes with - // clang and gcc, but it does not require C++14. - auto ForEachInArray = [](const json &_v, const char *member, - const std::function &cb) -> bool -#endif - { - json_const_iterator itm; - if (FindMember(_v, member, itm) && IsArray(GetValue(itm))) { - const json &root = GetValue(itm); - auto it = ArrayBegin(root); - auto end = ArrayEnd(root); - for (; it != end; ++it) { - if (!cb(*it)) return false; - } - } - return true; - }; - - // 2. Parse extensionUsed - { - ForEachInArray(v, "extensionsUsed", [&](const json &o) { - std::string str; - GetString(o, str); - model->extensionsUsed.emplace_back(std::move(str)); - return true; - }); - } - - { - ForEachInArray(v, "extensionsRequired", [&](const json &o) { - std::string str; - GetString(o, str); - model->extensionsRequired.emplace_back(std::move(str)); - return true; - }); - } - - // 3. Parse Buffer - { - bool success = ForEachInArray(v, "buffers", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`buffers' does not contain an JSON object."; - } - return false; - } - Buffer buffer; - if (!ParseBuffer(&buffer, err, o, - store_original_json_for_extras_and_extensions_, &fs, - base_dir, is_binary_, bin_data_, bin_size_)) { - return false; - } - - model->buffers.emplace_back(std::move(buffer)); - return true; - }); - - if (!success) { - return false; - } - } - // 4. Parse BufferView - { - bool success = ForEachInArray(v, "bufferViews", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`bufferViews' does not contain an JSON object."; - } - return false; - } - BufferView bufferView; - if (!ParseBufferView(&bufferView, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->bufferViews.emplace_back(std::move(bufferView)); - return true; - }); - - if (!success) { - return false; - } - } - - // 5. Parse Accessor - { - bool success = ForEachInArray(v, "accessors", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`accessors' does not contain an JSON object."; - } - return false; - } - Accessor accessor; - if (!ParseAccessor(&accessor, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->accessors.emplace_back(std::move(accessor)); - return true; - }); - - if (!success) { - return false; - } - } - - // 6. Parse Mesh - { - bool success = ForEachInArray(v, "meshes", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`meshes' does not contain an JSON object."; - } - return false; - } - Mesh mesh; - if (!ParseMesh(&mesh, model, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->meshes.emplace_back(std::move(mesh)); - return true; - }); - - if (!success) { - return false; - } - } - - // Assign missing bufferView target types - // - Look for missing Mesh indices - // - Look for missing Mesh attributes - for (auto &mesh : model->meshes) { - for (auto &primitive : mesh.primitives) { - if (primitive.indices > - -1) // has indices from parsing step, must be Element Array Buffer - { - if (size_t(primitive.indices) >= model->accessors.size()) { - if (err) { - (*err) += "primitive indices accessor out of bounds"; - } - return false; - } - - auto bufferView = - model->accessors[size_t(primitive.indices)].bufferView; - if (bufferView < 0 || size_t(bufferView) >= model->bufferViews.size()) { - if (err) { - (*err) += "accessor[" + std::to_string(primitive.indices) + - "] invalid bufferView"; - } - return false; - } - - model->bufferViews[size_t(bufferView)].target = - TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; - // we could optionally check if acessors' bufferView type is Scalar, as - // it should be - } - - for (auto &attribute : primitive.attributes) { - model - ->bufferViews[size_t( - model->accessors[size_t(attribute.second)].bufferView)] - .target = TINYGLTF_TARGET_ARRAY_BUFFER; - } - - for (auto &target : primitive.targets) { - for (auto &attribute : target) { - auto bufferView = - model->accessors[size_t(attribute.second)].bufferView; - // bufferView could be null(-1) for sparse morph target - if (bufferView >= 0) { - model->bufferViews[size_t(bufferView)].target = - TINYGLTF_TARGET_ARRAY_BUFFER; - } - } - } - } - } - - // 7. Parse Node - { - bool success = ForEachInArray(v, "nodes", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`nodes' does not contain an JSON object."; - } - return false; - } - Node node; - if (!ParseNode(&node, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->nodes.emplace_back(std::move(node)); - return true; - }); - - if (!success) { - return false; - } - } - - // 8. Parse scenes. - { - bool success = ForEachInArray(v, "scenes", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`scenes' does not contain an JSON object."; - } - return false; - } - std::vector nodes; - ParseIntegerArrayProperty(&nodes, err, o, "nodes", false); - - Scene scene; - scene.nodes = std::move(nodes); - - ParseStringProperty(&scene.name, err, o, "name", false); - - ParseExtensionsProperty(&scene.extensions, err, o); - ParseExtrasProperty(&scene.extras, o); - - if (store_original_json_for_extras_and_extensions_) { - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - model->extensions_json_string = JsonToString(GetValue(it)); - } - } - { - json_const_iterator it; - if (FindMember(o, "extras", it)) { - model->extras_json_string = JsonToString(GetValue(it)); - } - } - } - - model->scenes.emplace_back(std::move(scene)); - return true; - }); - - if (!success) { - return false; - } - } - - // 9. Parse default scenes. - { - json_const_iterator rootIt; - int iVal; - if (FindMember(v, "scene", rootIt) && GetInt(GetValue(rootIt), iVal)) { - model->defaultScene = iVal; - } - } - - // 10. Parse Material - { - bool success = ForEachInArray(v, "materials", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`materials' does not contain an JSON object."; - } - return false; - } - Material material; - ParseStringProperty(&material.name, err, o, "name", false); - - if (!ParseMaterial(&material, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->materials.emplace_back(std::move(material)); - return true; - }); - - if (!success) { - return false; - } - } - - // 11. Parse Image - { - int idx = 0; - bool success = ForEachInArray(v, "images", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "image[" + std::to_string(idx) + "] is not a JSON object."; - } - return false; - } - Image image; - if (!ParseImage(&image, idx, err, warn, o, - store_original_json_for_extras_and_extensions_, base_dir, - &fs, &this->LoadImageData, load_image_user_data_)) { - return false; - } - - if (image.bufferView != -1) { - // Load image from the buffer view. - if (size_t(image.bufferView) >= model->bufferViews.size()) { - if (err) { - std::stringstream ss; - ss << "image[" << idx << "] bufferView \"" << image.bufferView - << "\" not found in the scene." << std::endl; - (*err) += ss.str(); - } - return false; - } - - const BufferView &bufferView = - model->bufferViews[size_t(image.bufferView)]; - if (size_t(bufferView.buffer) >= model->buffers.size()) { - if (err) { - std::stringstream ss; - ss << "image[" << idx << "] buffer \"" << bufferView.buffer - << "\" not found in the scene." << std::endl; - (*err) += ss.str(); - } - return false; - } - const Buffer &buffer = model->buffers[size_t(bufferView.buffer)]; - - if (*LoadImageData == nullptr) { - if (err) { - (*err) += "No LoadImageData callback specified.\n"; - } - return false; - } - bool ret = LoadImageData( - &image, idx, err, warn, image.width, image.height, - &buffer.data[bufferView.byteOffset], - static_cast(bufferView.byteLength), load_image_user_data_); - if (!ret) { - return false; - } - } - - model->images.emplace_back(std::move(image)); - ++idx; - return true; - }); - - if (!success) { - return false; - } - } - - // 12. Parse Texture - { - bool success = ForEachInArray(v, "textures", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`textures' does not contain an JSON object."; - } - return false; - } - Texture texture; - if (!ParseTexture(&texture, err, o, - store_original_json_for_extras_and_extensions_, - base_dir)) { - return false; - } - - model->textures.emplace_back(std::move(texture)); - return true; - }); - - if (!success) { - return false; - } - } - - // 13. Parse Animation - { - bool success = ForEachInArray(v, "animations", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`animations' does not contain an JSON object."; - } - return false; - } - Animation animation; - if (!ParseAnimation(&animation, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->animations.emplace_back(std::move(animation)); - return true; - }); - - if (!success) { - return false; - } - } - - // 14. Parse Skin - { - bool success = ForEachInArray(v, "skins", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`skins' does not contain an JSON object."; - } - return false; - } - Skin skin; - if (!ParseSkin(&skin, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->skins.emplace_back(std::move(skin)); - return true; - }); - - if (!success) { - return false; - } - } - - // 15. Parse Sampler - { - bool success = ForEachInArray(v, "samplers", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`samplers' does not contain an JSON object."; - } - return false; - } - Sampler sampler; - if (!ParseSampler(&sampler, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->samplers.emplace_back(std::move(sampler)); - return true; - }); - - if (!success) { - return false; - } - } - - // 16. Parse Camera - { - bool success = ForEachInArray(v, "cameras", [&](const json &o) { - if (!IsObject(o)) { - if (err) { - (*err) += "`cameras' does not contain an JSON object."; - } - return false; - } - Camera camera; - if (!ParseCamera(&camera, err, o, - store_original_json_for_extras_and_extensions_)) { - return false; - } - - model->cameras.emplace_back(std::move(camera)); - return true; - }); - - if (!success) { - return false; - } - } - - // 17. Parse Extensions - ParseExtensionsProperty(&model->extensions, err, v); - - // 18. Specific extension implementations - { - json_const_iterator rootIt; - if (FindMember(v, "extensions", rootIt) && IsObject(GetValue(rootIt))) { - const json &root = GetValue(rootIt); - - json_const_iterator it(ObjectBegin(root)); - json_const_iterator itEnd(ObjectEnd(root)); - for (; it != itEnd; ++it) { - // parse KHR_lights_punctual extension - std::string key(GetKey(it)); - if ((key == "KHR_lights_punctual") && IsObject(GetValue(it))) { - const json &object = GetValue(it); - json_const_iterator itLight; - if (FindMember(object, "lights", itLight)) { - const json &lights = GetValue(itLight); - if (!IsArray(lights)) { - continue; - } - - auto arrayIt(ArrayBegin(lights)); - auto arrayItEnd(ArrayEnd(lights)); - for (; arrayIt != arrayItEnd; ++arrayIt) { - Light light; - if (!ParseLight(&light, err, *arrayIt, - store_original_json_for_extras_and_extensions_)) { - return false; - } - model->lights.emplace_back(std::move(light)); - } - } - } - } - } - } - - // 19. Parse Extras - ParseExtrasProperty(&model->extras, v); - - if (store_original_json_for_extras_and_extensions_) { - model->extras_json_string = JsonToString(v["extras"]); - model->extensions_json_string = JsonToString(v["extensions"]); - } - - return true; - } - - bool TinyGLTF::LoadASCIIFromString(Model *model, std::string *err, - std::string *warn, const char *str, - unsigned int length, - const std::string &base_dir, - unsigned int check_sections) { - is_binary_ = false; - bin_data_ = nullptr; - bin_size_ = 0; - - return LoadFromString(model, err, warn, str, length, base_dir, - check_sections); - } - - bool TinyGLTF::LoadASCIIFromFile(Model *model, std::string *err, - std::string *warn, const std::string &filename, - unsigned int check_sections) { - std::stringstream ss; - - if (fs.ReadWholeFile == nullptr) { - // Programmer error, assert() ? - ss << "Failed to read file: " << filename - << ": one or more FS callback not set" << std::endl; - if (err) { - (*err) = ss.str(); - } - return false; - } - - std::vector data; - std::string fileerr; - bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); - if (!fileread) { - ss << "Failed to read file: " << filename << ": " << fileerr << std::endl; - if (err) { - (*err) = ss.str(); - } - return false; - } - - size_t sz = data.size(); - if (sz == 0) { - if (err) { - (*err) = "Empty file."; - } - return false; - } - - std::string basedir = GetBaseDir(filename); - - bool ret = LoadASCIIFromString( - model, err, warn, reinterpret_cast(&data.at(0)), - static_cast(data.size()), basedir, check_sections); - - return ret; - } - - bool TinyGLTF::LoadBinaryFromMemory(Model *model, std::string *err, - std::string *warn, - const unsigned char *bytes, - unsigned int size, - const std::string &base_dir, - unsigned int check_sections) { - if (size < 20) { - if (err) { - (*err) = "Too short data size for glTF Binary."; - } - return false; - } - - if (bytes[0] == 'g' && bytes[1] == 'l' && bytes[2] == 'T' && - bytes[3] == 'F') { - // ok - } else { - if (err) { - (*err) = "Invalid magic."; - } - return false; - } - - unsigned int version; // 4 bytes - unsigned int length; // 4 bytes - unsigned int model_length; // 4 bytes - unsigned int model_format; // 4 bytes; - - // @todo { Endian swap for big endian machine. } - memcpy(&version, bytes + 4, 4); - swap4(&version); - memcpy(&length, bytes + 8, 4); - swap4(&length); - memcpy(&model_length, bytes + 12, 4); - swap4(&model_length); - memcpy(&model_format, bytes + 16, 4); - swap4(&model_format); - - // In case the Bin buffer is not present, the size is exactly 20 + size of - // JSON contents, - // so use "greater than" operator. - if ((20 + model_length > size) || (model_length < 1) || (length > size) || - (20 + model_length > length) || - (model_format != 0x4E4F534A)) { // 0x4E4F534A = JSON format. - if (err) { - (*err) = "Invalid glTF binary."; - } - return false; - } - - // Extract JSON string. - std::string jsonString(reinterpret_cast(&bytes[20]), - model_length); - - is_binary_ = true; - bin_data_ = bytes + 20 + model_length + - 8; // 4 bytes (buffer_length) + 4 bytes(buffer_format) - bin_size_ = - length - (20 + model_length); // extract header + JSON scene data. - - bool ret = LoadFromString(model, err, warn, - reinterpret_cast(&bytes[20]), - model_length, base_dir, check_sections); - if (!ret) { - return ret; - } - - return true; - } - - bool TinyGLTF::LoadBinaryFromFile(Model *model, std::string *err, - std::string *warn, - const std::string &filename, - unsigned int check_sections) { - std::stringstream ss; - - if (fs.ReadWholeFile == nullptr) { - // Programmer error, assert() ? - ss << "Failed to read file: " << filename - << ": one or more FS callback not set" << std::endl; - if (err) { - (*err) = ss.str(); - } - return false; - } - - std::vector data; - std::string fileerr; - bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); - if (!fileread) { - ss << "Failed to read file: " << filename << ": " << fileerr << std::endl; - if (err) { - (*err) = ss.str(); - } - return false; - } - - std::string basedir = GetBaseDir(filename); - - bool ret = LoadBinaryFromMemory(model, err, warn, &data.at(0), - static_cast(data.size()), - basedir, check_sections); - - return ret; - } - - /////////////////////// - // GLTF Serialization - /////////////////////// - namespace { - json JsonFromString(const char *s) { -#ifdef TINYGLTF_USE_RAPIDJSON - return json(s, GetAllocator()); -#else - return json(s); -#endif - } - - void JsonAssign(json &dest, const json &src) { -#ifdef TINYGLTF_USE_RAPIDJSON - dest.CopyFrom(src, GetAllocator()); -#else - dest = src; -#endif - } - - void JsonAddMember(json &o, const char *key, json &&value) { -#ifdef TINYGLTF_USE_RAPIDJSON - if (!o.IsObject()) { - o.SetObject(); - } - o.AddMember(json(key, GetAllocator()), std::move(value), GetAllocator()); -#else - o[key] = std::move(value); -#endif - } - - void JsonPushBack(json &o, json &&value) { -#ifdef TINYGLTF_USE_RAPIDJSON - o.PushBack(std::move(value), GetAllocator()); -#else - o.push_back(std::move(value)); -#endif - } - - bool JsonIsNull(const json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - return o.IsNull(); -#else - return o.is_null(); -#endif - } - - void JsonSetObject(json &o) { -#ifdef TINYGLTF_USE_RAPIDJSON - o.SetObject(); -#else - o = o.object({}); -#endif - } - - void JsonReserveArray(json &o, size_t s) { -#ifdef TINYGLTF_USE_RAPIDJSON - o.SetArray(); - o.Reserve(static_cast(s), GetAllocator()); -#endif - (void)(o); - (void)(s); - } - } // namespace - - // typedef std::pair json_object_pair; - - template - static void SerializeNumberProperty(const std::string &key, T number, - json &obj) { - // obj.insert( - // json_object_pair(key, json(static_cast(number)))); - // obj[key] = static_cast(number); - JsonAddMember(obj, key.c_str(), json(number)); - } - -#ifdef TINYGLTF_USE_RAPIDJSON - template <> - void SerializeNumberProperty(const std::string &key, size_t number, json &obj) { - JsonAddMember(obj, key.c_str(), json(static_cast(number))); - } -#endif - - template - static void SerializeNumberArrayProperty(const std::string &key, - const std::vector &value, - json &obj) { - if (value.empty()) return; - - json ary; - JsonReserveArray(ary, value.size()); - for (const auto &s : value) { - JsonPushBack(ary, json(s)); - } - JsonAddMember(obj, key.c_str(), std::move(ary)); - } - - static void SerializeStringProperty(const std::string &key, - const std::string &value, json &obj) { - JsonAddMember(obj, key.c_str(), JsonFromString(value.c_str())); - } - - static void SerializeStringArrayProperty(const std::string &key, - const std::vector &value, - json &obj) { - json ary; - JsonReserveArray(ary, value.size()); - for (auto &s : value) { - JsonPushBack(ary, JsonFromString(s.c_str())); - } - JsonAddMember(obj, key.c_str(), std::move(ary)); - } - - static bool ValueToJson(const Value &value, json *ret) { - json obj; -#ifdef TINYGLTF_USE_RAPIDJSON - switch (value.Type()) { - case REAL_TYPE: - obj.SetDouble(value.Get()); - break; - case INT_TYPE: - obj.SetInt(value.Get()); - break; - case BOOL_TYPE: - obj.SetBool(value.Get()); - break; - case STRING_TYPE: - obj.SetString(value.Get().c_str(), GetAllocator()); - break; - case ARRAY_TYPE: { - obj.SetArray(); - obj.Reserve(static_cast(value.ArrayLen()), - GetAllocator()); - for (unsigned int i = 0; i < value.ArrayLen(); ++i) { - Value elementValue = value.Get(int(i)); - json elementJson; - if (ValueToJson(value.Get(int(i)), &elementJson)) - obj.PushBack(std::move(elementJson), GetAllocator()); - } - break; - } - case BINARY_TYPE: - // TODO - // obj = json(value.Get>()); - return false; - break; - case OBJECT_TYPE: { - obj.SetObject(); - Value::Object objMap = value.Get(); - for (auto &it : objMap) { - json elementJson; - if (ValueToJson(it.second, &elementJson)) { - obj.AddMember(json(it.first.c_str(), GetAllocator()), - std::move(elementJson), GetAllocator()); - } - } - break; - } - case NULL_TYPE: - default: - return false; - } -#else - switch (value.Type()) { - case REAL_TYPE: - obj = json(value.Get()); - break; - case INT_TYPE: - obj = json(value.Get()); - break; - case BOOL_TYPE: - obj = json(value.Get()); - break; - case STRING_TYPE: - obj = json(value.Get()); - break; - case ARRAY_TYPE: { - for (unsigned int i = 0; i < value.ArrayLen(); ++i) { - Value elementValue = value.Get(int(i)); - json elementJson; - if (ValueToJson(value.Get(int(i)), &elementJson)) - obj.push_back(elementJson); - } - break; - } - case BINARY_TYPE: - // TODO - // obj = json(value.Get>()); - return false; - break; - case OBJECT_TYPE: { - Value::Object objMap = value.Get(); - for (auto &it : objMap) { - json elementJson; - if (ValueToJson(it.second, &elementJson)) obj[it.first] = elementJson; - } - break; - } - case NULL_TYPE: - default: - return false; - } -#endif - if (ret) *ret = std::move(obj); - return true; - } - - static void SerializeValue(const std::string &key, const Value &value, - json &obj) { - json ret; - if (ValueToJson(value, &ret)) { - JsonAddMember(obj, key.c_str(), std::move(ret)); - } - } - - static void SerializeGltfBufferData(const std::vector &data, - json &o) { - std::string header = "data:application/octet-stream;base64,"; - if (data.size() > 0) { - std::string encodedData = - base64_encode(&data[0], static_cast(data.size())); - SerializeStringProperty("uri", header + encodedData, o); - } else { - // Issue #229 - // size 0 is allowd. Just emit mime header. - SerializeStringProperty("uri", header, o); - } - } - - static bool SerializeGltfBufferData(const std::vector &data, - const std::string &binFilename) { -#ifdef _WIN32 -#if defined(__GLIBCXX__) // mingw - int file_descriptor = _wopen(UTF8ToWchar(binFilename).c_str(), - _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); - __gnu_cxx::stdio_filebuf wfile_buf( - file_descriptor, std::ios_base::out | std::ios_base::binary); - std::ostream output(&wfile_buf); - if (!wfile_buf.is_open()) return false; -#elif defined(_MSC_VER) - std::ofstream output(UTF8ToWchar(binFilename).c_str(), std::ofstream::binary); - if (!output.is_open()) return false; -#else - std::ofstream output(binFilename.c_str(), std::ofstream::binary); - if (!output.is_open()) return false; -#endif -#else - std::ofstream output(binFilename.c_str(), std::ofstream::binary); - if (!output.is_open()) return false; -#endif - if (data.size() > 0) { - output.write(reinterpret_cast(&data[0]), - std::streamsize(data.size())); - } else { - // Issue #229 - // size 0 will be still valid buffer data. - // write empty file. - } - return true; - } - -#if 0 // FIXME(syoyo): not used. will be removed in the future release. - static void SerializeParameterMap(ParameterMap ¶m, json &o) { - for (ParameterMap::iterator paramIt = param.begin(); paramIt != param.end(); - ++paramIt) { - if (paramIt->second.number_array.size()) { - SerializeNumberArrayProperty(paramIt->first, - paramIt->second.number_array, o); - } else if (paramIt->second.json_double_value.size()) { - json json_double_value; - for (std::map::iterator it = - paramIt->second.json_double_value.begin(); - it != paramIt->second.json_double_value.end(); ++it) { - if (it->first == "index") { - json_double_value[it->first] = paramIt->second.TextureIndex(); - } else { - json_double_value[it->first] = it->second; - } - } - - o[paramIt->first] = json_double_value; - } else if (!paramIt->second.string_value.empty()) { - SerializeStringProperty(paramIt->first, paramIt->second.string_value, o); - } else if (paramIt->second.has_number_value) { - o[paramIt->first] = paramIt->second.number_value; - } else { - o[paramIt->first] = paramIt->second.bool_value; - } - } - } -#endif - - static void SerializeExtensionMap(const ExtensionMap &extensions, json &o) { - if (!extensions.size()) return; - - json extMap; - for (ExtensionMap::const_iterator extIt = extensions.begin(); - extIt != extensions.end(); ++extIt) { - // Allow an empty object for extension(#97) - json ret; - bool isNull = true; - if (ValueToJson(extIt->second, &ret)) { - isNull = JsonIsNull(ret); - JsonAddMember(extMap, extIt->first.c_str(), std::move(ret)); - } - if (isNull) { - if (!(extIt->first.empty())) { // name should not be empty, but for sure - // create empty object so that an extension name is still included in - // json. - json empty; - JsonSetObject(empty); - JsonAddMember(extMap, extIt->first.c_str(), std::move(empty)); - } - } - } - JsonAddMember(o, "extensions", std::move(extMap)); - } - - static void SerializeGltfAccessor(Accessor &accessor, json &o) { - if (accessor.bufferView >= 0) - SerializeNumberProperty("bufferView", accessor.bufferView, o); - - if (accessor.byteOffset != 0) - SerializeNumberProperty("byteOffset", int(accessor.byteOffset), o); - - SerializeNumberProperty("componentType", accessor.componentType, o); - SerializeNumberProperty("count", accessor.count, o); - SerializeNumberArrayProperty("min", accessor.minValues, o); - SerializeNumberArrayProperty("max", accessor.maxValues, o); - if (accessor.normalized) - SerializeValue("normalized", Value(accessor.normalized), o); - std::string type; - switch (accessor.type) { - case TINYGLTF_TYPE_SCALAR: - type = "SCALAR"; - break; - case TINYGLTF_TYPE_VEC2: - type = "VEC2"; - break; - case TINYGLTF_TYPE_VEC3: - type = "VEC3"; - break; - case TINYGLTF_TYPE_VEC4: - type = "VEC4"; - break; - case TINYGLTF_TYPE_MAT2: - type = "MAT2"; - break; - case TINYGLTF_TYPE_MAT3: - type = "MAT3"; - break; - case TINYGLTF_TYPE_MAT4: - type = "MAT4"; - break; - } - - SerializeStringProperty("type", type, o); - if (!accessor.name.empty()) SerializeStringProperty("name", accessor.name, o); - - if (accessor.extras.Type() != NULL_TYPE) { - SerializeValue("extras", accessor.extras, o); - } - } - - static void SerializeGltfAnimationChannel(AnimationChannel &channel, json &o) { - SerializeNumberProperty("sampler", channel.sampler, o); - { - json target; - SerializeNumberProperty("node", channel.target_node, target); - SerializeStringProperty("path", channel.target_path, target); - - SerializeExtensionMap(channel.target_extensions, target); - - JsonAddMember(o, "target", std::move(target)); - } - - if (channel.extras.Type() != NULL_TYPE) { - SerializeValue("extras", channel.extras, o); - } - - SerializeExtensionMap(channel.extensions, o); - } - - static void SerializeGltfAnimationSampler(AnimationSampler &sampler, json &o) { - SerializeNumberProperty("input", sampler.input, o); - SerializeNumberProperty("output", sampler.output, o); - SerializeStringProperty("interpolation", sampler.interpolation, o); - - if (sampler.extras.Type() != NULL_TYPE) { - SerializeValue("extras", sampler.extras, o); - } - } - - static void SerializeGltfAnimation(Animation &animation, json &o) { - if (!animation.name.empty()) - SerializeStringProperty("name", animation.name, o); - - { - json channels; - JsonReserveArray(channels, animation.channels.size()); - for (unsigned int i = 0; i < animation.channels.size(); ++i) { - json channel; - AnimationChannel gltfChannel = animation.channels[i]; - SerializeGltfAnimationChannel(gltfChannel, channel); - JsonPushBack(channels, std::move(channel)); - } - - JsonAddMember(o, "channels", std::move(channels)); - } - - { - json samplers; - JsonReserveArray(samplers, animation.samplers.size()); - for (unsigned int i = 0; i < animation.samplers.size(); ++i) { - json sampler; - AnimationSampler gltfSampler = animation.samplers[i]; - SerializeGltfAnimationSampler(gltfSampler, sampler); - JsonPushBack(samplers, std::move(sampler)); - } - JsonAddMember(o, "samplers", std::move(samplers)); - } - - if (animation.extras.Type() != NULL_TYPE) { - SerializeValue("extras", animation.extras, o); - } - - SerializeExtensionMap(animation.extensions, o); - } - - static void SerializeGltfAsset(Asset &asset, json &o) { - if (!asset.generator.empty()) { - SerializeStringProperty("generator", asset.generator, o); - } - - if (!asset.copyright.empty()) { - SerializeStringProperty("copyright", asset.copyright, o); - } - - if (!asset.version.empty()) { - SerializeStringProperty("version", asset.version, o); - } - - if (asset.extras.Keys().size()) { - SerializeValue("extras", asset.extras, o); - } - - SerializeExtensionMap(asset.extensions, o); - } - - static void SerializeGltfBufferBin(Buffer &buffer, json &o, - std::vector &binBuffer) { - SerializeNumberProperty("byteLength", buffer.data.size(), o); - binBuffer = buffer.data; - - if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); - - if (buffer.extras.Type() != NULL_TYPE) { - SerializeValue("extras", buffer.extras, o); - } - } - - static void SerializeGltfBuffer(Buffer &buffer, json &o) { - SerializeNumberProperty("byteLength", buffer.data.size(), o); - SerializeGltfBufferData(buffer.data, o); - - if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); - - if (buffer.extras.Type() != NULL_TYPE) { - SerializeValue("extras", buffer.extras, o); - } - } - - static bool SerializeGltfBuffer(Buffer &buffer, json &o, - const std::string &binFilename, - const std::string &binBaseFilename) { - if (!SerializeGltfBufferData(buffer.data, binFilename)) return false; - SerializeNumberProperty("byteLength", buffer.data.size(), o); - SerializeStringProperty("uri", binBaseFilename, o); - - if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); - - if (buffer.extras.Type() != NULL_TYPE) { - SerializeValue("extras", buffer.extras, o); - } - return true; - } - - static void SerializeGltfBufferView(BufferView &bufferView, json &o) { - SerializeNumberProperty("buffer", bufferView.buffer, o); - SerializeNumberProperty("byteLength", bufferView.byteLength, o); - - // byteStride is optional, minimum allowed is 4 - if (bufferView.byteStride >= 4) { - SerializeNumberProperty("byteStride", bufferView.byteStride, o); - } - // byteOffset is optional, default is 0 - if (bufferView.byteOffset > 0) { - SerializeNumberProperty("byteOffset", bufferView.byteOffset, o); - } - // Target is optional, check if it contains a valid value - if (bufferView.target == TINYGLTF_TARGET_ARRAY_BUFFER || - bufferView.target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER) { - SerializeNumberProperty("target", bufferView.target, o); - } - if (bufferView.name.size()) { - SerializeStringProperty("name", bufferView.name, o); - } - - if (bufferView.extras.Type() != NULL_TYPE) { - SerializeValue("extras", bufferView.extras, o); - } - } - - static void SerializeGltfImage(Image &image, json &o) { - // if uri empty, the mimeType and bufferview should be set - if (image.uri.empty()) { - SerializeStringProperty("mimeType", image.mimeType, o); - SerializeNumberProperty("bufferView", image.bufferView, o); - } else { - // TODO(syoyo): dlib::urilencode? - SerializeStringProperty("uri", image.uri, o); - } - - if (image.name.size()) { - SerializeStringProperty("name", image.name, o); - } - - if (image.extras.Type() != NULL_TYPE) { - SerializeValue("extras", image.extras, o); - } - - SerializeExtensionMap(image.extensions, o); - } - - static void SerializeGltfTextureInfo(TextureInfo &texinfo, json &o) { - SerializeNumberProperty("index", texinfo.index, o); - - if (texinfo.texCoord != 0) { - SerializeNumberProperty("texCoord", texinfo.texCoord, o); - } - - if (texinfo.extras.Type() != NULL_TYPE) { - SerializeValue("extras", texinfo.extras, o); - } - - SerializeExtensionMap(texinfo.extensions, o); - } - - static void SerializeGltfNormalTextureInfo(NormalTextureInfo &texinfo, - json &o) { - SerializeNumberProperty("index", texinfo.index, o); - - if (texinfo.texCoord != 0) { - SerializeNumberProperty("texCoord", texinfo.texCoord, o); - } - - if (!TINYGLTF_DOUBLE_EQUAL(texinfo.scale, 1.0)) { - SerializeNumberProperty("scale", texinfo.scale, o); - } - - if (texinfo.extras.Type() != NULL_TYPE) { - SerializeValue("extras", texinfo.extras, o); - } - - SerializeExtensionMap(texinfo.extensions, o); - } - - static void SerializeGltfOcclusionTextureInfo(OcclusionTextureInfo &texinfo, - json &o) { - SerializeNumberProperty("index", texinfo.index, o); - - if (texinfo.texCoord != 0) { - SerializeNumberProperty("texCoord", texinfo.texCoord, o); - } - - if (!TINYGLTF_DOUBLE_EQUAL(texinfo.strength, 1.0)) { - SerializeNumberProperty("strength", texinfo.strength, o); - } - - if (texinfo.extras.Type() != NULL_TYPE) { - SerializeValue("extras", texinfo.extras, o); - } - - SerializeExtensionMap(texinfo.extensions, o); - } - - static void SerializeGltfPbrMetallicRoughness(PbrMetallicRoughness &pbr, - json &o) { - std::vector default_baseColorFactor = {1.0, 1.0, 1.0, 1.0}; - if (!Equals(pbr.baseColorFactor, default_baseColorFactor)) { - SerializeNumberArrayProperty("baseColorFactor", pbr.baseColorFactor, - o); - } - - if (!TINYGLTF_DOUBLE_EQUAL(pbr.metallicFactor, 1.0)) { - SerializeNumberProperty("metallicFactor", pbr.metallicFactor, o); - } - - if (!TINYGLTF_DOUBLE_EQUAL(pbr.roughnessFactor, 1.0)) { - SerializeNumberProperty("roughnessFactor", pbr.roughnessFactor, o); - } - - if (pbr.baseColorTexture.index > -1) { - json texinfo; - SerializeGltfTextureInfo(pbr.baseColorTexture, texinfo); - JsonAddMember(o, "baseColorTexture", std::move(texinfo)); - } - - if (pbr.metallicRoughnessTexture.index > -1) { - json texinfo; - SerializeGltfTextureInfo(pbr.metallicRoughnessTexture, texinfo); - JsonAddMember(o, "metallicRoughnessTexture", std::move(texinfo)); - } - - SerializeExtensionMap(pbr.extensions, o); - - if (pbr.extras.Type() != NULL_TYPE) { - SerializeValue("extras", pbr.extras, o); - } - } - - static void SerializeGltfMaterial(Material &material, json &o) { - if (material.name.size()) { - SerializeStringProperty("name", material.name, o); - } - - // QUESTION(syoyo): Write material parameters regardless of its default value? - - if (!TINYGLTF_DOUBLE_EQUAL(material.alphaCutoff, 0.5)) { - SerializeNumberProperty("alphaCutoff", material.alphaCutoff, o); - } - - if (material.alphaMode.compare("OPAQUE") != 0) { - SerializeStringProperty("alphaMode", material.alphaMode, o); - } - - if (material.doubleSided != false) - JsonAddMember(o, "doubleSided", json(material.doubleSided)); - - if (material.normalTexture.index > -1) { - json texinfo; - SerializeGltfNormalTextureInfo(material.normalTexture, texinfo); - JsonAddMember(o, "normalTexture", std::move(texinfo)); - } - - if (material.occlusionTexture.index > -1) { - json texinfo; - SerializeGltfOcclusionTextureInfo(material.occlusionTexture, texinfo); - JsonAddMember(o, "occlusionTexture", std::move(texinfo)); - } - - if (material.emissiveTexture.index > -1) { - json texinfo; - SerializeGltfTextureInfo(material.emissiveTexture, texinfo); - JsonAddMember(o, "emissiveTexture", std::move(texinfo)); - } - - std::vector default_emissiveFactor = {0.0, 0.0, 0.0}; - if (!Equals(material.emissiveFactor, default_emissiveFactor)) { - SerializeNumberArrayProperty("emissiveFactor", - material.emissiveFactor, o); - } - - { - json pbrMetallicRoughness; - SerializeGltfPbrMetallicRoughness(material.pbrMetallicRoughness, - pbrMetallicRoughness); - // Issue 204 - // Do not serialize `pbrMetallicRoughness` if pbrMetallicRoughness has all - // default values(json is null). Otherwise it will serialize to - // `pbrMetallicRoughness : null`, which cannot be read by other glTF - // importers(and validators). - // - if (!JsonIsNull(pbrMetallicRoughness)) { - JsonAddMember(o, "pbrMetallicRoughness", std::move(pbrMetallicRoughness)); - } - } - -#if 0 // legacy way. just for the record. - if (material.values.size()) { - json pbrMetallicRoughness; - SerializeParameterMap(material.values, pbrMetallicRoughness); - JsonAddMember(o, "pbrMetallicRoughness", std::move(pbrMetallicRoughness)); - } - - SerializeParameterMap(material.additionalValues, o); -#else - -#endif - - SerializeExtensionMap(material.extensions, o); - - if (material.extras.Type() != NULL_TYPE) { - SerializeValue("extras", material.extras, o); - } - } - - static void SerializeGltfMesh(Mesh &mesh, json &o) { - json primitives; - JsonReserveArray(primitives, mesh.primitives.size()); - for (unsigned int i = 0; i < mesh.primitives.size(); ++i) { - json primitive; - const Primitive &gltfPrimitive = mesh.primitives[i]; // don't make a copy - { - json attributes; - for (auto attrIt = gltfPrimitive.attributes.begin(); - attrIt != gltfPrimitive.attributes.end(); ++attrIt) { - SerializeNumberProperty(attrIt->first, attrIt->second, attributes); - } - - JsonAddMember(primitive, "attributes", std::move(attributes)); - } - - // Indicies is optional - if (gltfPrimitive.indices > -1) { - SerializeNumberProperty("indices", gltfPrimitive.indices, primitive); - } - // Material is optional - if (gltfPrimitive.material > -1) { - SerializeNumberProperty("material", gltfPrimitive.material, - primitive); - } - SerializeNumberProperty("mode", gltfPrimitive.mode, primitive); - - // Morph targets - if (gltfPrimitive.targets.size()) { - json targets; - JsonReserveArray(targets, gltfPrimitive.targets.size()); - for (unsigned int k = 0; k < gltfPrimitive.targets.size(); ++k) { - json targetAttributes; - std::map targetData = gltfPrimitive.targets[k]; - for (std::map::iterator attrIt = targetData.begin(); - attrIt != targetData.end(); ++attrIt) { - SerializeNumberProperty(attrIt->first, attrIt->second, - targetAttributes); - } - JsonPushBack(targets, std::move(targetAttributes)); - } - JsonAddMember(primitive, "targets", std::move(targets)); - } - - SerializeExtensionMap(gltfPrimitive.extensions, primitive); - - if (gltfPrimitive.extras.Type() != NULL_TYPE) { - SerializeValue("extras", gltfPrimitive.extras, primitive); - } - - JsonPushBack(primitives, std::move(primitive)); - } - - JsonAddMember(o, "primitives", std::move(primitives)); - - if (mesh.weights.size()) { - SerializeNumberArrayProperty("weights", mesh.weights, o); - } - - if (mesh.name.size()) { - SerializeStringProperty("name", mesh.name, o); - } - - SerializeExtensionMap(mesh.extensions, o); - if (mesh.extras.Type() != NULL_TYPE) { - SerializeValue("extras", mesh.extras, o); - } - } - - static void SerializeSpotLight(SpotLight &spot, json &o) { - SerializeNumberProperty("innerConeAngle", spot.innerConeAngle, o); - SerializeNumberProperty("outerConeAngle", spot.outerConeAngle, o); - SerializeExtensionMap(spot.extensions, o); - if (spot.extras.Type() != NULL_TYPE) { - SerializeValue("extras", spot.extras, o); - } - } - - static void SerializeGltfLight(Light &light, json &o) { - if (!light.name.empty()) SerializeStringProperty("name", light.name, o); - SerializeNumberProperty("intensity", light.intensity, o); - if (light.range > 0.0) { - SerializeNumberProperty("range", light.range, o); - } - SerializeNumberArrayProperty("color", light.color, o); - SerializeStringProperty("type", light.type, o); - if (light.type == "spot") { - json spot; - SerializeSpotLight(light.spot, spot); - JsonAddMember(o, "spot", std::move(spot)); - } - SerializeExtensionMap(light.extensions, o); - if (light.extras.Type() != NULL_TYPE) { - SerializeValue("extras", light.extras, o); - } - } - - static void SerializeGltfNode(Node &node, json &o) { - if (node.translation.size() > 0) { - SerializeNumberArrayProperty("translation", node.translation, o); - } - if (node.rotation.size() > 0) { - SerializeNumberArrayProperty("rotation", node.rotation, o); - } - if (node.scale.size() > 0) { - SerializeNumberArrayProperty("scale", node.scale, o); - } - if (node.matrix.size() > 0) { - SerializeNumberArrayProperty("matrix", node.matrix, o); - } - if (node.mesh != -1) { - SerializeNumberProperty("mesh", node.mesh, o); - } - - if (node.skin != -1) { - SerializeNumberProperty("skin", node.skin, o); - } - - if (node.camera != -1) { - SerializeNumberProperty("camera", node.camera, o); - } - - if (node.weights.size() > 0) { - SerializeNumberArrayProperty("weights", node.weights, o); - } - - if (node.extras.Type() != NULL_TYPE) { - SerializeValue("extras", node.extras, o); - } - - SerializeExtensionMap(node.extensions, o); - if (!node.name.empty()) SerializeStringProperty("name", node.name, o); - SerializeNumberArrayProperty("children", node.children, o); - } - - static void SerializeGltfSampler(Sampler &sampler, json &o) { - if (sampler.magFilter != -1) { - SerializeNumberProperty("magFilter", sampler.magFilter, o); - } - if (sampler.minFilter != -1) { - SerializeNumberProperty("minFilter", sampler.minFilter, o); - } - SerializeNumberProperty("wrapR", sampler.wrapR, o); - SerializeNumberProperty("wrapS", sampler.wrapS, o); - SerializeNumberProperty("wrapT", sampler.wrapT, o); - - if (sampler.extras.Type() != NULL_TYPE) { - SerializeValue("extras", sampler.extras, o); - } - } - - static void SerializeGltfOrthographicCamera(const OrthographicCamera &camera, - json &o) { - SerializeNumberProperty("zfar", camera.zfar, o); - SerializeNumberProperty("znear", camera.znear, o); - SerializeNumberProperty("xmag", camera.xmag, o); - SerializeNumberProperty("ymag", camera.ymag, o); - - if (camera.extras.Type() != NULL_TYPE) { - SerializeValue("extras", camera.extras, o); - } - } - - static void SerializeGltfPerspectiveCamera(const PerspectiveCamera &camera, - json &o) { - SerializeNumberProperty("zfar", camera.zfar, o); - SerializeNumberProperty("znear", camera.znear, o); - if (camera.aspectRatio > 0) { - SerializeNumberProperty("aspectRatio", camera.aspectRatio, o); - } - - if (camera.yfov > 0) { - SerializeNumberProperty("yfov", camera.yfov, o); - } - - if (camera.extras.Type() != NULL_TYPE) { - SerializeValue("extras", camera.extras, o); - } - } - - static void SerializeGltfCamera(const Camera &camera, json &o) { - SerializeStringProperty("type", camera.type, o); - if (!camera.name.empty()) { - SerializeStringProperty("name", camera.name, o); - } - - if (camera.type.compare("orthographic") == 0) { - json orthographic; - SerializeGltfOrthographicCamera(camera.orthographic, orthographic); - JsonAddMember(o, "orthographic", std::move(orthographic)); - } else if (camera.type.compare("perspective") == 0) { - json perspective; - SerializeGltfPerspectiveCamera(camera.perspective, perspective); - JsonAddMember(o, "perspective", std::move(perspective)); - } else { - // ??? - } - - if (camera.extras.Type() != NULL_TYPE) { - SerializeValue("extras", camera.extras, o); - } - SerializeExtensionMap(camera.extensions, o); - } - - static void SerializeGltfScene(Scene &scene, json &o) { - SerializeNumberArrayProperty("nodes", scene.nodes, o); - - if (scene.name.size()) { - SerializeStringProperty("name", scene.name, o); - } - if (scene.extras.Type() != NULL_TYPE) { - SerializeValue("extras", scene.extras, o); - } - SerializeExtensionMap(scene.extensions, o); - } - - static void SerializeGltfSkin(Skin &skin, json &o) { - if (skin.inverseBindMatrices != -1) - SerializeNumberProperty("inverseBindMatrices", skin.inverseBindMatrices, o); - - SerializeNumberArrayProperty("joints", skin.joints, o); - SerializeNumberProperty("skeleton", skin.skeleton, o); - if (skin.name.size()) { - SerializeStringProperty("name", skin.name, o); - } - } - - static void SerializeGltfTexture(Texture &texture, json &o) { - if (texture.sampler > -1) { - SerializeNumberProperty("sampler", texture.sampler, o); - } - if (texture.source > -1) { - SerializeNumberProperty("source", texture.source, o); - } - if (texture.name.size()) { - SerializeStringProperty("name", texture.name, o); - } - if (texture.extras.Type() != NULL_TYPE) { - SerializeValue("extras", texture.extras, o); - } - SerializeExtensionMap(texture.extensions, o); - } - - /// - /// Serialize all properties except buffers and images. - /// - static void SerializeGltfModel(Model *model, json &o) { - // ACCESSORS - if (model->accessors.size()) { - json accessors; - JsonReserveArray(accessors, model->accessors.size()); - for (unsigned int i = 0; i < model->accessors.size(); ++i) { - json accessor; - SerializeGltfAccessor(model->accessors[i], accessor); - JsonPushBack(accessors, std::move(accessor)); - } - JsonAddMember(o, "accessors", std::move(accessors)); - } - - // ANIMATIONS - if (model->animations.size()) { - json animations; - JsonReserveArray(animations, model->animations.size()); - for (unsigned int i = 0; i < model->animations.size(); ++i) { - if (model->animations[i].channels.size()) { - json animation; - SerializeGltfAnimation(model->animations[i], animation); - JsonPushBack(animations, std::move(animation)); - } - } - - JsonAddMember(o, "animations", std::move(animations)); - } - - // ASSET - json asset; - SerializeGltfAsset(model->asset, asset); - JsonAddMember(o, "asset", std::move(asset)); - - // BUFFERVIEWS - if (model->bufferViews.size()) { - json bufferViews; - JsonReserveArray(bufferViews, model->bufferViews.size()); - for (unsigned int i = 0; i < model->bufferViews.size(); ++i) { - json bufferView; - SerializeGltfBufferView(model->bufferViews[i], bufferView); - JsonPushBack(bufferViews, std::move(bufferView)); - } - JsonAddMember(o, "bufferViews", std::move(bufferViews)); - } - - // Extensions required - if (model->extensionsRequired.size()) { - SerializeStringArrayProperty("extensionsRequired", - model->extensionsRequired, o); - } - - // MATERIALS - if (model->materials.size()) { - json materials; - JsonReserveArray(materials, model->materials.size()); - for (unsigned int i = 0; i < model->materials.size(); ++i) { - json material; - SerializeGltfMaterial(model->materials[i], material); - JsonPushBack(materials, std::move(material)); - } - JsonAddMember(o, "materials", std::move(materials)); - } - - // MESHES - if (model->meshes.size()) { - json meshes; - JsonReserveArray(meshes, model->meshes.size()); - for (unsigned int i = 0; i < model->meshes.size(); ++i) { - json mesh; - SerializeGltfMesh(model->meshes[i], mesh); - JsonPushBack(meshes, std::move(mesh)); - } - JsonAddMember(o, "meshes", std::move(meshes)); - } - - // NODES - if (model->nodes.size()) { - json nodes; - JsonReserveArray(nodes, model->nodes.size()); - for (unsigned int i = 0; i < model->nodes.size(); ++i) { - json node; - SerializeGltfNode(model->nodes[i], node); - JsonPushBack(nodes, std::move(node)); - } - JsonAddMember(o, "nodes", std::move(nodes)); - } - - // SCENE - if (model->defaultScene > -1) { - SerializeNumberProperty("scene", model->defaultScene, o); - } - - // SCENES - if (model->scenes.size()) { - json scenes; - JsonReserveArray(scenes, model->scenes.size()); - for (unsigned int i = 0; i < model->scenes.size(); ++i) { - json currentScene; - SerializeGltfScene(model->scenes[i], currentScene); - JsonPushBack(scenes, std::move(currentScene)); - } - JsonAddMember(o, "scenes", std::move(scenes)); - } - - // SKINS - if (model->skins.size()) { - json skins; - JsonReserveArray(skins, model->skins.size()); - for (unsigned int i = 0; i < model->skins.size(); ++i) { - json skin; - SerializeGltfSkin(model->skins[i], skin); - JsonPushBack(skins, std::move(skin)); - } - JsonAddMember(o, "skins", std::move(skins)); - } - - // TEXTURES - if (model->textures.size()) { - json textures; - JsonReserveArray(textures, model->textures.size()); - for (unsigned int i = 0; i < model->textures.size(); ++i) { - json texture; - SerializeGltfTexture(model->textures[i], texture); - JsonPushBack(textures, std::move(texture)); - } - JsonAddMember(o, "textures", std::move(textures)); - } - - // SAMPLERS - if (model->samplers.size()) { - json samplers; - JsonReserveArray(samplers, model->samplers.size()); - for (unsigned int i = 0; i < model->samplers.size(); ++i) { - json sampler; - SerializeGltfSampler(model->samplers[i], sampler); - JsonPushBack(samplers, std::move(sampler)); - } - JsonAddMember(o, "samplers", std::move(samplers)); - } - - // CAMERAS - if (model->cameras.size()) { - json cameras; - JsonReserveArray(cameras, model->cameras.size()); - for (unsigned int i = 0; i < model->cameras.size(); ++i) { - json camera; - SerializeGltfCamera(model->cameras[i], camera); - JsonPushBack(cameras, std::move(camera)); - } - JsonAddMember(o, "cameras", std::move(cameras)); - } - - // EXTENSIONS - SerializeExtensionMap(model->extensions, o); - - auto extensionsUsed = model->extensionsUsed; - - // LIGHTS as KHR_lights_punctual - if (model->lights.size()) { - json lights; - JsonReserveArray(lights, model->lights.size()); - for (unsigned int i = 0; i < model->lights.size(); ++i) { - json light; - SerializeGltfLight(model->lights[i], light); - JsonPushBack(lights, std::move(light)); - } - json khr_lights_cmn; - JsonAddMember(khr_lights_cmn, "lights", std::move(lights)); - json ext_j; - - { - json_const_iterator it; - if (FindMember(o, "extensions", it)) { - JsonAssign(ext_j, GetValue(it)); - } - } - - JsonAddMember(ext_j, "KHR_lights_punctual", std::move(khr_lights_cmn)); - - JsonAddMember(o, "extensions", std::move(ext_j)); - - // Also add "KHR_lights_punctual" to `extensionsUsed` - { - auto has_khr_lights_punctual = - std::find_if(extensionsUsed.begin(), extensionsUsed.end(), - [](const std::string &s) { - return (s.compare("KHR_lights_punctual") == 0); - }); - - if (has_khr_lights_punctual == extensionsUsed.end()) { - extensionsUsed.push_back("KHR_lights_punctual"); - } - } - } - - // Extensions used - if (model->extensionsUsed.size()) { - SerializeStringArrayProperty("extensionsUsed", extensionsUsed, o); - } - - // EXTRAS - if (model->extras.Type() != NULL_TYPE) { - SerializeValue("extras", model->extras, o); - } - } - - static bool WriteGltfStream(std::ostream &stream, const std::string &content) { - stream << content << std::endl; - return true; - } - - static bool WriteGltfFile(const std::string &output, - const std::string &content) { -#ifdef _WIN32 -#if defined(_MSC_VER) - std::ofstream gltfFile(UTF8ToWchar(output).c_str()); -#elif defined(__GLIBCXX__) - int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), - _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); - __gnu_cxx::stdio_filebuf wfile_buf( - file_descriptor, std::ios_base::out | std::ios_base::binary); - std::ostream gltfFile(&wfile_buf); - if (!wfile_buf.is_open()) return false; -#else - std::ofstream gltfFile(output.c_str()); - if (!gltfFile.is_open()) return false; -#endif -#else - std::ofstream gltfFile(output.c_str()); - if (!gltfFile.is_open()) return false; -#endif - return WriteGltfStream(gltfFile, content); - } - - static void WriteBinaryGltfStream(std::ostream &stream, - const std::string &content, - const std::vector &binBuffer) { - const std::string header = "glTF"; - const int version = 2; - - // https://stackoverflow.com/questions/3407012/c-rounding-up-to-the-nearest-multiple-of-a-number - auto roundUp = [](uint32_t numToRound, uint32_t multiple) { - if (multiple == 0) return numToRound; - - uint32_t remainder = numToRound % multiple; - if (remainder == 0) return numToRound; - - return numToRound + multiple - remainder; - }; - - const uint32_t padding_size = - roundUp(uint32_t(content.size()), 4) - uint32_t(content.size()); - - // 12 bytes for header, JSON content length, 8 bytes for JSON chunk info. - // Chunk data must be located at 4-byte boundary. - const uint32_t length = - 12 + 8 + roundUp(uint32_t(content.size()), 4) + - (binBuffer.size() ? (8 + roundUp(uint32_t(binBuffer.size()), 4)) : 0); - - stream.write(header.c_str(), std::streamsize(header.size())); - stream.write(reinterpret_cast(&version), sizeof(version)); - stream.write(reinterpret_cast(&length), sizeof(length)); - - // JSON chunk info, then JSON data - const uint32_t model_length = uint32_t(content.size()) + padding_size; - const uint32_t model_format = 0x4E4F534A; - stream.write(reinterpret_cast(&model_length), - sizeof(model_length)); - stream.write(reinterpret_cast(&model_format), - sizeof(model_format)); - stream.write(content.c_str(), std::streamsize(content.size())); - - // Chunk must be multiplies of 4, so pad with spaces - if (padding_size > 0) { - const std::string padding = std::string(size_t(padding_size), ' '); - stream.write(padding.c_str(), std::streamsize(padding.size())); - } - if (binBuffer.size() > 0) { - const uint32_t bin_padding_size = - roundUp(uint32_t(binBuffer.size()), 4) - uint32_t(binBuffer.size()); - // BIN chunk info, then BIN data - const uint32_t bin_length = uint32_t(binBuffer.size()) + bin_padding_size; - const uint32_t bin_format = 0x004e4942; - stream.write(reinterpret_cast(&bin_length), - sizeof(bin_length)); - stream.write(reinterpret_cast(&bin_format), - sizeof(bin_format)); - stream.write(reinterpret_cast(binBuffer.data()), - std::streamsize(binBuffer.size())); - // Chunksize must be multiplies of 4, so pad with zeroes - if (bin_padding_size > 0) { - const std::vector padding = - std::vector(size_t(bin_padding_size), 0); - stream.write(reinterpret_cast(padding.data()), - std::streamsize(padding.size())); - } - } - } - - static void WriteBinaryGltfFile(const std::string &output, - const std::string &content, - const std::vector &binBuffer) { -#ifdef _WIN32 -#if defined(_MSC_VER) - std::ofstream gltfFile(UTF8ToWchar(output).c_str(), std::ios::binary); -#elif defined(__GLIBCXX__) - int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), - _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); - __gnu_cxx::stdio_filebuf wfile_buf( - file_descriptor, std::ios_base::out | std::ios_base::binary); - std::ostream gltfFile(&wfile_buf); -#else - std::ofstream gltfFile(output.c_str(), std::ios::binary); -#endif -#else - std::ofstream gltfFile(output.c_str(), std::ios::binary); -#endif - WriteBinaryGltfStream(gltfFile, content, binBuffer); - } - - bool TinyGLTF::WriteGltfSceneToStream(Model *model, std::ostream &stream, - bool prettyPrint = true, - bool writeBinary = false) { - JsonDocument output; - - /// Serialize all properties except buffers and images. - SerializeGltfModel(model, output); - - // BUFFERS - std::vector binBuffer; - if (model->buffers.size()) { - json buffers; - JsonReserveArray(buffers, model->buffers.size()); - for (unsigned int i = 0; i < model->buffers.size(); ++i) { - json buffer; - if (writeBinary && i == 0 && model->buffers[i].uri.empty()) { - SerializeGltfBufferBin(model->buffers[i], buffer, binBuffer); - } else { - SerializeGltfBuffer(model->buffers[i], buffer); - } - JsonPushBack(buffers, std::move(buffer)); - } - JsonAddMember(output, "buffers", std::move(buffers)); - } - - // IMAGES - if (model->images.size()) { - json images; - JsonReserveArray(images, model->images.size()); - for (unsigned int i = 0; i < model->images.size(); ++i) { - json image; - - std::string dummystring = ""; - // UpdateImageObject need baseDir but only uses it if embeddedImages is - // enabled, since we won't write separate images when writing to a stream - // we - UpdateImageObject(model->images[i], dummystring, int(i), false, - &this->WriteImageData, this->write_image_user_data_); - SerializeGltfImage(model->images[i], image); - JsonPushBack(images, std::move(image)); - } - JsonAddMember(output, "images", std::move(images)); - } - - if (writeBinary) { - WriteBinaryGltfStream(stream, JsonToString(output), binBuffer); - } else { - WriteGltfStream(stream, JsonToString(output, prettyPrint ? 2 : -1)); - } - - return true; - } - - bool TinyGLTF::WriteGltfSceneToFile(Model *model, const std::string &filename, - bool embedImages = false, - bool embedBuffers = false, - bool prettyPrint = true, - bool writeBinary = false) { - JsonDocument output; - std::string defaultBinFilename = GetBaseFilename(filename); - std::string defaultBinFileExt = ".bin"; - std::string::size_type pos = - defaultBinFilename.rfind('.', defaultBinFilename.length()); - - if (pos != std::string::npos) { - defaultBinFilename = defaultBinFilename.substr(0, pos); - } - std::string baseDir = GetBaseDir(filename); - if (baseDir.empty()) { - baseDir = "./"; - } - /// Serialize all properties except buffers and images. - SerializeGltfModel(model, output); - - // BUFFERS - std::vector usedUris; - std::vector binBuffer; - if (model->buffers.size()) { - json buffers; - JsonReserveArray(buffers, model->buffers.size()); - for (unsigned int i = 0; i < model->buffers.size(); ++i) { - json buffer; - if (writeBinary && i == 0 && model->buffers[i].uri.empty()) { - SerializeGltfBufferBin(model->buffers[i], buffer, binBuffer); - } else if (embedBuffers) { - SerializeGltfBuffer(model->buffers[i], buffer); - } else { - std::string binSavePath; - std::string binUri; - if (!model->buffers[i].uri.empty() && - !IsDataURI(model->buffers[i].uri)) { - binUri = model->buffers[i].uri; - } else { - binUri = defaultBinFilename + defaultBinFileExt; - bool inUse = true; - int numUsed = 0; - while (inUse) { - inUse = false; - for (const std::string &usedName : usedUris) { - if (binUri.compare(usedName) != 0) continue; - inUse = true; - binUri = defaultBinFilename + std::to_string(numUsed++) + - defaultBinFileExt; - break; - } - } - } - usedUris.push_back(binUri); - binSavePath = JoinPath(baseDir, binUri); - if (!SerializeGltfBuffer(model->buffers[i], buffer, binSavePath, - binUri)) { - return false; - } - } - JsonPushBack(buffers, std::move(buffer)); - } - JsonAddMember(output, "buffers", std::move(buffers)); - } - - // IMAGES - if (model->images.size()) { - json images; - JsonReserveArray(images, model->images.size()); - for (unsigned int i = 0; i < model->images.size(); ++i) { - json image; - - UpdateImageObject(model->images[i], baseDir, int(i), embedImages, - &this->WriteImageData, this->write_image_user_data_); - SerializeGltfImage(model->images[i], image); - JsonPushBack(images, std::move(image)); - } - JsonAddMember(output, "images", std::move(images)); - } - - if (writeBinary) { - WriteBinaryGltfFile(filename, JsonToString(output), binBuffer); - } else { - WriteGltfFile(filename, JsonToString(output, (prettyPrint ? 2 : -1))); - } - - return true; - } - -} // namespace tinygltf - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - +// +// Header-only tiny glTF 2.0 loader and serializer. +// +// +// The MIT License (MIT) +// +// Copyright (c) 2015 - 2020 Syoyo Fujita, Aurélien Chatelain and many +// contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Version: +// - v2.4.2 Decode percent-encoded URI. +// - v2.4.1 Fix some glTF object class does not have `extensions` and/or +// `extras` property. +// - v2.4.0 Experimental RapidJSON and C++14 support(Thanks to @jrkoone). +// - v2.3.1 Set default value of minFilter and magFilter in Sampler to -1. +// - v2.3.0 Modified Material representation according to glTF 2.0 schema +// (and introduced TextureInfo class) +// Change the behavior of `Value::IsNumber`. It return true either the +// value is int or real. +// - v2.2.0 Add loading 16bit PNG support. Add Sparse accessor support(Thanks +// to @Ybalrid) +// - v2.1.0 Add draco compression. +// - v2.0.1 Add comparsion feature(Thanks to @Selmar). +// - v2.0.0 glTF 2.0!. +// +// Tiny glTF loader is using following third party libraries: +// +// - jsonhpp: C++ JSON library. +// - base64: base64 decode/encode library. +// - stb_image: Image loading library. +// +#ifndef TINY_GLTF_H_ +#define TINY_GLTF_H_ + +#include +#include +#include // std::fabs +#include +#include +#include +#include +#include +#include +#include + +#ifndef TINYGLTF_USE_CPP14 +#include +#endif + +#ifdef __ANDROID__ +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS +#include +#endif +#endif + +#ifdef __GNUC__ +#if (__GNUC__ < 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ <= 8)) +#define TINYGLTF_NOEXCEPT +#else +#define TINYGLTF_NOEXCEPT noexcept +#endif +#else +#define TINYGLTF_NOEXCEPT noexcept +#endif + +#define DEFAULT_METHODS(x) \ + ~x() = default; \ + x(const x &) = default; \ + x(x &&) TINYGLTF_NOEXCEPT = default; \ + x &operator=(const x &) = default; \ + x &operator=(x &&) TINYGLTF_NOEXCEPT = default; + +namespace tinygltf { + +#define TINYGLTF_MODE_POINTS (0) +#define TINYGLTF_MODE_LINE (1) +#define TINYGLTF_MODE_LINE_LOOP (2) +#define TINYGLTF_MODE_LINE_STRIP (3) +#define TINYGLTF_MODE_TRIANGLES (4) +#define TINYGLTF_MODE_TRIANGLE_STRIP (5) +#define TINYGLTF_MODE_TRIANGLE_FAN (6) + +#define TINYGLTF_COMPONENT_TYPE_BYTE (5120) +#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE (5121) +#define TINYGLTF_COMPONENT_TYPE_SHORT (5122) +#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT (5123) +#define TINYGLTF_COMPONENT_TYPE_INT (5124) +#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT (5125) +#define TINYGLTF_COMPONENT_TYPE_FLOAT (5126) +#define TINYGLTF_COMPONENT_TYPE_DOUBLE (5130) + +#define TINYGLTF_TEXTURE_FILTER_NEAREST (9728) +#define TINYGLTF_TEXTURE_FILTER_LINEAR (9729) +#define TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST (9984) +#define TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST (9985) +#define TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR (9986) +#define TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR (9987) + +#define TINYGLTF_TEXTURE_WRAP_REPEAT (10497) +#define TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE (33071) +#define TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT (33648) + + // Redeclarations of the above for technique.parameters. +#define TINYGLTF_PARAMETER_TYPE_BYTE (5120) +#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE (5121) +#define TINYGLTF_PARAMETER_TYPE_SHORT (5122) +#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_SHORT (5123) +#define TINYGLTF_PARAMETER_TYPE_INT (5124) +#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_INT (5125) +#define TINYGLTF_PARAMETER_TYPE_FLOAT (5126) + +#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC2 (35664) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC3 (35665) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC4 (35666) + +#define TINYGLTF_PARAMETER_TYPE_INT_VEC2 (35667) +#define TINYGLTF_PARAMETER_TYPE_INT_VEC3 (35668) +#define TINYGLTF_PARAMETER_TYPE_INT_VEC4 (35669) + +#define TINYGLTF_PARAMETER_TYPE_BOOL (35670) +#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC2 (35671) +#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC3 (35672) +#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC4 (35673) + +#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT2 (35674) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT3 (35675) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT4 (35676) + +#define TINYGLTF_PARAMETER_TYPE_SAMPLER_2D (35678) + + // End parameter types + +#define TINYGLTF_TYPE_VEC2 (2) +#define TINYGLTF_TYPE_VEC3 (3) +#define TINYGLTF_TYPE_VEC4 (4) +#define TINYGLTF_TYPE_MAT2 (32 + 2) +#define TINYGLTF_TYPE_MAT3 (32 + 3) +#define TINYGLTF_TYPE_MAT4 (32 + 4) +#define TINYGLTF_TYPE_SCALAR (64 + 1) +#define TINYGLTF_TYPE_VECTOR (64 + 4) +#define TINYGLTF_TYPE_MATRIX (64 + 16) + +#define TINYGLTF_IMAGE_FORMAT_JPEG (0) +#define TINYGLTF_IMAGE_FORMAT_PNG (1) +#define TINYGLTF_IMAGE_FORMAT_BMP (2) +#define TINYGLTF_IMAGE_FORMAT_GIF (3) + +#define TINYGLTF_TEXTURE_FORMAT_ALPHA (6406) +#define TINYGLTF_TEXTURE_FORMAT_RGB (6407) +#define TINYGLTF_TEXTURE_FORMAT_RGBA (6408) +#define TINYGLTF_TEXTURE_FORMAT_LUMINANCE (6409) +#define TINYGLTF_TEXTURE_FORMAT_LUMINANCE_ALPHA (6410) + +#define TINYGLTF_TEXTURE_TARGET_TEXTURE2D (3553) +#define TINYGLTF_TEXTURE_TYPE_UNSIGNED_BYTE (5121) + +#define TINYGLTF_TARGET_ARRAY_BUFFER (34962) +#define TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER (34963) + +#define TINYGLTF_SHADER_TYPE_VERTEX_SHADER (35633) +#define TINYGLTF_SHADER_TYPE_FRAGMENT_SHADER (35632) + +#define TINYGLTF_DOUBLE_EPS (1.e-12) +#define TINYGLTF_DOUBLE_EQUAL(a, b) (std::fabs((b) - (a)) < TINYGLTF_DOUBLE_EPS) + +#ifdef __ANDROID__ +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS + AAssetManager *asset_manager = nullptr; +#endif +#endif + + typedef enum { + NULL_TYPE = 0, + REAL_TYPE = 1, + INT_TYPE = 2, + BOOL_TYPE = 3, + STRING_TYPE = 4, + ARRAY_TYPE = 5, + BINARY_TYPE = 6, + OBJECT_TYPE = 7 + } Type; + + static inline int32_t GetComponentSizeInBytes(uint32_t componentType) { + if (componentType == TINYGLTF_COMPONENT_TYPE_BYTE) { + return 1; + } else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { + return 1; + } else if (componentType == TINYGLTF_COMPONENT_TYPE_SHORT) { + return 2; + } else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + return 2; + } else if (componentType == TINYGLTF_COMPONENT_TYPE_INT) { + return 4; + } else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + return 4; + } else if (componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { + return 4; + } else if (componentType == TINYGLTF_COMPONENT_TYPE_DOUBLE) { + return 8; + } else { + // Unknown componenty type + return -1; + } + } + + static inline int32_t GetNumComponentsInType(uint32_t ty) { + if (ty == TINYGLTF_TYPE_SCALAR) { + return 1; + } else if (ty == TINYGLTF_TYPE_VEC2) { + return 2; + } else if (ty == TINYGLTF_TYPE_VEC3) { + return 3; + } else if (ty == TINYGLTF_TYPE_VEC4) { + return 4; + } else if (ty == TINYGLTF_TYPE_MAT2) { + return 4; + } else if (ty == TINYGLTF_TYPE_MAT3) { + return 9; + } else if (ty == TINYGLTF_TYPE_MAT4) { + return 16; + } else { + // Unknown componenty type + return -1; + } + } + + // TODO(syoyo): Move these functions to TinyGLTF class + bool IsDataURI(const std::string &in); + bool DecodeDataURI(std::vector *out, std::string &mime_type, + const std::string &in, size_t reqBytes, bool checkSize); + +#ifdef __clang__ +#pragma clang diagnostic push + // Suppress warning for : static Value null_value + // https://stackoverflow.com/questions/15708411/how-to-deal-with-global-constructor-warning-in-clang +#pragma clang diagnostic ignored "-Wexit-time-destructors" +#pragma clang diagnostic ignored "-Wpadded" +#endif + + // Simple class to represent JSON object + class Value { + public: + typedef std::vector Array; + typedef std::map Object; + + Value() + : type_(NULL_TYPE), + int_value_(0), + real_value_(0.0), + boolean_value_(false) {} + + explicit Value(bool b) : type_(BOOL_TYPE) { boolean_value_ = b; } + explicit Value(int i) : type_(INT_TYPE) { + int_value_ = i; + real_value_ = i; + } + explicit Value(double n) : type_(REAL_TYPE) { real_value_ = n; } + explicit Value(const std::string &s) : type_(STRING_TYPE) { + string_value_ = s; + } + explicit Value(std::string &&s) + : type_(STRING_TYPE), string_value_(std::move(s)) {} + explicit Value(const unsigned char *p, size_t n) : type_(BINARY_TYPE) { + binary_value_.resize(n); + memcpy(binary_value_.data(), p, n); + } + explicit Value(std::vector &&v) noexcept + : type_(BINARY_TYPE), + binary_value_(std::move(v)) {} + explicit Value(const Array &a) : type_(ARRAY_TYPE) { array_value_ = a; } + explicit Value(Array &&a) noexcept : type_(ARRAY_TYPE), + array_value_(std::move(a)) {} + + explicit Value(const Object &o) : type_(OBJECT_TYPE) { object_value_ = o; } + explicit Value(Object &&o) noexcept : type_(OBJECT_TYPE), + object_value_(std::move(o)) {} + + DEFAULT_METHODS(Value) + + char Type() const { return static_cast(type_); } + + bool IsBool() const { return (type_ == BOOL_TYPE); } + + bool IsInt() const { return (type_ == INT_TYPE); } + + bool IsNumber() const { return (type_ == REAL_TYPE) || (type_ == INT_TYPE); } + + bool IsReal() const { return (type_ == REAL_TYPE); } + + bool IsString() const { return (type_ == STRING_TYPE); } + + bool IsBinary() const { return (type_ == BINARY_TYPE); } + + bool IsArray() const { return (type_ == ARRAY_TYPE); } + + bool IsObject() const { return (type_ == OBJECT_TYPE); } + + // Use this function if you want to have number value as double. + double GetNumberAsDouble() const { + if (type_ == INT_TYPE) { + return double(int_value_); + } else { + return real_value_; + } + } + + // Use this function if you want to have number value as int. + // TODO(syoyo): Support int value larger than 32 bits + int GetNumberAsInt() const { + if (type_ == REAL_TYPE) { + return int(real_value_); + } else { + return int_value_; + } + } + + // Accessor + template + const T &Get() const; + template + T &Get(); + + // Lookup value from an array + const Value &Get(int idx) const { + static Value null_value; + assert(IsArray()); + assert(idx >= 0); + return (static_cast(idx) < array_value_.size()) + ? array_value_[static_cast(idx)] + : null_value; + } + + // Lookup value from a key-value pair + const Value &Get(const std::string &key) const { + static Value null_value; + assert(IsObject()); + Object::const_iterator it = object_value_.find(key); + return (it != object_value_.end()) ? it->second : null_value; + } + + size_t ArrayLen() const { + if (!IsArray()) return 0; + return array_value_.size(); + } + + // Valid only for object type. + bool Has(const std::string &key) const { + if (!IsObject()) return false; + Object::const_iterator it = object_value_.find(key); + return (it != object_value_.end()) ? true : false; + } + + // List keys + std::vector Keys() const { + std::vector keys; + if (!IsObject()) return keys; // empty + + for (Object::const_iterator it = object_value_.begin(); + it != object_value_.end(); ++it) { + keys.push_back(it->first); + } + + return keys; + } + + size_t Size() const { return (IsArray() ? ArrayLen() : Keys().size()); } + + bool operator==(const tinygltf::Value &other) const; + + protected: + int type_ = NULL_TYPE; + + int int_value_ = 0; + double real_value_ = 0.0; + std::string string_value_; + std::vector binary_value_; + Array array_value_; + Object object_value_; + bool boolean_value_ = false; + }; + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#define TINYGLTF_VALUE_GET(ctype, var) \ + template <> \ + inline const ctype &Value::Get() const { \ + return var; \ + } \ + template <> \ + inline ctype &Value::Get() { \ + return var; \ + } + TINYGLTF_VALUE_GET(bool, boolean_value_) + TINYGLTF_VALUE_GET(double, real_value_) + TINYGLTF_VALUE_GET(int, int_value_) + TINYGLTF_VALUE_GET(std::string, string_value_) + TINYGLTF_VALUE_GET(std::vector, binary_value_) + TINYGLTF_VALUE_GET(Value::Array, array_value_) + TINYGLTF_VALUE_GET(Value::Object, object_value_) +#undef TINYGLTF_VALUE_GET + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat" +#pragma clang diagnostic ignored "-Wpadded" +#endif + + /// Agregate object for representing a color + using ColorValue = std::array; + + // === legacy interface ==== + // TODO(syoyo): Deprecate `Parameter` class. + struct Parameter { + bool bool_value = false; + bool has_number_value = false; + std::string string_value; + std::vector number_array; + std::map json_double_value; + double number_value = 0.0; + + // context sensitive methods. depending the type of the Parameter you are + // accessing, these are either valid or not + // If this parameter represent a texture map in a material, will return the + // texture index + + /// Return the index of a texture if this Parameter is a texture map. + /// Returned value is only valid if the parameter represent a texture from a + /// material + int TextureIndex() const { + const auto it = json_double_value.find("index"); + if (it != std::end(json_double_value)) { + return int(it->second); + } + return -1; + } + + /// Return the index of a texture coordinate set if this Parameter is a + /// texture map. Returned value is only valid if the parameter represent a + /// texture from a material + int TextureTexCoord() const { + const auto it = json_double_value.find("texCoord"); + if (it != std::end(json_double_value)) { + return int(it->second); + } + // As per the spec, if texCoord is ommited, this parameter is 0 + return 0; + } + + /// Return the scale of a texture if this Parameter is a normal texture map. + /// Returned value is only valid if the parameter represent a normal texture + /// from a material + double TextureScale() const { + const auto it = json_double_value.find("scale"); + if (it != std::end(json_double_value)) { + return it->second; + } + // As per the spec, if scale is ommited, this paramter is 1 + return 1; + } + + /// Return the strength of a texture if this Parameter is a an occlusion map. + /// Returned value is only valid if the parameter represent an occlusion map + /// from a material + double TextureStrength() const { + const auto it = json_double_value.find("strength"); + if (it != std::end(json_double_value)) { + return it->second; + } + // As per the spec, if strenghth is ommited, this parameter is 1 + return 1; + } + + /// Material factor, like the roughness or metalness of a material + /// Returned value is only valid if the parameter represent a texture from a + /// material + double Factor() const { return number_value; } + + /// Return the color of a material + /// Returned value is only valid if the parameter represent a texture from a + /// material + ColorValue ColorFactor() const { + return { + {// this agregate intialize the std::array object, and uses C++11 RVO. + number_array[0], number_array[1], number_array[2], + (number_array.size() > 3 ? number_array[3] : 1.0)}}; + } + + Parameter() = default; + DEFAULT_METHODS(Parameter) + bool operator==(const Parameter &) const; + }; + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif + + typedef std::map ParameterMap; + typedef std::map ExtensionMap; + + struct AnimationChannel { + int sampler; // required + int target_node; // required (index of the node to target) + std::string target_path; // required in ["translation", "rotation", "scale", + // "weights"] + Value extras; + ExtensionMap extensions; + ExtensionMap target_extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + std::string target_extensions_json_string; + + AnimationChannel() : sampler(-1), target_node(-1) {} + DEFAULT_METHODS(AnimationChannel) + bool operator==(const AnimationChannel &) const; + }; + + struct AnimationSampler { + int input; // required + int output; // required + std::string interpolation; // "LINEAR", "STEP","CUBICSPLINE" or user defined + // string. default "LINEAR" + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + AnimationSampler() : input(-1), output(-1), interpolation("LINEAR") {} + DEFAULT_METHODS(AnimationSampler) + bool operator==(const AnimationSampler &) const; + }; + + struct Animation { + std::string name; + std::vector channels; + std::vector samplers; + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Animation() = default; + DEFAULT_METHODS(Animation) + bool operator==(const Animation &) const; + }; + + struct Skin { + std::string name; + int inverseBindMatrices; // required here but not in the spec + int skeleton; // The index of the node used as a skeleton root + std::vector joints; // Indices of skeleton nodes + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Skin() { + inverseBindMatrices = -1; + skeleton = -1; + } + DEFAULT_METHODS(Skin) + bool operator==(const Skin &) const; + }; + + struct Sampler { + std::string name; + // glTF 2.0 spec does not define default value for `minFilter` and + // `magFilter`. Set -1 in TinyGLTF(issue #186) + int minFilter = + -1; // optional. -1 = no filter defined. ["NEAREST", "LINEAR", + // "NEAREST_MIPMAP_LINEAR", "LINEAR_MIPMAP_NEAREST", + // "NEAREST_MIPMAP_LINEAR", "LINEAR_MIPMAP_LINEAR"] + int magFilter = + -1; // optional. -1 = no filter defined. ["NEAREST", "LINEAR"] + int wrapS = + TINYGLTF_TEXTURE_WRAP_REPEAT; // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT", + // "REPEAT"], default "REPEAT" + int wrapT = + TINYGLTF_TEXTURE_WRAP_REPEAT; // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT", + // "REPEAT"], default "REPEAT" + int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT; // TinyGLTF extension + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Sampler() + : minFilter(-1), + magFilter(-1), + wrapS(TINYGLTF_TEXTURE_WRAP_REPEAT), + wrapT(TINYGLTF_TEXTURE_WRAP_REPEAT), + wrapR(TINYGLTF_TEXTURE_WRAP_REPEAT) {} + DEFAULT_METHODS(Sampler) + bool operator==(const Sampler &) const; + }; + + struct Image { + std::string name; + int width; + int height; + int component; + int bits; // bit depth per channel. 8(byte), 16 or 32. + int pixel_type; // pixel type(TINYGLTF_COMPONENT_TYPE_***). usually + // UBYTE(bits = 8) or USHORT(bits = 16) + std::vector image; + int bufferView; // (required if no uri) + std::string mimeType; // (required if no uri) ["image/jpeg", "image/png", + // "image/bmp", "image/gif"] + std::string uri; // (required if no mimeType) uri is not decoded(e.g. + // whitespace may be represented as %20) + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + // When this flag is true, data is stored to `image` in as-is format(e.g. jpeg + // compressed for "image/jpeg" mime) This feature is good if you use custom + // image loader function. (e.g. delayed decoding of images for faster glTF + // parsing) Default parser for Image does not provide as-is loading feature at + // the moment. (You can manipulate this by providing your own LoadImageData + // function) + bool as_is; + + Image() : as_is(false) { + bufferView = -1; + width = -1; + height = -1; + component = -1; + bits = -1; + pixel_type = -1; + } + DEFAULT_METHODS(Image) + + bool operator==(const Image &) const; + }; + + struct Texture { + std::string name; + + int sampler; + int source; + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Texture() : sampler(-1), source(-1) {} + DEFAULT_METHODS(Texture) + + bool operator==(const Texture &) const; + }; + + struct TextureInfo { + int index = -1; // required. + int texCoord; // The set index of texture's TEXCOORD attribute used for + // texture coordinate mapping. + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + TextureInfo() : index(-1), texCoord(0) {} + DEFAULT_METHODS(TextureInfo) + bool operator==(const TextureInfo &) const; + }; + + struct NormalTextureInfo { + int index = -1; // required + int texCoord; // The set index of texture's TEXCOORD attribute used for + // texture coordinate mapping. + double scale; // scaledNormal = normalize(( + // * 2.0 - 1.0) * vec3(, , 1.0)) + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + NormalTextureInfo() : index(-1), texCoord(0), scale(1.0) {} + DEFAULT_METHODS(NormalTextureInfo) + bool operator==(const NormalTextureInfo &) const; + }; + + struct OcclusionTextureInfo { + int index = -1; // required + int texCoord; // The set index of texture's TEXCOORD attribute used for + // texture coordinate mapping. + double strength; // occludedColor = lerp(color, color * , ) + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + OcclusionTextureInfo() : index(-1), texCoord(0), strength(1.0) {} + DEFAULT_METHODS(OcclusionTextureInfo) + bool operator==(const OcclusionTextureInfo &) const; + }; + + // pbrMetallicRoughness class defined in glTF 2.0 spec. + struct PbrMetallicRoughness { + std::vector baseColorFactor; // len = 4. default [1,1,1,1] + TextureInfo baseColorTexture; + double metallicFactor; // default 1 + double roughnessFactor; // default 1 + TextureInfo metallicRoughnessTexture; + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + PbrMetallicRoughness() + : baseColorFactor(std::vector{1.0, 1.0, 1.0, 1.0}), + metallicFactor(1.0), + roughnessFactor(1.0) {} + DEFAULT_METHODS(PbrMetallicRoughness) + bool operator==(const PbrMetallicRoughness &) const; + }; + + // Each extension should be stored in a ParameterMap. + // members not in the values could be included in the ParameterMap + // to keep a single material model + struct Material { + std::string name; + + std::vector emissiveFactor{ 0.0, 0.0, 0.0 }; // length 3. default [0, 0, 0] + std::string alphaMode; // default "OPAQUE" + double alphaCutoff; // default 0.5 + bool doubleSided; // default false; + + PbrMetallicRoughness pbrMetallicRoughness; + + NormalTextureInfo normalTexture; + OcclusionTextureInfo occlusionTexture; + TextureInfo emissiveTexture; + + // For backward compatibility + // TODO(syoyo): Remove `values` and `additionalValues` in the next release. + ParameterMap values; + ParameterMap additionalValues; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Material() : alphaMode("OPAQUE"), alphaCutoff(0.5), doubleSided(false) {} + DEFAULT_METHODS(Material) + + bool operator==(const Material &) const; + }; + + struct BufferView { + std::string name; + int buffer{-1}; // Required + size_t byteOffset{0}; // minimum 0, default 0 + size_t byteLength{0}; // required, minimum 1. 0 = invalid + size_t byteStride{0}; // minimum 4, maximum 252 (multiple of 4), default 0 = + // understood to be tightly packed + int target{0}; // ["ARRAY_BUFFER", "ELEMENT_ARRAY_BUFFER"] for vertex indices + // or atttribs. Could be 0 for other data + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + bool dracoDecoded{false}; // Flag indicating this has been draco decoded + + BufferView() + : buffer(-1), + byteOffset(0), + byteLength(0), + byteStride(0), + target(0), + dracoDecoded(false) {} + DEFAULT_METHODS(BufferView) + bool operator==(const BufferView &) const; + }; + + struct Accessor { + int bufferView; // optional in spec but required here since sparse accessor + // are not supported + std::string name; + size_t byteOffset; + bool normalized; // optional. + int componentType; // (required) One of TINYGLTF_COMPONENT_TYPE_*** + size_t count; // required + int type; // (required) One of TINYGLTF_TYPE_*** .. + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + std::vector minValues; // optional + std::vector maxValues; // optional + + struct { + int count; + bool isSparse; + struct { + int byteOffset; + int bufferView; + int componentType; // a TINYGLTF_COMPONENT_TYPE_ value + } indices; + struct { + int bufferView; + int byteOffset; + } values; + } sparse; + + /// + /// Utility function to compute byteStride for a given bufferView object. + /// Returns -1 upon invalid glTF value or parameter configuration. + /// + int ByteStride(const BufferView &bufferViewObject) const { + if (bufferViewObject.byteStride == 0) { + // Assume data is tightly packed. + int componentSizeInBytes = + GetComponentSizeInBytes(static_cast(componentType)); + if (componentSizeInBytes <= 0) { + return -1; + } + + int numComponents = GetNumComponentsInType(static_cast(type)); + if (numComponents <= 0) { + return -1; + } + + return componentSizeInBytes * numComponents; + } else { + // Check if byteStride is a mulple of the size of the accessor's component + // type. + int componentSizeInBytes = + GetComponentSizeInBytes(static_cast(componentType)); + if (componentSizeInBytes <= 0) { + return -1; + } + + if ((bufferViewObject.byteStride % uint32_t(componentSizeInBytes)) != 0) { + return -1; + } + return static_cast(bufferViewObject.byteStride); + } + + // unreachable return 0; + } + + Accessor() + : bufferView(-1), + byteOffset(0), + normalized(false), + componentType(-1), + count(0), + type(-1) { + sparse.isSparse = false; + } + DEFAULT_METHODS(Accessor) + bool operator==(const tinygltf::Accessor &) const; + }; + + struct PerspectiveCamera { + double aspectRatio; // min > 0 + double yfov; // required. min > 0 + double zfar; // min > 0 + double znear; // required. min > 0 + + PerspectiveCamera() + : aspectRatio(0.0), + yfov(0.0), + zfar(0.0) // 0 = use infinite projecton matrix + , + znear(0.0) {} + DEFAULT_METHODS(PerspectiveCamera) + bool operator==(const PerspectiveCamera &) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + }; + + struct OrthographicCamera { + double xmag; // required. must not be zero. + double ymag; // required. must not be zero. + double zfar; // required. `zfar` must be greater than `znear`. + double znear; // required + + OrthographicCamera() : xmag(0.0), ymag(0.0), zfar(0.0), znear(0.0) {} + DEFAULT_METHODS(OrthographicCamera) + bool operator==(const OrthographicCamera &) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + }; + + struct Camera { + std::string type; // required. "perspective" or "orthographic" + std::string name; + + PerspectiveCamera perspective; + OrthographicCamera orthographic; + + Camera() {} + DEFAULT_METHODS(Camera) + bool operator==(const Camera &) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + }; + + struct Primitive { + std::map attributes; // (required) A dictionary object of + // integer, where each integer + // is the index of the accessor + // containing an attribute. + int material; // The index of the material to apply to this primitive + // when rendering. + int indices; // The index of the accessor that contains the indices. + int mode; // one of TINYGLTF_MODE_*** + std::vector > targets; // array of morph targets, + // where each target is a dict with attribues in ["POSITION, "NORMAL", + // "TANGENT"] pointing + // to their corresponding accessors + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Primitive() { + material = -1; + indices = -1; + mode = -1; + } + DEFAULT_METHODS(Primitive) + bool operator==(const Primitive &) const; + }; + + struct Mesh { + std::string name; + std::vector primitives; + std::vector weights; // weights to be applied to the Morph Targets + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Mesh() = default; + DEFAULT_METHODS(Mesh) + bool operator==(const Mesh &) const; + }; + + class Node { + public: + Node() : camera(-1), skin(-1), mesh(-1) {} + + DEFAULT_METHODS(Node) + + bool operator==(const Node &) const; + + int camera; // the index of the camera referenced by this node + + std::string name; + int skin; + int mesh; + std::vector children; + std::vector rotation; // length must be 0 or 4 + std::vector scale; // length must be 0 or 3 + std::vector translation; // length must be 0 or 3 + std::vector matrix; // length must be 0 or 16 + std::vector weights; // The weights of the instantiated Morph Target + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + }; + + struct Buffer { + std::string name; + std::vector data; + std::string + uri; // considered as required here but not in the spec (need to clarify) + // uri is not decoded(e.g. whitespace may be represented as %20) + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Buffer() = default; + DEFAULT_METHODS(Buffer) + bool operator==(const Buffer &) const; + }; + + struct Asset { + std::string version; // required + std::string generator; + std::string minVersion; + std::string copyright; + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Asset() = default; + DEFAULT_METHODS(Asset) + bool operator==(const Asset &) const; + }; + + struct Scene { + std::string name; + std::vector nodes; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Scene() = default; + DEFAULT_METHODS(Scene) + bool operator==(const Scene &) const; + }; + + struct SpotLight { + double innerConeAngle; + double outerConeAngle; + + SpotLight() : innerConeAngle(0.0), outerConeAngle(0.7853981634) {} + DEFAULT_METHODS(SpotLight) + bool operator==(const SpotLight &) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + }; + + struct Light { + std::string name; + std::vector color; + double intensity{1.0}; + std::string type; + double range{0.0}; // 0.0 = inifinite + SpotLight spot; + + Light() : intensity(1.0), range(0.0) {} + DEFAULT_METHODS(Light) + + bool operator==(const Light &) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + }; + + class Model { + public: + Model() = default; + DEFAULT_METHODS(Model) + + bool operator==(const Model &) const; + + std::vector accessors; + std::vector animations; + std::vector buffers; + std::vector bufferViews; + std::vector materials; + std::vector meshes; + std::vector nodes; + std::vector textures; + std::vector images; + std::vector skins; + std::vector samplers; + std::vector cameras; + std::vector scenes; + std::vector lights; + + int defaultScene = -1; + std::vector extensionsUsed; + std::vector extensionsRequired; + + Asset asset; + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + }; + + enum SectionCheck { + NO_REQUIRE = 0x00, + REQUIRE_VERSION = 0x01, + REQUIRE_SCENE = 0x02, + REQUIRE_SCENES = 0x04, + REQUIRE_NODES = 0x08, + REQUIRE_ACCESSORS = 0x10, + REQUIRE_BUFFERS = 0x20, + REQUIRE_BUFFER_VIEWS = 0x40, + REQUIRE_ALL = 0x7f + }; + + /// + /// LoadImageDataFunction type. Signature for custom image loading callbacks. + /// + typedef bool (*LoadImageDataFunction)(Image *, const int, std::string *, + std::string *, int, int, + const unsigned char *, int, void *); + + /// + /// WriteImageDataFunction type. Signature for custom image writing callbacks. + /// + typedef bool (*WriteImageDataFunction)(const std::string *, const std::string *, + Image *, bool, void *); + +#ifndef TINYGLTF_NO_STB_IMAGE + // Declaration of default image loader callback + bool LoadImageData(Image *image, const int image_idx, std::string *err, + std::string *warn, int req_width, int req_height, + const unsigned char *bytes, int size, void *); +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE + // Declaration of default image writer callback + bool WriteImageData(const std::string *basepath, const std::string *filename, + Image *image, bool embedImages, void *); +#endif + + /// + /// FilExistsFunction type. Signature for custom filesystem callbacks. + /// + typedef bool (*FileExistsFunction)(const std::string &abs_filename, void *); + + /// + /// ExpandFilePathFunction type. Signature for custom filesystem callbacks. + /// + typedef std::string (*ExpandFilePathFunction)(const std::string &, void *); + + /// + /// ReadWholeFileFunction type. Signature for custom filesystem callbacks. + /// + typedef bool (*ReadWholeFileFunction)(std::vector *, + std::string *, const std::string &, + void *); + + /// + /// WriteWholeFileFunction type. Signature for custom filesystem callbacks. + /// + typedef bool (*WriteWholeFileFunction)(std::string *, const std::string &, + const std::vector &, + void *); + + /// + /// A structure containing all required filesystem callbacks and a pointer to + /// their user data. + /// + struct FsCallbacks { + FileExistsFunction FileExists; + ExpandFilePathFunction ExpandFilePath; + ReadWholeFileFunction ReadWholeFile; + WriteWholeFileFunction WriteWholeFile; + + void *user_data; // An argument that is passed to all fs callbacks + }; + +#ifndef TINYGLTF_NO_FS + // Declaration of default filesystem callbacks + + bool FileExists(const std::string &abs_filename, void *); + + /// + /// Expand file path(e.g. `~` to home directory on posix, `%APPDATA%` to + /// `C:\Users\tinygltf\AppData`) + /// + /// @param[in] filepath File path string. Assume UTF-8 + /// @param[in] userdata User data. Set to `nullptr` if you don't need it. + /// + std::string ExpandFilePath(const std::string &filepath, void *userdata); + + bool ReadWholeFile(std::vector *out, std::string *err, + const std::string &filepath, void *); + + bool WriteWholeFile(std::string *err, const std::string &filepath, + const std::vector &contents, void *); +#endif + + /// + /// glTF Parser/Serialier context. + /// + class TinyGLTF { + public: +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat" +#endif + + TinyGLTF() : bin_data_(nullptr), bin_size_(0), is_binary_(false) {} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + + ~TinyGLTF() {} + + /// + /// Loads glTF ASCII asset from a file. + /// Set warning message to `warn` for example it fails to load asserts. + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadASCIIFromFile(Model *model, std::string *err, std::string *warn, + const std::string &filename, + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Loads glTF ASCII asset from string(memory). + /// `length` = strlen(str); + /// Set warning message to `warn` for example it fails to load asserts. + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadASCIIFromString(Model *model, std::string *err, std::string *warn, + const char *str, const unsigned int length, + const std::string &base_dir, + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Loads glTF binary asset from a file. + /// Set warning message to `warn` for example it fails to load asserts. + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadBinaryFromFile(Model *model, std::string *err, std::string *warn, + const std::string &filename, + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Loads glTF binary asset from memory. + /// `length` = strlen(str); + /// Set warning message to `warn` for example it fails to load asserts. + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadBinaryFromMemory(Model *model, std::string *err, std::string *warn, + const unsigned char *bytes, + const unsigned int length, + const std::string &base_dir = "", + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Write glTF to stream, buffers and images will be embeded + /// + bool WriteGltfSceneToStream(Model *model, std::ostream &stream, + bool prettyPrint, bool writeBinary); + + /// + /// Write glTF to file. + /// + bool WriteGltfSceneToFile(Model *model, const std::string &filename, + bool embedImages, bool embedBuffers, + bool prettyPrint, bool writeBinary); + + /// + /// Set callback to use for loading image data + /// + void SetImageLoader(LoadImageDataFunction LoadImageData, void *user_data); + + /// + /// Set callback to use for writing image data + /// + void SetImageWriter(WriteImageDataFunction WriteImageData, void *user_data); + + /// + /// Set callbacks to use for filesystem (fs) access and their user data + /// + void SetFsCallbacks(FsCallbacks callbacks); + + /// + /// Set serializing default values(default = false). + /// When true, default values are force serialized to .glTF. + /// This may be helpfull if you want to serialize a full description of glTF + /// data. + /// + /// TODO(LTE): Supply parsing option as function arguments to + /// `LoadASCIIFromFile()` and others, not by a class method + /// + void SetSerializeDefaultValues(const bool enabled) { + serialize_default_values_ = enabled; + } + + bool GetSerializeDefaultValues() const { return serialize_default_values_; } + + /// + /// Store original JSON string for `extras` and `extensions`. + /// This feature will be useful when the user want to reconstruct custom data + /// structure from JSON string. + /// + void SetStoreOriginalJSONForExtrasAndExtensions(const bool enabled) { + store_original_json_for_extras_and_extensions_ = enabled; + } + + bool GetStoreOriginalJSONForExtrasAndExtensions() const { + return store_original_json_for_extras_and_extensions_; + } + + private: + /// + /// Loads glTF asset from string(memory). + /// `length` = strlen(str); + /// Set warning message to `warn` for example it fails to load asserts + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadFromString(Model *model, std::string *err, std::string *warn, + const char *str, const unsigned int length, + const std::string &base_dir, unsigned int check_sections); + + const unsigned char *bin_data_ = nullptr; + size_t bin_size_ = 0; + bool is_binary_ = false; + + bool serialize_default_values_ = false; ///< Serialize default values? + + bool store_original_json_for_extras_and_extensions_ = false; + + FsCallbacks fs = { +#ifndef TINYGLTF_NO_FS + &tinygltf::FileExists, &tinygltf::ExpandFilePath, + &tinygltf::ReadWholeFile, &tinygltf::WriteWholeFile, + + nullptr // Fs callback user data +#else + nullptr, nullptr, nullptr, nullptr, + + nullptr // Fs callback user data +#endif + }; + + LoadImageDataFunction LoadImageData = +#ifndef TINYGLTF_NO_STB_IMAGE + &tinygltf::LoadImageData; +#else + nullptr; +#endif + void *load_image_user_data_ = reinterpret_cast(&fs); + + WriteImageDataFunction WriteImageData = +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE + &tinygltf::WriteImageData; +#else + nullptr; +#endif + void *write_image_user_data_ = reinterpret_cast(&fs); + }; + +#ifdef __clang__ +#pragma clang diagnostic pop // -Wpadded +#endif + +} // namespace tinygltf + +#endif // TINY_GLTF_H_ + +#if defined(TINYGLTF_IMPLEMENTATION) || defined(__INTELLISENSE__) +#include + //#include +#ifndef TINYGLTF_NO_FS +#include +#include +#endif +#include + +#ifdef __clang__ + // Disable some warnings for external files. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" +#pragma clang diagnostic ignored "-Wexit-time-destructors" +#pragma clang diagnostic ignored "-Wconversion" +#pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic ignored "-Wglobal-constructors" +#if __has_warning("-Wreserved-id-macro") +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif +#pragma clang diagnostic ignored "-Wdisabled-macro-expansion" +#pragma clang diagnostic ignored "-Wpadded" +#pragma clang diagnostic ignored "-Wc++98-compat" +#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#pragma clang diagnostic ignored "-Wswitch-enum" +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#pragma clang diagnostic ignored "-Wweak-vtables" +#pragma clang diagnostic ignored "-Wcovered-switch-default" +#if __has_warning("-Wdouble-promotion") +#pragma clang diagnostic ignored "-Wdouble-promotion" +#endif +#if __has_warning("-Wcomma") +#pragma clang diagnostic ignored "-Wcomma" +#endif +#if __has_warning("-Wzero-as-null-pointer-constant") +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#endif +#if __has_warning("-Wcast-qual") +#pragma clang diagnostic ignored "-Wcast-qual" +#endif +#if __has_warning("-Wmissing-variable-declarations") +#pragma clang diagnostic ignored "-Wmissing-variable-declarations" +#endif +#if __has_warning("-Wmissing-prototypes") +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#endif +#if __has_warning("-Wcast-align") +#pragma clang diagnostic ignored "-Wcast-align" +#endif +#if __has_warning("-Wnewline-eof") +#pragma clang diagnostic ignored "-Wnewline-eof" +#endif +#if __has_warning("-Wunused-parameter") +#pragma clang diagnostic ignored "-Wunused-parameter" +#endif +#if __has_warning("-Wmismatched-tags") +#pragma clang diagnostic ignored "-Wmismatched-tags" +#endif +#if __has_warning("-Wextra-semi-stmt") +#pragma clang diagnostic ignored "-Wextra-semi-stmt" +#endif +#endif + + // Disable GCC warnigs +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" +#endif // __GNUC__ + +#ifndef TINYGLTF_NO_INCLUDE_JSON +#ifndef TINYGLTF_USE_RAPIDJSON +#include "json.hpp" +#else +#include "document.h" +#include "prettywriter.h" +#include "rapidjson.h" +#include "stringbuffer.h" +#include "writer.h" +#endif +#endif + +#ifdef TINYGLTF_ENABLE_DRACO +#include "draco/compression/decode.h" +#include "draco/core/decoder_buffer.h" +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE +#ifndef TINYGLTF_NO_INCLUDE_STB_IMAGE +#include "stb_image.h" +#endif +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE +#ifndef TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE +#include "stb_image_write.h" +#endif +#endif + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#ifdef _WIN32 + + // issue 143. + // Define NOMINMAX to avoid min/max defines, + // but undef it after included windows.h +#ifndef NOMINMAX +#define TINYGLTF_INTERNAL_NOMINMAX +#define NOMINMAX +#endif + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#define TINYGLTF_INTERNAL_WIN32_LEAN_AND_MEAN +#endif +#include // include API for expanding a file path + +#ifdef TINYGLTF_INTERNAL_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#endif + +#if defined(TINYGLTF_INTERNAL_NOMINMAX) +#undef NOMINMAX +#endif + +#if defined(__GLIBCXX__) // mingw + +#include // _O_RDONLY + +#include // fstream (all sorts of IO stuff) + stdio_filebuf (=streambuf) + +#endif + +#elif !defined(__ANDROID__) +#include +#endif + +#if defined(__sparcv9) + // Big endian +#else +#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || MINIZ_X86_OR_X64_CPU +#define TINYGLTF_LITTLE_ENDIAN 1 +#endif +#endif + +namespace { +#ifdef TINYGLTF_USE_RAPIDJSON + +#ifdef TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR + // This uses the RapidJSON CRTAllocator. It is thread safe and multiple + // documents may be active at once. + using json = + rapidjson::GenericValue, rapidjson::CrtAllocator>; + using json_const_iterator = json::ConstMemberIterator; + using json_const_array_iterator = json const *; + using JsonDocument = + rapidjson::GenericDocument, rapidjson::CrtAllocator>; + rapidjson::CrtAllocator s_CrtAllocator; // stateless and thread safe + rapidjson::CrtAllocator &GetAllocator() { return s_CrtAllocator; } +#else + // This uses the default RapidJSON MemoryPoolAllocator. It is very fast, but + // not thread safe. Only a single JsonDocument may be active at any one time, + // meaning only a single gltf load/save can be active any one time. + using json = rapidjson::Value; + using json_const_iterator = json::ConstMemberIterator; + using json_const_array_iterator = json const *; + rapidjson::Document *s_pActiveDocument = nullptr; + rapidjson::Document::AllocatorType &GetAllocator() { + assert(s_pActiveDocument); // Root json node must be JsonDocument type + return s_pActiveDocument->GetAllocator(); + } + +#ifdef __clang__ +#pragma clang diagnostic push + // Suppress JsonDocument(JsonDocument &&rhs) noexcept +#pragma clang diagnostic ignored "-Wunused-member-function" +#endif + + struct JsonDocument : public rapidjson::Document { + JsonDocument() { + assert(s_pActiveDocument == + nullptr); // When using default allocator, only one document can be + // active at a time, if you need multiple active at once, + // define TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR + s_pActiveDocument = this; + } + JsonDocument(const JsonDocument &) = delete; + JsonDocument(JsonDocument &&rhs) noexcept + : rapidjson::Document(std::move(rhs)) { + s_pActiveDocument = this; + rhs.isNil = true; + } + ~JsonDocument() { + if (!isNil) { + s_pActiveDocument = nullptr; + } + } + + private: + bool isNil = false; + }; + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR + +#else + using nlohmann::json; + using json_const_iterator = json::const_iterator; + using json_const_array_iterator = json_const_iterator; + using JsonDocument = json; +#endif + + void JsonParse(JsonDocument &doc, const char *str, size_t length, + bool throwExc = false) { +#ifdef TINYGLTF_USE_RAPIDJSON + (void)throwExc; + doc.Parse(str, length); +#else + doc = json::parse(str, str + length, nullptr, throwExc); +#endif + } +} // namespace + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat" +#endif + +namespace tinygltf { + + // Equals function for Value, for recursivity + static bool Equals(const tinygltf::Value &one, const tinygltf::Value &other) { + if (one.Type() != other.Type()) return false; + + switch (one.Type()) { + case NULL_TYPE: + return true; + case BOOL_TYPE: + return one.Get() == other.Get(); + case REAL_TYPE: + return TINYGLTF_DOUBLE_EQUAL(one.Get(), other.Get()); + case INT_TYPE: + return one.Get() == other.Get(); + case OBJECT_TYPE: { + auto oneObj = one.Get(); + auto otherObj = other.Get(); + if (oneObj.size() != otherObj.size()) return false; + for (auto &it : oneObj) { + auto otherIt = otherObj.find(it.first); + if (otherIt == otherObj.end()) return false; + + if (!Equals(it.second, otherIt->second)) return false; + } + return true; + } + case ARRAY_TYPE: { + if (one.Size() != other.Size()) return false; + for (int i = 0; i < int(one.Size()); ++i) + if (!Equals(one.Get(i), other.Get(i))) return false; + return true; + } + case STRING_TYPE: + return one.Get() == other.Get(); + case BINARY_TYPE: + return one.Get >() == + other.Get >(); + default: { + // unhandled type + return false; + } + } + } + + // Equals function for std::vector using TINYGLTF_DOUBLE_EPSILON + static bool Equals(const std::vector &one, + const std::vector &other) { + if (one.size() != other.size()) return false; + for (int i = 0; i < int(one.size()); ++i) { + if (!TINYGLTF_DOUBLE_EQUAL(one[size_t(i)], other[size_t(i)])) return false; + } + return true; + } + + bool Accessor::operator==(const Accessor &other) const { + return this->bufferView == other.bufferView && + this->byteOffset == other.byteOffset && + this->componentType == other.componentType && + this->count == other.count && this->extensions == other.extensions && + this->extras == other.extras && + Equals(this->maxValues, other.maxValues) && + Equals(this->minValues, other.minValues) && this->name == other.name && + this->normalized == other.normalized && this->type == other.type; + } + bool Animation::operator==(const Animation &other) const { + return this->channels == other.channels && + this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && this->samplers == other.samplers; + } + bool AnimationChannel::operator==(const AnimationChannel &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->target_node == other.target_node && + this->target_path == other.target_path && + this->sampler == other.sampler; + } + bool AnimationSampler::operator==(const AnimationSampler &other) const { + return this->extras == other.extras && this->extensions == other.extensions && + this->input == other.input && + this->interpolation == other.interpolation && + this->output == other.output; + } + bool Asset::operator==(const Asset &other) const { + return this->copyright == other.copyright && + this->extensions == other.extensions && this->extras == other.extras && + this->generator == other.generator && + this->minVersion == other.minVersion && this->version == other.version; + } + bool Buffer::operator==(const Buffer &other) const { + return this->data == other.data && this->extensions == other.extensions && + this->extras == other.extras && this->name == other.name && + this->uri == other.uri; + } + bool BufferView::operator==(const BufferView &other) const { + return this->buffer == other.buffer && this->byteLength == other.byteLength && + this->byteOffset == other.byteOffset && + this->byteStride == other.byteStride && this->name == other.name && + this->target == other.target && this->extensions == other.extensions && + this->extras == other.extras && + this->dracoDecoded == other.dracoDecoded; + } + bool Camera::operator==(const Camera &other) const { + return this->name == other.name && this->extensions == other.extensions && + this->extras == other.extras && + this->orthographic == other.orthographic && + this->perspective == other.perspective && this->type == other.type; + } + bool Image::operator==(const Image &other) const { + return this->bufferView == other.bufferView && + this->component == other.component && + this->extensions == other.extensions && this->extras == other.extras && + this->height == other.height && this->image == other.image && + this->mimeType == other.mimeType && this->name == other.name && + this->uri == other.uri && this->width == other.width; + } + bool Light::operator==(const Light &other) const { + return Equals(this->color, other.color) && this->name == other.name && + this->type == other.type; + } + bool Material::operator==(const Material &other) const { + return (this->pbrMetallicRoughness == other.pbrMetallicRoughness) && + (this->normalTexture == other.normalTexture) && + (this->occlusionTexture == other.occlusionTexture) && + (this->emissiveTexture == other.emissiveTexture) && + Equals(this->emissiveFactor, other.emissiveFactor) && + (this->alphaMode == other.alphaMode) && + TINYGLTF_DOUBLE_EQUAL(this->alphaCutoff, other.alphaCutoff) && + (this->doubleSided == other.doubleSided) && + (this->extensions == other.extensions) && + (this->extras == other.extras) && (this->values == other.values) && + (this->additionalValues == other.additionalValues) && + (this->name == other.name); + } + bool Mesh::operator==(const Mesh &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && Equals(this->weights, other.weights) && + this->primitives == other.primitives; + } + bool Model::operator==(const Model &other) const { + return this->accessors == other.accessors && + this->animations == other.animations && this->asset == other.asset && + this->buffers == other.buffers && + this->bufferViews == other.bufferViews && + this->cameras == other.cameras && + this->defaultScene == other.defaultScene && + this->extensions == other.extensions && + this->extensionsRequired == other.extensionsRequired && + this->extensionsUsed == other.extensionsUsed && + this->extras == other.extras && this->images == other.images && + this->lights == other.lights && this->materials == other.materials && + this->meshes == other.meshes && this->nodes == other.nodes && + this->samplers == other.samplers && this->scenes == other.scenes && + this->skins == other.skins && this->textures == other.textures; + } + bool Node::operator==(const Node &other) const { + return this->camera == other.camera && this->children == other.children && + this->extensions == other.extensions && this->extras == other.extras && + Equals(this->matrix, other.matrix) && this->mesh == other.mesh && + this->name == other.name && Equals(this->rotation, other.rotation) && + Equals(this->scale, other.scale) && this->skin == other.skin && + Equals(this->translation, other.translation) && + Equals(this->weights, other.weights); + } + bool SpotLight::operator==(const SpotLight &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->innerConeAngle, other.innerConeAngle) && + TINYGLTF_DOUBLE_EQUAL(this->outerConeAngle, other.outerConeAngle); + } + bool OrthographicCamera::operator==(const OrthographicCamera &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->xmag, other.xmag) && + TINYGLTF_DOUBLE_EQUAL(this->ymag, other.ymag) && + TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && + TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); + } + bool Parameter::operator==(const Parameter &other) const { + if (this->bool_value != other.bool_value || + this->has_number_value != other.has_number_value) + return false; + + if (!TINYGLTF_DOUBLE_EQUAL(this->number_value, other.number_value)) + return false; + + if (this->json_double_value.size() != other.json_double_value.size()) + return false; + for (auto &it : this->json_double_value) { + auto otherIt = other.json_double_value.find(it.first); + if (otherIt == other.json_double_value.end()) return false; + + if (!TINYGLTF_DOUBLE_EQUAL(it.second, otherIt->second)) return false; + } + + if (!Equals(this->number_array, other.number_array)) return false; + + if (this->string_value != other.string_value) return false; + + return true; + } + bool PerspectiveCamera::operator==(const PerspectiveCamera &other) const { + return TINYGLTF_DOUBLE_EQUAL(this->aspectRatio, other.aspectRatio) && + this->extensions == other.extensions && this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->yfov, other.yfov) && + TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && + TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); + } + bool Primitive::operator==(const Primitive &other) const { + return this->attributes == other.attributes && this->extras == other.extras && + this->indices == other.indices && this->material == other.material && + this->mode == other.mode && this->targets == other.targets; + } + bool Sampler::operator==(const Sampler &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->magFilter == other.magFilter && + this->minFilter == other.minFilter && this->name == other.name && + this->wrapR == other.wrapR && this->wrapS == other.wrapS && + this->wrapT == other.wrapT; + } + bool Scene::operator==(const Scene &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && this->nodes == other.nodes; + } + bool Skin::operator==(const Skin &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->inverseBindMatrices == other.inverseBindMatrices && + this->joints == other.joints && this->name == other.name && + this->skeleton == other.skeleton; + } + bool Texture::operator==(const Texture &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->name == other.name && this->sampler == other.sampler && + this->source == other.source; + } + bool TextureInfo::operator==(const TextureInfo &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->index == other.index && this->texCoord == other.texCoord; + } + bool NormalTextureInfo::operator==(const NormalTextureInfo &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->index == other.index && this->texCoord == other.texCoord && + TINYGLTF_DOUBLE_EQUAL(this->scale, other.scale); + } + bool OcclusionTextureInfo::operator==(const OcclusionTextureInfo &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + this->index == other.index && this->texCoord == other.texCoord && + TINYGLTF_DOUBLE_EQUAL(this->strength, other.strength); + } + bool PbrMetallicRoughness::operator==(const PbrMetallicRoughness &other) const { + return this->extensions == other.extensions && this->extras == other.extras && + (this->baseColorTexture == other.baseColorTexture) && + (this->metallicRoughnessTexture == other.metallicRoughnessTexture) && + Equals(this->baseColorFactor, other.baseColorFactor) && + TINYGLTF_DOUBLE_EQUAL(this->metallicFactor, other.metallicFactor) && + TINYGLTF_DOUBLE_EQUAL(this->roughnessFactor, other.roughnessFactor); + } + bool Value::operator==(const Value &other) const { + return Equals(*this, other); + } + + static void swap4(unsigned int *val) { +#ifdef TINYGLTF_LITTLE_ENDIAN + (void)val; +#else + unsigned int tmp = *val; + unsigned char *dst = reinterpret_cast(val); + unsigned char *src = reinterpret_cast(&tmp); + + dst[0] = src[3]; + dst[1] = src[2]; + dst[2] = src[1]; + dst[3] = src[0]; +#endif + } + + static std::string JoinPath(const std::string &path0, + const std::string &path1) { + if (path0.empty()) { + return path1; + } else { + // check '/' + char lastChar = *path0.rbegin(); + if (lastChar != '/') { + return path0 + std::string("/") + path1; + } else { + return path0 + path1; + } + } + } + + static std::string FindFile(const std::vector &paths, + const std::string &filepath, FsCallbacks *fs) { + if (fs == nullptr || fs->ExpandFilePath == nullptr || + fs->FileExists == nullptr) { + // Error, fs callback[s] missing + return std::string(); + } + + for (size_t i = 0; i < paths.size(); i++) { + std::string absPath = + fs->ExpandFilePath(JoinPath(paths[i], filepath), fs->user_data); + if (fs->FileExists(absPath, fs->user_data)) { + return absPath; + } + } + + return std::string(); + } + + static std::string GetFilePathExtension(const std::string &FileName) { + if (FileName.find_last_of(".") != std::string::npos) + return FileName.substr(FileName.find_last_of(".") + 1); + return ""; + } + + static std::string GetBaseDir(const std::string &filepath) { + if (filepath.find_last_of("/\\") != std::string::npos) + return filepath.substr(0, filepath.find_last_of("/\\")); + return ""; + } + + // https://stackoverflow.com/questions/8520560/get-a-file-name-from-a-path + static std::string GetBaseFilename(const std::string &filepath) { + return filepath.substr(filepath.find_last_of("/\\") + 1); + } + + std::string base64_encode(unsigned char const *, unsigned int len); + std::string base64_decode(std::string const &s); + + /* + base64.cpp and base64.h + + Copyright (C) 2004-2008 René Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + René Nyffenegger rene.nyffenegger@adp-gmbh.ch + + */ + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" +#pragma clang diagnostic ignored "-Wconversion" +#endif + + static inline bool is_base64(unsigned char c) { + return (isalnum(c) || (c == '+') || (c == '/')); + } + + std::string base64_encode(unsigned char const *bytes_to_encode, + unsigned int in_len) { + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + const char *base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = + ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = + ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; (i < 4); i++) ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) { + for (j = i; j < 3; j++) char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = + ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = + ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + + for (j = 0; (j < i + 1); j++) ret += base64_chars[char_array_4[j]]; + + while ((i++ < 3)) ret += '='; + } + + return ret; + } + + std::string base64_decode(std::string const &encoded_string) { + int in_len = static_cast(encoded_string.size()); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + while (in_len-- && (encoded_string[in_] != '=') && + is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; + in_++; + if (i == 4) { + for (i = 0; i < 4; i++) + char_array_4[i] = + static_cast(base64_chars.find(char_array_4[i])); + + char_array_3[0] = + (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = + ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = i; j < 4; j++) char_array_4[j] = 0; + + for (j = 0; j < 4; j++) + char_array_4[j] = + static_cast(base64_chars.find(char_array_4[j])); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = + ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; + } + + return ret; + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + + // https://github.com/syoyo/tinygltf/issues/228 + // TODO(syoyo): Use uriparser https://uriparser.github.io/ for stricter Uri + // decoding? + // + // https://stackoverflow.com/questions/18307429/encode-decode-url-in-c + // http://dlib.net/dlib/server/server_http.cpp.html + + // --- dlib beign ------------------------------------------------------------ + // Copyright (C) 2003 Davis E. King (davis@dlib.net) + // License: Boost Software License See LICENSE.txt for the full license. + + namespace dlib { + +#if 0 + inline unsigned char to_hex( unsigned char x ) + { + return x + (x > 9 ? ('A'-10) : '0'); + } + + const std::string urlencode( const std::string& s ) + { + std::ostringstream os; + + for ( std::string::const_iterator ci = s.begin(); ci != s.end(); ++ci ) + { + if ( (*ci >= 'a' && *ci <= 'z') || + (*ci >= 'A' && *ci <= 'Z') || + (*ci >= '0' && *ci <= '9') ) + { // allowed + os << *ci; + } + else if ( *ci == ' ') + { + os << '+'; + } + else + { + os << '%' << to_hex(static_cast(*ci >> 4)) << to_hex(static_cast(*ci % 16)); + } + } + + return os.str(); + } +#endif + + inline unsigned char from_hex(unsigned char ch) { + if (ch <= '9' && ch >= '0') + ch -= '0'; + else if (ch <= 'f' && ch >= 'a') + ch -= 'a' - 10; + else if (ch <= 'F' && ch >= 'A') + ch -= 'A' - 10; + else + ch = 0; + return ch; + } + + static const std::string urldecode(const std::string &str) { + using namespace std; + string result; + string::size_type i; + for (i = 0; i < str.size(); ++i) { + if (str[i] == '+') { + result += ' '; + } else if (str[i] == '%' && str.size() > i + 2) { + const unsigned char ch1 = + from_hex(static_cast(str[i + 1])); + const unsigned char ch2 = + from_hex(static_cast(str[i + 2])); + const unsigned char ch = static_cast((ch1 << 4) | ch2); + result += static_cast(ch); + i += 2; + } else { + result += str[i]; + } + } + return result; + } + + } // namespace dlib + // --- dlib end -------------------------------------------------------------- + + static bool LoadExternalFile(std::vector *out, std::string *err, + std::string *warn, const std::string &filename, + const std::string &basedir, bool required, + size_t reqBytes, bool checkSize, FsCallbacks *fs) { + if (fs == nullptr || fs->FileExists == nullptr || + fs->ExpandFilePath == nullptr || fs->ReadWholeFile == nullptr) { + // This is a developer error, assert() ? + if (err) { + (*err) += "FS callback[s] not set\n"; + } + return false; + } + + std::string *failMsgOut = required ? err : warn; + + out->clear(); + + std::vector paths; + paths.push_back(basedir); + paths.push_back("."); + + std::string filepath = FindFile(paths, filename, fs); + if (filepath.empty() || filename.empty()) { + if (failMsgOut) { + (*failMsgOut) += "File not found : " + filename + "\n"; + } + return false; + } + + std::vector buf; + std::string fileReadErr; + bool fileRead = + fs->ReadWholeFile(&buf, &fileReadErr, filepath, fs->user_data); + if (!fileRead) { + if (failMsgOut) { + (*failMsgOut) += + "File read error : " + filepath + " : " + fileReadErr + "\n"; + } + return false; + } + + size_t sz = buf.size(); + if (sz == 0) { + if (failMsgOut) { + (*failMsgOut) += "File is empty : " + filepath + "\n"; + } + return false; + } + + if (checkSize) { + if (reqBytes == sz) { + out->swap(buf); + return true; + } else { + std::stringstream ss; + ss << "File size mismatch : " << filepath << ", requestedBytes " + << reqBytes << ", but got " << sz << std::endl; + if (failMsgOut) { + (*failMsgOut) += ss.str(); + } + return false; + } + } + + out->swap(buf); + return true; + } + + void TinyGLTF::SetImageLoader(LoadImageDataFunction func, void *user_data) { + LoadImageData = func; + load_image_user_data_ = user_data; + } + +#ifndef TINYGLTF_NO_STB_IMAGE + bool LoadImageData(Image *image, const int image_idx, std::string *err, + std::string *warn, int req_width, int req_height, + const unsigned char *bytes, int size, void *user_data) { + (void)user_data; + (void)warn; + + int w = 0, h = 0, comp = 0, req_comp = 0; + + unsigned char *data = nullptr; + + // force 32-bit textures for common Vulkan compatibility. It appears that + // some GPU drivers do not support 24-bit images for Vulkan + req_comp = 4; + int bits = 8; + int pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + + // It is possible that the image we want to load is a 16bit per channel image + // We are going to attempt to load it as 16bit per channel, and if it worked, + // set the image data accodingly. We are casting the returned pointer into + // unsigned char, because we are representing "bytes". But we are updating + // the Image metadata to signal that this image uses 2 bytes (16bits) per + // channel: + if (stbi_is_16_bit_from_memory(bytes, size)) { + data = reinterpret_cast( + stbi_load_16_from_memory(bytes, size, &w, &h, &comp, req_comp)); + if (data) { + bits = 16; + pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT; + } + } + + // at this point, if data is still NULL, it means that the image wasn't + // 16bit per channel, we are going to load it as a normal 8bit per channel + // mage as we used to do: + // if image cannot be decoded, ignore parsing and keep it by its path + // don't break in this case + // FIXME we should only enter this function if the image is embedded. If + // image->uri references + // an image file, it should be left as it is. Image loading should not be + // mandatory (to support other formats) + if (!data) data = stbi_load_from_memory(bytes, size, &w, &h, &comp, req_comp); + if (!data) { + // NOTE: you can use `warn` instead of `err` + if (err) { + (*err) += + "Unknown image format. STB cannot decode image data for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + "\".\n"; + } + return false; + } + + if ((w < 1) || (h < 1)) { + stbi_image_free(data); + if (err) { + (*err) += "Invalid image data for image[" + std::to_string(image_idx) + + "] name = \"" + image->name + "\"\n"; + } + return false; + } + + if (req_width > 0) { + if (req_width != w) { + stbi_image_free(data); + if (err) { + (*err) += "Image width mismatch for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\"\n"; + } + return false; + } + } + + if (req_height > 0) { + if (req_height != h) { + stbi_image_free(data); + if (err) { + (*err) += "Image height mismatch. for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\"\n"; + } + return false; + } + } + + image->width = w; + image->height = h; + image->component = req_comp; + image->bits = bits; + image->pixel_type = pixel_type; + image->image.resize(static_cast(w * h * req_comp) * size_t(bits / 8)); + std::copy(data, data + w * h * req_comp * (bits / 8), image->image.begin()); + stbi_image_free(data); + + return true; + } +#endif + + void TinyGLTF::SetImageWriter(WriteImageDataFunction func, void *user_data) { + WriteImageData = func; + write_image_user_data_ = user_data; + } + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE + static void WriteToMemory_stbi(void *context, void *data, int size) { + std::vector *buffer = + reinterpret_cast *>(context); + + unsigned char *pData = reinterpret_cast(data); + + buffer->insert(buffer->end(), pData, pData + size); + } + + bool WriteImageData(const std::string *basepath, const std::string *filename, + Image *image, bool embedImages, void *fsPtr) { + const std::string ext = GetFilePathExtension(*filename); + + // Write image to temporary buffer + std::string header; + std::vector data; + + if (ext == "png") { + if ((image->bits != 8) || + (image->pixel_type != TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE)) { + // Unsupported pixel format + return false; + } + + if (!stbi_write_png_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0], 0)) { + return false; + } + header = "data:image/png;base64,"; + } else if (ext == "jpg") { + if (!stbi_write_jpg_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0], 100)) { + return false; + } + header = "data:image/jpeg;base64,"; + } else if (ext == "bmp") { + if (!stbi_write_bmp_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0])) { + return false; + } + header = "data:image/bmp;base64,"; + } else if (!embedImages) { + // Error: can't output requested format to file + return false; + } + + if (embedImages) { + // Embed base64-encoded image into URI + if (data.size()) { + image->uri = + header + + base64_encode(&data[0], static_cast(data.size())); + } else { + // Throw error? + } + } else { + // Write image to disc + FsCallbacks *fs = reinterpret_cast(fsPtr); + if ((fs != nullptr) && (fs->WriteWholeFile != nullptr)) { + const std::string imagefilepath = JoinPath(*basepath, *filename); + std::string writeError; + if (!fs->WriteWholeFile(&writeError, imagefilepath, data, + fs->user_data)) { + // Could not write image file to disc; Throw error ? + return false; + } + } else { + // Throw error? + } + image->uri = *filename; + } + + return true; + } +#endif + + void TinyGLTF::SetFsCallbacks(FsCallbacks callbacks) { fs = callbacks; } + +#ifdef _WIN32 + static inline std::wstring UTF8ToWchar(const std::string &str) { + int wstr_size = + MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0); + std::wstring wstr(wstr_size, 0); + MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &wstr[0], + (int)wstr.size()); + return wstr; + } + + static inline std::string WcharToUTF8(const std::wstring &wstr) { + int str_size = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), + nullptr, 0, NULL, NULL); + std::string str(str_size, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), &str[0], + (int)str.size(), NULL, NULL); + return str; + } +#endif + +#ifndef TINYGLTF_NO_FS + // Default implementations of filesystem functions + + bool FileExists(const std::string &abs_filename, void *) { + bool ret; +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS + if (asset_manager) { + AAsset *asset = AAssetManager_open(asset_manager, abs_filename.c_str(), + AASSET_MODE_STREAMING); + if (!asset) { + return false; + } + AAsset_close(asset); + ret = true; + } else { + return false; + } +#else +#ifdef _WIN32 +#if defined(_MSC_VER) || defined(__GLIBCXX__) + FILE *fp = nullptr; + errno_t err = _wfopen_s(&fp, UTF8ToWchar(abs_filename).c_str(), L"rb"); + if (err != 0) { + return false; + } +#else + FILE *fp = nullptr; + errno_t err = fopen_s(&fp, abs_filename.c_str(), "rb"); + if (err != 0) { + return false; + } +#endif + +#else + FILE *fp = fopen(abs_filename.c_str(), "rb"); +#endif + if (fp) { + ret = true; + fclose(fp); + } else { + ret = false; + } +#endif + + return ret; + } + + std::string ExpandFilePath(const std::string &filepath, void *) { +#ifdef _WIN32 + // Assume input `filepath` is encoded in UTF-8 + std::wstring wfilepath = UTF8ToWchar(filepath); + DWORD wlen = ExpandEnvironmentStringsW(wfilepath.c_str(), nullptr, 0); + wchar_t *wstr = new wchar_t[wlen]; + ExpandEnvironmentStringsW(wfilepath.c_str(), wstr, wlen); + + std::wstring ws(wstr); + delete[] wstr; + return WcharToUTF8(ws); + +#else + +#if defined(TARGET_OS_IPHONE) || defined(TARGET_IPHONE_SIMULATOR) || \ + defined(__ANDROID__) || defined(__EMSCRIPTEN__) + // no expansion + std::string s = filepath; +#else + std::string s; + wordexp_t p; + + if (filepath.empty()) { + return ""; + } + + // Quote the string to keep any spaces in filepath intact. + std::string quoted_path = "\"" + filepath + "\""; + // char** w; + int ret = wordexp(quoted_path.c_str(), &p, 0); + if (ret) { + // err + s = filepath; + return s; + } + + // Use first element only. + if (p.we_wordv) { + s = std::string(p.we_wordv[0]); + wordfree(&p); + } else { + s = filepath; + } + +#endif + + return s; +#endif + } + + bool ReadWholeFile(std::vector *out, std::string *err, + const std::string &filepath, void *) { +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS + if (asset_manager) { + AAsset *asset = AAssetManager_open(asset_manager, filepath.c_str(), + AASSET_MODE_STREAMING); + if (!asset) { + if (err) { + (*err) += "File open error : " + filepath + "\n"; + } + return false; + } + size_t size = AAsset_getLength(asset); + if (size == 0) { + if (err) { + (*err) += "Invalid file size : " + filepath + + " (does the path point to a directory?)"; + } + return false; + } + out->resize(size); + AAsset_read(asset, reinterpret_cast(&out->at(0)), size); + AAsset_close(asset); + return true; + } else { + if (err) { + (*err) += "No asset manager specified : " + filepath + "\n"; + } + return false; + } +#else +#ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = + _wopen(UTF8ToWchar(filepath).c_str(), _O_RDONLY | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, std::ios_base::in); + std::istream f(&wfile_buf); +#elif defined(_MSC_VER) || defined(_LIBCPP_VERSION) + // For libcxx, assume _LIBCPP_HAS_OPEN_WITH_WCHAR is defined to accept + // `wchar_t *` + std::ifstream f(UTF8ToWchar(filepath).c_str(), std::ifstream::binary); +#else + // Unknown compiler/runtime + std::ifstream f(filepath.c_str(), std::ifstream::binary); +#endif +#else + std::ifstream f(filepath.c_str(), std::ifstream::binary); +#endif + if (!f) { + if (err) { + (*err) += "File open error : " + filepath + "\n"; + } + return false; + } + + f.seekg(0, f.end); + size_t sz = static_cast(f.tellg()); + f.seekg(0, f.beg); + + if (int64_t(sz) < 0) { + if (err) { + (*err) += "Invalid file size : " + filepath + + " (does the path point to a directory?)"; + } + return false; + } else if (sz == 0) { + if (err) { + (*err) += "File is empty : " + filepath + "\n"; + } + return false; + } + + out->resize(sz); + f.read(reinterpret_cast(&out->at(0)), + static_cast(sz)); + + return true; +#endif + } + + bool WriteWholeFile(std::string *err, const std::string &filepath, + const std::vector &contents, void *) { +#ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = _wopen(UTF8ToWchar(filepath).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream f(&wfile_buf); +#elif defined(_MSC_VER) + std::ofstream f(UTF8ToWchar(filepath).c_str(), std::ofstream::binary); +#else // clang? + std::ofstream f(filepath.c_str(), std::ofstream::binary); +#endif +#else + std::ofstream f(filepath.c_str(), std::ofstream::binary); +#endif + if (!f) { + if (err) { + (*err) += "File open error for writing : " + filepath + "\n"; + } + return false; + } + + f.write(reinterpret_cast(&contents.at(0)), + static_cast(contents.size())); + if (!f) { + if (err) { + (*err) += "File write error: " + filepath + "\n"; + } + return false; + } + + return true; + } + +#endif // TINYGLTF_NO_FS + + static std::string MimeToExt(const std::string &mimeType) { + if (mimeType == "image/jpeg") { + return "jpg"; + } else if (mimeType == "image/png") { + return "png"; + } else if (mimeType == "image/bmp") { + return "bmp"; + } else if (mimeType == "image/gif") { + return "gif"; + } + + return ""; + } + + static void UpdateImageObject(Image &image, std::string &baseDir, int index, + bool embedImages, + WriteImageDataFunction *WriteImageData = nullptr, + void *user_data = nullptr) { + std::string filename; + std::string ext; + // If image has uri, use it it as a filename + if (image.uri.size()) { + filename = GetBaseFilename(image.uri); + ext = GetFilePathExtension(filename); + } else if (image.bufferView != -1) { + // If there's no URI and the data exists in a buffer, + // don't change properties or write images + } else if (image.name.size()) { + ext = MimeToExt(image.mimeType); + // Otherwise use name as filename + filename = image.name + "." + ext; + } else { + ext = MimeToExt(image.mimeType); + // Fallback to index of image as filename + filename = std::to_string(index) + "." + ext; + } + + // If callback is set, modify image data object + if (*WriteImageData != nullptr && !filename.empty()) { + std::string uri; + (*WriteImageData)(&baseDir, &filename, &image, embedImages, user_data); + } + } + + bool IsDataURI(const std::string &in) { + std::string header = "data:application/octet-stream;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/jpeg;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/png;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/bmp;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/gif;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:text/plain;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:application/gltf-buffer;base64,"; + if (in.find(header) == 0) { + return true; + } + + return false; + } + + bool DecodeDataURI(std::vector *out, std::string &mime_type, + const std::string &in, size_t reqBytes, bool checkSize) { + std::string header = "data:application/octet-stream;base64,"; + std::string data; + if (in.find(header) == 0) { + data = base64_decode(in.substr(header.size())); // cut mime string. + } + + if (data.empty()) { + header = "data:image/jpeg;base64,"; + if (in.find(header) == 0) { + mime_type = "image/jpeg"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/png;base64,"; + if (in.find(header) == 0) { + mime_type = "image/png"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/bmp;base64,"; + if (in.find(header) == 0) { + mime_type = "image/bmp"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/gif;base64,"; + if (in.find(header) == 0) { + mime_type = "image/gif"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:text/plain;base64,"; + if (in.find(header) == 0) { + mime_type = "text/plain"; + data = base64_decode(in.substr(header.size())); + } + } + + if (data.empty()) { + header = "data:application/gltf-buffer;base64,"; + if (in.find(header) == 0) { + data = base64_decode(in.substr(header.size())); + } + } + + // TODO(syoyo): Allow empty buffer? #229 + if (data.empty()) { + return false; + } + + if (checkSize) { + if (data.size() != reqBytes) { + return false; + } + out->resize(reqBytes); + } else { + out->resize(data.size()); + } + std::copy(data.begin(), data.end(), out->begin()); + return true; + } + + namespace { + bool GetInt(const json &o, int &val) { +#ifdef TINYGLTF_USE_RAPIDJSON + if (!o.IsDouble()) { + if (o.IsInt()) { + val = o.GetInt(); + return true; + } else if (o.IsUint()) { + val = static_cast(o.GetUint()); + return true; + } else if (o.IsInt64()) { + val = static_cast(o.GetInt64()); + return true; + } else if (o.IsUint64()) { + val = static_cast(o.GetUint64()); + return true; + } + } + + return false; +#else + auto type = o.type(); + + if ((type == json::value_t::number_integer) || + (type == json::value_t::number_unsigned)) { + val = static_cast(o.get()); + return true; + } + + return false; +#endif + } + +#ifdef TINYGLTF_USE_RAPIDJSON + bool GetDouble(const json &o, double &val) { + if (o.IsDouble()) { + val = o.GetDouble(); + return true; + } + + return false; + } +#endif + + bool GetNumber(const json &o, double &val) { +#ifdef TINYGLTF_USE_RAPIDJSON + if (o.IsNumber()) { + val = o.GetDouble(); + return true; + } + + return false; +#else + if (o.is_number()) { + val = o.get(); + return true; + } + + return false; +#endif + } + + bool GetString(const json &o, std::string &val) { +#ifdef TINYGLTF_USE_RAPIDJSON + if (o.IsString()) { + val = o.GetString(); + return true; + } + + return false; +#else + if (o.type() == json::value_t::string) { + val = o.get(); + return true; + } + + return false; +#endif + } + + bool IsArray(const json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + return o.IsArray(); +#else + return o.is_array(); +#endif + } + + json_const_array_iterator ArrayBegin(const json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + return o.Begin(); +#else + return o.begin(); +#endif + } + + json_const_array_iterator ArrayEnd(const json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + return o.End(); +#else + return o.end(); +#endif + } + + bool IsObject(const json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + return o.IsObject(); +#else + return o.is_object(); +#endif + } + + json_const_iterator ObjectBegin(const json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + return o.MemberBegin(); +#else + return o.begin(); +#endif + } + + json_const_iterator ObjectEnd(const json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + return o.MemberEnd(); +#else + return o.end(); +#endif + } + + // Making this a const char* results in a pointer to a temporary when + // TINYGLTF_USE_RAPIDJSON is off. + std::string GetKey(json_const_iterator &it) { +#ifdef TINYGLTF_USE_RAPIDJSON + return it->name.GetString(); +#else + return it.key().c_str(); +#endif + } + + bool FindMember(const json &o, const char *member, json_const_iterator &it) { +#ifdef TINYGLTF_USE_RAPIDJSON + if (!o.IsObject()) { + return false; + } + it = o.FindMember(member); + return it != o.MemberEnd(); +#else + it = o.find(member); + return it != o.end(); +#endif + } + + const json &GetValue(json_const_iterator &it) { +#ifdef TINYGLTF_USE_RAPIDJSON + return it->value; +#else + return it.value(); +#endif + } + + std::string JsonToString(const json &o, int spacing = -1) { +#ifdef TINYGLTF_USE_RAPIDJSON + using namespace rapidjson; + StringBuffer buffer; + if (spacing == -1) { + Writer writer(buffer); + o.Accept(writer); + } else { + PrettyWriter writer(buffer); + writer.SetIndent(' ', uint32_t(spacing)); + o.Accept(writer); + } + return buffer.GetString(); +#else + return o.dump(spacing); +#endif + } + + } // namespace + + static bool ParseJsonAsValue(Value *ret, const json &o) { + Value val{}; +#ifdef TINYGLTF_USE_RAPIDJSON + using rapidjson::Type; + switch (o.GetType()) { + case Type::kObjectType: { + Value::Object value_object; + for (auto it = o.MemberBegin(); it != o.MemberEnd(); ++it) { + Value entry; + ParseJsonAsValue(&entry, it->value); + if (entry.Type() != NULL_TYPE) + value_object.emplace(GetKey(it), std::move(entry)); + } + if (value_object.size() > 0) val = Value(std::move(value_object)); + } break; + case Type::kArrayType: { + Value::Array value_array; + value_array.reserve(o.Size()); + for (auto it = o.Begin(); it != o.End(); ++it) { + Value entry; + ParseJsonAsValue(&entry, *it); + if (entry.Type() != NULL_TYPE) + value_array.emplace_back(std::move(entry)); + } + if (value_array.size() > 0) val = Value(std::move(value_array)); + } break; + case Type::kStringType: + val = Value(std::string(o.GetString())); + break; + case Type::kFalseType: + case Type::kTrueType: + val = Value(o.GetBool()); + break; + case Type::kNumberType: + if (!o.IsDouble()) { + int i = 0; + GetInt(o, i); + val = Value(i); + } else { + double d = 0.0; + GetDouble(o, d); + val = Value(d); + } + break; + case Type::kNullType: + break; + // all types are covered, so no `case default` + } +#else + switch (o.type()) { + case json::value_t::object: { + Value::Object value_object; + for (auto it = o.begin(); it != o.end(); it++) { + Value entry; + ParseJsonAsValue(&entry, it.value()); + if (entry.Type() != NULL_TYPE) + value_object.emplace(it.key(), std::move(entry)); + } + if (value_object.size() > 0) val = Value(std::move(value_object)); + } break; + case json::value_t::array: { + Value::Array value_array; + value_array.reserve(o.size()); + for (auto it = o.begin(); it != o.end(); it++) { + Value entry; + ParseJsonAsValue(&entry, it.value()); + if (entry.Type() != NULL_TYPE) + value_array.emplace_back(std::move(entry)); + } + if (value_array.size() > 0) val = Value(std::move(value_array)); + } break; + case json::value_t::string: + val = Value(o.get()); + break; + case json::value_t::boolean: + val = Value(o.get()); + break; + case json::value_t::number_integer: + case json::value_t::number_unsigned: + val = Value(static_cast(o.get())); + break; + case json::value_t::number_float: + val = Value(o.get()); + break; + case json::value_t::null: + case json::value_t::discarded: + // default: + break; + } +#endif + if (ret) *ret = std::move(val); + + return val.Type() != NULL_TYPE; + } + + static bool ParseExtrasProperty(Value *ret, const json &o) { + json_const_iterator it; + if (!FindMember(o, "extras", it)) { + return false; + } + + return ParseJsonAsValue(ret, GetValue(it)); + } + + static bool ParseBooleanProperty(bool *ret, std::string *err, const json &o, + const std::string &property, + const bool required, + const std::string &parent_node = "") { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + auto &value = GetValue(it); + + bool isBoolean; + bool boolValue = false; +#ifdef TINYGLTF_USE_RAPIDJSON + isBoolean = value.IsBool(); + if (isBoolean) { + boolValue = value.GetBool(); + } +#else + isBoolean = value.is_boolean(); + if (isBoolean) { + boolValue = value.get(); + } +#endif + if (!isBoolean) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a bool type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = boolValue; + } + + return true; + } + + static bool ParseIntegerProperty(int *ret, std::string *err, const json &o, + const std::string &property, + const bool required, + const std::string &parent_node = "") { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + int intValue; + bool isInt = GetInt(GetValue(it), intValue); + if (!isInt) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an integer type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = intValue; + } + + return true; + } + + static bool ParseUnsignedProperty(size_t *ret, std::string *err, const json &o, + const std::string &property, + const bool required, + const std::string &parent_node = "") { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + auto &value = GetValue(it); + + size_t uValue = 0; + bool isUValue; +#ifdef TINYGLTF_USE_RAPIDJSON + isUValue = false; + if (value.IsUint()) { + uValue = value.GetUint(); + isUValue = true; + } else if (value.IsUint64()) { + uValue = value.GetUint64(); + isUValue = true; + } +#else + isUValue = value.is_number_unsigned(); + if (isUValue) { + uValue = value.get(); + } +#endif + if (!isUValue) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a positive integer.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = uValue; + } + + return true; + } + + static bool ParseNumberProperty(double *ret, std::string *err, const json &o, + const std::string &property, + const bool required, + const std::string &parent_node = "") { + json_const_iterator it; + + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + double numberValue; + bool isNumber = GetNumber(GetValue(it), numberValue); + + if (!isNumber) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a number type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = numberValue; + } + + return true; + } + + static bool ParseNumberArrayProperty(std::vector *ret, std::string *err, + const json &o, const std::string &property, + bool required, + const std::string &parent_node = "") { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + if (!IsArray(GetValue(it))) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an array"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + ret->clear(); + auto end = ArrayEnd(GetValue(it)); + for (auto i = ArrayBegin(GetValue(it)); i != end; ++i) { + double numberValue; + const bool isNumber = GetNumber(*i, numberValue); + if (!isNumber) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a number.\n"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + ret->push_back(numberValue); + } + + return true; + } + + static bool ParseIntegerArrayProperty(std::vector *ret, std::string *err, + const json &o, + const std::string &property, + bool required, + const std::string &parent_node = "") { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + if (!IsArray(GetValue(it))) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an array"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + ret->clear(); + auto end = ArrayEnd(GetValue(it)); + for (auto i = ArrayBegin(GetValue(it)); i != end; ++i) { + int numberValue; + bool isNumber = GetInt(*i, numberValue); + if (!isNumber) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an integer type.\n"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + ret->push_back(numberValue); + } + + return true; + } + + static bool ParseStringProperty( + std::string *ret, std::string *err, const json &o, + const std::string &property, bool required, + const std::string &parent_node = std::string()) { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (parent_node.empty()) { + (*err) += ".\n"; + } else { + (*err) += " in `" + parent_node + "'.\n"; + } + } + } + return false; + } + + std::string strValue; + if (!GetString(GetValue(it), strValue)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a string type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = std::move(strValue); + } + + return true; + } + + static bool ParseStringIntegerProperty(std::map *ret, + std::string *err, const json &o, + const std::string &property, + bool required, + const std::string &parent = "") { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + if (!parent.empty()) { + (*err) += + "'" + property + "' property is missing in " + parent + ".\n"; + } else { + (*err) += "'" + property + "' property is missing.\n"; + } + } + } + return false; + } + + const json &dict = GetValue(it); + + // Make sure we are dealing with an object / dictionary. + if (!IsObject(dict)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an object.\n"; + } + } + return false; + } + + ret->clear(); + + json_const_iterator dictIt(ObjectBegin(dict)); + json_const_iterator dictItEnd(ObjectEnd(dict)); + + for (; dictIt != dictItEnd; ++dictIt) { + int intVal; + if (!GetInt(GetValue(dictIt), intVal)) { + if (required) { + if (err) { + (*err) += "'" + property + "' value is not an integer type.\n"; + } + } + return false; + } + + // Insert into the list. + (*ret)[GetKey(dictIt)] = intVal; + } + return true; + } + + static bool ParseJSONProperty(std::map *ret, + std::string *err, const json &o, + const std::string &property, bool required) { + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing. \n'"; + } + } + return false; + } + + const json &obj = GetValue(it); + + if (!IsObject(obj)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a JSON object.\n"; + } + } + return false; + } + + ret->clear(); + + json_const_iterator it2(ObjectBegin(obj)); + json_const_iterator itEnd(ObjectEnd(obj)); + for (; it2 != itEnd; ++it2) { + double numVal; + if (GetNumber(GetValue(it2), numVal)) + ret->emplace(std::string(GetKey(it2)), numVal); + } + + return true; + } + + static bool ParseParameterProperty(Parameter *param, std::string *err, + const json &o, const std::string &prop, + bool required) { + // A parameter value can either be a string or an array of either a boolean or + // a number. Booleans of any kind aren't supported here. Granted, it + // complicates the Parameter structure and breaks it semantically in the sense + // that the client probably works off the assumption that if the string is + // empty the vector is used, etc. Would a tagged union work? + if (ParseStringProperty(¶m->string_value, err, o, prop, false)) { + // Found string property. + return true; + } else if (ParseNumberArrayProperty(¶m->number_array, err, o, prop, + false)) { + // Found a number array. + return true; + } else if (ParseNumberProperty(¶m->number_value, err, o, prop, false)) { + return param->has_number_value = true; + } else if (ParseJSONProperty(¶m->json_double_value, err, o, prop, + false)) { + return true; + } else if (ParseBooleanProperty(¶m->bool_value, err, o, prop, false)) { + return true; + } else { + if (required) { + if (err) { + (*err) += "parameter must be a string or number / number array.\n"; + } + } + return false; + } + } + + static bool ParseExtensionsProperty(ExtensionMap *ret, std::string *err, + const json &o) { + (void)err; + + json_const_iterator it; + if (!FindMember(o, "extensions", it)) { + return false; + } + + auto &obj = GetValue(it); + if (!IsObject(obj)) { + return false; + } + ExtensionMap extensions; + json_const_iterator extIt = ObjectBegin(obj); // it.value().begin(); + json_const_iterator extEnd = ObjectEnd(obj); + for (; extIt != extEnd; ++extIt) { + auto &itObj = GetValue(extIt); + if (!IsObject(itObj)) continue; + std::string key(GetKey(extIt)); + if (!ParseJsonAsValue(&extensions[key], itObj)) { + if (!key.empty()) { + // create empty object so that an extension object is still of type + // object + extensions[key] = Value{Value::Object{}}; + } + } + } + if (ret) { + (*ret) = std::move(extensions); + } + return true; + } + + static bool ParseAsset(Asset *asset, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + ParseStringProperty(&asset->version, err, o, "version", true, "Asset"); + ParseStringProperty(&asset->generator, err, o, "generator", false, "Asset"); + ParseStringProperty(&asset->minVersion, err, o, "minVersion", false, "Asset"); + ParseStringProperty(&asset->copyright, err, o, "copyright", false, "Asset"); + + ParseExtensionsProperty(&asset->extensions, err, o); + + // Unity exporter version is added as extra here + ParseExtrasProperty(&(asset->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + asset->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + asset->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseImage(Image *image, const int image_idx, std::string *err, + std::string *warn, const json &o, + bool store_original_json_for_extras_and_extensions, + const std::string &basedir, FsCallbacks *fs, + LoadImageDataFunction *LoadImageData = nullptr, + void *load_image_user_data = nullptr) { + // A glTF image must either reference a bufferView or an image uri + + // schema says oneOf [`bufferView`, `uri`] + // TODO(syoyo): Check the type of each parameters. + json_const_iterator it; + bool hasBufferView = FindMember(o, "bufferView", it); + bool hasURI = FindMember(o, "uri", it); + + ParseStringProperty(&image->name, err, o, "name", false); + + if (hasBufferView && hasURI) { + // Should not both defined. + if (err) { + (*err) += + "Only one of `bufferView` or `uri` should be defined, but both are " + "defined for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + "\"\n"; + } + return false; + } + + if (!hasBufferView && !hasURI) { + if (err) { + (*err) += "Neither required `bufferView` nor `uri` defined for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\"\n"; + } + return false; + } + + ParseExtensionsProperty(&image->extensions, err, o); + ParseExtrasProperty(&image->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator eit; + if (FindMember(o, "extensions", eit)) { + image->extensions_json_string = JsonToString(GetValue(eit)); + } + } + { + json_const_iterator eit; + if (FindMember(o, "extras", eit)) { + image->extras_json_string = JsonToString(GetValue(eit)); + } + } + } + + if (hasBufferView) { + int bufferView = -1; + if (!ParseIntegerProperty(&bufferView, err, o, "bufferView", true)) { + if (err) { + (*err) += "Failed to parse `bufferView` for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\"\n"; + } + return false; + } + + std::string mime_type; + ParseStringProperty(&mime_type, err, o, "mimeType", false); + + int width = 0; + ParseIntegerProperty(&width, err, o, "width", false); + + int height = 0; + ParseIntegerProperty(&height, err, o, "height", false); + + // Just only save some information here. Loading actual image data from + // bufferView is done after this `ParseImage` function. + image->bufferView = bufferView; + image->mimeType = mime_type; + image->width = width; + image->height = height; + + return true; + } + + // Parse URI & Load image data. + + std::string uri; + std::string tmp_err; + if (!ParseStringProperty(&uri, &tmp_err, o, "uri", true)) { + if (err) { + (*err) += "Failed to parse `uri` for image[" + std::to_string(image_idx) + + "] name = \"" + image->name + "\".\n"; + } + return false; + } + + std::vector img; + + if (IsDataURI(uri)) { + if (!DecodeDataURI(&img, image->mimeType, uri, 0, false)) { + if (err) { + (*err) += "Failed to decode 'uri' for image[" + + std::to_string(image_idx) + "] name = [" + image->name + + "]\n"; + } + return false; + } + } else { + // Assume external file + // Keep texture path (for textures that cannot be decoded) + image->uri = uri; +#ifdef TINYGLTF_NO_EXTERNAL_IMAGE + return true; +#endif + std::string decoded_uri = dlib::urldecode(uri); + if (!LoadExternalFile(&img, err, warn, decoded_uri, basedir, + /* required */ false, /* required bytes */ 0, + /* checksize */ false, fs)) { + if (warn) { + (*warn) += "Failed to load external 'uri' for image[" + + std::to_string(image_idx) + "] name = [" + image->name + + "]\n"; + } + // If the image cannot be loaded, keep uri as image->uri. + return true; + } + + if (img.empty()) { + if (warn) { + (*warn) += "Image data is empty for image[" + + std::to_string(image_idx) + "] name = [" + image->name + + "] \n"; + } + return false; + } + } + + if (*LoadImageData == nullptr) { + if (err) { + (*err) += "No LoadImageData callback specified.\n"; + } + return false; + } + return (*LoadImageData)(image, image_idx, err, warn, 0, 0, &img.at(0), + static_cast(img.size()), load_image_user_data); + } + + static bool ParseTexture(Texture *texture, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions, + const std::string &basedir) { + (void)basedir; + int sampler = -1; + int source = -1; + ParseIntegerProperty(&sampler, err, o, "sampler", false); + + ParseIntegerProperty(&source, err, o, "source", false); + + texture->sampler = sampler; + texture->source = source; + + ParseExtensionsProperty(&texture->extensions, err, o); + ParseExtrasProperty(&texture->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texture->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texture->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + ParseStringProperty(&texture->name, err, o, "name", false); + + return true; + } + + static bool ParseTextureInfo( + TextureInfo *texinfo, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + if (texinfo == nullptr) { + return false; + } + + if (!ParseIntegerProperty(&texinfo->index, err, o, "index", + /* required */ true, "TextureInfo")) { + return false; + } + + ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); + + ParseExtensionsProperty(&texinfo->extensions, err, o); + ParseExtrasProperty(&texinfo->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texinfo->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texinfo->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseNormalTextureInfo( + NormalTextureInfo *texinfo, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + if (texinfo == nullptr) { + return false; + } + + if (!ParseIntegerProperty(&texinfo->index, err, o, "index", + /* required */ true, "NormalTextureInfo")) { + return false; + } + + ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); + ParseNumberProperty(&texinfo->scale, err, o, "scale", false); + + ParseExtensionsProperty(&texinfo->extensions, err, o); + ParseExtrasProperty(&texinfo->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texinfo->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texinfo->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseOcclusionTextureInfo( + OcclusionTextureInfo *texinfo, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + if (texinfo == nullptr) { + return false; + } + + if (!ParseIntegerProperty(&texinfo->index, err, o, "index", + /* required */ true, "NormalTextureInfo")) { + return false; + } + + ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); + ParseNumberProperty(&texinfo->strength, err, o, "strength", false); + + ParseExtensionsProperty(&texinfo->extensions, err, o); + ParseExtrasProperty(&texinfo->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texinfo->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texinfo->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseBuffer(Buffer *buffer, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions, + FsCallbacks *fs, const std::string &basedir, + bool is_binary = false, + const unsigned char *bin_data = nullptr, + size_t bin_size = 0) { + size_t byteLength; + if (!ParseUnsignedProperty(&byteLength, err, o, "byteLength", true, + "Buffer")) { + return false; + } + + // In glTF 2.0, uri is not mandatory anymore + buffer->uri.clear(); + ParseStringProperty(&buffer->uri, err, o, "uri", false, "Buffer"); + + // having an empty uri for a non embedded image should not be valid + if (!is_binary && buffer->uri.empty()) { + if (err) { + (*err) += "'uri' is missing from non binary glTF file buffer.\n"; + } + } + + json_const_iterator type; + if (FindMember(o, "type", type)) { + std::string typeStr; + if (GetString(GetValue(type), typeStr)) { + if (typeStr.compare("arraybuffer") == 0) { + // buffer.type = "arraybuffer"; + } + } + } + + if (is_binary) { + // Still binary glTF accepts external dataURI. + if (!buffer->uri.empty()) { + // First try embedded data URI. + if (IsDataURI(buffer->uri)) { + std::string mime_type; + if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, byteLength, + true)) { + if (err) { + (*err) += + "Failed to decode 'uri' : " + buffer->uri + " in Buffer\n"; + } + return false; + } + } else { + // External .bin file. + std::string decoded_uri = dlib::urldecode(buffer->uri); + if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, + decoded_uri, basedir, /* required */ true, + byteLength, /* checkSize */ true, fs)) { + return false; + } + } + } else { + // load data from (embedded) binary data + + if ((bin_size == 0) || (bin_data == nullptr)) { + if (err) { + (*err) += "Invalid binary data in `Buffer'.\n"; + } + return false; + } + + if (byteLength > bin_size) { + if (err) { + std::stringstream ss; + ss << "Invalid `byteLength'. Must be equal or less than binary size: " + "`byteLength' = " + << byteLength << ", binary size = " << bin_size << std::endl; + (*err) += ss.str(); + } + return false; + } + + // Read buffer data + buffer->data.resize(static_cast(byteLength)); + memcpy(&(buffer->data.at(0)), bin_data, static_cast(byteLength)); + } + + } else { + if (IsDataURI(buffer->uri)) { + std::string mime_type; + if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, byteLength, + true)) { + if (err) { + (*err) += "Failed to decode 'uri' : " + buffer->uri + " in Buffer\n"; + } + return false; + } + } else { + // Assume external .bin file. + std::string decoded_uri = dlib::urldecode(buffer->uri); + if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, decoded_uri, + basedir, /* required */ true, byteLength, + /* checkSize */ true, fs)) { + return false; + } + } + } + + ParseStringProperty(&buffer->name, err, o, "name", false); + + ParseExtensionsProperty(&buffer->extensions, err, o); + ParseExtrasProperty(&buffer->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + buffer->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + buffer->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseBufferView( + BufferView *bufferView, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + int buffer = -1; + if (!ParseIntegerProperty(&buffer, err, o, "buffer", true, "BufferView")) { + return false; + } + + size_t byteOffset = 0; + ParseUnsignedProperty(&byteOffset, err, o, "byteOffset", false); + + size_t byteLength = 1; + if (!ParseUnsignedProperty(&byteLength, err, o, "byteLength", true, + "BufferView")) { + return false; + } + + size_t byteStride = 0; + if (!ParseUnsignedProperty(&byteStride, err, o, "byteStride", false)) { + // Spec says: When byteStride of referenced bufferView is not defined, it + // means that accessor elements are tightly packed, i.e., effective stride + // equals the size of the element. + // We cannot determine the actual byteStride until Accessor are parsed, thus + // set 0(= tightly packed) here(as done in OpenGL's VertexAttribPoiner) + byteStride = 0; + } + + if ((byteStride > 252) || ((byteStride % 4) != 0)) { + if (err) { + std::stringstream ss; + ss << "Invalid `byteStride' value. `byteStride' must be the multiple of " + "4 : " + << byteStride << std::endl; + + (*err) += ss.str(); + } + return false; + } + + int target = 0; + ParseIntegerProperty(&target, err, o, "target", false); + if ((target == TINYGLTF_TARGET_ARRAY_BUFFER) || + (target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER)) { + // OK + } else { + target = 0; + } + bufferView->target = target; + + ParseStringProperty(&bufferView->name, err, o, "name", false); + + ParseExtensionsProperty(&bufferView->extensions, err, o); + ParseExtrasProperty(&bufferView->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + bufferView->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + bufferView->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + bufferView->buffer = buffer; + bufferView->byteOffset = byteOffset; + bufferView->byteLength = byteLength; + bufferView->byteStride = byteStride; + return true; + } + + static bool ParseSparseAccessor(Accessor *accessor, std::string *err, + const json &o) { + accessor->sparse.isSparse = true; + + int count = 0; + ParseIntegerProperty(&count, err, o, "count", true); + + json_const_iterator indices_iterator; + json_const_iterator values_iterator; + if (!FindMember(o, "indices", indices_iterator)) { + (*err) = "the sparse object of this accessor doesn't have indices"; + return false; + } + + if (!FindMember(o, "values", values_iterator)) { + (*err) = "the sparse object ob ths accessor doesn't have values"; + return false; + } + + const json &indices_obj = GetValue(indices_iterator); + const json &values_obj = GetValue(values_iterator); + + int indices_buffer_view = 0, indices_byte_offset = 0, component_type = 0; + ParseIntegerProperty(&indices_buffer_view, err, indices_obj, "bufferView", + true); + ParseIntegerProperty(&indices_byte_offset, err, indices_obj, "byteOffset", + true); + ParseIntegerProperty(&component_type, err, indices_obj, "componentType", + true); + + int values_buffer_view = 0, values_byte_offset = 0; + ParseIntegerProperty(&values_buffer_view, err, values_obj, "bufferView", + true); + ParseIntegerProperty(&values_byte_offset, err, values_obj, "byteOffset", + true); + + accessor->sparse.count = count; + accessor->sparse.indices.bufferView = indices_buffer_view; + accessor->sparse.indices.byteOffset = indices_byte_offset; + accessor->sparse.indices.componentType = component_type; + accessor->sparse.values.bufferView = values_buffer_view; + accessor->sparse.values.byteOffset = values_byte_offset; + + // todo check theses values + + return true; + } + + static bool ParseAccessor(Accessor *accessor, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + int bufferView = -1; + ParseIntegerProperty(&bufferView, err, o, "bufferView", false, "Accessor"); + + size_t byteOffset = 0; + ParseUnsignedProperty(&byteOffset, err, o, "byteOffset", false, "Accessor"); + + bool normalized = false; + ParseBooleanProperty(&normalized, err, o, "normalized", false, "Accessor"); + + size_t componentType = 0; + if (!ParseUnsignedProperty(&componentType, err, o, "componentType", true, + "Accessor")) { + return false; + } + + size_t count = 0; + if (!ParseUnsignedProperty(&count, err, o, "count", true, "Accessor")) { + return false; + } + + std::string type; + if (!ParseStringProperty(&type, err, o, "type", true, "Accessor")) { + return false; + } + + if (type.compare("SCALAR") == 0) { + accessor->type = TINYGLTF_TYPE_SCALAR; + } else if (type.compare("VEC2") == 0) { + accessor->type = TINYGLTF_TYPE_VEC2; + } else if (type.compare("VEC3") == 0) { + accessor->type = TINYGLTF_TYPE_VEC3; + } else if (type.compare("VEC4") == 0) { + accessor->type = TINYGLTF_TYPE_VEC4; + } else if (type.compare("MAT2") == 0) { + accessor->type = TINYGLTF_TYPE_MAT2; + } else if (type.compare("MAT3") == 0) { + accessor->type = TINYGLTF_TYPE_MAT3; + } else if (type.compare("MAT4") == 0) { + accessor->type = TINYGLTF_TYPE_MAT4; + } else { + std::stringstream ss; + ss << "Unsupported `type` for accessor object. Got \"" << type << "\"\n"; + if (err) { + (*err) += ss.str(); + } + return false; + } + + ParseStringProperty(&accessor->name, err, o, "name", false); + + accessor->minValues.clear(); + accessor->maxValues.clear(); + ParseNumberArrayProperty(&accessor->minValues, err, o, "min", false, + "Accessor"); + + ParseNumberArrayProperty(&accessor->maxValues, err, o, "max", false, + "Accessor"); + + accessor->count = count; + accessor->bufferView = bufferView; + accessor->byteOffset = byteOffset; + accessor->normalized = normalized; + { + if (componentType >= TINYGLTF_COMPONENT_TYPE_BYTE && + componentType <= TINYGLTF_COMPONENT_TYPE_DOUBLE) { + // OK + accessor->componentType = int(componentType); + } else { + std::stringstream ss; + ss << "Invalid `componentType` in accessor. Got " << componentType + << "\n"; + if (err) { + (*err) += ss.str(); + } + return false; + } + } + + ParseExtensionsProperty(&(accessor->extensions), err, o); + ParseExtrasProperty(&(accessor->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + accessor->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + accessor->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + // check if accessor has a "sparse" object: + json_const_iterator iterator; + if (FindMember(o, "sparse", iterator)) { + // here this accessor has a "sparse" subobject + return ParseSparseAccessor(accessor, err, GetValue(iterator)); + } + + return true; + } + +#ifdef TINYGLTF_ENABLE_DRACO + + static void DecodeIndexBuffer(draco::Mesh *mesh, size_t componentSize, + std::vector &outBuffer) { + if (componentSize == 4) { + assert(sizeof(mesh->face(draco::FaceIndex(0))[0]) == componentSize); + memcpy(outBuffer.data(), &mesh->face(draco::FaceIndex(0))[0], + outBuffer.size()); + } else { + size_t faceStride = componentSize * 3; + for (draco::FaceIndex f(0); f < mesh->num_faces(); ++f) { + const draco::Mesh::Face &face = mesh->face(f); + if (componentSize == 2) { + uint16_t indices[3] = {(uint16_t)face[0].value(), + (uint16_t)face[1].value(), + (uint16_t)face[2].value()}; + memcpy(outBuffer.data() + f.value() * faceStride, &indices[0], + faceStride); + } else { + uint8_t indices[3] = {(uint8_t)face[0].value(), + (uint8_t)face[1].value(), + (uint8_t)face[2].value()}; + memcpy(outBuffer.data() + f.value() * faceStride, &indices[0], + faceStride); + } + } + } + } + + template + static bool GetAttributeForAllPoints(draco::Mesh *mesh, + const draco::PointAttribute *pAttribute, + std::vector &outBuffer) { + size_t byteOffset = 0; + T values[4] = {0, 0, 0, 0}; + for (draco::PointIndex i(0); i < mesh->num_points(); ++i) { + const draco::AttributeValueIndex val_index = pAttribute->mapped_index(i); + if (!pAttribute->ConvertValue(val_index, pAttribute->num_components(), + values)) + return false; + + memcpy(outBuffer.data() + byteOffset, &values[0], + sizeof(T) * pAttribute->num_components()); + byteOffset += sizeof(T) * pAttribute->num_components(); + } + + return true; + } + + static bool GetAttributeForAllPoints(uint32_t componentType, draco::Mesh *mesh, + const draco::PointAttribute *pAttribute, + std::vector &outBuffer) { + bool decodeResult = false; + switch (componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_BYTE: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_SHORT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_INT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_DOUBLE: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + default: + return false; + } + + return decodeResult; + } + + static bool ParseDracoExtension(Primitive *primitive, Model *model, + std::string *err, + const Value &dracoExtensionValue) { + auto bufferViewValue = dracoExtensionValue.Get("bufferView"); + if (!bufferViewValue.IsInt()) return false; + auto attributesValue = dracoExtensionValue.Get("attributes"); + if (!attributesValue.IsObject()) return false; + + auto attributesObject = attributesValue.Get(); + int bufferView = bufferViewValue.Get(); + + BufferView &view = model->bufferViews[bufferView]; + Buffer &buffer = model->buffers[view.buffer]; + // BufferView has already been decoded + if (view.dracoDecoded) return true; + view.dracoDecoded = true; + + const char *bufferViewData = + reinterpret_cast(buffer.data.data() + view.byteOffset); + size_t bufferViewSize = view.byteLength; + + // decode draco + draco::DecoderBuffer decoderBuffer; + decoderBuffer.Init(bufferViewData, bufferViewSize); + draco::Decoder decoder; + auto decodeResult = decoder.DecodeMeshFromBuffer(&decoderBuffer); + if (!decodeResult.ok()) { + return false; + } + const std::unique_ptr &mesh = decodeResult.value(); + + // create new bufferView for indices + if (primitive->indices >= 0) { + int32_t componentSize = GetComponentSizeInBytes( + model->accessors[primitive->indices].componentType); + Buffer decodedIndexBuffer; + decodedIndexBuffer.data.resize(mesh->num_faces() * 3 * componentSize); + + DecodeIndexBuffer(mesh.get(), componentSize, decodedIndexBuffer.data); + + model->buffers.emplace_back(std::move(decodedIndexBuffer)); + + BufferView decodedIndexBufferView; + decodedIndexBufferView.buffer = int(model->buffers.size() - 1); + decodedIndexBufferView.byteLength = + int(mesh->num_faces() * 3 * componentSize); + decodedIndexBufferView.byteOffset = 0; + decodedIndexBufferView.byteStride = 0; + decodedIndexBufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER; + model->bufferViews.emplace_back(std::move(decodedIndexBufferView)); + + model->accessors[primitive->indices].bufferView = + int(model->bufferViews.size() - 1); + model->accessors[primitive->indices].count = int(mesh->num_faces() * 3); + } + + for (const auto &attribute : attributesObject) { + if (!attribute.second.IsInt()) return false; + auto primitiveAttribute = primitive->attributes.find(attribute.first); + if (primitiveAttribute == primitive->attributes.end()) return false; + + int dracoAttributeIndex = attribute.second.Get(); + const auto pAttribute = mesh->GetAttributeByUniqueId(dracoAttributeIndex); + const auto pBuffer = pAttribute->buffer(); + const auto componentType = + model->accessors[primitiveAttribute->second].componentType; + + // Create a new buffer for this decoded buffer + Buffer decodedBuffer; + size_t bufferSize = mesh->num_points() * pAttribute->num_components() * + GetComponentSizeInBytes(componentType); + decodedBuffer.data.resize(bufferSize); + + if (!GetAttributeForAllPoints(componentType, mesh.get(), pAttribute, + decodedBuffer.data)) + return false; + + model->buffers.emplace_back(std::move(decodedBuffer)); + + BufferView decodedBufferView; + decodedBufferView.buffer = int(model->buffers.size() - 1); + decodedBufferView.byteLength = bufferSize; + decodedBufferView.byteOffset = pAttribute->byte_offset(); + decodedBufferView.byteStride = pAttribute->byte_stride(); + decodedBufferView.target = primitive->indices >= 0 + ? TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER + : TINYGLTF_TARGET_ARRAY_BUFFER; + model->bufferViews.emplace_back(std::move(decodedBufferView)); + + model->accessors[primitiveAttribute->second].bufferView = + int(model->bufferViews.size() - 1); + model->accessors[primitiveAttribute->second].count = + int(mesh->num_points()); + } + + return true; + } +#endif + + static bool ParsePrimitive(Primitive *primitive, Model *model, std::string *err, + const json &o, + bool store_original_json_for_extras_and_extensions) { + int material = -1; + ParseIntegerProperty(&material, err, o, "material", false); + primitive->material = material; + + int mode = TINYGLTF_MODE_TRIANGLES; + ParseIntegerProperty(&mode, err, o, "mode", false); + primitive->mode = mode; // Why only triangled were supported ? + + int indices = -1; + ParseIntegerProperty(&indices, err, o, "indices", false); + primitive->indices = indices; + if (!ParseStringIntegerProperty(&primitive->attributes, err, o, "attributes", + true, "Primitive")) { + return false; + } + + // Look for morph targets + json_const_iterator targetsObject; + if (FindMember(o, "targets", targetsObject) && + IsArray(GetValue(targetsObject))) { + auto targetsObjectEnd = ArrayEnd(GetValue(targetsObject)); + for (json_const_array_iterator i = ArrayBegin(GetValue(targetsObject)); + i != targetsObjectEnd; ++i) { + std::map targetAttribues; + + const json &dict = *i; + if (IsObject(dict)) { + json_const_iterator dictIt(ObjectBegin(dict)); + json_const_iterator dictItEnd(ObjectEnd(dict)); + + for (; dictIt != dictItEnd; ++dictIt) { + int iVal; + if (GetInt(GetValue(dictIt), iVal)) + targetAttribues[GetKey(dictIt)] = iVal; + } + primitive->targets.emplace_back(std::move(targetAttribues)); + } + } + } + + ParseExtrasProperty(&(primitive->extras), o); + ParseExtensionsProperty(&primitive->extensions, err, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + primitive->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + primitive->extras_json_string = JsonToString(GetValue(it)); + } + } + } + +#ifdef TINYGLTF_ENABLE_DRACO + auto dracoExtension = + primitive->extensions.find("KHR_draco_mesh_compression"); + if (dracoExtension != primitive->extensions.end()) { + ParseDracoExtension(primitive, model, err, dracoExtension->second); + } +#else + (void)model; +#endif + + return true; + } + + static bool ParseMesh(Mesh *mesh, Model *model, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + ParseStringProperty(&mesh->name, err, o, "name", false); + + mesh->primitives.clear(); + json_const_iterator primObject; + if (FindMember(o, "primitives", primObject) && + IsArray(GetValue(primObject))) { + json_const_array_iterator primEnd = ArrayEnd(GetValue(primObject)); + for (json_const_array_iterator i = ArrayBegin(GetValue(primObject)); + i != primEnd; ++i) { + Primitive primitive; + if (ParsePrimitive(&primitive, model, err, *i, + store_original_json_for_extras_and_extensions)) { + // Only add the primitive if the parsing succeeds. + mesh->primitives.emplace_back(std::move(primitive)); + } + } + } + + // Should probably check if has targets and if dimensions fit + ParseNumberArrayProperty(&mesh->weights, err, o, "weights", false); + + ParseExtensionsProperty(&mesh->extensions, err, o); + ParseExtrasProperty(&(mesh->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + mesh->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + mesh->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseNode(Node *node, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + ParseStringProperty(&node->name, err, o, "name", false); + + int skin = -1; + ParseIntegerProperty(&skin, err, o, "skin", false); + node->skin = skin; + + // Matrix and T/R/S are exclusive + if (!ParseNumberArrayProperty(&node->matrix, err, o, "matrix", false)) { + ParseNumberArrayProperty(&node->rotation, err, o, "rotation", false); + ParseNumberArrayProperty(&node->scale, err, o, "scale", false); + ParseNumberArrayProperty(&node->translation, err, o, "translation", false); + } + + int camera = -1; + ParseIntegerProperty(&camera, err, o, "camera", false); + node->camera = camera; + + int mesh = -1; + ParseIntegerProperty(&mesh, err, o, "mesh", false); + node->mesh = mesh; + + node->children.clear(); + ParseIntegerArrayProperty(&node->children, err, o, "children", false); + + ParseNumberArrayProperty(&node->weights, err, o, "weights", false); + + ParseExtensionsProperty(&node->extensions, err, o); + ParseExtrasProperty(&(node->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + node->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + node->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParsePbrMetallicRoughness( + PbrMetallicRoughness *pbr, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + if (pbr == nullptr) { + return false; + } + + std::vector baseColorFactor; + if (ParseNumberArrayProperty(&baseColorFactor, err, o, "baseColorFactor", + /* required */ false)) { + if (baseColorFactor.size() != 4) { + if (err) { + (*err) += + "Array length of `baseColorFactor` parameter in " + "pbrMetallicRoughness must be 4, but got " + + std::to_string(baseColorFactor.size()) + "\n"; + } + return false; + } + pbr->baseColorFactor = baseColorFactor; + } + + { + json_const_iterator it; + if (FindMember(o, "baseColorTexture", it)) { + ParseTextureInfo(&pbr->baseColorTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "metallicRoughnessTexture", it)) { + ParseTextureInfo(&pbr->metallicRoughnessTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + ParseNumberProperty(&pbr->metallicFactor, err, o, "metallicFactor", false); + ParseNumberProperty(&pbr->roughnessFactor, err, o, "roughnessFactor", false); + + ParseExtensionsProperty(&pbr->extensions, err, o); + ParseExtrasProperty(&pbr->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + pbr->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + pbr->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseMaterial(Material *material, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + ParseStringProperty(&material->name, err, o, "name", /* required */ false); + + if (ParseNumberArrayProperty(&material->emissiveFactor, err, o, + "emissiveFactor", + /* required */ false)) { + if (material->emissiveFactor.size() != 3) { + if (err) { + (*err) += + "Array length of `emissiveFactor` parameter in " + "material must be 3, but got " + + std::to_string(material->emissiveFactor.size()) + "\n"; + } + return false; + } + } else { + // fill with default values + material->emissiveFactor = {0.0, 0.0, 0.0}; + } + + ParseStringProperty(&material->alphaMode, err, o, "alphaMode", + /* required */ false); + ParseNumberProperty(&material->alphaCutoff, err, o, "alphaCutoff", + /* required */ false); + ParseBooleanProperty(&material->doubleSided, err, o, "doubleSided", + /* required */ false); + + { + json_const_iterator it; + if (FindMember(o, "pbrMetallicRoughness", it)) { + ParsePbrMetallicRoughness(&material->pbrMetallicRoughness, err, + GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "normalTexture", it)) { + ParseNormalTextureInfo(&material->normalTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "occlusionTexture", it)) { + ParseOcclusionTextureInfo(&material->occlusionTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "emissiveTexture", it)) { + ParseTextureInfo(&material->emissiveTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + // Old code path. For backward compatibility, we still store material values + // as Parameter. This will create duplicated information for + // example(pbrMetallicRoughness), but should be neglible in terms of memory + // consumption. + // TODO(syoyo): Remove in the next major release. + material->values.clear(); + material->additionalValues.clear(); + + json_const_iterator it(ObjectBegin(o)); + json_const_iterator itEnd(ObjectEnd(o)); + + for (; it != itEnd; ++it) { + std::string key(GetKey(it)); + if (key == "pbrMetallicRoughness") { + if (IsObject(GetValue(it))) { + const json &values_object = GetValue(it); + + json_const_iterator itVal(ObjectBegin(values_object)); + json_const_iterator itValEnd(ObjectEnd(values_object)); + + for (; itVal != itValEnd; ++itVal) { + Parameter param; + if (ParseParameterProperty(¶m, err, values_object, GetKey(itVal), + false)) { + material->values.emplace(GetKey(itVal), std::move(param)); + } + } + } + } else if (key == "extensions" || key == "extras") { + // done later, skip, otherwise poorly parsed contents will be saved in the + // parametermap and serialized again later + } else { + Parameter param; + if (ParseParameterProperty(¶m, err, o, key, false)) { + // names of materials have already been parsed. Putting it in this map + // doesn't correctly reflext the glTF specification + if (key != "name") + material->additionalValues.emplace(std::move(key), std::move(param)); + } + } + } + + material->extensions.clear(); + ParseExtensionsProperty(&material->extensions, err, o); + ParseExtrasProperty(&(material->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator eit; + if (FindMember(o, "extensions", eit)) { + material->extensions_json_string = JsonToString(GetValue(eit)); + } + } + { + json_const_iterator eit; + if (FindMember(o, "extras", eit)) { + material->extras_json_string = JsonToString(GetValue(eit)); + } + } + } + + return true; + } + + static bool ParseAnimationChannel( + AnimationChannel *channel, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + int samplerIndex = -1; + int targetIndex = -1; + if (!ParseIntegerProperty(&samplerIndex, err, o, "sampler", true, + "AnimationChannel")) { + if (err) { + (*err) += "`sampler` field is missing in animation channels\n"; + } + return false; + } + + json_const_iterator targetIt; + if (FindMember(o, "target", targetIt) && IsObject(GetValue(targetIt))) { + const json &target_object = GetValue(targetIt); + + if (!ParseIntegerProperty(&targetIndex, err, target_object, "node", true)) { + if (err) { + (*err) += "`node` field is missing in animation.channels.target\n"; + } + return false; + } + + if (!ParseStringProperty(&channel->target_path, err, target_object, "path", + true)) { + if (err) { + (*err) += "`path` field is missing in animation.channels.target\n"; + } + return false; + } + ParseExtensionsProperty(&channel->target_extensions, err, target_object); + if (store_original_json_for_extras_and_extensions) { + json_const_iterator it; + if (FindMember(target_object, "extensions", it)) { + channel->target_extensions_json_string = JsonToString(GetValue(it)); + } + } + } + + channel->sampler = samplerIndex; + channel->target_node = targetIndex; + + ParseExtensionsProperty(&channel->extensions, err, o); + ParseExtrasProperty(&(channel->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + channel->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + channel->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseAnimation(Animation *animation, std::string *err, + const json &o, + bool store_original_json_for_extras_and_extensions) { + { + json_const_iterator channelsIt; + if (FindMember(o, "channels", channelsIt) && + IsArray(GetValue(channelsIt))) { + json_const_array_iterator channelEnd = ArrayEnd(GetValue(channelsIt)); + for (json_const_array_iterator i = ArrayBegin(GetValue(channelsIt)); + i != channelEnd; ++i) { + AnimationChannel channel; + if (ParseAnimationChannel( + &channel, err, *i, + store_original_json_for_extras_and_extensions)) { + // Only add the channel if the parsing succeeds. + animation->channels.emplace_back(std::move(channel)); + } + } + } + } + + { + json_const_iterator samplerIt; + if (FindMember(o, "samplers", samplerIt) && IsArray(GetValue(samplerIt))) { + const json &sampler_array = GetValue(samplerIt); + + json_const_array_iterator it = ArrayBegin(sampler_array); + json_const_array_iterator itEnd = ArrayEnd(sampler_array); + + for (; it != itEnd; ++it) { + const json &s = *it; + + AnimationSampler sampler; + int inputIndex = -1; + int outputIndex = -1; + if (!ParseIntegerProperty(&inputIndex, err, s, "input", true)) { + if (err) { + (*err) += "`input` field is missing in animation.sampler\n"; + } + return false; + } + ParseStringProperty(&sampler.interpolation, err, s, "interpolation", + false); + if (!ParseIntegerProperty(&outputIndex, err, s, "output", true)) { + if (err) { + (*err) += "`output` field is missing in animation.sampler\n"; + } + return false; + } + sampler.input = inputIndex; + sampler.output = outputIndex; + ParseExtensionsProperty(&(sampler.extensions), err, o); + ParseExtrasProperty(&(sampler.extras), s); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator eit; + if (FindMember(o, "extensions", eit)) { + sampler.extensions_json_string = JsonToString(GetValue(eit)); + } + } + { + json_const_iterator eit; + if (FindMember(o, "extras", eit)) { + sampler.extras_json_string = JsonToString(GetValue(eit)); + } + } + } + + animation->samplers.emplace_back(std::move(sampler)); + } + } + } + + ParseStringProperty(&animation->name, err, o, "name", false); + + ParseExtensionsProperty(&animation->extensions, err, o); + ParseExtrasProperty(&(animation->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + animation->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + animation->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseSampler(Sampler *sampler, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + ParseStringProperty(&sampler->name, err, o, "name", false); + + int minFilter = -1; + int magFilter = -1; + int wrapS = TINYGLTF_TEXTURE_WRAP_REPEAT; + int wrapT = TINYGLTF_TEXTURE_WRAP_REPEAT; + int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT; + ParseIntegerProperty(&minFilter, err, o, "minFilter", false); + ParseIntegerProperty(&magFilter, err, o, "magFilter", false); + ParseIntegerProperty(&wrapS, err, o, "wrapS", false); + ParseIntegerProperty(&wrapT, err, o, "wrapT", false); + ParseIntegerProperty(&wrapR, err, o, "wrapR", false); // tinygltf extension + + // TODO(syoyo): Check the value is alloed one. + // (e.g. we allow 9728(NEAREST), but don't allow 9727) + + sampler->minFilter = minFilter; + sampler->magFilter = magFilter; + sampler->wrapS = wrapS; + sampler->wrapT = wrapT; + sampler->wrapR = wrapR; + + ParseExtensionsProperty(&(sampler->extensions), err, o); + ParseExtrasProperty(&(sampler->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + sampler->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + sampler->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseSkin(Skin *skin, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + ParseStringProperty(&skin->name, err, o, "name", false, "Skin"); + + std::vector joints; + if (!ParseIntegerArrayProperty(&joints, err, o, "joints", false, "Skin")) { + return false; + } + skin->joints = std::move(joints); + + int skeleton = -1; + ParseIntegerProperty(&skeleton, err, o, "skeleton", false, "Skin"); + skin->skeleton = skeleton; + + int invBind = -1; + ParseIntegerProperty(&invBind, err, o, "inverseBindMatrices", true, "Skin"); + skin->inverseBindMatrices = invBind; + + ParseExtensionsProperty(&(skin->extensions), err, o); + ParseExtrasProperty(&(skin->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + skin->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + skin->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParsePerspectiveCamera( + PerspectiveCamera *camera, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + double yfov = 0.0; + if (!ParseNumberProperty(&yfov, err, o, "yfov", true, "OrthographicCamera")) { + return false; + } + + double znear = 0.0; + if (!ParseNumberProperty(&znear, err, o, "znear", true, + "PerspectiveCamera")) { + return false; + } + + double aspectRatio = 0.0; // = invalid + ParseNumberProperty(&aspectRatio, err, o, "aspectRatio", false, + "PerspectiveCamera"); + + double zfar = 0.0; // = invalid + ParseNumberProperty(&zfar, err, o, "zfar", false, "PerspectiveCamera"); + + camera->aspectRatio = aspectRatio; + camera->zfar = zfar; + camera->yfov = yfov; + camera->znear = znear; + + ParseExtensionsProperty(&camera->extensions, err, o); + ParseExtrasProperty(&(camera->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + camera->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + camera->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + // TODO(syoyo): Validate parameter values. + + return true; + } + + static bool ParseSpotLight(SpotLight *light, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + ParseNumberProperty(&light->innerConeAngle, err, o, "innerConeAngle", false); + ParseNumberProperty(&light->outerConeAngle, err, o, "outerConeAngle", false); + + ParseExtensionsProperty(&light->extensions, err, o); + ParseExtrasProperty(&light->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + light->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + light->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + // TODO(syoyo): Validate parameter values. + + return true; + } + + static bool ParseOrthographicCamera( + OrthographicCamera *camera, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + double xmag = 0.0; + if (!ParseNumberProperty(&xmag, err, o, "xmag", true, "OrthographicCamera")) { + return false; + } + + double ymag = 0.0; + if (!ParseNumberProperty(&ymag, err, o, "ymag", true, "OrthographicCamera")) { + return false; + } + + double zfar = 0.0; + if (!ParseNumberProperty(&zfar, err, o, "zfar", true, "OrthographicCamera")) { + return false; + } + + double znear = 0.0; + if (!ParseNumberProperty(&znear, err, o, "znear", true, + "OrthographicCamera")) { + return false; + } + + ParseExtensionsProperty(&camera->extensions, err, o); + ParseExtrasProperty(&(camera->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + camera->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + camera->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + camera->xmag = xmag; + camera->ymag = ymag; + camera->zfar = zfar; + camera->znear = znear; + + // TODO(syoyo): Validate parameter values. + + return true; + } + + static bool ParseCamera(Camera *camera, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + if (!ParseStringProperty(&camera->type, err, o, "type", true, "Camera")) { + return false; + } + + if (camera->type.compare("orthographic") == 0) { + json_const_iterator orthoIt; + if (!FindMember(o, "orthographic", orthoIt)) { + if (err) { + std::stringstream ss; + ss << "Orhographic camera description not found." << std::endl; + (*err) += ss.str(); + } + return false; + } + + const json &v = GetValue(orthoIt); + if (!IsObject(v)) { + if (err) { + std::stringstream ss; + ss << "\"orthographic\" is not a JSON object." << std::endl; + (*err) += ss.str(); + } + return false; + } + + if (!ParseOrthographicCamera( + &camera->orthographic, err, v, + store_original_json_for_extras_and_extensions)) { + return false; + } + } else if (camera->type.compare("perspective") == 0) { + json_const_iterator perspIt; + if (!FindMember(o, "perspective", perspIt)) { + if (err) { + std::stringstream ss; + ss << "Perspective camera description not found." << std::endl; + (*err) += ss.str(); + } + return false; + } + + const json &v = GetValue(perspIt); + if (!IsObject(v)) { + if (err) { + std::stringstream ss; + ss << "\"perspective\" is not a JSON object." << std::endl; + (*err) += ss.str(); + } + return false; + } + + if (!ParsePerspectiveCamera( + &camera->perspective, err, v, + store_original_json_for_extras_and_extensions)) { + return false; + } + } else { + if (err) { + std::stringstream ss; + ss << "Invalid camera type: \"" << camera->type + << "\". Must be \"perspective\" or \"orthographic\"" << std::endl; + (*err) += ss.str(); + } + return false; + } + + ParseStringProperty(&camera->name, err, o, "name", false); + + ParseExtensionsProperty(&camera->extensions, err, o); + ParseExtrasProperty(&(camera->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + camera->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + camera->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + static bool ParseLight(Light *light, std::string *err, const json &o, + bool store_original_json_for_extras_and_extensions) { + if (!ParseStringProperty(&light->type, err, o, "type", true)) { + return false; + } + + if (light->type == "spot") { + json_const_iterator spotIt; + if (!FindMember(o, "spot", spotIt)) { + if (err) { + std::stringstream ss; + ss << "Spot light description not found." << std::endl; + (*err) += ss.str(); + } + return false; + } + + const json &v = GetValue(spotIt); + if (!IsObject(v)) { + if (err) { + std::stringstream ss; + ss << "\"spot\" is not a JSON object." << std::endl; + (*err) += ss.str(); + } + return false; + } + + if (!ParseSpotLight(&light->spot, err, v, + store_original_json_for_extras_and_extensions)) { + return false; + } + } + + ParseStringProperty(&light->name, err, o, "name", false); + ParseNumberArrayProperty(&light->color, err, o, "color", false); + ParseNumberProperty(&light->range, err, o, "range", false); + ParseNumberProperty(&light->intensity, err, o, "intensity", false); + ParseExtensionsProperty(&light->extensions, err, o); + ParseExtrasProperty(&(light->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + light->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + light->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; + } + + bool TinyGLTF::LoadFromString(Model *model, std::string *err, std::string *warn, + const char *json_str, + unsigned int json_str_length, + const std::string &base_dir, + unsigned int check_sections) { + if (json_str_length < 4) { + if (err) { + (*err) = "JSON string too short.\n"; + } + return false; + } + + JsonDocument v; + +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || \ + defined(_CPPUNWIND)) && \ + !defined(TINYGLTF_NOEXCEPTION) + try { + JsonParse(v, json_str, json_str_length, true); + + } catch (const std::exception &e) { + if (err) { + (*err) = e.what(); + } + return false; + } +#else + { + JsonParse(v, json_str, json_str_length); + + if (!IsObject(v)) { + // Assume parsing was failed. + if (err) { + (*err) = "Failed to parse JSON object\n"; + } + return false; + } + } +#endif + + if (!IsObject(v)) { + // root is not an object. + if (err) { + (*err) = "Root element is not a JSON object\n"; + } + return false; + } + + { + bool version_found = false; + json_const_iterator it; + if (FindMember(v, "asset", it) && IsObject(GetValue(it))) { + auto &itObj = GetValue(it); + json_const_iterator version_it; + std::string versionStr; + if (FindMember(itObj, "version", version_it) && + GetString(GetValue(version_it), versionStr)) { + version_found = true; + } + } + if (version_found) { + // OK + } else if (check_sections & REQUIRE_VERSION) { + if (err) { + (*err) += "\"asset\" object not found in .gltf or not an object type\n"; + } + return false; + } + } + + // scene is not mandatory. + // FIXME Maybe a better way to handle it than removing the code + + auto IsArrayMemberPresent = [](const json &_v, const char *name) -> bool { + json_const_iterator it; + return FindMember(_v, name, it) && IsArray(GetValue(it)); + }; + + { + if ((check_sections & REQUIRE_SCENES) && + !IsArrayMemberPresent(v, "scenes")) { + if (err) { + (*err) += "\"scenes\" object not found in .gltf or not an array type\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_NODES) && !IsArrayMemberPresent(v, "nodes")) { + if (err) { + (*err) += "\"nodes\" object not found in .gltf\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_ACCESSORS) && + !IsArrayMemberPresent(v, "accessors")) { + if (err) { + (*err) += "\"accessors\" object not found in .gltf\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_BUFFERS) && + !IsArrayMemberPresent(v, "buffers")) { + if (err) { + (*err) += "\"buffers\" object not found in .gltf\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_BUFFER_VIEWS) && + !IsArrayMemberPresent(v, "bufferViews")) { + if (err) { + (*err) += "\"bufferViews\" object not found in .gltf\n"; + } + return false; + } + } + + model->buffers.clear(); + model->bufferViews.clear(); + model->accessors.clear(); + model->meshes.clear(); + model->cameras.clear(); + model->nodes.clear(); + model->extensionsUsed.clear(); + model->extensionsRequired.clear(); + model->extensions.clear(); + model->defaultScene = -1; + + // 1. Parse Asset + { + json_const_iterator it; + if (FindMember(v, "asset", it) && IsObject(GetValue(it))) { + const json &root = GetValue(it); + + ParseAsset(&model->asset, err, root, + store_original_json_for_extras_and_extensions_); + } + } + +#ifdef TINYGLTF_USE_CPP14 + auto ForEachInArray = [](const json &_v, const char *member, + const auto &cb) -> bool +#else + // The std::function<> implementation can be less efficient because it will + // allocate heap when the size of the captured lambda is above 16 bytes with + // clang and gcc, but it does not require C++14. + auto ForEachInArray = [](const json &_v, const char *member, + const std::function &cb) -> bool +#endif + { + json_const_iterator itm; + if (FindMember(_v, member, itm) && IsArray(GetValue(itm))) { + const json &root = GetValue(itm); + auto it = ArrayBegin(root); + auto end = ArrayEnd(root); + for (; it != end; ++it) { + if (!cb(*it)) return false; + } + } + return true; + }; + + // 2. Parse extensionUsed + { + ForEachInArray(v, "extensionsUsed", [&](const json &o) { + std::string str; + GetString(o, str); + model->extensionsUsed.emplace_back(std::move(str)); + return true; + }); + } + + { + ForEachInArray(v, "extensionsRequired", [&](const json &o) { + std::string str; + GetString(o, str); + model->extensionsRequired.emplace_back(std::move(str)); + return true; + }); + } + + // 3. Parse Buffer + { + bool success = ForEachInArray(v, "buffers", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`buffers' does not contain an JSON object."; + } + return false; + } + Buffer buffer; + if (!ParseBuffer(&buffer, err, o, + store_original_json_for_extras_and_extensions_, &fs, + base_dir, is_binary_, bin_data_, bin_size_)) { + return false; + } + + model->buffers.emplace_back(std::move(buffer)); + return true; + }); + + if (!success) { + return false; + } + } + // 4. Parse BufferView + { + bool success = ForEachInArray(v, "bufferViews", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`bufferViews' does not contain an JSON object."; + } + return false; + } + BufferView bufferView; + if (!ParseBufferView(&bufferView, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->bufferViews.emplace_back(std::move(bufferView)); + return true; + }); + + if (!success) { + return false; + } + } + + // 5. Parse Accessor + { + bool success = ForEachInArray(v, "accessors", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`accessors' does not contain an JSON object."; + } + return false; + } + Accessor accessor; + if (!ParseAccessor(&accessor, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->accessors.emplace_back(std::move(accessor)); + return true; + }); + + if (!success) { + return false; + } + } + + // 6. Parse Mesh + { + bool success = ForEachInArray(v, "meshes", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`meshes' does not contain an JSON object."; + } + return false; + } + Mesh mesh; + if (!ParseMesh(&mesh, model, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->meshes.emplace_back(std::move(mesh)); + return true; + }); + + if (!success) { + return false; + } + } + + // Assign missing bufferView target types + // - Look for missing Mesh indices + // - Look for missing Mesh attributes + for (auto &mesh : model->meshes) { + for (auto &primitive : mesh.primitives) { + if (primitive.indices > + -1) // has indices from parsing step, must be Element Array Buffer + { + if (size_t(primitive.indices) >= model->accessors.size()) { + if (err) { + (*err) += "primitive indices accessor out of bounds"; + } + return false; + } + + auto bufferView = + model->accessors[size_t(primitive.indices)].bufferView; + if (bufferView < 0 || size_t(bufferView) >= model->bufferViews.size()) { + if (err) { + (*err) += "accessor[" + std::to_string(primitive.indices) + + "] invalid bufferView"; + } + return false; + } + + model->bufferViews[size_t(bufferView)].target = + TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; + // we could optionally check if acessors' bufferView type is Scalar, as + // it should be + } + + for (auto &attribute : primitive.attributes) { + model + ->bufferViews[size_t( + model->accessors[size_t(attribute.second)].bufferView)] + .target = TINYGLTF_TARGET_ARRAY_BUFFER; + } + + for (auto &target : primitive.targets) { + for (auto &attribute : target) { + auto bufferView = + model->accessors[size_t(attribute.second)].bufferView; + // bufferView could be null(-1) for sparse morph target + if (bufferView >= 0) { + model->bufferViews[size_t(bufferView)].target = + TINYGLTF_TARGET_ARRAY_BUFFER; + } + } + } + } + } + + // 7. Parse Node + { + bool success = ForEachInArray(v, "nodes", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`nodes' does not contain an JSON object."; + } + return false; + } + Node node; + if (!ParseNode(&node, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->nodes.emplace_back(std::move(node)); + return true; + }); + + if (!success) { + return false; + } + } + + // 8. Parse scenes. + { + bool success = ForEachInArray(v, "scenes", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`scenes' does not contain an JSON object."; + } + return false; + } + std::vector nodes; + ParseIntegerArrayProperty(&nodes, err, o, "nodes", false); + + Scene scene; + scene.nodes = std::move(nodes); + + ParseStringProperty(&scene.name, err, o, "name", false); + + ParseExtensionsProperty(&scene.extensions, err, o); + ParseExtrasProperty(&scene.extras, o); + + if (store_original_json_for_extras_and_extensions_) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + model->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + model->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + model->scenes.emplace_back(std::move(scene)); + return true; + }); + + if (!success) { + return false; + } + } + + // 9. Parse default scenes. + { + json_const_iterator rootIt; + int iVal; + if (FindMember(v, "scene", rootIt) && GetInt(GetValue(rootIt), iVal)) { + model->defaultScene = iVal; + } + } + + // 10. Parse Material + { + bool success = ForEachInArray(v, "materials", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`materials' does not contain an JSON object."; + } + return false; + } + Material material; + ParseStringProperty(&material.name, err, o, "name", false); + + if (!ParseMaterial(&material, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->materials.emplace_back(std::move(material)); + return true; + }); + + if (!success) { + return false; + } + } + + // 11. Parse Image + { + int idx = 0; + bool success = ForEachInArray(v, "images", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "image[" + std::to_string(idx) + "] is not a JSON object."; + } + return false; + } + Image image; + if (!ParseImage(&image, idx, err, warn, o, + store_original_json_for_extras_and_extensions_, base_dir, + &fs, &this->LoadImageData, load_image_user_data_)) { + return false; + } + + if (image.bufferView != -1) { + // Load image from the buffer view. + if (size_t(image.bufferView) >= model->bufferViews.size()) { + if (err) { + std::stringstream ss; + ss << "image[" << idx << "] bufferView \"" << image.bufferView + << "\" not found in the scene." << std::endl; + (*err) += ss.str(); + } + return false; + } + + const BufferView &bufferView = + model->bufferViews[size_t(image.bufferView)]; + if (size_t(bufferView.buffer) >= model->buffers.size()) { + if (err) { + std::stringstream ss; + ss << "image[" << idx << "] buffer \"" << bufferView.buffer + << "\" not found in the scene." << std::endl; + (*err) += ss.str(); + } + return false; + } + const Buffer &buffer = model->buffers[size_t(bufferView.buffer)]; + + if (*LoadImageData == nullptr) { + if (err) { + (*err) += "No LoadImageData callback specified.\n"; + } + return false; + } + bool ret = LoadImageData( + &image, idx, err, warn, image.width, image.height, + &buffer.data[bufferView.byteOffset], + static_cast(bufferView.byteLength), load_image_user_data_); + if (!ret) { + return false; + } + } + + model->images.emplace_back(std::move(image)); + ++idx; + return true; + }); + + if (!success) { + return false; + } + } + + // 12. Parse Texture + { + bool success = ForEachInArray(v, "textures", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`textures' does not contain an JSON object."; + } + return false; + } + Texture texture; + if (!ParseTexture(&texture, err, o, + store_original_json_for_extras_and_extensions_, + base_dir)) { + return false; + } + + model->textures.emplace_back(std::move(texture)); + return true; + }); + + if (!success) { + return false; + } + } + + // 13. Parse Animation + { + bool success = ForEachInArray(v, "animations", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`animations' does not contain an JSON object."; + } + return false; + } + Animation animation; + if (!ParseAnimation(&animation, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->animations.emplace_back(std::move(animation)); + return true; + }); + + if (!success) { + return false; + } + } + + // 14. Parse Skin + { + bool success = ForEachInArray(v, "skins", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`skins' does not contain an JSON object."; + } + return false; + } + Skin skin; + if (!ParseSkin(&skin, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->skins.emplace_back(std::move(skin)); + return true; + }); + + if (!success) { + return false; + } + } + + // 15. Parse Sampler + { + bool success = ForEachInArray(v, "samplers", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`samplers' does not contain an JSON object."; + } + return false; + } + Sampler sampler; + if (!ParseSampler(&sampler, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->samplers.emplace_back(std::move(sampler)); + return true; + }); + + if (!success) { + return false; + } + } + + // 16. Parse Camera + { + bool success = ForEachInArray(v, "cameras", [&](const json &o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`cameras' does not contain an JSON object."; + } + return false; + } + Camera camera; + if (!ParseCamera(&camera, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->cameras.emplace_back(std::move(camera)); + return true; + }); + + if (!success) { + return false; + } + } + + // 17. Parse Extensions + ParseExtensionsProperty(&model->extensions, err, v); + + // 18. Specific extension implementations + { + json_const_iterator rootIt; + if (FindMember(v, "extensions", rootIt) && IsObject(GetValue(rootIt))) { + const json &root = GetValue(rootIt); + + json_const_iterator it(ObjectBegin(root)); + json_const_iterator itEnd(ObjectEnd(root)); + for (; it != itEnd; ++it) { + // parse KHR_lights_punctual extension + std::string key(GetKey(it)); + if ((key == "KHR_lights_punctual") && IsObject(GetValue(it))) { + const json &object = GetValue(it); + json_const_iterator itLight; + if (FindMember(object, "lights", itLight)) { + const json &lights = GetValue(itLight); + if (!IsArray(lights)) { + continue; + } + + auto arrayIt(ArrayBegin(lights)); + auto arrayItEnd(ArrayEnd(lights)); + for (; arrayIt != arrayItEnd; ++arrayIt) { + Light light; + if (!ParseLight(&light, err, *arrayIt, + store_original_json_for_extras_and_extensions_)) { + return false; + } + model->lights.emplace_back(std::move(light)); + } + } + } + } + } + } + + // 19. Parse Extras + ParseExtrasProperty(&model->extras, v); + + if (store_original_json_for_extras_and_extensions_) { + model->extras_json_string = JsonToString(v["extras"]); + model->extensions_json_string = JsonToString(v["extensions"]); + } + + return true; + } + + bool TinyGLTF::LoadASCIIFromString(Model *model, std::string *err, + std::string *warn, const char *str, + unsigned int length, + const std::string &base_dir, + unsigned int check_sections) { + is_binary_ = false; + bin_data_ = nullptr; + bin_size_ = 0; + + return LoadFromString(model, err, warn, str, length, base_dir, + check_sections); + } + + bool TinyGLTF::LoadASCIIFromFile(Model *model, std::string *err, + std::string *warn, const std::string &filename, + unsigned int check_sections) { + std::stringstream ss; + + if (fs.ReadWholeFile == nullptr) { + // Programmer error, assert() ? + ss << "Failed to read file: " << filename + << ": one or more FS callback not set" << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + std::vector data; + std::string fileerr; + bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); + if (!fileread) { + ss << "Failed to read file: " << filename << ": " << fileerr << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + size_t sz = data.size(); + if (sz == 0) { + if (err) { + (*err) = "Empty file."; + } + return false; + } + + std::string basedir = GetBaseDir(filename); + + bool ret = LoadASCIIFromString( + model, err, warn, reinterpret_cast(&data.at(0)), + static_cast(data.size()), basedir, check_sections); + + return ret; + } + + bool TinyGLTF::LoadBinaryFromMemory(Model *model, std::string *err, + std::string *warn, + const unsigned char *bytes, + unsigned int size, + const std::string &base_dir, + unsigned int check_sections) { + if (size < 20) { + if (err) { + (*err) = "Too short data size for glTF Binary."; + } + return false; + } + + if (bytes[0] == 'g' && bytes[1] == 'l' && bytes[2] == 'T' && + bytes[3] == 'F') { + // ok + } else { + if (err) { + (*err) = "Invalid magic."; + } + return false; + } + + unsigned int version; // 4 bytes + unsigned int length; // 4 bytes + unsigned int model_length; // 4 bytes + unsigned int model_format; // 4 bytes; + + // @todo { Endian swap for big endian machine. } + memcpy(&version, bytes + 4, 4); + swap4(&version); + memcpy(&length, bytes + 8, 4); + swap4(&length); + memcpy(&model_length, bytes + 12, 4); + swap4(&model_length); + memcpy(&model_format, bytes + 16, 4); + swap4(&model_format); + + // In case the Bin buffer is not present, the size is exactly 20 + size of + // JSON contents, + // so use "greater than" operator. + if ((20 + model_length > size) || (model_length < 1) || (length > size) || + (20 + model_length > length) || + (model_format != 0x4E4F534A)) { // 0x4E4F534A = JSON format. + if (err) { + (*err) = "Invalid glTF binary."; + } + return false; + } + + // Extract JSON string. + std::string jsonString(reinterpret_cast(&bytes[20]), + model_length); + + is_binary_ = true; + bin_data_ = bytes + 20 + model_length + + 8; // 4 bytes (buffer_length) + 4 bytes(buffer_format) + bin_size_ = + length - (20 + model_length); // extract header + JSON scene data. + + bool ret = LoadFromString(model, err, warn, + reinterpret_cast(&bytes[20]), + model_length, base_dir, check_sections); + if (!ret) { + return ret; + } + + return true; + } + + bool TinyGLTF::LoadBinaryFromFile(Model *model, std::string *err, + std::string *warn, + const std::string &filename, + unsigned int check_sections) { + std::stringstream ss; + + if (fs.ReadWholeFile == nullptr) { + // Programmer error, assert() ? + ss << "Failed to read file: " << filename + << ": one or more FS callback not set" << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + std::vector data; + std::string fileerr; + bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); + if (!fileread) { + ss << "Failed to read file: " << filename << ": " << fileerr << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + std::string basedir = GetBaseDir(filename); + + bool ret = LoadBinaryFromMemory(model, err, warn, &data.at(0), + static_cast(data.size()), + basedir, check_sections); + + return ret; + } + + /////////////////////// + // GLTF Serialization + /////////////////////// + namespace { + json JsonFromString(const char *s) { +#ifdef TINYGLTF_USE_RAPIDJSON + return json(s, GetAllocator()); +#else + return json(s); +#endif + } + + void JsonAssign(json &dest, const json &src) { +#ifdef TINYGLTF_USE_RAPIDJSON + dest.CopyFrom(src, GetAllocator()); +#else + dest = src; +#endif + } + + void JsonAddMember(json &o, const char *key, json &&value) { +#ifdef TINYGLTF_USE_RAPIDJSON + if (!o.IsObject()) { + o.SetObject(); + } + o.AddMember(json(key, GetAllocator()), std::move(value), GetAllocator()); +#else + o[key] = std::move(value); +#endif + } + + void JsonPushBack(json &o, json &&value) { +#ifdef TINYGLTF_USE_RAPIDJSON + o.PushBack(std::move(value), GetAllocator()); +#else + o.push_back(std::move(value)); +#endif + } + + bool JsonIsNull(const json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + return o.IsNull(); +#else + return o.is_null(); +#endif + } + + void JsonSetObject(json &o) { +#ifdef TINYGLTF_USE_RAPIDJSON + o.SetObject(); +#else + o = o.object({}); +#endif + } + + void JsonReserveArray(json &o, size_t s) { +#ifdef TINYGLTF_USE_RAPIDJSON + o.SetArray(); + o.Reserve(static_cast(s), GetAllocator()); +#endif + (void)(o); + (void)(s); + } + } // namespace + + // typedef std::pair json_object_pair; + + template + static void SerializeNumberProperty(const std::string &key, T number, + json &obj) { + // obj.insert( + // json_object_pair(key, json(static_cast(number)))); + // obj[key] = static_cast(number); + JsonAddMember(obj, key.c_str(), json(number)); + } + +#ifdef TINYGLTF_USE_RAPIDJSON + template <> + void SerializeNumberProperty(const std::string &key, size_t number, json &obj) { + JsonAddMember(obj, key.c_str(), json(static_cast(number))); + } +#endif + + template + static void SerializeNumberArrayProperty(const std::string &key, + const std::vector &value, + json &obj) { + if (value.empty()) return; + + json ary; + JsonReserveArray(ary, value.size()); + for (const auto &s : value) { + JsonPushBack(ary, json(s)); + } + JsonAddMember(obj, key.c_str(), std::move(ary)); + } + + static void SerializeStringProperty(const std::string &key, + const std::string &value, json &obj) { + JsonAddMember(obj, key.c_str(), JsonFromString(value.c_str())); + } + + static void SerializeStringArrayProperty(const std::string &key, + const std::vector &value, + json &obj) { + json ary; + JsonReserveArray(ary, value.size()); + for (auto &s : value) { + JsonPushBack(ary, JsonFromString(s.c_str())); + } + JsonAddMember(obj, key.c_str(), std::move(ary)); + } + + static bool ValueToJson(const Value &value, json *ret) { + json obj; +#ifdef TINYGLTF_USE_RAPIDJSON + switch (value.Type()) { + case REAL_TYPE: + obj.SetDouble(value.Get()); + break; + case INT_TYPE: + obj.SetInt(value.Get()); + break; + case BOOL_TYPE: + obj.SetBool(value.Get()); + break; + case STRING_TYPE: + obj.SetString(value.Get().c_str(), GetAllocator()); + break; + case ARRAY_TYPE: { + obj.SetArray(); + obj.Reserve(static_cast(value.ArrayLen()), + GetAllocator()); + for (unsigned int i = 0; i < value.ArrayLen(); ++i) { + Value elementValue = value.Get(int(i)); + json elementJson; + if (ValueToJson(value.Get(int(i)), &elementJson)) + obj.PushBack(std::move(elementJson), GetAllocator()); + } + break; + } + case BINARY_TYPE: + // TODO + // obj = json(value.Get>()); + return false; + break; + case OBJECT_TYPE: { + obj.SetObject(); + Value::Object objMap = value.Get(); + for (auto &it : objMap) { + json elementJson; + if (ValueToJson(it.second, &elementJson)) { + obj.AddMember(json(it.first.c_str(), GetAllocator()), + std::move(elementJson), GetAllocator()); + } + } + break; + } + case NULL_TYPE: + default: + return false; + } +#else + switch (value.Type()) { + case REAL_TYPE: + obj = json(value.Get()); + break; + case INT_TYPE: + obj = json(value.Get()); + break; + case BOOL_TYPE: + obj = json(value.Get()); + break; + case STRING_TYPE: + obj = json(value.Get()); + break; + case ARRAY_TYPE: { + for (unsigned int i = 0; i < value.ArrayLen(); ++i) { + Value elementValue = value.Get(int(i)); + json elementJson; + if (ValueToJson(value.Get(int(i)), &elementJson)) + obj.push_back(elementJson); + } + break; + } + case BINARY_TYPE: + // TODO + // obj = json(value.Get>()); + return false; + break; + case OBJECT_TYPE: { + Value::Object objMap = value.Get(); + for (auto &it : objMap) { + json elementJson; + if (ValueToJson(it.second, &elementJson)) obj[it.first] = elementJson; + } + break; + } + case NULL_TYPE: + default: + return false; + } +#endif + if (ret) *ret = std::move(obj); + return true; + } + + static void SerializeValue(const std::string &key, const Value &value, + json &obj) { + json ret; + if (ValueToJson(value, &ret)) { + JsonAddMember(obj, key.c_str(), std::move(ret)); + } + } + + static void SerializeGltfBufferData(const std::vector &data, + json &o) { + std::string header = "data:application/octet-stream;base64,"; + if (data.size() > 0) { + std::string encodedData = + base64_encode(&data[0], static_cast(data.size())); + SerializeStringProperty("uri", header + encodedData, o); + } else { + // Issue #229 + // size 0 is allowd. Just emit mime header. + SerializeStringProperty("uri", header, o); + } + } + + static bool SerializeGltfBufferData(const std::vector &data, + const std::string &binFilename) { +#ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = _wopen(UTF8ToWchar(binFilename).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream output(&wfile_buf); + if (!wfile_buf.is_open()) return false; +#elif defined(_MSC_VER) + std::ofstream output(UTF8ToWchar(binFilename).c_str(), std::ofstream::binary); + if (!output.is_open()) return false; +#else + std::ofstream output(binFilename.c_str(), std::ofstream::binary); + if (!output.is_open()) return false; +#endif +#else + std::ofstream output(binFilename.c_str(), std::ofstream::binary); + if (!output.is_open()) return false; +#endif + if (data.size() > 0) { + output.write(reinterpret_cast(&data[0]), + std::streamsize(data.size())); + } else { + // Issue #229 + // size 0 will be still valid buffer data. + // write empty file. + } + return true; + } + +#if 0 // FIXME(syoyo): not used. will be removed in the future release. + static void SerializeParameterMap(ParameterMap ¶m, json &o) { + for (ParameterMap::iterator paramIt = param.begin(); paramIt != param.end(); + ++paramIt) { + if (paramIt->second.number_array.size()) { + SerializeNumberArrayProperty(paramIt->first, + paramIt->second.number_array, o); + } else if (paramIt->second.json_double_value.size()) { + json json_double_value; + for (std::map::iterator it = + paramIt->second.json_double_value.begin(); + it != paramIt->second.json_double_value.end(); ++it) { + if (it->first == "index") { + json_double_value[it->first] = paramIt->second.TextureIndex(); + } else { + json_double_value[it->first] = it->second; + } + } + + o[paramIt->first] = json_double_value; + } else if (!paramIt->second.string_value.empty()) { + SerializeStringProperty(paramIt->first, paramIt->second.string_value, o); + } else if (paramIt->second.has_number_value) { + o[paramIt->first] = paramIt->second.number_value; + } else { + o[paramIt->first] = paramIt->second.bool_value; + } + } + } +#endif + + static void SerializeExtensionMap(const ExtensionMap &extensions, json &o) { + if (!extensions.size()) return; + + json extMap; + for (ExtensionMap::const_iterator extIt = extensions.begin(); + extIt != extensions.end(); ++extIt) { + // Allow an empty object for extension(#97) + json ret; + bool isNull = true; + if (ValueToJson(extIt->second, &ret)) { + isNull = JsonIsNull(ret); + JsonAddMember(extMap, extIt->first.c_str(), std::move(ret)); + } + if (isNull) { + if (!(extIt->first.empty())) { // name should not be empty, but for sure + // create empty object so that an extension name is still included in + // json. + json empty; + JsonSetObject(empty); + JsonAddMember(extMap, extIt->first.c_str(), std::move(empty)); + } + } + } + JsonAddMember(o, "extensions", std::move(extMap)); + } + + static void SerializeGltfAccessor(Accessor &accessor, json &o) { + if (accessor.bufferView >= 0) + SerializeNumberProperty("bufferView", accessor.bufferView, o); + + if (accessor.byteOffset != 0) + SerializeNumberProperty("byteOffset", int(accessor.byteOffset), o); + + SerializeNumberProperty("componentType", accessor.componentType, o); + SerializeNumberProperty("count", accessor.count, o); + SerializeNumberArrayProperty("min", accessor.minValues, o); + SerializeNumberArrayProperty("max", accessor.maxValues, o); + if (accessor.normalized) + SerializeValue("normalized", Value(accessor.normalized), o); + std::string type; + switch (accessor.type) { + case TINYGLTF_TYPE_SCALAR: + type = "SCALAR"; + break; + case TINYGLTF_TYPE_VEC2: + type = "VEC2"; + break; + case TINYGLTF_TYPE_VEC3: + type = "VEC3"; + break; + case TINYGLTF_TYPE_VEC4: + type = "VEC4"; + break; + case TINYGLTF_TYPE_MAT2: + type = "MAT2"; + break; + case TINYGLTF_TYPE_MAT3: + type = "MAT3"; + break; + case TINYGLTF_TYPE_MAT4: + type = "MAT4"; + break; + } + + SerializeStringProperty("type", type, o); + if (!accessor.name.empty()) SerializeStringProperty("name", accessor.name, o); + + if (accessor.extras.Type() != NULL_TYPE) { + SerializeValue("extras", accessor.extras, o); + } + } + + static void SerializeGltfAnimationChannel(AnimationChannel &channel, json &o) { + SerializeNumberProperty("sampler", channel.sampler, o); + { + json target; + SerializeNumberProperty("node", channel.target_node, target); + SerializeStringProperty("path", channel.target_path, target); + + SerializeExtensionMap(channel.target_extensions, target); + + JsonAddMember(o, "target", std::move(target)); + } + + if (channel.extras.Type() != NULL_TYPE) { + SerializeValue("extras", channel.extras, o); + } + + SerializeExtensionMap(channel.extensions, o); + } + + static void SerializeGltfAnimationSampler(AnimationSampler &sampler, json &o) { + SerializeNumberProperty("input", sampler.input, o); + SerializeNumberProperty("output", sampler.output, o); + SerializeStringProperty("interpolation", sampler.interpolation, o); + + if (sampler.extras.Type() != NULL_TYPE) { + SerializeValue("extras", sampler.extras, o); + } + } + + static void SerializeGltfAnimation(Animation &animation, json &o) { + if (!animation.name.empty()) + SerializeStringProperty("name", animation.name, o); + + { + json channels; + JsonReserveArray(channels, animation.channels.size()); + for (unsigned int i = 0; i < animation.channels.size(); ++i) { + json channel; + AnimationChannel gltfChannel = animation.channels[i]; + SerializeGltfAnimationChannel(gltfChannel, channel); + JsonPushBack(channels, std::move(channel)); + } + + JsonAddMember(o, "channels", std::move(channels)); + } + + { + json samplers; + JsonReserveArray(samplers, animation.samplers.size()); + for (unsigned int i = 0; i < animation.samplers.size(); ++i) { + json sampler; + AnimationSampler gltfSampler = animation.samplers[i]; + SerializeGltfAnimationSampler(gltfSampler, sampler); + JsonPushBack(samplers, std::move(sampler)); + } + JsonAddMember(o, "samplers", std::move(samplers)); + } + + if (animation.extras.Type() != NULL_TYPE) { + SerializeValue("extras", animation.extras, o); + } + + SerializeExtensionMap(animation.extensions, o); + } + + static void SerializeGltfAsset(Asset &asset, json &o) { + if (!asset.generator.empty()) { + SerializeStringProperty("generator", asset.generator, o); + } + + if (!asset.copyright.empty()) { + SerializeStringProperty("copyright", asset.copyright, o); + } + + if (!asset.version.empty()) { + SerializeStringProperty("version", asset.version, o); + } + + if (asset.extras.Keys().size()) { + SerializeValue("extras", asset.extras, o); + } + + SerializeExtensionMap(asset.extensions, o); + } + + static void SerializeGltfBufferBin(Buffer &buffer, json &o, + std::vector &binBuffer) { + SerializeNumberProperty("byteLength", buffer.data.size(), o); + binBuffer = buffer.data; + + if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } + } + + static void SerializeGltfBuffer(Buffer &buffer, json &o) { + SerializeNumberProperty("byteLength", buffer.data.size(), o); + SerializeGltfBufferData(buffer.data, o); + + if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } + } + + static bool SerializeGltfBuffer(Buffer &buffer, json &o, + const std::string &binFilename, + const std::string &binBaseFilename) { + if (!SerializeGltfBufferData(buffer.data, binFilename)) return false; + SerializeNumberProperty("byteLength", buffer.data.size(), o); + SerializeStringProperty("uri", binBaseFilename, o); + + if (buffer.name.size()) SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } + return true; + } + + static void SerializeGltfBufferView(BufferView &bufferView, json &o) { + SerializeNumberProperty("buffer", bufferView.buffer, o); + SerializeNumberProperty("byteLength", bufferView.byteLength, o); + + // byteStride is optional, minimum allowed is 4 + if (bufferView.byteStride >= 4) { + SerializeNumberProperty("byteStride", bufferView.byteStride, o); + } + // byteOffset is optional, default is 0 + if (bufferView.byteOffset > 0) { + SerializeNumberProperty("byteOffset", bufferView.byteOffset, o); + } + // Target is optional, check if it contains a valid value + if (bufferView.target == TINYGLTF_TARGET_ARRAY_BUFFER || + bufferView.target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER) { + SerializeNumberProperty("target", bufferView.target, o); + } + if (bufferView.name.size()) { + SerializeStringProperty("name", bufferView.name, o); + } + + if (bufferView.extras.Type() != NULL_TYPE) { + SerializeValue("extras", bufferView.extras, o); + } + } + + static void SerializeGltfImage(Image &image, json &o) { + // if uri empty, the mimeType and bufferview should be set + if (image.uri.empty()) { + SerializeStringProperty("mimeType", image.mimeType, o); + SerializeNumberProperty("bufferView", image.bufferView, o); + } else { + // TODO(syoyo): dlib::urilencode? + SerializeStringProperty("uri", image.uri, o); + } + + if (image.name.size()) { + SerializeStringProperty("name", image.name, o); + } + + if (image.extras.Type() != NULL_TYPE) { + SerializeValue("extras", image.extras, o); + } + + SerializeExtensionMap(image.extensions, o); + } + + static void SerializeGltfTextureInfo(TextureInfo &texinfo, json &o) { + SerializeNumberProperty("index", texinfo.index, o); + + if (texinfo.texCoord != 0) { + SerializeNumberProperty("texCoord", texinfo.texCoord, o); + } + + if (texinfo.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texinfo.extras, o); + } + + SerializeExtensionMap(texinfo.extensions, o); + } + + static void SerializeGltfNormalTextureInfo(NormalTextureInfo &texinfo, + json &o) { + SerializeNumberProperty("index", texinfo.index, o); + + if (texinfo.texCoord != 0) { + SerializeNumberProperty("texCoord", texinfo.texCoord, o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(texinfo.scale, 1.0)) { + SerializeNumberProperty("scale", texinfo.scale, o); + } + + if (texinfo.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texinfo.extras, o); + } + + SerializeExtensionMap(texinfo.extensions, o); + } + + static void SerializeGltfOcclusionTextureInfo(OcclusionTextureInfo &texinfo, + json &o) { + SerializeNumberProperty("index", texinfo.index, o); + + if (texinfo.texCoord != 0) { + SerializeNumberProperty("texCoord", texinfo.texCoord, o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(texinfo.strength, 1.0)) { + SerializeNumberProperty("strength", texinfo.strength, o); + } + + if (texinfo.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texinfo.extras, o); + } + + SerializeExtensionMap(texinfo.extensions, o); + } + + static void SerializeGltfPbrMetallicRoughness(PbrMetallicRoughness &pbr, + json &o) { + std::vector default_baseColorFactor = {1.0, 1.0, 1.0, 1.0}; + if (!Equals(pbr.baseColorFactor, default_baseColorFactor)) { + SerializeNumberArrayProperty("baseColorFactor", pbr.baseColorFactor, + o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(pbr.metallicFactor, 1.0)) { + SerializeNumberProperty("metallicFactor", pbr.metallicFactor, o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(pbr.roughnessFactor, 1.0)) { + SerializeNumberProperty("roughnessFactor", pbr.roughnessFactor, o); + } + + if (pbr.baseColorTexture.index > -1) { + json texinfo; + SerializeGltfTextureInfo(pbr.baseColorTexture, texinfo); + JsonAddMember(o, "baseColorTexture", std::move(texinfo)); + } + + if (pbr.metallicRoughnessTexture.index > -1) { + json texinfo; + SerializeGltfTextureInfo(pbr.metallicRoughnessTexture, texinfo); + JsonAddMember(o, "metallicRoughnessTexture", std::move(texinfo)); + } + + SerializeExtensionMap(pbr.extensions, o); + + if (pbr.extras.Type() != NULL_TYPE) { + SerializeValue("extras", pbr.extras, o); + } + } + + static void SerializeGltfMaterial(Material &material, json &o) { + if (material.name.size()) { + SerializeStringProperty("name", material.name, o); + } + + // QUESTION(syoyo): Write material parameters regardless of its default value? + + if (!TINYGLTF_DOUBLE_EQUAL(material.alphaCutoff, 0.5)) { + SerializeNumberProperty("alphaCutoff", material.alphaCutoff, o); + } + + if (material.alphaMode.compare("OPAQUE") != 0) { + SerializeStringProperty("alphaMode", material.alphaMode, o); + } + + if (material.doubleSided != false) + JsonAddMember(o, "doubleSided", json(material.doubleSided)); + + if (material.normalTexture.index > -1) { + json texinfo; + SerializeGltfNormalTextureInfo(material.normalTexture, texinfo); + JsonAddMember(o, "normalTexture", std::move(texinfo)); + } + + if (material.occlusionTexture.index > -1) { + json texinfo; + SerializeGltfOcclusionTextureInfo(material.occlusionTexture, texinfo); + JsonAddMember(o, "occlusionTexture", std::move(texinfo)); + } + + if (material.emissiveTexture.index > -1) { + json texinfo; + SerializeGltfTextureInfo(material.emissiveTexture, texinfo); + JsonAddMember(o, "emissiveTexture", std::move(texinfo)); + } + + std::vector default_emissiveFactor = {0.0, 0.0, 0.0}; + if (!Equals(material.emissiveFactor, default_emissiveFactor)) { + SerializeNumberArrayProperty("emissiveFactor", + material.emissiveFactor, o); + } + + { + json pbrMetallicRoughness; + SerializeGltfPbrMetallicRoughness(material.pbrMetallicRoughness, + pbrMetallicRoughness); + // Issue 204 + // Do not serialize `pbrMetallicRoughness` if pbrMetallicRoughness has all + // default values(json is null). Otherwise it will serialize to + // `pbrMetallicRoughness : null`, which cannot be read by other glTF + // importers(and validators). + // + if (!JsonIsNull(pbrMetallicRoughness)) { + JsonAddMember(o, "pbrMetallicRoughness", std::move(pbrMetallicRoughness)); + } + } + +#if 0 // legacy way. just for the record. + if (material.values.size()) { + json pbrMetallicRoughness; + SerializeParameterMap(material.values, pbrMetallicRoughness); + JsonAddMember(o, "pbrMetallicRoughness", std::move(pbrMetallicRoughness)); + } + + SerializeParameterMap(material.additionalValues, o); +#else + +#endif + + SerializeExtensionMap(material.extensions, o); + + if (material.extras.Type() != NULL_TYPE) { + SerializeValue("extras", material.extras, o); + } + } + + static void SerializeGltfMesh(Mesh &mesh, json &o) { + json primitives; + JsonReserveArray(primitives, mesh.primitives.size()); + for (unsigned int i = 0; i < mesh.primitives.size(); ++i) { + json primitive; + const Primitive &gltfPrimitive = mesh.primitives[i]; // don't make a copy + { + json attributes; + for (auto attrIt = gltfPrimitive.attributes.begin(); + attrIt != gltfPrimitive.attributes.end(); ++attrIt) { + SerializeNumberProperty(attrIt->first, attrIt->second, attributes); + } + + JsonAddMember(primitive, "attributes", std::move(attributes)); + } + + // Indicies is optional + if (gltfPrimitive.indices > -1) { + SerializeNumberProperty("indices", gltfPrimitive.indices, primitive); + } + // Material is optional + if (gltfPrimitive.material > -1) { + SerializeNumberProperty("material", gltfPrimitive.material, + primitive); + } + SerializeNumberProperty("mode", gltfPrimitive.mode, primitive); + + // Morph targets + if (gltfPrimitive.targets.size()) { + json targets; + JsonReserveArray(targets, gltfPrimitive.targets.size()); + for (unsigned int k = 0; k < gltfPrimitive.targets.size(); ++k) { + json targetAttributes; + std::map targetData = gltfPrimitive.targets[k]; + for (std::map::iterator attrIt = targetData.begin(); + attrIt != targetData.end(); ++attrIt) { + SerializeNumberProperty(attrIt->first, attrIt->second, + targetAttributes); + } + JsonPushBack(targets, std::move(targetAttributes)); + } + JsonAddMember(primitive, "targets", std::move(targets)); + } + + SerializeExtensionMap(gltfPrimitive.extensions, primitive); + + if (gltfPrimitive.extras.Type() != NULL_TYPE) { + SerializeValue("extras", gltfPrimitive.extras, primitive); + } + + JsonPushBack(primitives, std::move(primitive)); + } + + JsonAddMember(o, "primitives", std::move(primitives)); + + if (mesh.weights.size()) { + SerializeNumberArrayProperty("weights", mesh.weights, o); + } + + if (mesh.name.size()) { + SerializeStringProperty("name", mesh.name, o); + } + + SerializeExtensionMap(mesh.extensions, o); + if (mesh.extras.Type() != NULL_TYPE) { + SerializeValue("extras", mesh.extras, o); + } + } + + static void SerializeSpotLight(SpotLight &spot, json &o) { + SerializeNumberProperty("innerConeAngle", spot.innerConeAngle, o); + SerializeNumberProperty("outerConeAngle", spot.outerConeAngle, o); + SerializeExtensionMap(spot.extensions, o); + if (spot.extras.Type() != NULL_TYPE) { + SerializeValue("extras", spot.extras, o); + } + } + + static void SerializeGltfLight(Light &light, json &o) { + if (!light.name.empty()) SerializeStringProperty("name", light.name, o); + SerializeNumberProperty("intensity", light.intensity, o); + if (light.range > 0.0) { + SerializeNumberProperty("range", light.range, o); + } + SerializeNumberArrayProperty("color", light.color, o); + SerializeStringProperty("type", light.type, o); + if (light.type == "spot") { + json spot; + SerializeSpotLight(light.spot, spot); + JsonAddMember(o, "spot", std::move(spot)); + } + SerializeExtensionMap(light.extensions, o); + if (light.extras.Type() != NULL_TYPE) { + SerializeValue("extras", light.extras, o); + } + } + + static void SerializeGltfNode(Node &node, json &o) { + if (node.translation.size() > 0) { + SerializeNumberArrayProperty("translation", node.translation, o); + } + if (node.rotation.size() > 0) { + SerializeNumberArrayProperty("rotation", node.rotation, o); + } + if (node.scale.size() > 0) { + SerializeNumberArrayProperty("scale", node.scale, o); + } + if (node.matrix.size() > 0) { + SerializeNumberArrayProperty("matrix", node.matrix, o); + } + if (node.mesh != -1) { + SerializeNumberProperty("mesh", node.mesh, o); + } + + if (node.skin != -1) { + SerializeNumberProperty("skin", node.skin, o); + } + + if (node.camera != -1) { + SerializeNumberProperty("camera", node.camera, o); + } + + if (node.weights.size() > 0) { + SerializeNumberArrayProperty("weights", node.weights, o); + } + + if (node.extras.Type() != NULL_TYPE) { + SerializeValue("extras", node.extras, o); + } + + SerializeExtensionMap(node.extensions, o); + if (!node.name.empty()) SerializeStringProperty("name", node.name, o); + SerializeNumberArrayProperty("children", node.children, o); + } + + static void SerializeGltfSampler(Sampler &sampler, json &o) { + if (sampler.magFilter != -1) { + SerializeNumberProperty("magFilter", sampler.magFilter, o); + } + if (sampler.minFilter != -1) { + SerializeNumberProperty("minFilter", sampler.minFilter, o); + } + SerializeNumberProperty("wrapR", sampler.wrapR, o); + SerializeNumberProperty("wrapS", sampler.wrapS, o); + SerializeNumberProperty("wrapT", sampler.wrapT, o); + + if (sampler.extras.Type() != NULL_TYPE) { + SerializeValue("extras", sampler.extras, o); + } + } + + static void SerializeGltfOrthographicCamera(const OrthographicCamera &camera, + json &o) { + SerializeNumberProperty("zfar", camera.zfar, o); + SerializeNumberProperty("znear", camera.znear, o); + SerializeNumberProperty("xmag", camera.xmag, o); + SerializeNumberProperty("ymag", camera.ymag, o); + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } + } + + static void SerializeGltfPerspectiveCamera(const PerspectiveCamera &camera, + json &o) { + SerializeNumberProperty("zfar", camera.zfar, o); + SerializeNumberProperty("znear", camera.znear, o); + if (camera.aspectRatio > 0) { + SerializeNumberProperty("aspectRatio", camera.aspectRatio, o); + } + + if (camera.yfov > 0) { + SerializeNumberProperty("yfov", camera.yfov, o); + } + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } + } + + static void SerializeGltfCamera(const Camera &camera, json &o) { + SerializeStringProperty("type", camera.type, o); + if (!camera.name.empty()) { + SerializeStringProperty("name", camera.name, o); + } + + if (camera.type.compare("orthographic") == 0) { + json orthographic; + SerializeGltfOrthographicCamera(camera.orthographic, orthographic); + JsonAddMember(o, "orthographic", std::move(orthographic)); + } else if (camera.type.compare("perspective") == 0) { + json perspective; + SerializeGltfPerspectiveCamera(camera.perspective, perspective); + JsonAddMember(o, "perspective", std::move(perspective)); + } else { + // ??? + } + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } + SerializeExtensionMap(camera.extensions, o); + } + + static void SerializeGltfScene(Scene &scene, json &o) { + SerializeNumberArrayProperty("nodes", scene.nodes, o); + + if (scene.name.size()) { + SerializeStringProperty("name", scene.name, o); + } + if (scene.extras.Type() != NULL_TYPE) { + SerializeValue("extras", scene.extras, o); + } + SerializeExtensionMap(scene.extensions, o); + } + + static void SerializeGltfSkin(Skin &skin, json &o) { + if (skin.inverseBindMatrices != -1) + SerializeNumberProperty("inverseBindMatrices", skin.inverseBindMatrices, o); + + SerializeNumberArrayProperty("joints", skin.joints, o); + SerializeNumberProperty("skeleton", skin.skeleton, o); + if (skin.name.size()) { + SerializeStringProperty("name", skin.name, o); + } + } + + static void SerializeGltfTexture(Texture &texture, json &o) { + if (texture.sampler > -1) { + SerializeNumberProperty("sampler", texture.sampler, o); + } + if (texture.source > -1) { + SerializeNumberProperty("source", texture.source, o); + } + if (texture.name.size()) { + SerializeStringProperty("name", texture.name, o); + } + if (texture.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texture.extras, o); + } + SerializeExtensionMap(texture.extensions, o); + } + + /// + /// Serialize all properties except buffers and images. + /// + static void SerializeGltfModel(Model *model, json &o) { + // ACCESSORS + if (model->accessors.size()) { + json accessors; + JsonReserveArray(accessors, model->accessors.size()); + for (unsigned int i = 0; i < model->accessors.size(); ++i) { + json accessor; + SerializeGltfAccessor(model->accessors[i], accessor); + JsonPushBack(accessors, std::move(accessor)); + } + JsonAddMember(o, "accessors", std::move(accessors)); + } + + // ANIMATIONS + if (model->animations.size()) { + json animations; + JsonReserveArray(animations, model->animations.size()); + for (unsigned int i = 0; i < model->animations.size(); ++i) { + if (model->animations[i].channels.size()) { + json animation; + SerializeGltfAnimation(model->animations[i], animation); + JsonPushBack(animations, std::move(animation)); + } + } + + JsonAddMember(o, "animations", std::move(animations)); + } + + // ASSET + json asset; + SerializeGltfAsset(model->asset, asset); + JsonAddMember(o, "asset", std::move(asset)); + + // BUFFERVIEWS + if (model->bufferViews.size()) { + json bufferViews; + JsonReserveArray(bufferViews, model->bufferViews.size()); + for (unsigned int i = 0; i < model->bufferViews.size(); ++i) { + json bufferView; + SerializeGltfBufferView(model->bufferViews[i], bufferView); + JsonPushBack(bufferViews, std::move(bufferView)); + } + JsonAddMember(o, "bufferViews", std::move(bufferViews)); + } + + // Extensions required + if (model->extensionsRequired.size()) { + SerializeStringArrayProperty("extensionsRequired", + model->extensionsRequired, o); + } + + // MATERIALS + if (model->materials.size()) { + json materials; + JsonReserveArray(materials, model->materials.size()); + for (unsigned int i = 0; i < model->materials.size(); ++i) { + json material; + SerializeGltfMaterial(model->materials[i], material); + JsonPushBack(materials, std::move(material)); + } + JsonAddMember(o, "materials", std::move(materials)); + } + + // MESHES + if (model->meshes.size()) { + json meshes; + JsonReserveArray(meshes, model->meshes.size()); + for (unsigned int i = 0; i < model->meshes.size(); ++i) { + json mesh; + SerializeGltfMesh(model->meshes[i], mesh); + JsonPushBack(meshes, std::move(mesh)); + } + JsonAddMember(o, "meshes", std::move(meshes)); + } + + // NODES + if (model->nodes.size()) { + json nodes; + JsonReserveArray(nodes, model->nodes.size()); + for (unsigned int i = 0; i < model->nodes.size(); ++i) { + json node; + SerializeGltfNode(model->nodes[i], node); + JsonPushBack(nodes, std::move(node)); + } + JsonAddMember(o, "nodes", std::move(nodes)); + } + + // SCENE + if (model->defaultScene > -1) { + SerializeNumberProperty("scene", model->defaultScene, o); + } + + // SCENES + if (model->scenes.size()) { + json scenes; + JsonReserveArray(scenes, model->scenes.size()); + for (unsigned int i = 0; i < model->scenes.size(); ++i) { + json currentScene; + SerializeGltfScene(model->scenes[i], currentScene); + JsonPushBack(scenes, std::move(currentScene)); + } + JsonAddMember(o, "scenes", std::move(scenes)); + } + + // SKINS + if (model->skins.size()) { + json skins; + JsonReserveArray(skins, model->skins.size()); + for (unsigned int i = 0; i < model->skins.size(); ++i) { + json skin; + SerializeGltfSkin(model->skins[i], skin); + JsonPushBack(skins, std::move(skin)); + } + JsonAddMember(o, "skins", std::move(skins)); + } + + // TEXTURES + if (model->textures.size()) { + json textures; + JsonReserveArray(textures, model->textures.size()); + for (unsigned int i = 0; i < model->textures.size(); ++i) { + json texture; + SerializeGltfTexture(model->textures[i], texture); + JsonPushBack(textures, std::move(texture)); + } + JsonAddMember(o, "textures", std::move(textures)); + } + + // SAMPLERS + if (model->samplers.size()) { + json samplers; + JsonReserveArray(samplers, model->samplers.size()); + for (unsigned int i = 0; i < model->samplers.size(); ++i) { + json sampler; + SerializeGltfSampler(model->samplers[i], sampler); + JsonPushBack(samplers, std::move(sampler)); + } + JsonAddMember(o, "samplers", std::move(samplers)); + } + + // CAMERAS + if (model->cameras.size()) { + json cameras; + JsonReserveArray(cameras, model->cameras.size()); + for (unsigned int i = 0; i < model->cameras.size(); ++i) { + json camera; + SerializeGltfCamera(model->cameras[i], camera); + JsonPushBack(cameras, std::move(camera)); + } + JsonAddMember(o, "cameras", std::move(cameras)); + } + + // EXTENSIONS + SerializeExtensionMap(model->extensions, o); + + auto extensionsUsed = model->extensionsUsed; + + // LIGHTS as KHR_lights_punctual + if (model->lights.size()) { + json lights; + JsonReserveArray(lights, model->lights.size()); + for (unsigned int i = 0; i < model->lights.size(); ++i) { + json light; + SerializeGltfLight(model->lights[i], light); + JsonPushBack(lights, std::move(light)); + } + json khr_lights_cmn; + JsonAddMember(khr_lights_cmn, "lights", std::move(lights)); + json ext_j; + + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + JsonAssign(ext_j, GetValue(it)); + } + } + + JsonAddMember(ext_j, "KHR_lights_punctual", std::move(khr_lights_cmn)); + + JsonAddMember(o, "extensions", std::move(ext_j)); + + // Also add "KHR_lights_punctual" to `extensionsUsed` + { + auto has_khr_lights_punctual = + std::find_if(extensionsUsed.begin(), extensionsUsed.end(), + [](const std::string &s) { + return (s.compare("KHR_lights_punctual") == 0); + }); + + if (has_khr_lights_punctual == extensionsUsed.end()) { + extensionsUsed.push_back("KHR_lights_punctual"); + } + } + } + + // Extensions used + if (model->extensionsUsed.size()) { + SerializeStringArrayProperty("extensionsUsed", extensionsUsed, o); + } + + // EXTRAS + if (model->extras.Type() != NULL_TYPE) { + SerializeValue("extras", model->extras, o); + } + } + + static bool WriteGltfStream(std::ostream &stream, const std::string &content) { + stream << content << std::endl; + return true; + } + + static bool WriteGltfFile(const std::string &output, + const std::string &content) { +#ifdef _WIN32 +#if defined(_MSC_VER) + std::ofstream gltfFile(UTF8ToWchar(output).c_str()); +#elif defined(__GLIBCXX__) + int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream gltfFile(&wfile_buf); + if (!wfile_buf.is_open()) return false; +#else + std::ofstream gltfFile(output.c_str()); + if (!gltfFile.is_open()) return false; +#endif +#else + std::ofstream gltfFile(output.c_str()); + if (!gltfFile.is_open()) return false; +#endif + return WriteGltfStream(gltfFile, content); + } + + static void WriteBinaryGltfStream(std::ostream &stream, + const std::string &content, + const std::vector &binBuffer) { + const std::string header = "glTF"; + const int version = 2; + + // https://stackoverflow.com/questions/3407012/c-rounding-up-to-the-nearest-multiple-of-a-number + auto roundUp = [](uint32_t numToRound, uint32_t multiple) { + if (multiple == 0) return numToRound; + + uint32_t remainder = numToRound % multiple; + if (remainder == 0) return numToRound; + + return numToRound + multiple - remainder; + }; + + const uint32_t padding_size = + roundUp(uint32_t(content.size()), 4) - uint32_t(content.size()); + + // 12 bytes for header, JSON content length, 8 bytes for JSON chunk info. + // Chunk data must be located at 4-byte boundary. + const uint32_t length = + 12 + 8 + roundUp(uint32_t(content.size()), 4) + + (binBuffer.size() ? (8 + roundUp(uint32_t(binBuffer.size()), 4)) : 0); + + stream.write(header.c_str(), std::streamsize(header.size())); + stream.write(reinterpret_cast(&version), sizeof(version)); + stream.write(reinterpret_cast(&length), sizeof(length)); + + // JSON chunk info, then JSON data + const uint32_t model_length = uint32_t(content.size()) + padding_size; + const uint32_t model_format = 0x4E4F534A; + stream.write(reinterpret_cast(&model_length), + sizeof(model_length)); + stream.write(reinterpret_cast(&model_format), + sizeof(model_format)); + stream.write(content.c_str(), std::streamsize(content.size())); + + // Chunk must be multiplies of 4, so pad with spaces + if (padding_size > 0) { + const std::string padding = std::string(size_t(padding_size), ' '); + stream.write(padding.c_str(), std::streamsize(padding.size())); + } + if (binBuffer.size() > 0) { + const uint32_t bin_padding_size = + roundUp(uint32_t(binBuffer.size()), 4) - uint32_t(binBuffer.size()); + // BIN chunk info, then BIN data + const uint32_t bin_length = uint32_t(binBuffer.size()) + bin_padding_size; + const uint32_t bin_format = 0x004e4942; + stream.write(reinterpret_cast(&bin_length), + sizeof(bin_length)); + stream.write(reinterpret_cast(&bin_format), + sizeof(bin_format)); + stream.write(reinterpret_cast(binBuffer.data()), + std::streamsize(binBuffer.size())); + // Chunksize must be multiplies of 4, so pad with zeroes + if (bin_padding_size > 0) { + const std::vector padding = + std::vector(size_t(bin_padding_size), 0); + stream.write(reinterpret_cast(padding.data()), + std::streamsize(padding.size())); + } + } + } + + static void WriteBinaryGltfFile(const std::string &output, + const std::string &content, + const std::vector &binBuffer) { +#ifdef _WIN32 +#if defined(_MSC_VER) + std::ofstream gltfFile(UTF8ToWchar(output).c_str(), std::ios::binary); +#elif defined(__GLIBCXX__) + int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream gltfFile(&wfile_buf); +#else + std::ofstream gltfFile(output.c_str(), std::ios::binary); +#endif +#else + std::ofstream gltfFile(output.c_str(), std::ios::binary); +#endif + WriteBinaryGltfStream(gltfFile, content, binBuffer); + } + + bool TinyGLTF::WriteGltfSceneToStream(Model *model, std::ostream &stream, + bool prettyPrint = true, + bool writeBinary = false) { + JsonDocument output; + + /// Serialize all properties except buffers and images. + SerializeGltfModel(model, output); + + // BUFFERS + std::vector binBuffer; + if (model->buffers.size()) { + json buffers; + JsonReserveArray(buffers, model->buffers.size()); + for (unsigned int i = 0; i < model->buffers.size(); ++i) { + json buffer; + if (writeBinary && i == 0 && model->buffers[i].uri.empty()) { + SerializeGltfBufferBin(model->buffers[i], buffer, binBuffer); + } else { + SerializeGltfBuffer(model->buffers[i], buffer); + } + JsonPushBack(buffers, std::move(buffer)); + } + JsonAddMember(output, "buffers", std::move(buffers)); + } + + // IMAGES + if (model->images.size()) { + json images; + JsonReserveArray(images, model->images.size()); + for (unsigned int i = 0; i < model->images.size(); ++i) { + json image; + + std::string dummystring = ""; + // UpdateImageObject need baseDir but only uses it if embeddedImages is + // enabled, since we won't write separate images when writing to a stream + // we + UpdateImageObject(model->images[i], dummystring, int(i), false, + &this->WriteImageData, this->write_image_user_data_); + SerializeGltfImage(model->images[i], image); + JsonPushBack(images, std::move(image)); + } + JsonAddMember(output, "images", std::move(images)); + } + + if (writeBinary) { + WriteBinaryGltfStream(stream, JsonToString(output), binBuffer); + } else { + WriteGltfStream(stream, JsonToString(output, prettyPrint ? 2 : -1)); + } + + return true; + } + + bool TinyGLTF::WriteGltfSceneToFile(Model *model, const std::string &filename, + bool embedImages = false, + bool embedBuffers = false, + bool prettyPrint = true, + bool writeBinary = false) { + JsonDocument output; + std::string defaultBinFilename = GetBaseFilename(filename); + std::string defaultBinFileExt = ".bin"; + std::string::size_type pos = + defaultBinFilename.rfind('.', defaultBinFilename.length()); + + if (pos != std::string::npos) { + defaultBinFilename = defaultBinFilename.substr(0, pos); + } + std::string baseDir = GetBaseDir(filename); + if (baseDir.empty()) { + baseDir = "./"; + } + /// Serialize all properties except buffers and images. + SerializeGltfModel(model, output); + + // BUFFERS + std::vector usedUris; + std::vector binBuffer; + if (model->buffers.size()) { + json buffers; + JsonReserveArray(buffers, model->buffers.size()); + for (unsigned int i = 0; i < model->buffers.size(); ++i) { + json buffer; + if (writeBinary && i == 0 && model->buffers[i].uri.empty()) { + SerializeGltfBufferBin(model->buffers[i], buffer, binBuffer); + } else if (embedBuffers) { + SerializeGltfBuffer(model->buffers[i], buffer); + } else { + std::string binSavePath; + std::string binUri; + if (!model->buffers[i].uri.empty() && + !IsDataURI(model->buffers[i].uri)) { + binUri = model->buffers[i].uri; + } else { + binUri = defaultBinFilename + defaultBinFileExt; + bool inUse = true; + int numUsed = 0; + while (inUse) { + inUse = false; + for (const std::string &usedName : usedUris) { + if (binUri.compare(usedName) != 0) continue; + inUse = true; + binUri = defaultBinFilename + std::to_string(numUsed++) + + defaultBinFileExt; + break; + } + } + } + usedUris.push_back(binUri); + binSavePath = JoinPath(baseDir, binUri); + if (!SerializeGltfBuffer(model->buffers[i], buffer, binSavePath, + binUri)) { + return false; + } + } + JsonPushBack(buffers, std::move(buffer)); + } + JsonAddMember(output, "buffers", std::move(buffers)); + } + + // IMAGES + if (model->images.size()) { + json images; + JsonReserveArray(images, model->images.size()); + for (unsigned int i = 0; i < model->images.size(); ++i) { + json image; + + UpdateImageObject(model->images[i], baseDir, int(i), embedImages, + &this->WriteImageData, this->write_image_user_data_); + SerializeGltfImage(model->images[i], image); + JsonPushBack(images, std::move(image)); + } + JsonAddMember(output, "images", std::move(images)); + } + + if (writeBinary) { + WriteBinaryGltfFile(filename, JsonToString(output), binBuffer); + } else { + WriteGltfFile(filename, JsonToString(output, (prettyPrint ? 2 : -1))); + } + + return true; + } + +} // namespace tinygltf + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + #endif // TINYGLTF_IMPLEMENTATION \ No newline at end of file