From fc47e6799059af018053a71dd13cac172c5e7297 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 16 Feb 2018 13:56:59 -0800 Subject: [PATCH 001/286] Merging with master --- libraries/render-utils/src/AntialiasingEffect.cpp | 6 +++--- libraries/render-utils/src/VelocityBufferPass.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 7bc5328100..eb1b98aa1f 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -202,7 +202,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { if (!_antialiasingPipeline) { auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(taa_frag)); + auto ps = taa_frag::getShader(); gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; @@ -233,7 +233,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { const gpu::PipelinePointer& Antialiasing::getBlendPipeline() { if (!_blendPipeline) { auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(fxaa_blend_frag)); + auto ps = fxaa_blend_frag::getShader(); gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; @@ -254,7 +254,7 @@ const gpu::PipelinePointer& Antialiasing::getBlendPipeline() { const gpu::PipelinePointer& Antialiasing::getDebugBlendPipeline() { if (!_debugBlendPipeline) { auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(taa_blend_frag)); + auto ps = taa_blend_frag::getShader(); gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; diff --git a/libraries/render-utils/src/VelocityBufferPass.cpp b/libraries/render-utils/src/VelocityBufferPass.cpp index 26dc1c6848..4f25604798 100644 --- a/libraries/render-utils/src/VelocityBufferPass.cpp +++ b/libraries/render-utils/src/VelocityBufferPass.cpp @@ -147,7 +147,7 @@ void VelocityBufferPass::run(const render::RenderContextPointer& renderContext, const gpu::PipelinePointer& VelocityBufferPass::getCameraMotionPipeline() { if (!_cameraMotionPipeline) { auto vs = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); - auto ps = gpu::Shader::createPixel(std::string(velocityBuffer_cameraMotion_frag)); + auto ps = velocityBuffer_cameraMotion_frag::getShader(); gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; From 27ba9172b8e9d19f31caddbc935181f8f9072518 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 16 Feb 2018 17:20:20 -0800 Subject: [PATCH 002/286] identifying the bug with defered transform in hmd and trying to apply fix for taa --- libraries/render-utils/src/DeferredFrameTransform.cpp | 2 ++ libraries/render-utils/src/DeferredFrameTransform.h | 2 ++ libraries/render-utils/src/DeferredTransform.slh | 9 +++++++++ .../render-utils/src/velocityBuffer_cameraMotion.slf | 5 +++-- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/DeferredFrameTransform.cpp b/libraries/render-utils/src/DeferredFrameTransform.cpp index cee5786847..781161a8af 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.cpp +++ b/libraries/render-utils/src/DeferredFrameTransform.cpp @@ -48,6 +48,7 @@ void DeferredFrameTransform::update(RenderArgs* args) { frameTransformBuffer.projection[0] = frameTransformBuffer.projectionMono; frameTransformBuffer.stereoInfo = glm::vec4(0.0f, (float)args->_viewport.z, 0.0f, 0.0f); frameTransformBuffer.invpixelInfo = glm::vec4(1.0f / args->_viewport.z, 1.0f / args->_viewport.w, 0.0f, 0.0f); + frameTransformBuffer.invProjection[0] = glm::inverse(frameTransformBuffer.projection[0]); } else { mat4 projMats[2]; @@ -59,6 +60,7 @@ void DeferredFrameTransform::update(RenderArgs* args) { // Compose the mono Eye space to Stereo clip space Projection Matrix auto sideViewMat = projMats[i] * eyeViews[i]; frameTransformBuffer.projection[i] = sideViewMat; + frameTransformBuffer.invProjection[i] = glm::inverse(sideViewMat); } frameTransformBuffer.stereoInfo = glm::vec4(1.0f, (float)(args->_viewport.z >> 1), 0.0f, 1.0f); diff --git a/libraries/render-utils/src/DeferredFrameTransform.h b/libraries/render-utils/src/DeferredFrameTransform.h index a90effe053..0b5cb6a989 100644 --- a/libraries/render-utils/src/DeferredFrameTransform.h +++ b/libraries/render-utils/src/DeferredFrameTransform.h @@ -45,6 +45,8 @@ protected: glm::vec4 stereoInfo{ 0.0 }; // Mono proj matrix or Left and Right proj matrix going from Mono Eye space to side clip space glm::mat4 projection[2]; + // Inverse proj matrix or Left and Right proj matrix going from Mono Eye space to side clip space + glm::mat4 invProjection[2]; // THe mono projection for sure glm::mat4 projectionMono; // Inv View matrix from eye space (mono) to world space diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 25d352387f..f1765978eb 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -31,6 +31,7 @@ struct DeferredFrameTransform { vec4 _depthInfo; vec4 _stereoInfo; mat4 _projection[2]; + mat4 _invProjection[2]; mat4 _projectionMono; mat4 _viewInverse; mat4 _view; @@ -128,6 +129,14 @@ vec3 evalEyePositionFromZeye(int side, float Zeye, vec2 texcoord) { return vec3(Xe, Ye, Zeye); } +vec3 evalEyePositionFromZdb(int side, float Zdb, vec2 texcoord) { + // compute the view space position using the depth + vec3 clipPos; + clipPos.xyz = vec3(texcoord.xy, Zdb) * 2.0 - 1.0; + vec4 eyePos = frameTransform._invProjection[side] * vec4(clipPos.xyz, 1.0); + return eyePos.xyz / eyePos.w; +} + ivec2 getPixelPosTexcoordPosAndSide(in vec2 glFragCoord, out ivec2 pixelPos, out vec2 texcoordPos, out ivec4 stereoSide) { ivec2 fragPos = ivec2(glFragCoord.xy); diff --git a/libraries/render-utils/src/velocityBuffer_cameraMotion.slf b/libraries/render-utils/src/velocityBuffer_cameraMotion.slf index 7488f42535..00d5cdfb3d 100644 --- a/libraries/render-utils/src/velocityBuffer_cameraMotion.slf +++ b/libraries/render-utils/src/velocityBuffer_cameraMotion.slf @@ -26,14 +26,15 @@ void main(void) { ivec2 framePixelPos = getPixelPosTexcoordPosAndSide(gl_FragCoord.xy, pixelPos, texcoordPos, stereoSide); float Zdb = texelFetch(depthMap, ivec2(gl_FragCoord.xy), 0).x; - float Zeye = evalZeyeFromZdb(Zdb); + // float Zeye = evalZeyeFromZdb(Zdb); /* if (Zeye <= -getPosLinearDepthFar()) { outFragColor = vec4(0.5, 0.5, 0.0, 0.0); return; }*/ // The position of the pixel fragment in Eye space then in world space - vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); + //vec3 eyePos = evalEyePositionFromZeye(stereoSide.x, Zeye, texcoordPos); + vec3 eyePos = evalEyePositionFromZdb(stereoSide.x, Zdb, texcoordPos); vec3 worldPos = (frameTransform._viewInverse * cameraCorrection._correction * vec4(eyePos, 1.0)).xyz; vec3 prevEyePos = (cameraCorrection._prevCorrectionInverse * frameTransform._prevView * vec4(worldPos, 1.0)).xyz; From 6d02aa064a9b531c6d537f414d5fa9d425db5575 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 13 Mar 2018 10:53:21 -0700 Subject: [PATCH 003/286] REmove any change --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 14db81d01e..df91e0ca7b 100644 --- a/.gitignore +++ b/.gitignore @@ -78,10 +78,8 @@ TAGS node_modules npm-debug.log - # ignore qmlc files generated from qml as cache *.qmlc - # Android studio files *___jb_old___ From c237e34b7119467c17a0bea75228ecef45d0abe9 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 28 Mar 2018 17:47:20 -0700 Subject: [PATCH 004/286] Adding script tools --- .../utilities/render/inspectEngine.js | 39 +++++++++++++++++++ .../utilities/render/inspectEngine.qml | 19 +++++++++ 2 files changed, 58 insertions(+) create mode 100644 scripts/developer/utilities/render/inspectEngine.js create mode 100644 scripts/developer/utilities/render/inspectEngine.qml diff --git a/scripts/developer/utilities/render/inspectEngine.js b/scripts/developer/utilities/render/inspectEngine.js new file mode 100644 index 0000000000..7abce8b477 --- /dev/null +++ b/scripts/developer/utilities/render/inspectEngine.js @@ -0,0 +1,39 @@ +(function() { // BEGIN LOCAL_SCOPE + +function traverse(root, functor, depth) { + var subs = root.findChildren(/.*/) + depth++; + for (var i = 0; i Date: Wed, 28 Mar 2018 18:27:21 -0700 Subject: [PATCH 005/286] i dont't know --- .../utilities/render/inspectEngine.js | 22 ++++++++++++++----- .../utilities/render/inspectEngine.qml | 1 + 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/developer/utilities/render/inspectEngine.js b/scripts/developer/utilities/render/inspectEngine.js index 7abce8b477..82231b991e 100644 --- a/scripts/developer/utilities/render/inspectEngine.js +++ b/scripts/developer/utilities/render/inspectEngine.js @@ -1,17 +1,27 @@ (function() { // BEGIN LOCAL_SCOPE -function traverse(root, functor, depth) { +function task_traverse(root, functor, depth) { var subs = root.findChildren(/.*/) depth++; for (var i = 0; i Date: Wed, 28 Nov 2018 22:37:11 -0800 Subject: [PATCH 006/286] REmove cruft --- .../utilities/render/inspectEngine.js | 49 ------------------- .../utilities/render/inspectEngine.qml | 20 -------- 2 files changed, 69 deletions(-) delete mode 100644 scripts/developer/utilities/render/inspectEngine.js delete mode 100644 scripts/developer/utilities/render/inspectEngine.qml diff --git a/scripts/developer/utilities/render/inspectEngine.js b/scripts/developer/utilities/render/inspectEngine.js deleted file mode 100644 index 82231b991e..0000000000 --- a/scripts/developer/utilities/render/inspectEngine.js +++ /dev/null @@ -1,49 +0,0 @@ -(function() { // BEGIN LOCAL_SCOPE - -function task_traverse(root, functor, depth) { - var subs = root.findChildren(/.*/) - depth++; - for (var i = 0; i Date: Fri, 30 Nov 2018 00:42:49 -0800 Subject: [PATCH 007/286] exploring better stereo drawcall techniques --- .../hmd/DebugHmdDisplayPlugin.cpp | 5 +++ .../gpu-gl-common/src/gpu/gl/GLBackend.cpp | 8 +++- .../gpu-gl-common/src/gpu/gl/GLBackend.h | 10 ++++- .../src/gpu/gl/GLBackendTransform.cpp | 42 ++++++++++++++++++- libraries/gpu/src/gpu/Transform.slh | 9 +++- libraries/shaders/headers/450/header.glsl | 1 + tools/shadergen.py | 4 ++ 7 files changed, 73 insertions(+), 6 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index 40063652c8..3de3e590b9 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -26,6 +26,8 @@ bool DebugHmdDisplayPlugin::isSupported() const { void DebugHmdDisplayPlugin::resetSensors() { _currentRenderFrameInfo.renderPose = glm::mat4(); // identity + _currentRenderFrameInfo.renderPose = glm::translate(glm::mat4(), glm::vec3(0.0f, 1.76f, 0.0f)); + } bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { @@ -35,6 +37,8 @@ bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { // FIXME simulate head movement //_currentRenderFrameInfo.renderPose = ; //_currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; + _currentRenderFrameInfo.renderPose = glm::translate(glm::mat4(), glm::vec3(0.0f, 1.76f, 0.0f)); + _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; withNonPresentThreadLock([&] { _frameInfos[frameIndex] = _currentRenderFrameInfo; @@ -71,6 +75,7 @@ bool DebugHmdDisplayPlugin::internalActivate() { _eyeOffsets[1][3] = vec4{ 0.0327499993, 0.0, -0.0149999997, 1.0 }; _renderTargetSize = { 3024, 1680 }; _cullingProjection = _eyeProjections[0]; + // This must come after the initialization, so that the values calculated // above are available during the customizeContext call (when not running // in threaded present mode) diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp index c1ce05c18b..00373eb196 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp @@ -410,20 +410,24 @@ void GLBackend::render(const Batch& batch) { renderPassTransfer(batch); } -#ifdef GPU_STEREO_DRAWCALL_INSTANCED +//#ifdef GPU_STEREO_DRAWCALL_INSTANCED +#ifdef GPU_STEREO_VIEWPORT_CLIPPED if (_stereo.isStereo()) { glEnable(GL_CLIP_DISTANCE0); } #endif +//#endif { PROFILE_RANGE(render_gpu_gl_detail, _stereo.isStereo() ? "Render Stereo" : "Render"); renderPassDraw(batch); } -#ifdef GPU_STEREO_DRAWCALL_INSTANCED +//#ifdef GPU_STEREO_DRAWCALL_INSTANCED +#ifdef GPU_STEREO_VIEWPORT_CLIPPED if (_stereo.isStereo()) { glDisable(GL_CLIP_DISTANCE0); } #endif +//#endif // Restore the saved stereo state for the next batch _stereo._enable = savedStereo; diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h index 37dde5b08e..8a459c86ca 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h @@ -36,7 +36,8 @@ #define GPU_STEREO_TECHNIQUE_DOUBLED_SIMPLE #else //#define GPU_STEREO_TECHNIQUE_DOUBLED_SMARTER -#define GPU_STEREO_TECHNIQUE_INSTANCED +//#define GPU_STEREO_TECHNIQUE_INSTANCED +#define GPU_STEREO_TECHNIQUE_INSTANCED_MULTIVIEWPORT #endif // Let these be configured by the one define picked above @@ -51,6 +52,13 @@ #ifdef GPU_STEREO_TECHNIQUE_INSTANCED #define GPU_STEREO_DRAWCALL_INSTANCED +#define GPU_STEREO_VIEWPORT_CLIPPED +#define GPU_STEREO_CAMERA_BUFFER +#endif + +#ifdef GPU_STEREO_TECHNIQUE_INSTANCED_MULTIVIEWPORT +#define GPU_STEREO_DRAWCALL_INSTANCED +#define GPU_STEREO_MULTI_VIEWPORT #define GPU_STEREO_CAMERA_BUFFER #endif diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp index 2c2a4e254c..caa194764d 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp @@ -39,6 +39,45 @@ void GLBackend::do_setViewportTransform(const Batch& batch, size_t paramOffset) #ifdef GPU_STEREO_DRAWCALL_INSTANCED { + #ifdef GPU_STEREO_MULTI_VIEWPORT + ivec4& vp = _transform._viewport; + auto sideWidth = vp.z / 2; + + vec4 leftRight[3]; + + // Mono + leftRight[0] = vp; + + // Left side + leftRight[1] = vp; + leftRight[1].x = 0; + leftRight[1].z = sideWidth; + + // right side + leftRight[2] = vp; + leftRight[2].x = sideWidth; + leftRight[2].z = sideWidth; + + glViewportArrayv(0, 3, (float*)leftRight); + + // Where we assign the GL viewport + if (_stereo.isStereo()) { + + // ivec4 leftRight[3]; + // leftRight[0] = vp; + vp.z /= 2; + /* leftRight[1] = vp; // left side + leftRight[2] = vp; // right side + leftRight[2].x += vp.z; + glViewportArrayv(0, 3, (float*) leftRight); +*/ + if (_stereo._pass) { + vp.x += vp.z; + } + } else { + // glViewport(vp.x, vp.y, vp.z, vp.w); + } + #else ivec4& vp = _transform._viewport; glViewport(vp.x, vp.y, vp.z, vp.w); @@ -49,6 +88,7 @@ void GLBackend::do_setViewportTransform(const Batch& batch, size_t paramOffset) vp.x += vp.z; } } + #endif } #else if (!_inRenderTransferPass && !isStereo()) { @@ -123,7 +163,7 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo if (_invalidView || _invalidProj || _invalidViewport) { size_t offset = _cameraUboSize * _cameras.size(); - Vec2 finalJitter = _projectionJitter / Vec2(framebufferSize); + Vec2 finalJitter = _projectionJitter / Vec2(framebufferSize); _cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset)); if (stereo.isStereo()) { diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index 43205ba4c2..998204ce92 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -168,10 +168,15 @@ TransformObject getTransformObject() { vec2 eyeOffsetScale = vec2(-0.5, +0.5); uint eyeIndex = uint(_stereoSide); #ifndef GPU_GLES +#ifdef GPU_GL450 + gl_ViewportIndex = _stereoSide + 1; + // gl_ViewportIndex = 2 - _stereoSide; +#else gl_ClipDistance[0] = dot(<$clipPos$>, eyeClipEdge[eyeIndex]); #endif - float newClipPosX = <$clipPos$>.x * 0.5 + eyeOffsetScale[eyeIndex] * <$clipPos$>.w; - <$clipPos$>.x = newClipPosX; +#endif + // float newClipPosX = <$clipPos$>.x * 0.5 + eyeOffsetScale[eyeIndex] * <$clipPos$>.w; + // <$clipPos$>.x = newClipPosX; #endif #else diff --git a/libraries/shaders/headers/450/header.glsl b/libraries/shaders/headers/450/header.glsl index 6ce61b4378..a33b7634b1 100644 --- a/libraries/shaders/headers/450/header.glsl +++ b/libraries/shaders/headers/450/header.glsl @@ -1,4 +1,5 @@ #version 450 core +#extension GL_ARB_shader_viewport_layer_array : require #define GPU_GL450 #define GPU_SSBO_TRANSFORM_OBJECT #define BITFIELD int diff --git a/tools/shadergen.py b/tools/shadergen.py index ffbe1662ec..120fcc6bb7 100644 --- a/tools/shadergen.py +++ b/tools/shadergen.py @@ -190,6 +190,10 @@ def processCommand(line): if (dialect == '310es'): spirvCrossDialect = '320es' spirvCrossArgs = [spirvCrossExec, '--output', glslFile, spirvFile, '--version', spirvCrossDialect] if (dialect == '410'): spirvCrossArgs.append('--no-420pack-extension') + if (dialect == '450'): + spirvCrossArgs.append('--extension') + spirvCrossArgs.append('GL_ARB_shader_viewport_layer_array') + executeSubprocess(spirvCrossArgs) else: # This logic is necessary because cmake will agressively keep re-executing the shadergen From 9125f4ff41f43c5a1aaa0911a701cd5083a87a19 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 1 Mar 2019 17:45:33 -0800 Subject: [PATCH 008/286] Trying to hack the stereo for layered --- libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp | 3 +++ libraries/gpu/src/gpu/Transform.slh | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp index 130932238d..698a70af5a 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp @@ -47,6 +47,9 @@ void GLBackend::do_setViewportTransform(const Batch& batch, size_t paramOffset) // Mono leftRight[0] = vp; + // adding this here as im doing Layered, force the first viewport here to be half of it + leftRight[0].x = 0; + leftRight[0].z = sideWidth; // Left side leftRight[1] = vp; diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index 9298ddcba4..484ad7ebd2 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -169,8 +169,10 @@ TransformObject getTransformObject() { uint eyeIndex = uint(_stereoSide); #if !defined(GPU_GLES) || (defined(HAVE_EXT_clip_cull_distance) && !defined(VULKAN)) #ifdef GPU_GL450 - gl_ViewportIndex = _stereoSide + 1; + /* gl_ViewportIndex = _stereoSide + 1; // gl_ViewportIndex = 2 - _stereoSide; + */// THIs is the layered version + gl_Layer = _stereoSide; #else gl_ClipDistance[0] = dot(<$clipPos$>, eyeClipEdge[eyeIndex]); #endif From fe23ef1485e4a152e339d6f40b53deab374bdb31 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Sun, 3 Mar 2019 23:35:40 -0800 Subject: [PATCH 009/286] Adding the prop library --- .../developer/utilities/lib/prop/Global.qml | 44 +++++++ .../utilities/lib/prop/PropColor.qml | 0 .../developer/utilities/lib/prop/PropEnum.qml | 110 ++++++++++++++++++ .../developer/utilities/lib/prop/PropItem.qml | 62 ++++++++++ .../utilities/lib/prop/PropLabel.qml | 25 ++++ .../utilities/lib/prop/PropScalar.qml | 71 +++++++++++ .../utilities/lib/prop/PropSplitter.qml | 21 ++++ .../developer/utilities/lib/prop/PropText.qml | 24 ++++ scripts/developer/utilities/lib/prop/qmldir | 8 ++ scripts/developer/utilities/render/luci.qml | 59 ++++++++++ scripts/developer/utilities/render/luci2.js | 13 +++ 11 files changed, 437 insertions(+) create mode 100644 scripts/developer/utilities/lib/prop/Global.qml create mode 100644 scripts/developer/utilities/lib/prop/PropColor.qml create mode 100644 scripts/developer/utilities/lib/prop/PropEnum.qml create mode 100644 scripts/developer/utilities/lib/prop/PropItem.qml create mode 100644 scripts/developer/utilities/lib/prop/PropLabel.qml create mode 100644 scripts/developer/utilities/lib/prop/PropScalar.qml create mode 100644 scripts/developer/utilities/lib/prop/PropSplitter.qml create mode 100644 scripts/developer/utilities/lib/prop/PropText.qml create mode 100644 scripts/developer/utilities/lib/prop/qmldir create mode 100644 scripts/developer/utilities/render/luci.qml create mode 100644 scripts/developer/utilities/render/luci2.js diff --git a/scripts/developer/utilities/lib/prop/Global.qml b/scripts/developer/utilities/lib/prop/Global.qml new file mode 100644 index 0000000000..be189e3c96 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/Global.qml @@ -0,0 +1,44 @@ +// +// Prop/Global.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + + +Item { + HifiConstants { id: hifi } + id: root + + property real lineHeight: 32 + property real slimHeight: 24 + + property var color: hifi.colors.baseGray + property var colorBackHighlight: hifi.colors.baseGrayHighlight + property var colorBorderLight: hifi.colors.lightGray + property var colorBorderHighight: hifi.colors.blueHighlight + + property real fontSize: 12 + property var fontFamily: "Raleway" + property var fontWeight: Font.DemiBold + property var fontColor: hifi.colors.faintGray + + property var splitterRightWidthScale: 0.44 + property real splitterWidth: 4 + + property var labelTextAlign: Text.AlignRight + property var labelTextElide: Text.ElideMiddle + + property var valueAreaWidthScale: 0.3 * (1.0 - splitterRightWidthScale) + property var valueTextAlign: Text.AlignHCenter + property real valueBorderWidth: 1 + property real valueBorderRadius: 2 +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/PropColor.qml b/scripts/developer/utilities/lib/prop/PropColor.qml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/developer/utilities/lib/prop/PropEnum.qml b/scripts/developer/utilities/lib/prop/PropEnum.qml new file mode 100644 index 0000000000..fe6200d971 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropEnum.qml @@ -0,0 +1,110 @@ +// +// PropEnum.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +PropItem { + Global { id: global } + id: root + + property alias valueVar : valueCombo.currentIndex + property alias enums : valueCombo.model + + ComboBox { + id: valueCombo + + flat: true + + anchors.left: root.splitter.right + anchors.right: parent.right + anchors.verticalCenter: root.verticalCenter + height: global.slimHeight + + currentIndex: root.valueVarGetter() + onCurrentIndexChanged: { root.valueVarSetter(currentIndex); } + + delegate: ItemDelegate { + width: valueCombo.width + height: valueCombo.height + contentItem: PropText { + text: modelData + horizontalAlignment: global.valueTextAlign + } + background: Rectangle { + color:highlighted?global.colorBackHighlight:global.color; + } + highlighted: valueCombo.highlightedIndex === index + } + + indicator: Canvas { + id: canvas + x: valueCombo.width - width - valueCombo.rightPadding + y: valueCombo.topPadding + (valueCombo.availableHeight - height) / 2 + width: 12 + height: 8 + contextType: "2d" + + Connections { + target: valueCombo + onPressedChanged: canvas.requestPaint() + } + + onPaint: { + context.reset(); + context.moveTo(0, 0); + context.lineTo(width, 0); + context.lineTo(width / 2, height); + context.closePath(); + context.fillStyle = (valueCombo.pressed) ? global.colorBorderHighight : global.colorBorderLight; + context.fill(); + } + } + + contentItem: PropText { + leftPadding: 0 + rightPadding: valueCombo.indicator.width + valueCombo.spacing + + text: valueCombo.displayText + horizontalAlignment: global.valueTextAlign + } + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: global.color + border.color: valueCombo.popup.visible ? global.colorBorderHighight : global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + } + + popup: Popup { + y: valueCombo.height - 1 + width: valueCombo.width + implicitHeight: contentItem.implicitHeight + 2 + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: valueCombo.popup.visible ? valueCombo.delegateModel : null + currentIndex: valueCombo.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + } + + background: Rectangle { + color: global.color + border.color: global.colorBorderHighight + radius: global.valueBorderRadius + } + } + } +} diff --git a/scripts/developer/utilities/lib/prop/PropItem.qml b/scripts/developer/utilities/lib/prop/PropItem.qml new file mode 100644 index 0000000000..ee1e99a772 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropItem.qml @@ -0,0 +1,62 @@ +// +// PropItem.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Item { + Global { id: global } + + id: root + + // Prop item is designed to author an object[property]: + property var object: NULL + property string property: "" + + // value is accessed through the "valueVarSetter" and "valueVarGetter" + // By default, these just go get or set the value from the object[property] + // + function defaultGet() { return root.object[root.property]; } + function defaultSet(value) { root.object[root.property] = value; } + property var valueVarSetter: defaultSet + property var valueVarGetter: defaultGet + + // PropItem is stretching horizontally accross its parent + // Fixed height + anchors.left: parent.left + anchors.right: parent.right + height: global.lineHeight + + + // LabelControl And SplitterControl are on the left side of the PropItem + property bool showLabel: true + property alias labelControl: labelControl + property alias label: labelControl.text + + property var labelAreaWidth: root.width * global.splitterRightWidthScale - global.splitterWidth + + PropText { + id: labelControl + text: root.label + enabled: root.showLabel + + anchors.left: root.left + anchors.verticalCenter: root.verticalCenter + width: labelAreaWidth + } + + property alias splitter: splitterControl + PropSplitter { + id: splitterControl + + anchors.left: labelControl.right + size: global.splitterWidth + } + +} diff --git a/scripts/developer/utilities/lib/prop/PropLabel.qml b/scripts/developer/utilities/lib/prop/PropLabel.qml new file mode 100644 index 0000000000..9dbeffe0ec --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropLabel.qml @@ -0,0 +1,25 @@ +// +// PropLabel.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +Label { + Global { id: global } + + color: global.fontColor + font.pixelSize: global.fontSize + font.family: global.fontFamily + font.weight: global.fontWeight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: global.labelTextAlign + elide: global.labelTextElide +} + diff --git a/scripts/developer/utilities/lib/prop/PropScalar.qml b/scripts/developer/utilities/lib/prop/PropScalar.qml new file mode 100644 index 0000000000..29b42e2801 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropScalar.qml @@ -0,0 +1,71 @@ +// +// PropItem.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import controlsUit 1.0 as HifiControls + +PropItem { + Global { id: global } + id: root + + // Scalar Prop + property bool integral: false + property var numDigits: 2 + + + property alias valueVar : sliderControl.value + property alias min: sliderControl.minimumValue + property alias max: sliderControl.maximumValue + + + + property bool showValue: true + + + signal valueChanged(real value) + + Component.onCompleted: { + valueVar = root.valueVarGetter(); + } + + PropLabel { + id: valueLabel + enabled: root.showValue + + anchors.left: root.splitter.right + anchors.verticalCenter: root.verticalCenter + width: root.width * global.valueAreaWidthScale + horizontalAlignment: global.valueTextAlign + height: global.slimHeight + + text: sliderControl.value.toFixed(root.integral ? 0 : root.numDigits) + + background: Rectangle { + color: global.color + border.color: global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + } + } + + HifiControls.Slider { + id: sliderControl + stepSize: root.integral ? 1.0 : 0.0 + anchors.left: valueLabel.right + anchors.right: root.right + anchors.rightMargin: 0 + anchors.verticalCenter: root.verticalCenter + + onValueChanged: { root.valueVarSetter(value) } + } + + +} diff --git a/scripts/developer/utilities/lib/prop/PropSplitter.qml b/scripts/developer/utilities/lib/prop/PropSplitter.qml new file mode 100644 index 0000000000..25f668a6eb --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropSplitter.qml @@ -0,0 +1,21 @@ +// +// PropSplitter.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Item { + id: root + property real size + + width: size // Must be non-zero + height: size + + anchors.verticalCenter: parent.verticalCenter +} diff --git a/scripts/developer/utilities/lib/prop/PropText.qml b/scripts/developer/utilities/lib/prop/PropText.qml new file mode 100644 index 0000000000..b1669f3836 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropText.qml @@ -0,0 +1,24 @@ +// +// Prop/Text.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Text { + Global { id: global } + + color: global.fontColor + font.pixelSize: global.fontSize + font.family: global.fontFamily + font.weight: global.fontWeight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: global.labelTextAlign + elide: global.labelTextElide +} + diff --git a/scripts/developer/utilities/lib/prop/qmldir b/scripts/developer/utilities/lib/prop/qmldir new file mode 100644 index 0000000000..44e4889ab6 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/qmldir @@ -0,0 +1,8 @@ +PropGlobal 1.0 PropGlobal.qml +PropText 1.0 PropText.qml +PropLabel 1.0 PropLabel.qml +PropSplitter 1.0 PropSplitter.qml +PropItem 1.0 PropItem.qml +PropScalar 1.0 PropScalar.qml +PropEnum 1.0 PropEnum.qml +PropColor 1.0 PropColor.qml diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml new file mode 100644 index 0000000000..959a24e9be --- /dev/null +++ b/scripts/developer/utilities/render/luci.qml @@ -0,0 +1,59 @@ +// +// luci.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.7 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +import controlsUit 1.0 as HifiControls + +import "../lib/prop" as Prop + +Rectangle { + Prop.Global { id: prop;} + id: render; + anchors.fill: parent + color: prop.color; + + property var mainViewTask: Render.getConfig("RenderMainView") + + Column { + anchors.left: parent.left + anchors.right: parent.right + Repeater { + model: [ "Tone mapping exposure:ToneMapping:exposure:5.0:-5.0", + "Tone:ToneMapping:exposure:5.0:-5.0" + ] + Prop.PropScalar { + label: qsTr(modelData.split(":")[0]) + integral: false + object: render.mainViewTask.getConfig(modelData.split(":")[1]) + property: modelData.split(":")[2] + max: modelData.split(":")[3] + min: modelData.split(":")[4] + + anchors.left: parent.left + anchors.right: parent.right + } + } + Prop.PropEnum { + label: "Tone Curve" + object: render.mainViewTask.getConfig("ToneMapping") + property: "curve" + enums: [ + "RGB", + "SRGB", + "Reinhard", + "Filmic", + ] + anchors.left: parent.left + anchors.right: parent.right + } + } +} \ No newline at end of file diff --git a/scripts/developer/utilities/render/luci2.js b/scripts/developer/utilities/render/luci2.js new file mode 100644 index 0000000000..4aea49a059 --- /dev/null +++ b/scripts/developer/utilities/render/luci2.js @@ -0,0 +1,13 @@ +function openEngineTaskView() { + // Set up the qml ui + var qml = Script.resolvePath('luci.qml'); + var window = new OverlayWindow({ + title: 'luci qml', + source: qml, + width: 300, + height: 400 + }); + window.setPosition(200, 50); + //window.closed.connect(function() { Script.stop(); }); + } + openEngineTaskView(); \ No newline at end of file From 3d2614498be2de804e86d73527be5e26e88d3b7b Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 4 Mar 2019 18:07:02 -0800 Subject: [PATCH 010/286] ANd fuck --- .../developer/utilities/lib/prop/PropBool.qml | 35 +++++++ .../utilities/lib/prop/PropGroup.qml | 60 ++++++++++++ .../utilities/lib/prop/style/Global.qml | 44 +++++++++ .../utilities/lib/prop/style/PiComboBox.qml | 98 +++++++++++++++++++ .../utilities/lib/prop/style/PiLabel.qml | 25 +++++ .../utilities/lib/prop/style/PiSplitter.qml | 21 ++++ .../utilities/lib/prop/style/PiText.qml | 24 +++++ 7 files changed, 307 insertions(+) create mode 100644 scripts/developer/utilities/lib/prop/PropBool.qml create mode 100644 scripts/developer/utilities/lib/prop/PropGroup.qml create mode 100644 scripts/developer/utilities/lib/prop/style/Global.qml create mode 100644 scripts/developer/utilities/lib/prop/style/PiComboBox.qml create mode 100644 scripts/developer/utilities/lib/prop/style/PiLabel.qml create mode 100644 scripts/developer/utilities/lib/prop/style/PiSplitter.qml create mode 100644 scripts/developer/utilities/lib/prop/style/PiText.qml diff --git a/scripts/developer/utilities/lib/prop/PropBool.qml b/scripts/developer/utilities/lib/prop/PropBool.qml new file mode 100644 index 0000000000..e355398375 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropBool.qml @@ -0,0 +1,35 @@ +// +// PropBool.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import controlsUit 1.0 as HifiControls + +PropItem { + Global { id: global } + id: root + + property alias valueVar : checkboxControl.checked + + Component.onCompleted: { + valueVar = root.valueVarGetter(); + } + + HifiControls.CheckBox { + id: checkboxControl + + anchors.left: root.splitter.right + anchors.verticalCenter: root.verticalCenter + width: root.width * global.valueAreaWidthScale + height: global.slimHeight + + checked: root.valueVar + onCheckedChanged: { root.valueVarSetter(checked); } + } +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml new file mode 100644 index 0000000000..39294743b6 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -0,0 +1,60 @@ +// +// PropGroup.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Item { + Global { id: global } + id: root + + // Prop Group is designed to author an array of ProItems, they are defined with an array of the tuplets describing each individual item: + // [ ..., PropItemInfo, ...] + // PropItemInfo { + // "type": "PropXXXX", "object": object, "property": "propName" + // } + // + property var propItems: [] + + + property var label: "group" + + Column { + id: column + anchors.left: parent.left + anchors.right: parent.right + + PropLabel { + anchors.left: parent.left + anchors.right: parent.right + text: root.label + } + + + Component.onCompleted: { + var component = Qt.createComponent("PropBool.qml"); + component.label = "Test"; + for (var i=0; i Date: Mon, 4 Mar 2019 23:44:14 -0800 Subject: [PATCH 011/286] Fixing the bins and exploring the REpeater --- .../developer/utilities/lib/prop/Global.qml | 44 ---------- .../developer/utilities/lib/prop/PropBool.qml | 1 - .../developer/utilities/lib/prop/PropEnum.qml | 83 ++----------------- .../utilities/lib/prop/PropGroup.qml | 42 ++++++---- .../developer/utilities/lib/prop/PropItem.qml | 3 +- .../utilities/lib/prop/PropLabel.qml | 25 ------ .../utilities/lib/prop/PropSplitter.qml | 21 ----- .../developer/utilities/lib/prop/PropText.qml | 24 ------ scripts/developer/utilities/lib/prop/qmldir | 11 ++- .../utilities/lib/prop/style/Global.qml | 2 + .../utilities/lib/prop/style/PiComboBox.qml | 2 +- scripts/developer/utilities/render/luci.qml | 17 ++++ 12 files changed, 58 insertions(+), 217 deletions(-) delete mode 100644 scripts/developer/utilities/lib/prop/Global.qml delete mode 100644 scripts/developer/utilities/lib/prop/PropLabel.qml delete mode 100644 scripts/developer/utilities/lib/prop/PropSplitter.qml delete mode 100644 scripts/developer/utilities/lib/prop/PropText.qml diff --git a/scripts/developer/utilities/lib/prop/Global.qml b/scripts/developer/utilities/lib/prop/Global.qml deleted file mode 100644 index be189e3c96..0000000000 --- a/scripts/developer/utilities/lib/prop/Global.qml +++ /dev/null @@ -1,44 +0,0 @@ -// -// Prop/Global.qml -// -// Created by Sam Gateau on 3/2/2019 -// Copyright 2019 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.7 - -import stylesUit 1.0 -import controlsUit 1.0 as HifiControls - - -Item { - HifiConstants { id: hifi } - id: root - - property real lineHeight: 32 - property real slimHeight: 24 - - property var color: hifi.colors.baseGray - property var colorBackHighlight: hifi.colors.baseGrayHighlight - property var colorBorderLight: hifi.colors.lightGray - property var colorBorderHighight: hifi.colors.blueHighlight - - property real fontSize: 12 - property var fontFamily: "Raleway" - property var fontWeight: Font.DemiBold - property var fontColor: hifi.colors.faintGray - - property var splitterRightWidthScale: 0.44 - property real splitterWidth: 4 - - property var labelTextAlign: Text.AlignRight - property var labelTextElide: Text.ElideMiddle - - property var valueAreaWidthScale: 0.3 * (1.0 - splitterRightWidthScale) - property var valueTextAlign: Text.AlignHCenter - property real valueBorderWidth: 1 - property real valueBorderRadius: 2 -} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/PropBool.qml b/scripts/developer/utilities/lib/prop/PropBool.qml index e355398375..d8e4bad589 100644 --- a/scripts/developer/utilities/lib/prop/PropBool.qml +++ b/scripts/developer/utilities/lib/prop/PropBool.qml @@ -29,7 +29,6 @@ PropItem { width: root.width * global.valueAreaWidthScale height: global.slimHeight - checked: root.valueVar onCheckedChanged: { root.valueVarSetter(checked); } } } \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/PropEnum.qml b/scripts/developer/utilities/lib/prop/PropEnum.qml index fe6200d971..9446a267b3 100644 --- a/scripts/developer/utilities/lib/prop/PropEnum.qml +++ b/scripts/developer/utilities/lib/prop/PropEnum.qml @@ -18,7 +18,11 @@ PropItem { property alias valueVar : valueCombo.currentIndex property alias enums : valueCombo.model - ComboBox { + Component.onCompleted: { + valueVar = root.valueVarGetter(); + } + + PropComboBox { id: valueCombo flat: true @@ -28,83 +32,6 @@ PropItem { anchors.verticalCenter: root.verticalCenter height: global.slimHeight - currentIndex: root.valueVarGetter() onCurrentIndexChanged: { root.valueVarSetter(currentIndex); } - - delegate: ItemDelegate { - width: valueCombo.width - height: valueCombo.height - contentItem: PropText { - text: modelData - horizontalAlignment: global.valueTextAlign - } - background: Rectangle { - color:highlighted?global.colorBackHighlight:global.color; - } - highlighted: valueCombo.highlightedIndex === index - } - - indicator: Canvas { - id: canvas - x: valueCombo.width - width - valueCombo.rightPadding - y: valueCombo.topPadding + (valueCombo.availableHeight - height) / 2 - width: 12 - height: 8 - contextType: "2d" - - Connections { - target: valueCombo - onPressedChanged: canvas.requestPaint() - } - - onPaint: { - context.reset(); - context.moveTo(0, 0); - context.lineTo(width, 0); - context.lineTo(width / 2, height); - context.closePath(); - context.fillStyle = (valueCombo.pressed) ? global.colorBorderHighight : global.colorBorderLight; - context.fill(); - } - } - - contentItem: PropText { - leftPadding: 0 - rightPadding: valueCombo.indicator.width + valueCombo.spacing - - text: valueCombo.displayText - horizontalAlignment: global.valueTextAlign - } - - background: Rectangle { - implicitWidth: 120 - implicitHeight: 40 - color: global.color - border.color: valueCombo.popup.visible ? global.colorBorderHighight : global.colorBorderLight - border.width: global.valueBorderWidth - radius: global.valueBorderRadius - } - - popup: Popup { - y: valueCombo.height - 1 - width: valueCombo.width - implicitHeight: contentItem.implicitHeight + 2 - padding: 1 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: valueCombo.popup.visible ? valueCombo.delegateModel : null - currentIndex: valueCombo.highlightedIndex - - ScrollIndicator.vertical: ScrollIndicator { } - } - - background: Rectangle { - color: global.color - border.color: global.colorBorderHighight - radius: global.valueBorderRadius - } - } } } diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index 39294743b6..dd579af7eb 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -25,6 +25,19 @@ Item { property var label: "group" + /* Component.onCompleted: { + var component1 = Qt.createComponent("PropBool.qml"); + component1.label = "Test"; + for (var i=0; i Date: Tue, 5 Mar 2019 14:27:32 -0800 Subject: [PATCH 012/286] Dynamic creation of the propItem in the propGRoup! --- .../developer/utilities/lib/prop/PropEnum.qml | 3 +- .../utilities/lib/prop/PropGroup.qml | 62 ++++++++++++------- scripts/developer/utilities/render/luci.qml | 12 +++- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/scripts/developer/utilities/lib/prop/PropEnum.qml b/scripts/developer/utilities/lib/prop/PropEnum.qml index 9446a267b3..ff5dfd8161 100644 --- a/scripts/developer/utilities/lib/prop/PropEnum.qml +++ b/scripts/developer/utilities/lib/prop/PropEnum.qml @@ -19,7 +19,7 @@ PropItem { property alias enums : valueCombo.model Component.onCompleted: { - valueVar = root.valueVarGetter(); + // valueVar = root.valueVarGetter(); } PropComboBox { @@ -32,6 +32,7 @@ PropItem { anchors.verticalCenter: root.verticalCenter height: global.slimHeight + currentIndex: root.valueVarGetter() onCurrentIndexChanged: { root.valueVarSetter(currentIndex); } } } diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index dd579af7eb..1dfb957536 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -17,7 +17,7 @@ Item { // Prop Group is designed to author an array of ProItems, they are defined with an array of the tuplets describing each individual item: // [ ..., PropItemInfo, ...] // PropItemInfo { - // "type": "PropXXXX", "object": object, "property": "propName" + // type: "PropXXXX", object: JSobject, property: "propName" // } // property var propItems: [] @@ -25,18 +25,6 @@ Item { property var label: "group" - /* Component.onCompleted: { - var component1 = Qt.createComponent("PropBool.qml"); - component1.label = "Test"; - for (var i=0; i Date: Tue, 5 Mar 2019 20:22:19 -0800 Subject: [PATCH 013/286] This is working !!!! --- .../utilities/lib/jet/qml/TaskPropView.qml | 40 +++++++++++++++++++ .../developer/utilities/lib/jet/qml/qmldir | 1 + .../utilities/lib/prop/PropGroup.qml | 7 +++- scripts/developer/utilities/render/luci.qml | 11 ++++- 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 scripts/developer/utilities/lib/jet/qml/TaskPropView.qml diff --git a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml new file mode 100644 index 0000000000..350103021a --- /dev/null +++ b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml @@ -0,0 +1,40 @@ +// +// jet/TaskListView.qml +// +// Created by Sam Gateau, 2018/05/09 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 1.4 as Original +import QtQuick.Controls.Styles 1.4 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + +import "../../prop" as Prop + +import "../jet.js" as Jet + +Prop.PropGroup { + + id: root; + + property var rootConfig : Render + property var jobPath: "" + property alias jobName: root.label + + Component.onCompleted: { + var props = Jet.job_propKeys(rootConfig.getConfig(jobPath)); + //console.log(JSON.stringify(props)); + for (var p in props) { + root.propItems.push({"type": "PropBool", "object": rootConfig.getConfig(jobPath), "property":props[p] }) + } + root.updatePropItems(); + } + + +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/jet/qml/qmldir b/scripts/developer/utilities/lib/jet/qml/qmldir index e16820914b..2914c27c23 100644 --- a/scripts/developer/utilities/lib/jet/qml/qmldir +++ b/scripts/developer/utilities/lib/jet/qml/qmldir @@ -1,3 +1,4 @@ TaskList 1.0 TaskList.qml TaskViewList 1.0 TaskViewList.qml TaskTimeFrameView 1.0 TaskTimeFrameView.qml +TaskPropView 1.0 TaskPropView.qml \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index 1dfb957536..6f5607def4 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -42,8 +42,8 @@ Item { } height: column.height - Component.onCompleted: { - for (var i = 0; i < root.propItems.length; i++) { + function updatePropItems() { + for (var i = 0; i < root.propItems.length; i++) { var proItem = root.propItems[i]; switch(proItem.type) { case 'PropBool': { @@ -77,4 +77,7 @@ Item { } } } + Component.onCompleted: { + updatePropItems(); + } } diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml index d009d52f55..d5156c3cf7 100644 --- a/scripts/developer/utilities/render/luci.qml +++ b/scripts/developer/utilities/render/luci.qml @@ -14,6 +14,7 @@ import QtQuick.Layouts 1.3 import controlsUit 1.0 as HifiControls import "../lib/prop" as Prop +import "../lib/jet/qml" as Jet Rectangle { Prop.Global { id: prop;} @@ -62,7 +63,7 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right }*/ - Prop.PropGroup { + /* Prop.PropGroup { label: "My group" propItems: [ {"type": "PropBool", "object": render.mainViewTask.getConfig("LightingModel"), "property": "enableBackground"}, @@ -75,6 +76,14 @@ Rectangle { "Filmic", ]}, ] + anchors.left: parent.left + anchors.right: parent.right + }*/ + + Jet.TaskPropView { + jobPath: "RenderMainView.LightingModel" + label: "Le tone mapping Job" + anchors.left: parent.left anchors.right: parent.right } From f7896b64079b90bfed7a9474f8260307053a7f92 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 6 Mar 2019 22:24:43 -0800 Subject: [PATCH 014/286] Better propGroup --- .../utilities/lib/jet/qml/TaskPropView.qml | 6 +- .../utilities/lib/prop/PropGroup.qml | 126 ++++++++++++------ scripts/developer/utilities/render/luci.qml | 11 +- 3 files changed, 98 insertions(+), 45 deletions(-) diff --git a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml index 350103021a..2188df7cc0 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml @@ -25,13 +25,13 @@ Prop.PropGroup { property var rootConfig : Render property var jobPath: "" - property alias jobName: root.label + property alias label: root.label Component.onCompleted: { var props = Jet.job_propKeys(rootConfig.getConfig(jobPath)); - //console.log(JSON.stringify(props)); + console.log(JSON.stringify(props)); for (var p in props) { - root.propItems.push({"type": "PropBool", "object": rootConfig.getConfig(jobPath), "property":props[p] }) + root.propItems.push({"object": rootConfig.getConfig(jobPath), "property":props[p] }) } root.updatePropItems(); } diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index 6f5607def4..7b901b079d 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -25,56 +25,102 @@ Item { property var label: "group" - - Column { - id: column - anchors.left: parent.left - anchors.right: parent.right + Item { + id: header + height: global.slimHeight + anchors.left: parent.left + anchors.right: parent.right PropLabel { - anchors.left: parent.left - anchors.right: parent.right + id: labelControl + anchors.left: header.left + width: 0.8 * header.width + anchors.verticalCenter: header.verticalCenter text: root.label horizontalAlignment: Text.AlignHCenter } - - + Rectangle { + id: headerRect + color: global.color + border.color: global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + + anchors.left: labelControl.right + anchors.right: header.right + anchors.verticalCenter: header.verticalCenter + height: parent.height + } } - height: column.height + + Column { + id: column + anchors.top: header.bottom + anchors.left: parent.left + anchors.right: parent.right + clip: true + + // Where the propItems are added + } + height: header.height + column.height function updatePropItems() { for (var i = 0; i < root.propItems.length; i++) { var proItem = root.propItems[i]; - switch(proItem.type) { - case 'PropBool': { - var component = Qt.createComponent("PropBool.qml"); - component.createObject(column, { - "label": proItem.property, - "object": proItem.object, - "property": proItem.property - }) - } break; - case 'PropScalar': { - var component = Qt.createComponent("PropScalar.qml"); - component.createObject(column, { - "label": proItem.property, - "object": proItem.object, - "property": proItem.property, - "min": (proItem["min"] !== undefined ? proItem.min : 0.0), - "max": (proItem["max"] !== undefined ? proItem.max : 1.0), - "integer": (proItem["integral"] !== undefined ? proItem.integral : false), - }) - } break; - case 'PropEnum': { - var component = Qt.createComponent("PropEnum.qml"); - component.createObject(column, { - "label": proItem.property, - "object": proItem.object, - "property": proItem.property, - "enums": (proItem["enums"] !== undefined ? proItem.enums : ["Undefined Enums !!!"]), - }) - } break; - } + // valid object + if (proItem['object'] !== undefined && proItem['object'] !== null ) { + // valid property + if (proItem['property'] !== undefined && proItem.object[proItem.property] !== undefined) { + // check type + if (proItem['type'] === undefined) { + proItem['type'] = typeof(proItem.object[proItem.property]) + } + switch(proItem.type) { + case 'boolean': + case 'PropBool': { + var component = Qt.createComponent("PropBool.qml"); + component.createObject(column, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property + }) + } break; + case 'number': + case 'PropScalar': { + var component = Qt.createComponent("PropScalar.qml"); + component.createObject(column, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property, + "min": (proItem["min"] !== undefined ? proItem.min : 0.0), + "max": (proItem["max"] !== undefined ? proItem.max : 1.0), + "integer": (proItem["integral"] !== undefined ? proItem.integral : false), + }) + } break; + case 'PropEnum': { + var component = Qt.createComponent("PropEnum.qml"); + component.createObject(column, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property, + "enums": (proItem["enums"] !== undefined ? proItem.enums : ["Undefined Enums !!!"]), + }) + } break; + case 'object': { + var component = Qt.createComponent("PropItem.qml"); + component.createObject(column, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property, + }) + } break; + } + } else { + console.log('Invalid property: ' + JSON.stringify(proItem)); + } + } else { + console.log('Invalid object: ' + JSON.stringify(proItem)); + } } } Component.onCompleted: { diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml index d5156c3cf7..091a287e02 100644 --- a/scripts/developer/utilities/render/luci.qml +++ b/scripts/developer/utilities/render/luci.qml @@ -81,8 +81,15 @@ Rectangle { }*/ Jet.TaskPropView { - jobPath: "RenderMainView.LightingModel" - label: "Le tone mapping Job" + jobPath: "RenderMainView.ToneMapping" + label: "Le ToneMapping Job" + + anchors.left: parent.left + anchors.right: parent.right + } + Jet.TaskPropView { + jobPath: "RenderMainView.Antialiasing" + label: "Le Antialiasing Job" anchors.left: parent.left anchors.right: parent.right From 5b8116bec4046af2ed36e9b3a1749c0dbbd2a047 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 7 Mar 2019 17:52:11 -0800 Subject: [PATCH 015/286] make the group foldable --- scripts/developer/utilities/lib/prop/PropGroup.qml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index 7b901b079d..0b8d5621f4 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -25,6 +25,8 @@ Item { property var label: "group" + property var isUnfold: false + Item { id: header height: global.slimHeight @@ -39,6 +41,7 @@ Item { text: root.label horizontalAlignment: Text.AlignHCenter } + Rectangle { id: headerRect color: global.color @@ -50,11 +53,20 @@ Item { anchors.right: header.right anchors.verticalCenter: header.verticalCenter height: parent.height + + MouseArea{ + id: mousearea + anchors.fill: parent + onDoubleClicked: { + root.isUnfold = !root.isUnfold + } + } } } Column { id: column + visible: root.isUnfold anchors.top: header.bottom anchors.left: parent.left anchors.right: parent.right @@ -62,7 +74,7 @@ Item { // Where the propItems are added } - height: header.height + column.height + height: header.height + isUnfold * column.height function updatePropItems() { for (var i = 0; i < root.propItems.length; i++) { From a303f803ebe1933805fac15e986dd5d23042e887 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 8 Mar 2019 18:14:38 -0800 Subject: [PATCH 016/286] BReaking group --- scripts/developer/utilities/lib/prop/PropGroup.qml | 4 ++-- scripts/developer/utilities/render/luci.qml | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index 0b8d5621f4..eaba9d299f 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -36,7 +36,7 @@ Item { PropLabel { id: labelControl anchors.left: header.left - width: 0.8 * header.width + width: 0.9 * header.width anchors.verticalCenter: header.verticalCenter text: root.label horizontalAlignment: Text.AlignHCenter @@ -57,7 +57,7 @@ Item { MouseArea{ id: mousearea anchors.fill: parent - onDoubleClicked: { + onClicked: { root.isUnfold = !root.isUnfold } } diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml index 091a287e02..3f6b5b5c01 100644 --- a/scripts/developer/utilities/render/luci.qml +++ b/scripts/developer/utilities/render/luci.qml @@ -16,14 +16,17 @@ import controlsUit 1.0 as HifiControls import "../lib/prop" as Prop import "../lib/jet/qml" as Jet -Rectangle { + Original.ScrollView { Prop.Global { id: prop;} id: render; - anchors.fill: parent - color: prop.color; + anchors.fill: parent + + // color: prop.color; + + property var mainViewTask: Render.getConfig("RenderMainView") - + Column { anchors.left: parent.left anchors.right: parent.right From 530b871eef09bc74257cbaf9772cf7e60dfee1ea Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 11 Mar 2019 00:08:31 -0700 Subject: [PATCH 017/286] Getting the group styled and scrollview working all together --- .../developer/utilities/lib/prop/PropEnum.qml | 2 +- .../utilities/lib/prop/PropGroup.qml | 93 +++++++++------ .../utilities/lib/prop/PropScalar.qml | 1 - scripts/developer/utilities/lib/prop/qmldir | 1 + .../utilities/lib/prop/style/Global.qml | 5 +- .../utilities/lib/prop/style/PiCanvasIcon.qml | 50 ++++++++ .../utilities/lib/prop/style/PiComboBox.qml | 22 +--- scripts/developer/utilities/render/luci.qml | 112 ++++++------------ 8 files changed, 159 insertions(+), 127 deletions(-) create mode 100644 scripts/developer/utilities/lib/prop/style/PiCanvasIcon.qml diff --git a/scripts/developer/utilities/lib/prop/PropEnum.qml b/scripts/developer/utilities/lib/prop/PropEnum.qml index ff5dfd8161..87c2845c90 100644 --- a/scripts/developer/utilities/lib/prop/PropEnum.qml +++ b/scripts/developer/utilities/lib/prop/PropEnum.qml @@ -28,7 +28,7 @@ PropItem { flat: true anchors.left: root.splitter.right - anchors.right: parent.right + anchors.right: root.right anchors.verticalCenter: root.verticalCenter height: global.slimHeight diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index eaba9d299f..4465ec420e 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -25,56 +25,83 @@ Item { property var label: "group" - property var isUnfold: false - + property alias isUnfold: headerRect.icon + Item { id: header height: global.slimHeight anchors.left: parent.left anchors.right: parent.right - + + Item { + id: folder + anchors.left: header.left + width: headerRect.width * 2 + anchors.verticalCenter: header.verticalCenter + height: parent.height + + PropCanvasIcon { + id: headerRect + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + MouseArea{ + id: mousearea + anchors.fill: parent + onClicked: { + root.isUnfold = !root.isUnfold + } + } + } + } + PropLabel { id: labelControl - anchors.left: header.left - width: 0.9 * header.width + anchors.left: folder.right + anchors.right: header.right anchors.verticalCenter: header.verticalCenter text: root.label horizontalAlignment: Text.AlignHCenter } - Rectangle { - id: headerRect - color: global.color - border.color: global.colorBorderLight - border.width: global.valueBorderWidth - radius: global.valueBorderRadius - - anchors.left: labelControl.right - anchors.right: header.right - anchors.verticalCenter: header.verticalCenter - height: parent.height + /* Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + anchors.bottom: parent.bottom + color: global.colorBorderHighight + + visible: root.isUnfold + }*/ + } - MouseArea{ - id: mousearea - anchors.fill: parent - onClicked: { - root.isUnfold = !root.isUnfold - } - } + Rectangle { + visible: root.isUnfold + + color: "transparent" + border.color: global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: root.bottom + + Column { + id: column + // anchors.top: header.bottom + anchors.left: parent.left + anchors.right: parent.right + clip: true + + // Where the propItems are added } } - - Column { - id: column - visible: root.isUnfold - anchors.top: header.bottom - anchors.left: parent.left - anchors.right: parent.right - clip: true - // Where the propItems are added - } height: header.height + isUnfold * column.height + anchors.leftMargin: global.horizontalMargin + anchors.rightMargin: global.horizontalMargin function updatePropItems() { for (var i = 0; i < root.propItems.length; i++) { diff --git a/scripts/developer/utilities/lib/prop/PropScalar.qml b/scripts/developer/utilities/lib/prop/PropScalar.qml index 29b42e2801..684dd4fed4 100644 --- a/scripts/developer/utilities/lib/prop/PropScalar.qml +++ b/scripts/developer/utilities/lib/prop/PropScalar.qml @@ -61,7 +61,6 @@ PropItem { stepSize: root.integral ? 1.0 : 0.0 anchors.left: valueLabel.right anchors.right: root.right - anchors.rightMargin: 0 anchors.verticalCenter: root.verticalCenter onValueChanged: { root.valueVarSetter(value) } diff --git a/scripts/developer/utilities/lib/prop/qmldir b/scripts/developer/utilities/lib/prop/qmldir index 3e52395dab..2cd06d58ac 100644 --- a/scripts/developer/utilities/lib/prop/qmldir +++ b/scripts/developer/utilities/lib/prop/qmldir @@ -4,6 +4,7 @@ PropText 1.0 style/PiText.qml PropLabel 1.0 style/PiLabel.qml PropSplitter 1.0 style/PiSplitter.qml PropComboBox 1.0 style/PiComboBox.qml +PropCanvasIcon 1.0 style/PiCanvasIcon.qml PropItem 1.0 PropItem.qml PropScalar 1.0 PropScalar.qml diff --git a/scripts/developer/utilities/lib/prop/style/Global.qml b/scripts/developer/utilities/lib/prop/style/Global.qml index e772477611..6066a4f99b 100644 --- a/scripts/developer/utilities/lib/prop/style/Global.qml +++ b/scripts/developer/utilities/lib/prop/style/Global.qml @@ -21,7 +21,7 @@ Item { property real lineHeight: 32 property real slimHeight: 24 - property real horizontalMargin: 2 + property real horizontalMargin: 4 property var color: hifi.colors.baseGray property var colorBackHighlight: hifi.colors.baseGrayHighlight @@ -35,6 +35,9 @@ Item { property var splitterRightWidthScale: 0.45 property real splitterWidth: 8 + + property real iconWidth: fontSize + property real iconHeight: fontSize property var labelTextAlign: Text.AlignRight property var labelTextElide: Text.ElideMiddle diff --git a/scripts/developer/utilities/lib/prop/style/PiCanvasIcon.qml b/scripts/developer/utilities/lib/prop/style/PiCanvasIcon.qml new file mode 100644 index 0000000000..6a805ea4c6 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiCanvasIcon.qml @@ -0,0 +1,50 @@ +// +// Prop/style/PiFoldCanvas.qml +// +// Created by Sam Gateau on 3/9/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + + import QtQuick 2.7 +import QtQuick.Controls 2.2 + +Canvas { + Global { id: global } + + width: global.iconWidth + height: global.iconHeight + + property var icon: 0 + onIconChanged: function () { requestPaint() } + property var fillColor: global.colorBorderHighight + + contextType: "2d" + onPaint: { + context.reset(); + switch (icon) { + case 0: + case 1: + case 2: + default: { + if ((icon % 3) == 0) { + context.moveTo(width * 0.25, 0); + context.lineTo(width * 0.25, height); + context.lineTo(width, height / 2); + } else if ((icon % 3) == 1) { + context.moveTo(0, height * 0.25); + context.lineTo(width, height * 0.25); + context.lineTo(width / 2, height); + } else { + context.moveTo(0, height * 0.75); + context.lineTo(width, height* 0.75); + context.lineTo(width / 2, 0); + } + context.closePath(); + context.fillStyle = fillColor; + context.fill(); + }} + } +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/style/PiComboBox.qml b/scripts/developer/utilities/lib/prop/style/PiComboBox.qml index 9d002ed2b9..d9e029b702 100644 --- a/scripts/developer/utilities/lib/prop/style/PiComboBox.qml +++ b/scripts/developer/utilities/lib/prop/style/PiComboBox.qml @@ -33,28 +33,16 @@ ComboBox { highlighted: valueCombo.highlightedIndex === index } - indicator: Canvas { + indicator: PiCanvasIcon { id: canvas x: valueCombo.width - width - valueCombo.rightPadding y: valueCombo.topPadding + (valueCombo.availableHeight - height) / 2 - width: 12 - height: 8 - contextType: "2d" - Connections { + icon: 1 + /*Connections { target: valueCombo - onPressedChanged: canvas.requestPaint() - } - - onPaint: { - context.reset(); - context.moveTo(0, 0); - context.lineTo(width, 0); - context.lineTo(width / 2, height); - context.closePath(); - context.fillStyle = (valueCombo.pressed) ? global.colorBorderHighight : global.colorBorderLight; - context.fill(); - } + onPressedChanged: { canvas.icon = control.down + 1 } + }*/ } contentItem: PiText { diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml index 3f6b5b5c01..89794b037e 100644 --- a/scripts/developer/utilities/render/luci.qml +++ b/scripts/developer/utilities/render/luci.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import controlsUit 1.0 as HifiControls @@ -16,86 +16,50 @@ import controlsUit 1.0 as HifiControls import "../lib/prop" as Prop import "../lib/jet/qml" as Jet - Original.ScrollView { - Prop.Global { id: prop;} - id: render; +Rectangle { anchors.fill: parent - - // color: prop.color; - - - + id: render; property var mainViewTask: Render.getConfig("RenderMainView") - Column { - anchors.left: parent.left - anchors.right: parent.right - /* Repeater { - model: [ "Tone mapping exposure:ToneMapping:exposure:5.0:-5.0", - "Tone:ToneMapping:exposure:5.0:-5.0" + Prop.Global { id: global;} + color: global.color + + ScrollView { + id: control + anchors.fill: parent + clip: true + + Column { + width: render.width + + Prop.PropEnum { + label: "Tone Curve" + object: render.mainViewTask.getConfig("ToneMapping") + property: "curve" + enums: [ + "RGB", + "SRGB", + "Reinhard", + "Filmic", ] - Prop.PropScalar { - label: qsTr(modelData.split(":")[0]) - integral: false - object: render.mainViewTask.getConfig(modelData.split(":")[1]) - property: modelData.split(":")[2] - max: modelData.split(":")[3] - min: modelData.split(":")[4] + anchors.left: parent.left + anchors.right: parent.right + } + + Jet.TaskPropView { + jobPath: "RenderMainView.ToneMapping" + label: "Le ToneMapping Job" - anchors.left: parent.left - anchors.right: parent.right + anchors.left: parent.left + anchors.right: parent.right } - } - Prop.PropEnum { - label: "Tone Curve" - object: render.mainViewTask.getConfig("ToneMapping") - property: "curve" - enums: [ - "RGB", - "SRGB", - "Reinhard", - "Filmic", - ] - anchors.left: parent.left - anchors.right: parent.right - } - Prop.PropBool { - label: "Background" - object: render.mainViewTask.getConfig("LightingModel") - property: "enableBackground" - anchors.left: parent.left - anchors.right: parent.right - }*/ - /* Prop.PropGroup { - label: "My group" - propItems: [ - {"type": "PropBool", "object": render.mainViewTask.getConfig("LightingModel"), "property": "enableBackground"}, - {"type": "PropScalar", "object": render.mainViewTask.getConfig("ToneMapping"), "property": "exposure"}, - {"type": "PropBool", "object": render.mainViewTask.getConfig("LightingModel"), "property": "enableEmissive"}, - {"type": "PropEnum", "object": render.mainViewTask.getConfig("ToneMapping"), "property": "curve", enums: [ - "RGB", - "SRGB", - "Reinhard", - "Filmic", - ]}, - ] - anchors.left: parent.left - anchors.right: parent.right - }*/ + Jet.TaskPropView { + jobPath: "RenderMainView.Antialiasing" + label: "Le Antialiasing Job" - Jet.TaskPropView { - jobPath: "RenderMainView.ToneMapping" - label: "Le ToneMapping Job" - - anchors.left: parent.left - anchors.right: parent.right - } - Jet.TaskPropView { - jobPath: "RenderMainView.Antialiasing" - label: "Le Antialiasing Job" - - anchors.left: parent.left - anchors.right: parent.right + anchors.left: parent.left + anchors.right: parent.right + } } } } \ No newline at end of file From f2fc9c010219dd97f62e6517bf48f9b60c05f019 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Tue, 12 Mar 2019 16:40:28 -0700 Subject: [PATCH 018/286] Multiple task view --- scripts/developer/utilities/lib/jet/jet.js | 10 +++- .../utilities/lib/jet/qml/TaskPropView.qml | 32 ++++++++++-- .../utilities/lib/prop/PropGroup.qml | 51 +++++++++++-------- .../developer/utilities/lib/prop/PropItem.qml | 2 +- scripts/developer/utilities/render/luci.qml | 18 +++++-- 5 files changed, 83 insertions(+), 30 deletions(-) diff --git a/scripts/developer/utilities/lib/jet/jet.js b/scripts/developer/utilities/lib/jet/jet.js index 40563e4b2c..52c13c5279 100644 --- a/scripts/developer/utilities/lib/jet/jet.js +++ b/scripts/developer/utilities/lib/jet/jet.js @@ -10,7 +10,12 @@ // "use strict"; - // traverse task tree + // traverse task tree recursively + // + // @param root: the root job config from where to traverse + // @param functor: the functor function() which is applied on every subjobs of root traversed + // if return true, then 'task_tree' is called recursively on that subjob + // @param depth: the depth of the recurse loop since the initial call. function task_traverse(root, functor, depth) { if (root.isTask()) { depth++; @@ -22,6 +27,9 @@ function task_traverse(root, functor, depth) { } } } + +// same function as 'task_traverse' with the depth being 0 +// and visisting the root job first. function task_traverseTree(root, functor) { if (functor(root, 0, 0)) { task_traverse(root, functor, 0) diff --git a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml index 2188df7cc0..af4cbb1c9a 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml @@ -1,5 +1,5 @@ // -// jet/TaskListView.qml +// jet/TaskPropView.qml // // Created by Sam Gateau, 2018/05/09 // Copyright 2018 High Fidelity, Inc. @@ -27,13 +27,37 @@ Prop.PropGroup { property var jobPath: "" property alias label: root.label - Component.onCompleted: { + function populatePropItems() { + var propsModel = [] var props = Jet.job_propKeys(rootConfig.getConfig(jobPath)); console.log(JSON.stringify(props)); for (var p in props) { - root.propItems.push({"object": rootConfig.getConfig(jobPath), "property":props[p] }) + propsModel.push({"object": rootConfig.getConfig(jobPath), "property":props[p] }) } - root.updatePropItems(); + root.updatePropItems(propsModel); + + + Jet.task_traverse(rootConfig.getConfig(jobPath), + function(job, depth, index) { + var component = Qt.createComponent("./TaskPropView.qml"); + component.createObject(root.propItemsPanel, { + "label": job.objectName, + "rootConfig": root.rootConfig, + "jobPath": root.jobPath + '.' + job.objectName + }) + /* var component = Qt.createComponent("../../prop/PropItem.qml"); + component.createObject(root.propItemsPanel, { + "label": root.jobPath + '.' + job.objectName + ' num=' + index, + })*/ + // propsModel.push({"type": "printLabel", "label": root.jobPath + '.' + job.objectName + ' num=' + index }) + + return (depth < 1); + }, 0) + + } + + Component.onCompleted: { + populatePropItems() } diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index 4465ec420e..544a687f70 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -13,20 +13,12 @@ import QtQuick 2.7 Item { Global { id: global } id: root - - // Prop Group is designed to author an array of ProItems, they are defined with an array of the tuplets describing each individual item: - // [ ..., PropItemInfo, ...] - // PropItemInfo { - // type: "PropXXXX", object: JSobject, property: "propName" - // } - // - property var propItems: [] - property var label: "group" property alias isUnfold: headerRect.icon - + property alias propItemsPanel: propItemsContainer + Item { id: header height: global.slimHeight @@ -89,7 +81,7 @@ Item { anchors.bottom: root.bottom Column { - id: column + id: propItemsContainer // anchors.top: header.bottom anchors.left: parent.left anchors.right: parent.right @@ -99,13 +91,22 @@ Item { } } - height: header.height + isUnfold * column.height + height: header.height + isUnfold * propItemsContainer.height anchors.leftMargin: global.horizontalMargin anchors.rightMargin: global.horizontalMargin + anchors.left: parent.left + anchors.right: parent.right - function updatePropItems() { - for (var i = 0; i < root.propItems.length; i++) { - var proItem = root.propItems[i]; + + // Prop Group is designed to author an array of ProItems, they are defined with an array of the tuplets describing each individual item: + // [ ..., PropItemInfo, ...] + // PropItemInfo { + // type: "PropXXXX", object: JSobject, property: "propName" + // } + // + function updatePropItems(propItemsModel) { + for (var i = 0; i < propItemsModel.length; i++) { + var proItem = propItemsModel[i]; // valid object if (proItem['object'] !== undefined && proItem['object'] !== null ) { // valid property @@ -118,7 +119,7 @@ Item { case 'boolean': case 'PropBool': { var component = Qt.createComponent("PropBool.qml"); - component.createObject(column, { + component.createObject(propItemsContainer, { "label": proItem.property, "object": proItem.object, "property": proItem.property @@ -127,7 +128,7 @@ Item { case 'number': case 'PropScalar': { var component = Qt.createComponent("PropScalar.qml"); - component.createObject(column, { + component.createObject(propItemsContainer, { "label": proItem.property, "object": proItem.object, "property": proItem.property, @@ -138,7 +139,7 @@ Item { } break; case 'PropEnum': { var component = Qt.createComponent("PropEnum.qml"); - component.createObject(column, { + component.createObject(propItemsContainer, { "label": proItem.property, "object": proItem.object, "property": proItem.property, @@ -147,22 +148,32 @@ Item { } break; case 'object': { var component = Qt.createComponent("PropItem.qml"); - component.createObject(column, { + component.createObject(propItemsContainer, { "label": proItem.property, "object": proItem.object, "property": proItem.property, }) } break; + case 'printLabel': { + var component = Qt.createComponent("PropItem.qml"); + component.createObject(propItemsContainer, { + "label": proItem.property + }) + } break; } } else { console.log('Invalid property: ' + JSON.stringify(proItem)); } + } else if (proItem['type'] === 'printLabel') { + var component = Qt.createComponent("PropItem.qml"); + component.createObject(propItemsContainer, { + "label": proItem.label + }) } else { console.log('Invalid object: ' + JSON.stringify(proItem)); } } } Component.onCompleted: { - updatePropItems(); } } diff --git a/scripts/developer/utilities/lib/prop/PropItem.qml b/scripts/developer/utilities/lib/prop/PropItem.qml index 00314512f2..339ff10422 100644 --- a/scripts/developer/utilities/lib/prop/PropItem.qml +++ b/scripts/developer/utilities/lib/prop/PropItem.qml @@ -16,7 +16,7 @@ Item { id: root // Prop item is designed to author an object[property]: - property var object: NULL + property var object: {} property string property: "" // value is accessed through the "valueVarSetter" and "valueVarGetter" diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml index 89794b037e..85ca69e998 100644 --- a/scripts/developer/utilities/render/luci.qml +++ b/scripts/developer/utilities/render/luci.qml @@ -32,7 +32,7 @@ Rectangle { Column { width: render.width - Prop.PropEnum { + /* Prop.PropEnum { label: "Tone Curve" object: render.mainViewTask.getConfig("ToneMapping") property: "curve" @@ -44,9 +44,16 @@ Rectangle { ] anchors.left: parent.left anchors.right: parent.right - } - + } */ Jet.TaskPropView { + id: "the" + jobPath: "RenderMainView.RenderDeferredTask" + label: "Le Render Deferred Job" + + anchors.left: parent.left + anchors.right: parent.right + } + /* Jet.TaskPropView { jobPath: "RenderMainView.ToneMapping" label: "Le ToneMapping Job" @@ -59,7 +66,10 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right - } + }*/ } } + + Component.onCompleted: { + } } \ No newline at end of file From 36cecef1667f0897f802eaf2fdd45ac0c4399e01 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 26 Mar 2019 13:31:55 -0700 Subject: [PATCH 019/286] Support explicit timestamps in tracing, conditional event output based on duration --- libraries/shared/src/Profile.cpp | 27 +++++++++++++++++++++++++-- libraries/shared/src/Profile.h | 21 ++++++++++++++++++--- libraries/shared/src/Trace.cpp | 16 ++++++++++++++-- libraries/shared/src/Trace.h | 17 +++++++++++++++++ 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/libraries/shared/src/Profile.cpp b/libraries/shared/src/Profile.cpp index 272538e26d..018636ad5a 100644 --- a/libraries/shared/src/Profile.cpp +++ b/libraries/shared/src/Profile.cpp @@ -7,6 +7,7 @@ // #include "Profile.h" +#include Q_LOGGING_CATEGORY(trace_app, "trace.app") Q_LOGGING_CATEGORY(trace_app_detail, "trace.app.detail") @@ -41,14 +42,22 @@ static bool tracingEnabled() { return DependencyManager::isSet() && DependencyManager::get()->isEnabled(); } -Duration::Duration(const QLoggingCategory& category, const QString& name, uint32_t argbColor, uint64_t payload, const QVariantMap& baseArgs) : _name(name), _category(category) { +DurationBase::DurationBase(const QLoggingCategory& category, const QString& name) : _name(name), _category(category) { +} + +Duration::Duration(const QLoggingCategory& category, + const QString& name, + uint32_t argbColor, + uint64_t payload, + const QVariantMap& baseArgs) : + DurationBase(category, name) { if (tracingEnabled() && category.isDebugEnabled()) { QVariantMap args = baseArgs; args["nv_payload"] = QVariant::fromValue(payload); tracing::traceEvent(_category, _name, tracing::DurationBegin, "", args); #if defined(NSIGHT_TRACING) - nvtxEventAttributes_t eventAttrib { 0 }; + nvtxEventAttributes_t eventAttrib{ 0 }; eventAttrib.version = NVTX_VERSION; eventAttrib.size = NVTX_EVENT_ATTRIB_STRUCT_SIZE; eventAttrib.colorType = NVTX_COLOR_ARGB; @@ -98,3 +107,17 @@ void Duration::endRange(const QLoggingCategory& category, uint64_t rangeId) { #endif } +ConditionalDuration::ConditionalDuration(const QLoggingCategory& category, const QString& name, uint32_t minTime) : + DurationBase(category, name), _startTime(tracing::Tracer::now()), _minTime(minTime * USECS_PER_MSEC) { +} + +ConditionalDuration::~ConditionalDuration() { + if (tracingEnabled() && _category.isDebugEnabled()) { + auto endTime = tracing::Tracer::now(); + auto duration = endTime - _startTime; + if (duration >= _minTime) { + tracing::traceEvent(_category, _startTime, _name, tracing::DurationBegin); + tracing::traceEvent(_category, endTime, _name, tracing::DurationEnd); + } + } +} diff --git a/libraries/shared/src/Profile.h b/libraries/shared/src/Profile.h index dc2ed6e754..e7084b4f79 100644 --- a/libraries/shared/src/Profile.h +++ b/libraries/shared/src/Profile.h @@ -37,17 +37,31 @@ Q_DECLARE_LOGGING_CATEGORY(trace_startup) Q_DECLARE_LOGGING_CATEGORY(trace_workload) Q_DECLARE_LOGGING_CATEGORY(trace_baker) -class Duration { +class DurationBase { + +protected: + DurationBase(const QLoggingCategory& category, const QString& name); + const QString _name; + const QLoggingCategory& _category; +}; + +class Duration : public DurationBase { public: Duration(const QLoggingCategory& category, const QString& name, uint32_t argbColor = 0xff0000ff, uint64_t payload = 0, const QVariantMap& args = QVariantMap()); ~Duration(); static uint64_t beginRange(const QLoggingCategory& category, const char* name, uint32_t argbColor); static void endRange(const QLoggingCategory& category, uint64_t rangeId); +}; + +class ConditionalDuration : public DurationBase { +public: + ConditionalDuration(const QLoggingCategory& category, const QString& name, uint32_t minTime); + ~ConditionalDuration(); private: - QString _name; - const QLoggingCategory& _category; + const int64_t _startTime; + const int64_t _minTime; }; @@ -95,6 +109,7 @@ inline void metadata(const QString& metadataType, const QVariantMap& args) { } #define PROFILE_RANGE(category, name) Duration profileRangeThis(trace_##category(), name); +#define PROFILE_RANGE_IF_LONGER(category, name, ms) ConditionalDuration profileRangeThis(trace_##category(), name, ms); #define PROFILE_RANGE_EX(category, name, argbColor, payload, ...) Duration profileRangeThis(trace_##category(), name, argbColor, (uint64_t)payload, ##__VA_ARGS__); #define PROFILE_RANGE_BEGIN(category, rangeId, name, argbColor) rangeId = Duration::beginRange(trace_##category(), name, argbColor) #define PROFILE_RANGE_END(category, rangeId) Duration::endRange(trace_##category(), rangeId) diff --git a/libraries/shared/src/Trace.cpp b/libraries/shared/src/Trace.cpp index 3f6a2dd643..e9e77b55ae 100644 --- a/libraries/shared/src/Trace.cpp +++ b/libraries/shared/src/Trace.cpp @@ -176,6 +176,10 @@ void Tracer::serialize(const QString& filename) { #endif } +int64_t Tracer::now() { + return std::chrono::duration_cast(p_high_resolution_clock::now().time_since_epoch()).count(); +} + void Tracer::traceEvent(const QLoggingCategory& category, const QString& name, EventType type, qint64 timestamp, qint64 processID, qint64 threadID, @@ -226,9 +230,17 @@ void Tracer::traceEvent(const QLoggingCategory& category, return; } - auto timestamp = std::chrono::duration_cast(p_high_resolution_clock::now().time_since_epoch()).count(); + traceEvent(category, name, type, now(), id, args, extra); +} + +void Tracer::traceEvent(const QLoggingCategory& category, + const QString& name, EventType type, int64_t timestamp, const QString& id, + const QVariantMap& args, const QVariantMap& extra) { + if (!_enabled && type != Metadata) { + return; + } + auto processID = QCoreApplication::applicationPid(); auto threadID = int64_t(QThread::currentThreadId()); - traceEvent(category, name, type, timestamp, processID, threadID, id, args, extra); } diff --git a/libraries/shared/src/Trace.h b/libraries/shared/src/Trace.h index 1e1326968f..917530d6ca 100644 --- a/libraries/shared/src/Trace.h +++ b/libraries/shared/src/Trace.h @@ -78,11 +78,18 @@ struct TraceEvent { class Tracer : public Dependency { public: + static int64_t now(); void traceEvent(const QLoggingCategory& category, const QString& name, EventType type, const QString& id = "", const QVariantMap& args = QVariantMap(), const QVariantMap& extra = QVariantMap()); + void traceEvent(const QLoggingCategory& category, + const QString& name, EventType type, + int64_t timestamp, + const QString& id = "", + const QVariantMap& args = QVariantMap(), const QVariantMap& extra = QVariantMap()); + void startTracing(); void stopTracing(); void serialize(const QString& file); @@ -101,6 +108,16 @@ private: std::mutex _eventsMutex; }; +inline void traceEvent(const QLoggingCategory& category, int64_t timestamp, const QString& name, EventType type, const QString& id = "", const QVariantMap& args = {}, const QVariantMap& extra = {}) { + if (!DependencyManager::isSet()) { + return; + } + const auto& tracer = DependencyManager::get(); + if (tracer) { + tracer->traceEvent(category, name, type, timestamp, id, args, extra); + } +} + inline void traceEvent(const QLoggingCategory& category, const QString& name, EventType type, const QString& id = "", const QVariantMap& args = {}, const QVariantMap& extra = {}) { if (!DependencyManager::isSet()) { return; From 413091fed3d0c72f773087fc43d2c90fb0c1c3f0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 26 Mar 2019 13:32:29 -0700 Subject: [PATCH 020/286] Trace application notification handler events if longer than 2 ms --- interface/src/Application.cpp | 9 +++++++++ interface/src/Application.h | 1 + 2 files changed, 10 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 635932ea1c..afe109daf3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3961,6 +3961,15 @@ static void dumpEventQueue(QThread* thread) { } #endif // DEBUG_EVENT_QUEUE +bool Application::notify(QObject * object, QEvent * event) { + if (thread() == QThread::currentThread()) { + PROFILE_RANGE_IF_LONGER(app, "notify", 2) + return QApplication::notify(object, event); + } + + return QApplication::notify(object, event); +} + bool Application::event(QEvent* event) { if (_aboutToQuit) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 762ac9585a..545c223837 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -156,6 +156,7 @@ public: void updateCamera(RenderArgs& renderArgs, float deltaTime); void resizeGL(); + bool notify(QObject *, QEvent *) override; bool event(QEvent* event) override; bool eventFilter(QObject* object, QEvent* event) override; From 0c78c8fd879f9f03b0ede0429202d0e9f387d57f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 26 Mar 2019 13:38:52 -0700 Subject: [PATCH 021/286] Disable per-frame context switch on present thread --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 5bc84acc6a..2a29325581 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -109,7 +109,6 @@ public: Q_ASSERT(_context); _context->makeCurrent(); CHECK_GL_ERROR(); - _context->doneCurrent(); while (!_shutdown) { if (_pendingOtherThreadOperation) { PROFILE_RANGE(render, "MainThreadOp") @@ -129,6 +128,7 @@ public: Lock lock(_mutex); _condition.wait(lock, [&] { return _finishedOtherThreadOperation; }); } + _context->makeCurrent(); } // Check for a new display plugin @@ -140,18 +140,16 @@ public: if (newPlugin != currentPlugin) { // Deactivate the old plugin if (currentPlugin != nullptr) { - _context->makeCurrent(); currentPlugin->uncustomizeContext(); CHECK_GL_ERROR(); - _context->doneCurrent(); + // Force completion of all pending GL commands + glFinish(); } if (newPlugin) { bool hasVsync = true; QThread::setPriority(newPlugin->getPresentPriority()); bool wantVsync = newPlugin->wantVsync(); - _context->makeCurrent(); - CHECK_GL_ERROR(); #if defined(Q_OS_MAC) newPlugin->swapBuffers(); #endif @@ -163,7 +161,8 @@ public: newPlugin->setVsyncEnabled(hasVsync); newPlugin->customizeContext(); CHECK_GL_ERROR(); - _context->doneCurrent(); + // Force completion of all pending GL commands + glFinish(); } currentPlugin = newPlugin; _newPluginQueue.pop(); @@ -180,7 +179,6 @@ public: } // Execute the frame and present it to the display device. - _context->makeCurrent(); { PROFILE_RANGE(render, "PluginPresent") gl::globalLock(); @@ -188,9 +186,9 @@ public: gl::globalRelease(false); CHECK_GL_ERROR(); } - _context->doneCurrent(); } + _context->doneCurrent(); Lock lock(_mutex); _context->moveToThread(qApp->thread()); _shutdown = false; From 3d2b71bc7b015779187d743d8780a07a8e1c7de0 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 26 Mar 2019 17:16:13 -0700 Subject: [PATCH 022/286] remove all existing material parsing --- libraries/baking/src/FBXBaker.cpp | 111 +----- libraries/baking/src/FBXBaker.h | 8 +- libraries/baking/src/MaterialBaker.cpp | 8 + libraries/baking/src/MaterialBaker.h | 1 + libraries/baking/src/ModelBaker.cpp | 344 +------------------ libraries/baking/src/ModelBaker.h | 31 +- libraries/baking/src/OBJBaker.cpp | 72 ---- libraries/baking/src/OBJBaker.h | 2 - libraries/baking/src/baking/BakerLibrary.cpp | 12 +- libraries/baking/src/baking/BakerLibrary.h | 4 +- libraries/baking/src/baking/FSTBaker.cpp | 7 +- libraries/baking/src/baking/FSTBaker.h | 3 +- tools/oven/src/BakerCLI.cpp | 5 +- tools/oven/src/DomainBaker.cpp | 5 +- tools/oven/src/ui/ModelBakeWidget.cpp | 6 +- 15 files changed, 46 insertions(+), 573 deletions(-) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 2189e7bdc3..b7eb56c921 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -33,9 +33,8 @@ #include "ModelBakingLoggingCategory.h" #include "TextureBaker.h" -FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : - ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { +FBXBaker::FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputModelURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { if (hasBeenBaked) { // Look for the original model file one directory higher. Perhaps this is an oven output directory. QUrl originalRelativePath = QUrl("../original/" + inputModelURL.fileName().replace(BAKED_FBX_EXTENSION, FBX_EXTENSION)); @@ -45,15 +44,6 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText } void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { - _hfmModel = hfmModel; - - if (shouldStop()) { - return; - } - - // enumerate the models and textures found in the scene and start a bake for them - rewriteAndBakeSceneTextures(); - if (shouldStop()) { return; } @@ -114,15 +104,15 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const int meshIndex = 0; for (FBXNode& rootChild : _rootNode.children) { if (rootChild.name == "Objects") { - for (FBXNode& object : rootChild.children) { - if (object.name == "Geometry") { - if (object.properties.at(2) == "Mesh") { + for (auto object = rootChild.children.begin(); object != rootChild.children.end(); object++) { + if (object->name == "Geometry") { + if (object->properties.at(2) == "Mesh") { int meshNum = meshIndexToRuntimeOrder[meshIndex]; - replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); meshIndex++; } - } else if (object.name == "Model") { - for (FBXNode& modelChild : object.children) { + } else if (object->name == "Model") { + for (FBXNode& modelChild : object->children) { if (modelChild.name == "Properties60" || modelChild.name == "Properties70") { // This is a properties node // Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer @@ -142,10 +132,13 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const } else if (modelChild.name == "Vertices") { // This model is also a mesh int meshNum = meshIndexToRuntimeOrder[meshIndex]; - replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); meshIndex++; } } + } else if (object->name == "Texture" || object->name == "Video") { + // this is an embedded texture, we need to remove it from the FBX + object = rootChild.children.erase(object); } if (hasErrors()) { @@ -154,82 +147,4 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const } } } -} - -void FBXBaker::rewriteAndBakeSceneTextures() { - using namespace image::TextureUsage; - QHash textureTypes; - - // enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID - for (const auto& material : _hfmModel->materials) { - if (material.normalTexture.isBumpmap) { - textureTypes[material.normalTexture.id] = BUMP_TEXTURE; - } else { - textureTypes[material.normalTexture.id] = NORMAL_TEXTURE; - } - - textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE; - textureTypes[material.glossTexture.id] = GLOSS_TEXTURE; - textureTypes[material.roughnessTexture.id] = ROUGHNESS_TEXTURE; - textureTypes[material.specularTexture.id] = SPECULAR_TEXTURE; - textureTypes[material.metallicTexture.id] = METALLIC_TEXTURE; - textureTypes[material.emissiveTexture.id] = EMISSIVE_TEXTURE; - textureTypes[material.occlusionTexture.id] = OCCLUSION_TEXTURE; - textureTypes[material.lightmapTexture.id] = LIGHTMAP_TEXTURE; - } - - // enumerate the children of the root node - for (FBXNode& rootChild : _rootNode.children) { - - if (rootChild.name == "Objects") { - - // enumerate the objects - auto object = rootChild.children.begin(); - while (object != rootChild.children.end()) { - if (object->name == "Texture") { - - // double check that we didn't get an abort while baking the last texture - if (shouldStop()) { - return; - } - - // enumerate the texture children - for (FBXNode& textureChild : object->children) { - - if (textureChild.name == "RelativeFilename") { - QString hfmTextureFileName { textureChild.properties.at(0).toString() }; - - // grab the ID for this texture so we can figure out the - // texture type from the loaded materials - auto textureID { object->properties[0].toString() }; - auto textureType = textureTypes[textureID]; - - // Compress the texture information and return the new filename to be added into the FBX scene - auto bakedTextureFile = compressTexture(hfmTextureFileName, textureType); - - // If no errors or warnings have occurred during texture compression add the filename to the FBX scene - if (!bakedTextureFile.isNull()) { - textureChild.properties[0] = bakedTextureFile; - } else { - // if bake fails - return, if there were errors and continue, if there were warnings. - if (hasErrors()) { - return; - } else if (hasWarnings()) { - continue; - } - } - } - } - - ++object; - - } else if (object->name == "Video") { - // this is an embedded texture, we need to remove it from the FBX - object = rootChild.children.erase(object); - } else { - ++object; - } - } - } - } -} +} \ No newline at end of file diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 59ef5e349d..5daf8a8adf 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -31,20 +31,14 @@ using TextureBakerThreadGetter = std::function; class FBXBaker : public ModelBaker { Q_OBJECT public: - FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); protected: virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: void rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists); - void rewriteAndBakeSceneTextures(); void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); - - hfm::Model::Pointer _hfmModel; - - bool _pendingErrorEmission { false }; }; #endif // hifi_FBXBaker_h diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index dd1ba55e54..c35c16fc8a 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -64,6 +64,14 @@ void MaterialBaker::bake() { } } +void MaterialBaker::abort() { + Baker::abort(); + + for (auto& textureBaker : _textureBakers) { + textureBaker->abort(); + } +} + void MaterialBaker::loadMaterial() { if (!_isURL) { qCDebug(material_baking) << "Loading local material" << _materialData; diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index 41ce31380e..d89f16e57e 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -34,6 +34,7 @@ public: public slots: virtual void bake() override; + virtual void abort() override; signals: void originalMaterialLoaded(); diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 9568a81578..56673398a7 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -42,12 +42,10 @@ #include "baking/BakerLibrary.h" -ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : +ModelBaker::ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : _modelURL(inputModelURL), _bakedOutputDir(bakedOutputDirectory), _originalOutputDir(originalOutputDirectory), - _textureThreadGetter(inputTextureThreadGetter), _hasBeenBaked(hasBeenBaked) { auto bakedFilename = _modelURL.fileName(); @@ -250,28 +248,6 @@ void ModelBaker::bakeSourceCopy() { dracoMaterialLists = baker.getDracoMaterialLists(); } - // Populate _textureContentMap with path to content mappings, for quick lookup by URL - for (auto materialIt = bakedModel->materials.cbegin(); materialIt != bakedModel->materials.cend(); materialIt++) { - static const auto addTexture = [](QHash& textureContentMap, const hfm::Texture& texture) { - if (!textureContentMap.contains(texture.filename)) { - // Content may be empty, unless the data is inlined - textureContentMap[texture.filename] = texture.content; - } - }; - const hfm::Material& material = *materialIt; - addTexture(_textureContentMap, material.normalTexture); - addTexture(_textureContentMap, material.albedoTexture); - addTexture(_textureContentMap, material.opacityTexture); - addTexture(_textureContentMap, material.glossTexture); - addTexture(_textureContentMap, material.roughnessTexture); - addTexture(_textureContentMap, material.specularTexture); - addTexture(_textureContentMap, material.metallicTexture); - addTexture(_textureContentMap, material.emissiveTexture); - addTexture(_textureContentMap, material.occlusionTexture); - addTexture(_textureContentMap, material.scatteringTexture); - addTexture(_textureContentMap, material.lightmapTexture); - } - // Do format-specific baking bakeProcessedSource(bakedModel, dracoMeshes, dracoMaterialLists); @@ -291,8 +267,6 @@ void ModelBaker::bakeSourceCopy() { auto outputMapping = _mapping; outputMapping[FST_VERSION_FIELD] = FST_VERSION; outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); - // All textures will be found in the same directory as the model - outputMapping[TEXDIR_FIELD] = "."; hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); QFile fstOutputFile { outputFSTURL }; @@ -307,18 +281,15 @@ void ModelBaker::bakeSourceCopy() { _outputFiles.push_back(outputFSTURL); _outputMappingURL = outputFSTURL; - // check if we're already done with textures (in case we had none to re-write) - checkIfTexturesFinished(); + exportScene(); + qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL; + emit finished(); } void ModelBaker::abort() { Baker::abort(); - // tell our underlying TextureBaker instances to abort - // the ModelBaker will wait until all are aborted before emitting its own abort signal - for (auto& textureBaker : _bakingTextures) { - textureBaker->abort(); - } + _materialBaker->abort(); } bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { @@ -354,247 +325,6 @@ bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dr return true; } -QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) { - - QFileInfo modelTextureFileInfo { modelTextureFileName.replace("\\", "/") }; - - if (modelTextureFileInfo.suffix().toLower() == BAKED_TEXTURE_KTX_EXT.mid(1)) { - // re-baking a model that already references baked textures - // this is an error - return from here - handleError("Cannot re-bake a file that already references compressed textures"); - return QString::null; - } - - if (!image::getSupportedFormats().contains(modelTextureFileInfo.suffix())) { - // this is a texture format we don't bake, skip it - handleWarning(modelTextureFileName + " is not a bakeable texture format"); - return QString::null; - } - - // make sure this texture points to something and isn't one we've already re-mapped - QString textureChild { QString::null }; - if (!modelTextureFileInfo.filePath().isEmpty()) { - // check if this was an embedded texture that we already have in-memory content for - QByteArray textureContent; - - // figure out the URL to this texture, embedded or external - if (!modelTextureFileInfo.filePath().isEmpty()) { - textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit()); - } - auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull()); - - TextureKey textureKey { urlToTexture, textureType }; - auto bakingTextureIt = _bakingTextures.find(textureKey); - if (bakingTextureIt == _bakingTextures.cend()) { - // construct the new baked texture file name and file path - // ensuring that the baked texture will have a unique name - // even if there was another texture with the same name at a different path - QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); - - QString bakedTextureFilePath { - _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX - }; - - textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - - _outputFiles.push_back(bakedTextureFilePath); - - // bake this texture asynchronously - bakeTexture(textureKey, _bakedOutputDir, baseTextureFileName, textureContent); - } else { - // Fetch existing texture meta name - textureChild = (*bakingTextureIt)->getBaseFilename() + BAKED_META_TEXTURE_SUFFIX; - } - } - - qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName - << "to" << textureChild; - - return textureChild; -} - -void ModelBaker::bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { - // start a bake for this texture and add it to our list to keep track of - QSharedPointer bakingTexture{ - new TextureBaker(textureKey.first, textureKey.second, outputDir, "../", bakedFilename, textureContent), - &TextureBaker::deleteLater - }; - - // make sure we hear when the baking texture is done or aborted - connect(bakingTexture.data(), &Baker::finished, this, &ModelBaker::handleBakedTexture); - connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture); - - // keep a shared pointer to the baking texture - _bakingTextures.insert(textureKey, bakingTexture); - - // start baking the texture on one of our available worker threads - bakingTexture->moveToThread(_textureThreadGetter()); - QMetaObject::invokeMethod(bakingTexture.data(), "bake"); -} - -void ModelBaker::handleBakedTexture() { - TextureBaker* bakedTexture = qobject_cast(sender()); - qDebug() << "Handling baked texture" << bakedTexture->getTextureURL(); - - // make sure we haven't already run into errors, and that this is a valid texture - if (bakedTexture) { - if (!shouldStop()) { - if (!bakedTexture->hasErrors()) { - if (!_originalOutputDir.isEmpty()) { - // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture - - // use the path to the texture being baked to determine if this was an embedded or a linked texture - - // it is embeddded if the texure being baked was inside a folder with the name of the model - // since that is the fake URL we provide when baking external textures - - if (!_modelURL.isParentOf(bakedTexture->getTextureURL())) { - // for linked textures we want to save a copy of original texture beside the original model - - qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); - - // check if we have a relative path to use for the texture - auto relativeTexturePath = texturePathRelativeToModel(_modelURL, bakedTexture->getTextureURL()); - - QFile originalTextureFile{ - _originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName() - }; - - if (relativeTexturePath.length() > 0) { - // make the folders needed by the relative path - } - - if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { - qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() - << "for" << _modelURL; - } else { - handleError("Could not save original external texture " + originalTextureFile.fileName() - + " for " + _modelURL.toString()); - return; - } - } - } - - - // now that this texture has been baked and handled, we can remove that TextureBaker from our hash - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - - checkIfTexturesFinished(); - } else { - // there was an error baking this texture - add it to our list of errors - _errorList.append(bakedTexture->getErrors()); - - // we don't emit finished yet so that the other textures can finish baking first - _pendingErrorEmission = true; - - // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - - // abort any other ongoing texture bakes since we know we'll end up failing - for (auto& bakingTexture : _bakingTextures) { - bakingTexture->abort(); - } - - checkIfTexturesFinished(); - } - } else { - // we have errors to attend to, so we don't do extra processing for this texture - // but we do need to remove that TextureBaker from our list - // and then check if we're done with all textures - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - - checkIfTexturesFinished(); - } - } -} - -void ModelBaker::handleAbortedTexture() { - // grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore - TextureBaker* bakedTexture = qobject_cast(sender()); - - qDebug() << "Texture aborted: " << bakedTexture->getTextureURL(); - - if (bakedTexture) { - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - } - - // since a texture we were baking aborted, our status is also aborted - _shouldAbort.store(true); - - // abort any other ongoing texture bakes since we know we'll end up failing - for (auto& bakingTexture : _bakingTextures) { - bakingTexture->abort(); - } - - checkIfTexturesFinished(); -} - -QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded) { - QUrl urlToTexture; - - if (isEmbedded) { - urlToTexture = _modelURL.toString() + "/" + textureFileInfo.filePath(); - } else { - if (textureFileInfo.exists() && textureFileInfo.isFile()) { - // set the texture URL to the local texture that we have confirmed exists - urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); - } else { - // external texture that we'll need to download or find - - // this is a relative file path which will require different handling - // depending on the location of the original model - if (_modelURL.isLocalFile() && textureFileInfo.exists() && textureFileInfo.isFile()) { - // the absolute path we ran into for the texture in the model exists on this machine - // so use that file - urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); - } else { - // we didn't find the texture on this machine at the absolute path - // so assume that it is right beside the model to match the behaviour of interface - urlToTexture = _modelURL.resolved(textureFileInfo.fileName()); - } - } - } - - return urlToTexture; -} - -QString ModelBaker::texturePathRelativeToModel(QUrl modelURL, QUrl textureURL) { - auto modelPath = modelURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - - if (texturePath.startsWith(modelPath)) { - // texture path is a child of the model path, return the texture path without the model path - return texturePath.mid(modelPath.length()); - } else { - // the texture path was not a child of the model path, return the empty string - return ""; - } -} - -void ModelBaker::checkIfTexturesFinished() { - // check if we're done everything we need to do for this model - // and emit our finished signal if we're done - - if (_bakingTextures.isEmpty()) { - if (shouldStop()) { - // if we're checking for completion but we have errors - // that means one or more of our texture baking operations failed - - if (_pendingErrorEmission) { - setIsFinished(true); - } - - return; - } else { - qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL; - - texturesFinished(); - - setIsFinished(true); - } - } -} - void ModelBaker::setWasAborted(bool wasAborted) { if (wasAborted != _wasAborted.load()) { Baker::setWasAborted(wasAborted); @@ -605,70 +335,6 @@ void ModelBaker::setWasAborted(bool wasAborted) { } } -void ModelBaker::texturesFinished() { - embedTextureMetaData(); - exportScene(); -} - -void ModelBaker::embedTextureMetaData() { - std::vector embeddedTextureNodes; - - for (FBXNode& rootChild : _rootNode.children) { - if (rootChild.name == "Objects") { - qlonglong maxId = 0; - for (auto &child : rootChild.children) { - if (child.properties.length() == 3) { - maxId = std::max(maxId, child.properties[0].toLongLong()); - } - } - - for (auto& object : rootChild.children) { - if (object.name == "Texture") { - QVariant relativeFilename; - for (auto& child : object.children) { - if (child.name == "RelativeFilename") { - relativeFilename = child.properties[0]; - break; - } - } - - if (relativeFilename.isNull() - || !relativeFilename.toString().endsWith(BAKED_META_TEXTURE_SUFFIX)) { - continue; - } - if (object.properties.length() < 2) { - qWarning() << "Found texture with unexpected number of properties: " << object.name; - continue; - } - - FBXNode videoNode; - videoNode.name = "Video"; - videoNode.properties.append(++maxId); - videoNode.properties.append(object.properties[1]); - videoNode.properties.append("Clip"); - - QString bakedTextureFilePath { - _bakedOutputDir + "/" + relativeFilename.toString() - }; - - QFile textureFile { bakedTextureFilePath }; - if (!textureFile.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open: " << bakedTextureFilePath; - continue; - } - - videoNode.children.append({ "RelativeFilename", { relativeFilename }, { } }); - videoNode.children.append({ "Content", { textureFile.readAll() }, { } }); - - rootChild.children.append(videoNode); - - textureFile.close(); - } - } - } - } -} - void ModelBaker::exportScene() { auto fbxData = FBXWriter::encodeFBX(_rootNode); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index d9a559392f..599ca2dbab 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -18,17 +18,13 @@ #include #include "Baker.h" -#include "TextureBaker.h" -#include "baking/TextureFileNamer.h" +#include "MaterialBaker.h" #include "ModelBakingLoggingCategory.h" -#include - #include #include -using TextureBakerThreadGetter = std::function; using GetMaterialIDCallback = std::function ; static const QString FST_EXTENSION { ".fst" }; @@ -42,10 +38,7 @@ class ModelBaker : public Baker { Q_OBJECT public: - using TextureKey = QPair; - - ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); void setOutputURLSuffix(const QUrl& urlSuffix); void setMappingURL(const QUrl& mappingURL); @@ -54,7 +47,6 @@ public: void initializeOutputDirs(); bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); - QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; QUrl getModelURL() const { return _modelURL; } @@ -71,20 +63,15 @@ public slots: protected: void saveSourceModel(); virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) = 0; - void checkIfTexturesFinished(); - void texturesFinished(); - void embedTextureMetaData(); void exportScene(); FBXNode _rootNode; - QHash _textureContentMap; QUrl _modelURL; QUrl _outputURLSuffix; QUrl _mappingURL; hifi::VariantHash _mapping; QString _bakedOutputDir; QString _originalOutputDir; - TextureBakerThreadGetter _textureThreadGetter; QString _originalOutputModelPath; QString _outputMappingURL; QUrl _bakedModelURL; @@ -93,22 +80,10 @@ protected slots: void handleModelNetworkReply(); virtual void bakeSourceCopy(); -private slots: - void handleBakedTexture(); - void handleAbortedTexture(); - private: - QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false); - void bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent); - QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - - QMultiHash> _bakingTextures; - QHash _textureNameMatchCount; - bool _pendingErrorEmission { false }; - bool _hasBeenBaked { false }; - TextureFileNamer _textureFileNamer; + QSharedPointer _materialBaker; }; #endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 70bdeb2071..a2d0ab1094 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -132,55 +132,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h handleWarning("Baked mesh for OBJ model '" + _modelURL.toString() + "' is empty"); } - // Generating Texture Node - // iterate through mesh parts and process the associated textures - auto size = meshParts.size(); - for (int i = 0; i < size; i++) { - QString material = meshParts[i].materialID; - HFMMaterial currentMaterial = hfmModel->materials[material]; - if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { - auto textureID = nextNodeID(); - _mapTextureMaterial.emplace_back(textureID, i); - - FBXNode textureNode; - { - textureNode.name = TEXTURE_NODE_NAME; - textureNode.properties = { textureID, "texture" + QString::number(textureID) }; - } - - // Texture node child - TextureName node - FBXNode textureNameNode; - { - textureNameNode.name = TEXTURENAME_NODE_NAME; - QByteArray propertyString = (!currentMaterial.albedoTexture.filename.isEmpty()) ? "Kd" : "Ka"; - textureNameNode.properties = { propertyString }; - } - - // Texture node child - Relative Filename node - FBXNode relativeFilenameNode; - { - relativeFilenameNode.name = RELATIVEFILENAME_NODE_NAME; - } - - QByteArray textureFileName = (!currentMaterial.albedoTexture.filename.isEmpty()) ? currentMaterial.albedoTexture.filename : currentMaterial.specularTexture.filename; - - auto textureType = (!currentMaterial.albedoTexture.filename.isEmpty()) ? image::TextureUsage::Type::ALBEDO_TEXTURE : image::TextureUsage::Type::SPECULAR_TEXTURE; - - // Compress the texture using ModelBaker::compressTexture() and store compressed file's name in the node - auto textureFile = compressTexture(textureFileName, textureType); - if (textureFile.isNull()) { - // Baking failed return - handleError("Failed to compress texture: " + textureFileName); - return; - } - relativeFilenameNode.properties = { textureFile }; - - textureNode.children = { textureNameNode, relativeFilenameNode }; - - objectNode.children.append(textureNode); - } - } - // Generating Connections node connectionsNode.name = CONNECTIONS_NODE_NAME; @@ -199,29 +150,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h cNode.properties = { CONNECTIONS_NODE_PROPERTY, materialID, modelID }; connectionsNode.children.append(cNode); } - - // Connect textures to materials - for (const auto& texMat : _mapTextureMaterial) { - FBXNode cAmbientNode; - cAmbientNode.name = C_NODE_NAME; - cAmbientNode.properties = { - CONNECTIONS_NODE_PROPERTY_1, - texMat.first, - _materialIDs[texMat.second], - "AmbientFactor" - }; - connectionsNode.children.append(cAmbientNode); - - FBXNode cDiffuseNode; - cDiffuseNode.name = C_NODE_NAME; - cDiffuseNode.properties = { - CONNECTIONS_NODE_PROPERTY_1, - texMat.first, - _materialIDs[texMat.second], - "DiffuseColor" - }; - connectionsNode.children.append(cDiffuseNode); - } } // Set properties for material nodes diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index d1eced5452..9bd1431d28 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -35,9 +35,7 @@ private: void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } - NodeID _nodeID { 0 }; std::vector _materialIDs; - std::vector> _mapTextureMaterial; }; #endif // hifi_OBJBaker_h diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index 2afeef4800..356eb4cdb9 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -45,7 +45,7 @@ bool isModelBaked(const QUrl& bakeableModelURL) { return beforeModelExtension.endsWith(".baked"); } -std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) { +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath) { auto filename = bakeableModelURL.fileName(); // Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique @@ -59,20 +59,20 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked"; QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; - return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + return getModelBakerWithOutputDirectories(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory); } -std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) { +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) { auto filename = bakeableModelURL.fileName(); std::unique_ptr baker; if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); + baker = std::make_unique(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); + baker = std::make_unique(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + baker = std::make_unique(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory); //} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); } else { diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index a646c8d36a..8f82661b25 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -23,9 +23,9 @@ bool isModelBaked(const QUrl& bakeableModelURL); // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created -std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath); // Similar to getModelBaker, but gives control over where the output folders will be -std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory); +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory); #endif // hifi_BakerLibrary_h diff --git a/libraries/baking/src/baking/FSTBaker.cpp b/libraries/baking/src/baking/FSTBaker.cpp index acf3bfe1c7..176c35c059 100644 --- a/libraries/baking/src/baking/FSTBaker.cpp +++ b/libraries/baking/src/baking/FSTBaker.cpp @@ -18,9 +18,8 @@ #include -FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : - ModelBaker(inputMappingURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { +FSTBaker::FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputMappingURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { if (hasBeenBaked) { // Look for the original model file one directory higher. Perhaps this is an oven output directory. QUrl originalRelativePath = QUrl("../original/" + inputMappingURL.fileName().replace(BAKED_FST_EXTENSION, FST_EXTENSION)); @@ -70,7 +69,7 @@ void FSTBaker::bakeSourceCopy() { return; } - auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir); + auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _bakedOutputDir, _originalOutputDir); _modelBaker = std::unique_ptr(dynamic_cast(baker.release())); if (!_modelBaker) { handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); diff --git a/libraries/baking/src/baking/FSTBaker.h b/libraries/baking/src/baking/FSTBaker.h index 85c7c93a37..32997680f5 100644 --- a/libraries/baking/src/baking/FSTBaker.h +++ b/libraries/baking/src/baking/FSTBaker.h @@ -18,8 +18,7 @@ class FSTBaker : public ModelBaker { Q_OBJECT public: - FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual QUrl getFullOutputMappingURL() const override; diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index 2946db650c..ff8acf060e 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -49,10 +49,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& if (type == MODEL_EXTENSION || type == FBX_EXTENSION) { QUrl bakeableModelURL = getBakeableModelURL(inputUrl); if (!bakeableModelURL.isEmpty()) { - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - _baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputPath); + _baker = getModelBaker(bakeableModelURL, outputPath); if (_baker) { _baker->moveToThread(Oven::instance().getNextWorkerThread()); } diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 05745aad24..80ad410053 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -152,10 +152,7 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // setup a ModelBaker for this URL, as long as we don't already have one bool haveBaker = _modelBakers.contains(bakeableModelURL); if (!haveBaker) { - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &Baker::deleteLater); + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, _contentOutputPath).release(), &Baker::deleteLater); if (baker) { // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL // Note: The ModelBaker currently doesn't store this in the FST because the equal signs mess up FST parsing. diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 79ab733b0c..7561a8e009 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -180,11 +180,7 @@ void ModelBakeWidget::bakeButtonClicked() { QUrl bakeableModelURL = getBakeableModelURL(modelToBakeURL); if (!bakeableModelURL.isEmpty()) { - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - - std::unique_ptr baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputDirectory.path()); + std::unique_ptr baker = getModelBaker(bakeableModelURL, outputDirectory.path()); if (baker) { // everything seems to be in place, kick off a bake for this model now From 02d9331603037f38044757029613325f641a8ccf Mon Sep 17 00:00:00 2001 From: Angus Antley Date: Tue, 26 Mar 2019 14:57:07 -0700 Subject: [PATCH 023/286] This changes the avatar-animation json so that we have override animations for the right and left hand, similar to how we have whole body override animations. --- .../resources/avatar/avatar-animation.json | 3009 ++++++++++------- interface/src/avatar/MyAvatar.cpp | 17 + interface/src/avatar/MyAvatar.h | 2 + libraries/animation/src/Rig.cpp | 82 + libraries/animation/src/Rig.h | 26 + 5 files changed, 2000 insertions(+), 1136 deletions(-) diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 27e45daa7b..0f2e64f295 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -197,260 +197,136 @@ "id": "rightHandStateMachine", "type": "stateMachine", "data": { - "currentState": "rightHandGrasp", + "currentState": "rightHandAnimNone", "states": [ { - "id": "rightHandGrasp", - "interpTarget": 3, + "id": "rightHandAnimNone", + "interpTarget": 1, "interpDuration": 3, "transitions": [ - { "var": "isRightIndexPoint", "state": "rightIndexPoint" }, - { "var": "isRightThumbRaise", "state": "rightThumbRaise" }, - { "var": "isRightIndexPointAndThumbRaise", "state": "rightIndexPointAndThumbRaise" } + { "var": "rightHandAnimA", "state": "rightHandAnimA" }, + { "var": "rightHandAnimB", "state": "rightHandAnimB" } ] }, { - "id": "rightIndexPoint", - "interpTarget": 15, + "id": "rightHandAnimA", + "interpTarget": 1, "interpDuration": 3, "transitions": [ - { "var": "isRightHandGrasp", "state": "rightHandGrasp" }, - { "var": "isRightThumbRaise", "state": "rightThumbRaise" }, - { "var": "isRightIndexPointAndThumbRaise", "state": "rightIndexPointAndThumbRaise" } + { "var": "rightHandAnimNone", "state": "rightHandAnimNone" }, + { "var": "rightHandAnimB", "state": "rightHandAnimB" } ] }, { - "id": "rightThumbRaise", - "interpTarget": 15, + "id": "rightHandAnimB", + "interpTarget": 1, "interpDuration": 3, "transitions": [ - { "var": "isRightHandGrasp", "state": "rightHandGrasp" }, - { "var": "isRightIndexPoint", "state": "rightIndexPoint" }, - { "var": "isRightIndexPointAndThumbRaise", "state": "rightIndexPointAndThumbRaise" } - ] - }, - { - "id": "rightIndexPointAndThumbRaise", - "interpTarget": 15, - "interpDuration": 3, - "transitions": [ - { "var": "isRightHandGrasp", "state": "rightHandGrasp" }, - { "var": "isRightIndexPoint", "state": "rightIndexPoint" }, - { "var": "isRightThumbRaise", "state": "rightThumbRaise" } + { "var": "rightHandAnimNone", "state": "rightHandAnimNone" }, + { "var": "rightHandAnimA", "state": "rightHandAnimA" } ] } ] }, "children": [ { - "id": "rightHandGrasp", - "type": "blendLinear", - "data": { - "alpha": 0.0, - "alphaVar": "rightHandGraspAlpha" - }, - "children": [ - { - "id": "rightHandGraspOpen", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/hydra_pose_open_right.fbx", - "startFrame": 0.0, - "endFrame": 0.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "rightHandGraspClosed", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/hydra_pose_closed_right.fbx", - "startFrame": 0.0, - "endFrame": 0.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - } - ] - }, - { - "id": "rightIndexPoint", - "type": "blendLinear", - "data": { - "alpha": 0.0, - "alphaVar": "rightHandGraspAlpha" - }, - "children": [ - { - "id": "rightIndexPointOpen", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/touch_point_open_right.fbx", - "startFrame": 15.0, - "endFrame": 15.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "rightIndexPointClosed", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/touch_point_closed_right.fbx", - "startFrame": 15.0, - "endFrame": 15.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - } - ] - }, - { - "id": "rightThumbRaise", - "type": "blendLinear", - "data": { - "alpha": 0.0, - "alphaVar": "rightHandGraspAlpha" - }, - "children": [ - { - "id": "rightThumbRaiseOpen", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/touch_thumb_open_right.fbx", - "startFrame": 15.0, - "endFrame": 15.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "rightThumbRaiseClosed", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/touch_thumb_closed_right.fbx", - "startFrame": 15.0, - "endFrame": 15.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - } - ] - }, - { - "id": "rightIndexPointAndThumbRaise", - "type": "blendLinear", - "data": { - "alpha": 0.0, - "alphaVar": "rightHandGraspAlpha" - }, - "children": [ - { - "id": "rightIndexPointAndThumbRaiseOpen", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/touch_thumb_point_open_right.fbx", - "startFrame": 15.0, - "endFrame": 15.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "rightIndexPointAndThumbRaiseClosed", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/touch_thumb_point_closed_right.fbx", - "startFrame": 15.0, - "endFrame": 15.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - } - ] - } - ] - }, - { - "id": "leftHandOverlay", - "type": "overlay", - "data": { - "alpha": 0.0, - "boneSet": "leftHand", - "alphaVar": "leftHandOverlayAlpha" - }, - "children": [ - { - "id": "leftHandStateMachine", + "id": "rightHandAnimNone", "type": "stateMachine", "data": { - "currentState": "leftHandGrasp", + "currentState": "rightHandGrasp", "states": [ { - "id": "leftHandGrasp", + "id": "rightHandGrasp", "interpTarget": 3, "interpDuration": 3, "transitions": [ - { "var": "isLeftIndexPoint", "state": "leftIndexPoint" }, - { "var": "isLeftThumbRaise", "state": "leftThumbRaise" }, - { "var": "isLeftIndexPointAndThumbRaise", "state": "leftIndexPointAndThumbRaise" } + { + "var": "isRightIndexPoint", + "state": "rightIndexPoint" + }, + { + "var": "isRightThumbRaise", + "state": "rightThumbRaise" + }, + { + "var": "isRightIndexPointAndThumbRaise", + "state": "rightIndexPointAndThumbRaise" + } ] }, { - "id": "leftIndexPoint", + "id": "rightIndexPoint", "interpTarget": 15, "interpDuration": 3, "transitions": [ - { "var": "isLeftHandGrasp", "state": "leftHandGrasp" }, - { "var": "isLeftThumbRaise", "state": "leftThumbRaise" }, - { "var": "isLeftIndexPointAndThumbRaise", "state": "leftIndexPointAndThumbRaise" } + { + "var": "isRightHandGrasp", + "state": "rightHandGrasp" + }, + { + "var": "isRightThumbRaise", + "state": "rightThumbRaise" + }, + { + "var": "isRightIndexPointAndThumbRaise", + "state": "rightIndexPointAndThumbRaise" + } ] }, { - "id": "leftThumbRaise", + "id": "rightThumbRaise", "interpTarget": 15, "interpDuration": 3, "transitions": [ - { "var": "isLeftHandGrasp", "state": "leftHandGrasp" }, - { "var": "isLeftIndexPoint", "state": "leftIndexPoint" }, - { "var": "isLeftIndexPointAndThumbRaise", "state": "leftIndexPointAndThumbRaise" } + { + "var": "isRightHandGrasp", + "state": "rightHandGrasp" + }, + { + "var": "isRightIndexPoint", + "state": "rightIndexPoint" + }, + { + "var": "isRightIndexPointAndThumbRaise", + "state": "rightIndexPointAndThumbRaise" + } ] }, { - "id": "leftIndexPointAndThumbRaise", + "id": "rightIndexPointAndThumbRaise", "interpTarget": 15, "interpDuration": 3, "transitions": [ - { "var": "isLeftHandGrasp", "state": "leftHandGrasp" }, - { "var": "isLeftIndexPoint", "state": "leftIndexPoint" }, - { "var": "isLeftThumbRaise", "state": "leftThumbRaise" } + { + "var": "isRightHandGrasp", + "state": "rightHandGrasp" + }, + { + "var": "isRightIndexPoint", + "state": "rightIndexPoint" + }, + { + "var": "isRightThumbRaise", + "state": "rightThumbRaise" + } ] } ] }, "children": [ { - "id": "leftHandGrasp", + "id": "rightHandGrasp", "type": "blendLinear", "data": { "alpha": 0.0, - "alphaVar": "leftHandGraspAlpha" + "alphaVar": "rightHandGraspAlpha" }, "children": [ { - "id": "leftHandGraspOpen", + "id": "rightHandGraspOpen", "type": "clip", "data": { - "url": "qrc:///avatar/animations/hydra_pose_open_left.fbx", + "url": "qrc:///avatar/animations/hydra_pose_open_right.fbx", "startFrame": 0.0, "endFrame": 0.0, "timeScale": 1.0, @@ -459,12 +335,12 @@ "children": [] }, { - "id": "leftHandGraspClosed", + "id": "rightHandGraspClosed", "type": "clip", "data": { - "url": "qrc:///avatar/animations/hydra_pose_closed_left.fbx", - "startFrame": 10.0, - "endFrame": 10.0, + "url": "qrc:///avatar/animations/hydra_pose_closed_right.fbx", + "startFrame": 0.0, + "endFrame": 0.0, "timeScale": 1.0, "loopFlag": true }, @@ -473,18 +349,18 @@ ] }, { - "id": "leftIndexPoint", + "id": "rightIndexPoint", "type": "blendLinear", - "data": { + "data": { "alpha": 0.0, - "alphaVar": "leftHandGraspAlpha" + "alphaVar": "rightHandGraspAlpha" }, "children": [ { - "id": "leftIndexPointOpen", + "id": "rightIndexPointOpen", "type": "clip", "data": { - "url": "qrc:///avatar/animations/touch_point_open_left.fbx", + "url": "qrc:///avatar/animations/touch_point_open_right.fbx", "startFrame": 15.0, "endFrame": 15.0, "timeScale": 1.0, @@ -493,10 +369,10 @@ "children": [] }, { - "id": "leftIndexPointClosed", + "id": "rightIndexPointClosed", "type": "clip", "data": { - "url": "qrc:///avatar/animations/touch_point_closed_left.fbx", + "url": "qrc:///avatar/animations/touch_point_closed_right.fbx", "startFrame": 15.0, "endFrame": 15.0, "timeScale": 1.0, @@ -507,18 +383,18 @@ ] }, { - "id": "leftThumbRaise", + "id": "rightThumbRaise", "type": "blendLinear", - "data": { + "data": { "alpha": 0.0, - "alphaVar": "leftHandGraspAlpha" + "alphaVar": "rightHandGraspAlpha" }, "children": [ { - "id": "leftThumbRaiseOpen", + "id": "rightThumbRaiseOpen", "type": "clip", "data": { - "url": "qrc:///avatar/animations/touch_thumb_open_left.fbx", + "url": "qrc:///avatar/animations/touch_thumb_open_right.fbx", "startFrame": 15.0, "endFrame": 15.0, "timeScale": 1.0, @@ -527,10 +403,10 @@ "children": [] }, { - "id": "leftThumbRaiseClosed", + "id": "rightThumbRaiseClosed", "type": "clip", "data": { - "url": "qrc:///avatar/animations/touch_thumb_closed_left.fbx", + "url": "qrc:///avatar/animations/touch_thumb_closed_right.fbx", "startFrame": 15.0, "endFrame": 15.0, "timeScale": 1.0, @@ -541,18 +417,18 @@ ] }, { - "id": "leftIndexPointAndThumbRaise", + "id": "rightIndexPointAndThumbRaise", "type": "blendLinear", - "data": { + "data": { "alpha": 0.0, - "alphaVar": "leftHandGraspAlpha" + "alphaVar": "rightHandGraspAlpha" }, "children": [ { - "id": "leftIndexPointAndThumbRaiseOpen", + "id": "rightIndexPointAndThumbRaiseOpen", "type": "clip", "data": { - "url": "qrc:///avatar/animations/touch_thumb_point_open_left.fbx", + "url": "qrc:///avatar/animations/touch_thumb_point_open_right.fbx", "startFrame": 15.0, "endFrame": 15.0, "timeScale": 1.0, @@ -561,10 +437,10 @@ "children": [] }, { - "id": "leftIndexPointAndThumbRaiseClosed", + "id": "rightIndexPointAndThumbRaiseClosed", "type": "clip", "data": { - "url": "qrc:///avatar/animations/touch_thumb_point_closed_left.fbx", + "url": "qrc:///avatar/animations/touch_thumb_point_closed_right.fbx", "startFrame": 15.0, "endFrame": 15.0, "timeScale": 1.0, @@ -577,985 +453,1846 @@ ] }, { - "id": "mainStateMachine", - "type": "stateMachine", + "id": "rightHandAnimA", + "type": "clip", "data": { - "outputJoints": ["LeftFoot", "RightFoot"], - "currentState": "idle", - "states": [ - { - "id": "idle", - "interpTarget": 20, - "interpDuration": 8, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "idleToWalkFwd", - "interpTarget": 12, - "interpDuration": 8, - "transitions": [ - { "var": "idleToWalkFwdOnDone", "state": "WALKFWD" }, - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "idleSettle", - "interpTarget": 15, - "interpDuration": 8, - "interpType": "snapshotPrev", - "transitions": [ - {"var": "idleSettleOnDone", "state": "idle" }, - {"var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" } - ] - }, - { - "id": "WALKFWD", - "interpTarget": 35, - "interpDuration": 10, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotMoving", "state": "idleSettle" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "WALKBWD", - "interpTarget": 35, - "interpDuration": 10, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotMoving", "state": "idleSettle" }, - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "STRAFERIGHT", - "interpTarget": 25, - "interpDuration": 8, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotMoving", "state": "idleSettle" }, - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "STRAFELEFT", - "interpTarget": 25, - "interpDuration": 8, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotMoving", "state": "idleSettle" }, - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "turnRight", - "interpTarget": 6, - "interpDuration": 8, - "transitions": [ - { "var": "isNotTurning", "state": "idle" }, - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "turnLeft", - "interpTarget": 6, - "interpDuration": 8, - "transitions": [ - { "var": "isNotTurning", "state": "idle" }, - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "strafeRightHmd", - "interpTarget": 5, - "interpDuration": 8, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotMoving", "state": "idleSettle" }, - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" } - ] - }, - { - "id": "strafeLeftHmd", - "interpTarget": 5, - "interpDuration": 8, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotMoving", "state": "idleSettle" }, - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" } - ] - }, - { - "id": "fly", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotFlying", "state": "idleSettle" } - ] - }, - { - "id": "takeoffStand", - "interpTarget": 2, - "interpDuration": 2, - "transitions": [ - { "var": "isNotTakeoff", "state": "inAirStand" } - ] - }, - { - "id": "TAKEOFFRUN", - "interpTarget": 2, - "interpDuration": 2, - "transitions": [ - { "var": "isNotTakeoff", "state": "INAIRRUN" } - ] - }, - { - "id": "inAirStand", - "interpTarget": 3, - "interpDuration": 3, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotInAir", "state": "landStandImpact" } - ] - }, - { - "id": "INAIRRUN", - "interpTarget": 3, - "interpDuration": 3, - "interpType": "snapshotPrev", - "transitions": [ - { "var": "isNotInAir", "state": "WALKFWD" } - ] - }, - { - "id": "landStandImpact", - "interpTarget": 1, - "interpDuration": 1, - "transitions": [ - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "landStandImpactOnDone", "state": "landStand" } - ] - }, - { - "id": "landStand", - "interpTarget": 1, - "interpDuration": 1, - "transitions": [ - { "var": "isMovingForward", "state": "WALKFWD" }, - { "var": "isMovingBackward", "state": "WALKBWD" }, - { "var": "isMovingRight", "state": "STRAFERIGHT" }, - { "var": "isMovingLeft", "state": "STRAFELEFT" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" }, - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "isInAirStand", "state": "inAirStand" }, - { "var": "isInAirRun", "state": "INAIRRUN" }, - { "var": "landStandOnDone", "state": "idle" }, - { "var": "isMovingRightHmd", "state": "strafeRightHmd" }, - { "var": "isMovingLeftHmd", "state": "strafeLeftHmd" } - ] - }, - { - "id": "LANDRUN", - "interpTarget": 2, - "interpDuration": 2, - "transitions": [ - { "var": "isFlying", "state": "fly" }, - { "var": "isTakeoffStand", "state": "takeoffStand" }, - { "var": "isTakeoffRun", "state": "TAKEOFFRUN" }, - { "var": "landRunOnDone", "state": "WALKFWD" } - ] - } - ] + "url": "qrc:///avatar/animations/touch_thumb_point_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true }, - "children": [ - { - "id": "idle", - "type": "stateMachine", - "data": { - "currentState": "idleStand", - "states": [ + "children": [] + }, + { + "id": "rightHandAnimB", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftHandOverlay", + "type": "overlay", + "data": { + "alpha": 0.0, + "boneSet": "leftHand", + "alphaVar": "leftHandOverlayAlpha" + }, + "children": [ + { + "id": "leftHandStateMachine", + "type": "stateMachine", + "data": { + "currentState": "leftHandAnimNone", + "states": [ + { + "id": "leftHandAnimNone", + "interpTarget": 1, + "interpDuration": 3, + "transitions": [ + { + "var": "leftHandAnimA", + "state": "leftHandAnimA" + }, + { + "var": "leftHandAnimB", + "state": "leftHandAnimB" + } + ] + }, + { + "id": "leftHandAnimA", + "interpTarget": 1, + "interpDuration": 3, + "transitions": [ + { + "var": "leftHandAnimNone", + "state": "leftHandAnimNone" + }, + { + "var": "leftHandAnimB", + "state": "leftHandAnimB" + } + ] + }, + { + "id": "leftHandAnimB", + "interpTarget": 1, + "interpDuration": 3, + "transitions": [ + { + "var": "leftHandAnimNone", + "state": "leftHandAnimNone" + }, + { + "var": "leftHandAnimA", + "state": "leftHandAnimA" + } + ] + } + ] + }, + "children": [ + { + "id": "leftHandAnimNone", + "type": "stateMachine", + "data": { + "currentState": "leftHandGrasp", + "states": [ + { + "id": "leftHandGrasp", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftIndexPoint", + "state": "leftIndexPoint" + }, + { + "var": "isLeftThumbRaise", + "state": "leftThumbRaise" + }, + { + "var": "isLeftIndexPointAndThumbRaise", + "state": "leftIndexPointAndThumbRaise" + } + ] + }, + { + "id": "leftIndexPoint", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftHandGrasp", + "state": "leftHandGrasp" + }, + { + "var": "isLeftThumbRaise", + "state": "leftThumbRaise" + }, + { + "var": "isLeftIndexPointAndThumbRaise", + "state": "leftIndexPointAndThumbRaise" + } + ] + }, + { + "id": "leftThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftHandGrasp", + "state": "leftHandGrasp" + }, + { + "var": "isLeftIndexPoint", + "state": "leftIndexPoint" + }, + { + "var": "isLeftIndexPointAndThumbRaise", + "state": "leftIndexPointAndThumbRaise" + } + ] + }, + { + "id": "leftIndexPointAndThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftHandGrasp", + "state": "leftHandGrasp" + }, + { + "var": "isLeftIndexPoint", + "state": "leftIndexPoint" + }, + { + "var": "isLeftThumbRaise", + "state": "leftThumbRaise" + } + ] + } + ] + }, + "children": [ { - "id": "idleStand", - "interpTarget": 6, - "interpDuration": 10, - "transitions": [ - { "var": "isTalking", "state": "idleTalk" } + "id": "leftHandGrasp", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftHandGraspOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/hydra_pose_open_left.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandGraspClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/hydra_pose_closed_left.fbx", + "startFrame": 10.0, + "endFrame": 10.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } ] }, { - "id": "idleTalk", - "interpTarget": 6, - "interpDuration": 10, - "transitions": [ - { "var": "notIsTalking", "state": "idleStand" } + "id": "leftIndexPoint", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftIndexPointOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftIndexPointClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_point_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftThumbRaiseOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftThumbRaiseClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftIndexPointAndThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftIndexPointAndThumbRaiseOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftIndexPointAndThumbRaiseClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } ] } ] }, - "children": [ + { + "id": "leftHandAnimA", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandAnimB", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "mainStateMachine", + "type": "stateMachine", + "data": { + "outputJoints": [ "LeftFoot", "RightFoot" ], + "currentState": "idle", + "states": [ { - "id": "idleStand", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/idle.fbx", - "startFrame": 0.0, - "endFrame": 300.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + "id": "idle", + "interpTarget": 20, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] }, { - "id": "idleTalk", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/talk.fbx", - "startFrame": 0.0, - "endFrame": 800.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + "id": "idleToWalkFwd", + "interpTarget": 12, + "interpDuration": 8, + "transitions": [ + { + "var": "idleToWalkFwdOnDone", + "state": "WALKFWD" + }, + { + "var": "isNotMoving", + "state": "idle" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "idleSettle", + "interpTarget": 15, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "idleSettleOnDone", + "state": "idle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + } + ] + }, + { + "id": "WALKFWD", + "interpTarget": 35, + "interpDuration": 10, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "WALKBWD", + "interpTarget": 35, + "interpDuration": 10, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "STRAFERIGHT", + "interpTarget": 25, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "STRAFELEFT", + "interpTarget": 25, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "turnRight", + "interpTarget": 6, + "interpDuration": 8, + "transitions": [ + { + "var": "isNotTurning", + "state": "idle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "turnLeft", + "interpTarget": 6, + "interpDuration": 8, + "transitions": [ + { + "var": "isNotTurning", + "state": "idle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "strafeRightHmd", + "interpTarget": 5, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + } + ] + }, + { + "id": "strafeLeftHmd", + "interpTarget": 5, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + } + ] + }, + { + "id": "fly", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { + "var": "isNotFlying", + "state": "idleSettle" + } + ] + }, + { + "id": "takeoffStand", + "interpTarget": 2, + "interpDuration": 2, + "transitions": [ + { + "var": "isNotTakeoff", + "state": "inAirStand" + } + ] + }, + { + "id": "TAKEOFFRUN", + "interpTarget": 2, + "interpDuration": 2, + "transitions": [ + { + "var": "isNotTakeoff", + "state": "INAIRRUN" + } + ] + }, + { + "id": "inAirStand", + "interpTarget": 3, + "interpDuration": 3, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotInAir", + "state": "landStandImpact" + } + ] + }, + { + "id": "INAIRRUN", + "interpTarget": 3, + "interpDuration": 3, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotInAir", + "state": "WALKFWD" + } + ] + }, + { + "id": "landStandImpact", + "interpTarget": 1, + "interpDuration": 1, + "transitions": [ + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "landStandImpactOnDone", + "state": "landStand" + } + ] + }, + { + "id": "landStand", + "interpTarget": 1, + "interpDuration": 1, + "transitions": [ + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "landStandOnDone", + "state": "idle" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "LANDRUN", + "interpTarget": 2, + "interpDuration": 2, + "transitions": [ + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "landRunOnDone", + "state": "WALKFWD" + } + ] } ] }, - { - "id": "WALKFWD", - "type": "blendLinearMove", - "data": { - "alpha": 0.0, - "desiredSpeed": 1.4, - "characteristicSpeeds": [0.5, 1.8, 2.3, 3.2, 4.5], - "alphaVar": "moveForwardAlpha", - "desiredSpeedVar": "moveForwardSpeed" + "children": [ + { + "id": "idle", + "type": "stateMachine", + "data": { + "currentState": "idleStand", + "states": [ + { + "id": "idleStand", + "interpTarget": 6, + "interpDuration": 10, + "transitions": [ + { + "var": "isTalking", + "state": "idleTalk" + } + ] + }, + { + "id": "idleTalk", + "interpTarget": 6, + "interpDuration": 10, + "transitions": [ + { + "var": "notIsTalking", + "state": "idleStand" + } + ] + } + ] + }, + "children": [ + { + "id": "idleStand", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 300.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "idleTalk", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/talk.fbx", + "startFrame": 0.0, + "endFrame": 800.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] }, - "children": [ - { - "id": "walkFwdShort_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_short_fwd.fbx", - "startFrame": 0.0, - "endFrame": 39.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + { + "id": "WALKFWD", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.5, 1.8, 2.3, 3.2, 4.5 ], + "alphaVar": "moveForwardAlpha", + "desiredSpeedVar": "moveForwardSpeed" }, - { - "id": "walkFwdNormal_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_fwd.fbx", - "startFrame": 0.0, - "endFrame": 30.0, - "timeScale": 1.0, - "loopFlag": true + "children": [ + { + "id": "walkFwdShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_short_fwd.fbx", + "startFrame": 0.0, + "endFrame": 39.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "walkFwdFast_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_fwd_fast.fbx", - "startFrame": 0.0, - "endFrame": 25.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "walkFwdNormal_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "walkFwdJog_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jog_fwd.fbx", - "startFrame": 0.0, - "endFrame": 25.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "walkFwdFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_fwd_fast.fbx", + "startFrame": 0.0, + "endFrame": 25.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "walkFwdRun_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/run_fwd.fbx", - "startFrame": 1.0, - "endFrame": 22.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "walkFwdJog_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_fwd.fbx", + "startFrame": 0.0, + "endFrame": 25.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - } - ] - }, - { - "id": "idleToWalkFwd", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/idle_to_walk.fbx", - "startFrame": 1.0, - "endFrame": 13.0, - "timeScale": 1.0, - "loopFlag": false + { + "id": "walkFwdRun_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/run_fwd.fbx", + "startFrame": 1.0, + "endFrame": 22.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] }, - "children": [] - }, - { - "id": "idleSettle", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/settle_to_idle.fbx", - "startFrame": 1.0, - "endFrame": 59.0, - "timeScale": 1.0, - "loopFlag": false + { + "id": "idleToWalkFwd", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_to_walk.fbx", + "startFrame": 1.0, + "endFrame": 13.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] }, - "children": [] - }, - { - "id": "WALKBWD", - "type": "blendLinearMove", - "data": { - "alpha": 0.0, - "desiredSpeed": 1.4, - "characteristicSpeeds": [0.6, 1.6, 2.3, 3.1], - "alphaVar": "moveBackwardAlpha", - "desiredSpeedVar": "moveBackwardSpeed" + { + "id": "idleSettle", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/settle_to_idle.fbx", + "startFrame": 1.0, + "endFrame": 59.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] }, - "children": [ - { - "id": "walkBwdShort_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_short_bwd.fbx", - "startFrame": 0.0, - "endFrame": 38.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + { + "id": "WALKBWD", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.6, 1.6, 2.3, 3.1 ], + "alphaVar": "moveBackwardAlpha", + "desiredSpeedVar": "moveBackwardSpeed" }, - { - "id": "walkBwdFast_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_bwd_fast.fbx", - "startFrame": 0.0, - "endFrame": 27.0, - "timeScale": 1.0, - "loopFlag": true + "children": [ + { + "id": "walkBwdShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_short_bwd.fbx", + "startFrame": 0.0, + "endFrame": 38.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "jogBwd_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jog_bwd.fbx", - "startFrame": 0.0, - "endFrame": 24.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "walkBwdFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_bwd_fast.fbx", + "startFrame": 0.0, + "endFrame": 27.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "runBwd_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/run_bwd.fbx", - "startFrame": 0.0, - "endFrame": 16.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "jogBwd_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_bwd.fbx", + "startFrame": 0.0, + "endFrame": 24.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - } - ] - }, - { - "id": "turnLeft", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/turn_left.fbx", - "startFrame": 0.0, - "endFrame": 32.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "runBwd_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/run_bwd.fbx", + "startFrame": 0.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] }, - "children": [] - }, - { - "id": "turnRight", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/turn_left.fbx", - "startFrame": 0.0, - "endFrame": 32.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true + { + "id": "turnLeft", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 32.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "STRAFELEFT", - "type": "blendLinearMove", - "data": { - "alpha": 0.0, - "desiredSpeed": 1.4, - "characteristicSpeeds": [0.1, 0.5, 1.0, 2.6, 3.0], - "alphaVar": "moveLateralAlpha", - "desiredSpeedVar": "moveLateralSpeed" + { + "id": "turnRight", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 32.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] }, - "children": [ - { - "id": "strafeLeftShortStep_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_short_left.fbx", - "startFrame": 0.0, - "endFrame": 29.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + { + "id": "STRAFELEFT", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.1, 0.5, 1.0, 2.6, 3.0 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" }, - { - "id": "strafeLeftStep_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_left.fbx", - "startFrame": 0.0, - "endFrame": 20.0, - "timeScale": 1.0, - "loopFlag": true + "children": [ + { + "id": "strafeLeftShortStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "strafeLeftWalk_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_left.fbx", - "startFrame": 0.0, - "endFrame": 35.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "strafeLeftStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "strafeLeftWalkFast_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_left_fast.fbx", - "startFrame": 0.0, - "endFrame": 21.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "strafeLeftWalk_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "strafeLeftJog_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jog_left.fbx", - "startFrame": 0.0, - "endFrame": 24.0, - "timeScale": 1.0, - "loopFlag": true + { + "id": "strafeLeftWalkFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 21.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] - } - ] - }, - { - "id": "STRAFERIGHT", - "type": "blendLinearMove", - "data": { - "alpha": 0.0, - "desiredSpeed": 1.4, - "characteristicSpeeds": [0.1, 0.5, 1.0, 2.6, 3.0], - "alphaVar": "moveLateralAlpha", - "desiredSpeedVar": "moveLateralSpeed" + { + "id": "strafeLeftJog_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_left.fbx", + "startFrame": 0.0, + "endFrame": 24.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] }, - "children": [ { - "id": "strafeRightShortStep_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_short_left.fbx", - "startFrame": 0.0, - "endFrame": 29.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true - }, - "children": [] + { + "id": "STRAFERIGHT", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.1, 0.5, 1.0, 2.6, 3.0 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" }, - { - "id": "strafeRightStep_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_left.fbx", - "startFrame": 0.0, - "endFrame": 20.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true + "children": [ + { + "id": "strafeRightShortStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "strafeRightWalk_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_left.fbx", - "startFrame": 0.0, - "endFrame": 35.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true + { + "id": "strafeRightStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "strafeRightFast_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/walk_left_fast.fbx", - "startFrame": 0.0, - "endFrame": 21.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true + { + "id": "strafeRightWalk_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] }, - "children": [] - }, - { - "id": "strafeRightJog_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jog_left.fbx", - "startFrame": 0.0, - "endFrame": 24.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true + { + "id": "strafeRightFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 21.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] }, - "children": [] - } - ] - }, - { - "id": "strafeLeftHmd", - "type": "blendLinearMove", - "data": { - "alpha": 0.0, - "desiredSpeed": 1.4, - "characteristicSpeeds": [0, 0.5, 2.5], - "alphaVar": "moveLateralAlpha", - "desiredSpeedVar": "moveLateralSpeed" + { + "id": "strafeRightJog_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_left.fbx", + "startFrame": 0.0, + "endFrame": 24.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + } + ] }, - "children": [ - { - "id": "stepLeftShort_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_short_left.fbx", - "startFrame": 0.0, - "endFrame": 29.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + { + "id": "strafeLeftHmd", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0, 0.5, 2.5 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" }, - { - "id": "stepLeft_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_left.fbx", - "startFrame": 0.0, - "endFrame": 20.0, - "timeScale": 1.0, - "loopFlag": true + "children": [ + { + "id": "stepLeftShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] }, - "children": [] + { + "id": "stepLeft_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeftAnim_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "strafeRightHmd", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0, 0.5, 2.5 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" }, - { - "id": "strafeLeftAnim_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_left_fast.fbx", - "startFrame": 0.0, - "endFrame": 16.0, - "timeScale": 1.0, - "loopFlag": true + "children": [ + { + "id": "stepRightShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] }, - "children": [] - } - ] - }, - { - "id": "strafeRightHmd", - "type": "blendLinearMove", - "data": { - "alpha": 0.0, - "desiredSpeed": 1.4, - "characteristicSpeeds": [0, 0.5, 2.5], - "alphaVar": "moveLateralAlpha", - "desiredSpeedVar": "moveLateralSpeed" + { + "id": "stepRight_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "strafeRightAnim_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + } + ] }, - "children": [ - { - "id": "stepRightShort_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_short_left.fbx", - "startFrame": 0.0, - "endFrame": 29.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true - }, - "children": [] + { + "id": "fly", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/fly.fbx", + "startFrame": 1.0, + "endFrame": 80.0, + "timeScale": 1.0, + "loopFlag": true }, - { - "id": "stepRight_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_left.fbx", - "startFrame": 0.0, - "endFrame": 20.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true - }, - "children": [] + "children": [] + }, + { + "id": "takeoffStand", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_launch.fbx", + "startFrame": 2.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": false }, - { - "id": "strafeRightAnim_c", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/side_step_left_fast.fbx", - "startFrame": 0.0, - "endFrame": 16.0, - "timeScale": 1.0, - "loopFlag": true, - "mirrorFlag": true - }, - "children": [] - } - ] - }, - { - "id": "fly", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/fly.fbx", - "startFrame": 1.0, - "endFrame": 80.0, - "timeScale": 1.0, - "loopFlag": true + "children": [] }, - "children": [] - }, - { - "id": "takeoffStand", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_standing_launch.fbx", - "startFrame": 2.0, - "endFrame": 16.0, - "timeScale": 1.0, - "loopFlag": false - }, - "children": [] - }, - { - "id": "TAKEOFFRUN", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", - "startFrame": 4.0, - "endFrame": 15.0, - "timeScale": 1.0, - "loopFlag": false - }, - "children": [] - }, - { - "id": "inAirStand", - "type": "blendLinear", - "data": { - "alpha": 0.0, - "alphaVar": "inAirAlpha" - }, - "children": [ - { - "id": "inAirStandPreApex", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_standing_apex.fbx", - "startFrame": 0.0, - "endFrame": 0.0, - "timeScale": 1.0, - "loopFlag": false - }, - "children": [] + { + "id": "TAKEOFFRUN", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 4.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": false }, - { - "id": "inAirStandApex", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_standing_apex.fbx", - "startFrame": 1.0, - "endFrame": 1.0, - "timeScale": 1.0, - "loopFlag": false - }, - "children": [] + "children": [] + }, + { + "id": "inAirStand", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "inAirAlpha" }, - { - "id": "inAirStandPostApex", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_standing_apex.fbx", - "startFrame": 2.0, - "endFrame": 2.0, - "timeScale": 1.0, - "loopFlag": false + "children": [ + { + "id": "inAirStandPreApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_apex.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] }, - "children": [] - } - ] - }, - { - "id": "INAIRRUN", - "type": "blendLinear", - "data": { - "alpha": 0.0, - "alphaVar": "inAirAlpha" + { + "id": "inAirStandApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_apex.fbx", + "startFrame": 1.0, + "endFrame": 1.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirStandPostApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_apex.fbx", + "startFrame": 2.0, + "endFrame": 2.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] }, - "children": [ - { - "id": "inAirRunPreApex", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", - "startFrame": 16.0, - "endFrame": 16.0, - "timeScale": 1.0, - "loopFlag": false - }, - "children": [] + { + "id": "INAIRRUN", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "inAirAlpha" }, - { - "id": "inAirRunApex", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", - "startFrame": 22.0, - "endFrame": 22.0, - "timeScale": 1.0, - "loopFlag": false + "children": [ + { + "id": "inAirRunPreApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 16.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] }, - "children": [] + { + "id": "inAirRunApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 22.0, + "endFrame": 22.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirRunPostApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 33.0, + "endFrame": 33.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + }, + { + "id": "landStandImpact", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_land_settle.fbx", + "startFrame": 1.0, + "endFrame": 6.0, + "timeScale": 1.0, + "loopFlag": false }, - { - "id": "inAirRunPostApex", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", - "startFrame": 33.0, - "endFrame": 33.0, - "timeScale": 1.0, - "loopFlag": false - }, - "children": [] - } - ] - }, - { - "id": "landStandImpact", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_standing_land_settle.fbx", - "startFrame": 1.0, - "endFrame": 6.0, - "timeScale": 1.0, - "loopFlag": false + "children": [] }, - "children": [] - }, - { - "id": "landStand", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_standing_land_settle.fbx", - "startFrame": 6.0, - "endFrame": 68.0, - "timeScale": 1.0, - "loopFlag": false + { + "id": "landStand", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_land_settle.fbx", + "startFrame": 6.0, + "endFrame": 68.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] }, - "children": [] - }, - { - "id": "LANDRUN", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", - "startFrame": 29.0, - "endFrame": 40.0, - "timeScale": 1.0, - "loopFlag": false - }, - "children": [] - } - ] - } + { + "id": "LANDRUN", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 29.0, + "endFrame": 40.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + } + ] + } ] } - ] - } ] } ] diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 8bf6dcbd92..85cdafa529 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1188,6 +1188,15 @@ void MyAvatar::overrideAnimation(const QString& url, float fps, bool loop, float _skeletonModel->getRig().overrideAnimation(url, fps, loop, firstFrame, lastFrame); } +void MyAvatar::overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "overrideHandAnimation", Q_ARG(bool, isLeft), Q_ARG(const QString&, url), Q_ARG(float, fps), + Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); + return; + } + _skeletonModel->getRig().overrideHandAnimation(isLeft, url, fps, loop, firstFrame, lastFrame); +} + void MyAvatar::restoreAnimation() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "restoreAnimation"); @@ -1196,6 +1205,14 @@ void MyAvatar::restoreAnimation() { _skeletonModel->getRig().restoreAnimation(); } +void MyAvatar::restoreHandAnimation(bool isLeft) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "restoreHandAnimation", Q_ARG(bool, isLeft)); + return; + } + _skeletonModel->getRig().restoreHandAnimation(isLeft); +} + QStringList MyAvatar::getAnimationRoles() { if (QThread::currentThread() != thread()) { QStringList result; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index edb686a6a6..d1838c65fc 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -576,6 +576,7 @@ public: * }, 3000); */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + Q_INVOKABLE void overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc * Restores the default animations. @@ -594,6 +595,7 @@ public: * }, 3000); */ Q_INVOKABLE void restoreAnimation(); + Q_INVOKABLE void restoreHandAnimation(bool isLeft); /**jsdoc * Gets the current animation roles. diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 82ab067472..ab567ccedd 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -370,6 +370,88 @@ void Rig::restoreAnimation() { } } +void Rig::overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { + HandAnimState::ClipNodeEnum clipNodeEnum; + if (isLeft) { + if (_leftHandAnimState.clipNodeEnum == HandAnimState::None || _leftHandAnimState.clipNodeEnum == HandAnimState::B) { + clipNodeEnum = HandAnimState::A; + } else { + clipNodeEnum = HandAnimState::B; + } + } else { + if (_rightHandAnimState.clipNodeEnum == HandAnimState::None || _rightHandAnimState.clipNodeEnum == HandAnimState::B) { + clipNodeEnum = HandAnimState::A; + } else { + clipNodeEnum = HandAnimState::B; + } + } + + if (_animNode) { + std::shared_ptr clip; + if (isLeft) { + if (clipNodeEnum == HandAnimState::A) { + clip = std::dynamic_pointer_cast(_animNode->findByName("leftHandAnimA")); + } else { + clip = std::dynamic_pointer_cast(_animNode->findByName("leftHandAnimB")); + } + } else { + if (clipNodeEnum == HandAnimState::A) { + clip = std::dynamic_pointer_cast(_animNode->findByName("rightHandAnimA")); + } else { + clip = std::dynamic_pointer_cast(_animNode->findByName("rightHandAnimB")); + } + } + + if (clip) { + // set parameters + clip->setLoopFlag(loop); + clip->setStartFrame(firstFrame); + clip->setEndFrame(lastFrame); + const float REFERENCE_FRAMES_PER_SECOND = 30.0f; + float timeScale = fps / REFERENCE_FRAMES_PER_SECOND; + clip->setTimeScale(timeScale); + clip->loadURL(url); + } + } + + // notify the handAnimStateMachine the desired state. + if (isLeft) { + // store current hand anim state. + _leftHandAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame }; + _animVars.set("leftHandAnimNone", false); + _animVars.set("leftHandAnimA", clipNodeEnum == HandAnimState::A); + _animVars.set("leftHandAnimB", clipNodeEnum == HandAnimState::B); + } else { + // store current hand anim state. + _rightHandAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame }; + _animVars.set("rightHandAnimNone", false); + _animVars.set("rightHandAnimA", clipNodeEnum == HandAnimState::A); + _animVars.set("rightHandAnimB", clipNodeEnum == HandAnimState::B); + } +} + +void Rig::restoreHandAnimation(bool isLeft) { + if (isLeft) { + if (_leftHandAnimState.clipNodeEnum != HandAnimState::None) { + _leftHandAnimState.clipNodeEnum = HandAnimState::None; + + // notify the handAnimStateMachine the desired state. + _animVars.set("leftHandAnimNone", true); + _animVars.set("leftHandAnimA", false); + _animVars.set("leftHandAnimB", false); + } + } else { + if (_rightHandAnimState.clipNodeEnum != HandAnimState::None) { + _rightHandAnimState.clipNodeEnum = HandAnimState::None; + + // notify the handAnimStateMachine the desired state. + _animVars.set("rightHandAnimNone", true); + _animVars.set("rightHandAnimA", false); + _animVars.set("rightHandAnimB", false); + } + } +} + void Rig::overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { NetworkAnimState::ClipNodeEnum clipNodeEnum = NetworkAnimState::None; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index df13ff5c2b..c75bcf5651 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -118,6 +118,9 @@ public: void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreAnimation(); + void overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + void restoreHandAnimation(bool isLeft); + void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void triggerNetworkRole(const QString& role); void restoreNetworkAnimation(); @@ -356,6 +359,27 @@ protected: float blendTime; }; + struct HandAnimState { + enum ClipNodeEnum { + None = 0, + A, + B + }; + + HandAnimState() : clipNodeEnum(HandAnimState::None) {} + HandAnimState(ClipNodeEnum clipNodeEnumIn, const QString& urlIn, float fpsIn, bool loopIn, float firstFrameIn, float lastFrameIn) : + clipNodeEnum(clipNodeEnumIn), url(urlIn), fps(fpsIn), loop(loopIn), firstFrame(firstFrameIn), lastFrame(lastFrameIn) { + } + + + ClipNodeEnum clipNodeEnum; + QString url; + float fps; + bool loop; + float firstFrame; + float lastFrame; + }; + struct UserAnimState { enum ClipNodeEnum { None = 0, @@ -390,6 +414,8 @@ protected: UserAnimState _userAnimState; NetworkAnimState _networkAnimState; + HandAnimState _rightHandAnimState; + HandAnimState _leftHandAnimState; std::map _roleAnimStates; float _leftHandOverlayAlpha { 0.0f }; From aa4401801e481991954246130280c980ede44cb1 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Wed, 27 Mar 2019 11:54:45 +0100 Subject: [PATCH 024/286] Added new texture types : sky and ambient in place of just cube --- libraries/baking/src/TextureBaker.cpp | 2 +- .../src/RenderableZoneEntityItem.cpp | 4 ++-- libraries/image/src/image/Image.cpp | 4 +++- libraries/image/src/image/Image.h | 3 ++- .../src/material-networking/TextureCache.cpp | 11 ++++++++--- .../src/DeferredLightingEffect.cpp | 18 ++++++++++++++++-- tools/oven/src/BakerCLI.cpp | 5 +++-- tools/oven/src/DomainBaker.cpp | 4 ++-- tools/oven/src/ui/SkyboxBakeWidget.cpp | 2 +- 9 files changed, 38 insertions(+), 15 deletions(-) diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index d097b4765b..c37e61cb4b 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -206,7 +206,7 @@ void TextureBaker::processTexture() { } // Uncompressed KTX - if (_textureType == image::TextureUsage::Type::CUBE_TEXTURE) { + if (_textureType == image::TextureUsage::Type::SKY_TEXTURE || _textureType == image::TextureUsage::Type::AMBIENT_TEXTURE) { buffer->reset(); auto processedTexture = image::processImage(std::move(buffer), _textureURL.toString().toStdString(), image::ColorChannel::NONE, ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, false, gpu::BackendTarget::GL45, _abortProcessing); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 631148c27a..967ede0709 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -465,7 +465,7 @@ void ZoneEntityRenderer::setAmbientURL(const QString& ambientUrl) { } else { _pendingAmbientTexture = true; auto textureCache = DependencyManager::get(); - _ambientTexture = textureCache->getTexture(_ambientTextureURL, image::TextureUsage::CUBE_TEXTURE); + _ambientTexture = textureCache->getTexture(_ambientTextureURL, image::TextureUsage::AMBIENT_TEXTURE); // keep whatever is assigned on the ambient map/sphere until texture is loaded } @@ -506,7 +506,7 @@ void ZoneEntityRenderer::setSkyboxURL(const QString& skyboxUrl) { } else { _pendingSkyboxTexture = true; auto textureCache = DependencyManager::get(); - _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, image::TextureUsage::CUBE_TEXTURE); + _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, image::TextureUsage::SKY_TEXTURE); } } diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 4154a46c8d..88ca440908 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -100,7 +100,9 @@ TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, con return image::TextureUsage::createEmissiveTextureFromImage; case LIGHTMAP_TEXTURE: return image::TextureUsage::createLightmapTextureFromImage; - case CUBE_TEXTURE: + case SKY_TEXTURE: + return image::TextureUsage::createCubeTextureFromImageWithoutIrradiance; + case AMBIENT_TEXTURE: if (options.value("generateIrradiance", true).toBool()) { return image::TextureUsage::createCubeTextureFromImage; } else { diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index a64a9e4571..b816edac39 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -41,7 +41,8 @@ enum Type { ROUGHNESS_TEXTURE, GLOSS_TEXTURE, EMISSIVE_TEXTURE, - CUBE_TEXTURE, + SKY_TEXTURE, + AMBIENT_TEXTURE, OCCLUSION_TEXTURE, SCATTERING_TEXTURE = OCCLUSION_TEXTURE, LIGHTMAP_TEXTURE, diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 43f467266a..b15020de42 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -224,10 +224,14 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs return getResourceTexture(url); } auto modifiedUrl = url; - if (type == image::TextureUsage::CUBE_TEXTURE) { + if (type == image::TextureUsage::SKY_TEXTURE) { QUrlQuery query { url.query() }; query.addQueryItem("skybox", ""); modifiedUrl.setQuery(query.toString()); + } else if (type == image::TextureUsage::AMBIENT_TEXTURE) { + QUrlQuery query{ url.query() }; + query.addQueryItem("ambient", ""); + modifiedUrl.setQuery(query.toString()); } TextureExtra extra = { type, content, maxNumPixels, sourceChannel }; return ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash()(extra)).staticCast(); @@ -283,7 +287,8 @@ gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) { case image::TextureUsage::BUMP_TEXTURE: case image::TextureUsage::SPECULAR_TEXTURE: case image::TextureUsage::GLOSS_TEXTURE: - case image::TextureUsage::CUBE_TEXTURE: + case image::TextureUsage::SKY_TEXTURE: + case image::TextureUsage::AMBIENT_TEXTURE: case image::TextureUsage::STRICT_TEXTURE: default: break; @@ -408,7 +413,7 @@ void NetworkTexture::setExtra(void* extra) { _shouldFailOnRedirect = _currentlyLoadingResourceType != ResourceType::KTX; - if (_type == image::TextureUsage::CUBE_TEXTURE) { + if (_type == image::TextureUsage::SKY_TEXTURE) { setLoadPriority(this, SKYBOX_LOAD_PRIORITY); } else if (_currentlyLoadingResourceType == ResourceType::KTX) { setLoadPriority(this, HIGH_MIPS_LOAD_PRIORITY); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index ab9dea2325..b936060741 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -647,20 +647,34 @@ void RenderDeferred::run(const RenderContextPointer& renderContext, const Inputs void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { if (!_defaultLight || !_defaultBackground) { + auto defaultSkyboxURL = PathUtils::resourcesUrl() + "images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json"; + if (!_defaultSkyboxNetworkTexture) { PROFILE_RANGE(render, "Process Default Skybox"); _defaultSkyboxNetworkTexture = DependencyManager::get()->getTexture( - PathUtils::resourcesUrl() + "images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json", image::TextureUsage::CUBE_TEXTURE); + defaultSkyboxURL, image::TextureUsage::SKY_TEXTURE); + } + + if (!_defaultSkyboxAmbientTexture) { + PROFILE_RANGE(render, "Process Default Ambient map"); + _defaultSkyboxAmbientTexture = DependencyManager::get()->getTexture( + defaultSkyboxURL, image::TextureUsage::AMBIENT_TEXTURE); } if (_defaultSkyboxNetworkTexture && _defaultSkyboxNetworkTexture->isLoaded() && _defaultSkyboxNetworkTexture->getGPUTexture()) { - _defaultSkyboxAmbientTexture = _defaultSkyboxNetworkTexture->getGPUTexture(); _defaultSkybox->setCubemap(_defaultSkyboxAmbientTexture); } else { // Don't do anything until the skybox has loaded return; } + if (_defaultSkyboxAmbientTexture && _defaultSkyboxAmbientTexture->isLoaded() && _defaultSkyboxAmbientTexture->getGPUTexture()) { + _defaultSkyboxAmbientTexture = _defaultSkyboxAmbientTexture->getGPUTexture(); + } else { + // Don't do anything until the ambient box has been loaded + return; + } + auto lightStage = renderContext->_scene->getStage(); if (lightStage) { diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index 2946db650c..ba2703a895 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -82,8 +82,9 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, { "gloss", image::TextureUsage::GLOSS_TEXTURE }, { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, - { "cube", image::TextureUsage::CUBE_TEXTURE }, - { "skybox", image::TextureUsage::CUBE_TEXTURE }, + { "cube", image::TextureUsage::SKY_TEXTURE }, + { "skybox", image::TextureUsage::SKY_TEXTURE }, + { "ambient", image::TextureUsage::AMBIENT_TEXTURE }, { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 05745aad24..f7bb214a33 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -390,13 +390,13 @@ void DomainBaker::enumerateEntities() { if (entity.contains(AMBIENT_LIGHT_KEY)) { auto ambientLight = entity[AMBIENT_LIGHT_KEY].toObject(); if (ambientLight.contains(AMBIENT_URL_KEY)) { - addTextureBaker(AMBIENT_LIGHT_KEY + "." + AMBIENT_URL_KEY, ambientLight[AMBIENT_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + addTextureBaker(AMBIENT_LIGHT_KEY + "." + AMBIENT_URL_KEY, ambientLight[AMBIENT_URL_KEY].toString(), image::TextureUsage::AMBIENT_TEXTURE, *it); } } if (entity.contains(SKYBOX_KEY)) { auto skybox = entity[SKYBOX_KEY].toObject(); if (skybox.contains(SKYBOX_URL_KEY)) { - addTextureBaker(SKYBOX_KEY + "." + SKYBOX_URL_KEY, skybox[SKYBOX_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + addTextureBaker(SKYBOX_KEY + "." + SKYBOX_URL_KEY, skybox[SKYBOX_URL_KEY].toString(), image::TextureUsage::SKY_TEXTURE, *it); } } diff --git a/tools/oven/src/ui/SkyboxBakeWidget.cpp b/tools/oven/src/ui/SkyboxBakeWidget.cpp index 71ae0cbab0..113346c5e7 100644 --- a/tools/oven/src/ui/SkyboxBakeWidget.cpp +++ b/tools/oven/src/ui/SkyboxBakeWidget.cpp @@ -181,7 +181,7 @@ void SkyboxBakeWidget::bakeButtonClicked() { // everything seems to be in place, kick off a bake for this skybox now auto baker = std::unique_ptr { - new TextureBaker(skyboxToBakeURL, image::TextureUsage::CUBE_TEXTURE, outputDirectory.absolutePath()) + new TextureBaker(skyboxToBakeURL, image::TextureUsage::SKY_TEXTURE, outputDirectory.absolutePath()) }; // move the baker to a worker thread From a39fe7452ce7f828c9925b5358fe4de875cc7756 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Wed, 27 Mar 2019 16:31:08 +0100 Subject: [PATCH 025/286] Preparing for cubemap convolution --- libraries/image/src/image/CubeMap.cpp | 28 ++++ libraries/image/src/image/CubeMap.h | 43 ++++++ libraries/image/src/image/Image.cpp | 190 ++++++++++++++++++++------ libraries/image/src/image/Image.h | 16 ++- 4 files changed, 231 insertions(+), 46 deletions(-) create mode 100644 libraries/image/src/image/CubeMap.cpp create mode 100644 libraries/image/src/image/CubeMap.h diff --git a/libraries/image/src/image/CubeMap.cpp b/libraries/image/src/image/CubeMap.cpp new file mode 100644 index 0000000000..303cb98fe7 --- /dev/null +++ b/libraries/image/src/image/CubeMap.cpp @@ -0,0 +1,28 @@ +// +// CubeMap.h +// image/src/image +// +// Created by Olivier Prat on 03/27/2019. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "CubeMap.h" + +using namespace image; + +CubeMap::CubeMap(int width, int height, int mipCount) : + _width(width), _height(height) { + assert(mipCount >0 && _width > 0 && _height > 0); + _mips.resize(mipCount); + for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++) { + auto mipWidth = std::max(1, width >> mipLevel); + auto mipHeight = std::max(1, height >> mipLevel); + auto mipPixelCount = mipWidth * mipHeight; + + for (auto& face : _mips[mipLevel]) { + face.resize(mipPixelCount); + } + } +} diff --git a/libraries/image/src/image/CubeMap.h b/libraries/image/src/image/CubeMap.h new file mode 100644 index 0000000000..05a571cafc --- /dev/null +++ b/libraries/image/src/image/CubeMap.h @@ -0,0 +1,43 @@ +// +// CubeMap.h +// image/src/image +// +// Created by Olivier Prat on 03/27/2019. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_image_CubeMap_h +#define hifi_image_CubeMap_h + +#include +#include +#include +#include + +namespace image { + + class CubeMap { + public: + + using Face = std::vector; + using Faces = std::array; + + CubeMap(int width, int height, int mipCount); + + gpu::uint16 getMipCount() const { return (gpu::uint16)_mips.size(); } + Faces& editMip(gpu::uint16 mipLevel) { return _mips[mipLevel]; } + const Faces& getMip(gpu::uint16 mipLevel) const { return _mips[mipLevel]; } + + private: + + int _width; + int _height; + std::vector _mips; + }; + +} + +#endif // hifi_image_CubeMap_h diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 88ca440908..6e7e08ea89 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -30,6 +30,7 @@ #include "OpenEXRReader.h" #endif #include "ImageLogging.h" +#include "CubeMap.h" using namespace gpu; @@ -101,12 +102,12 @@ TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, con case LIGHTMAP_TEXTURE: return image::TextureUsage::createLightmapTextureFromImage; case SKY_TEXTURE: - return image::TextureUsage::createCubeTextureFromImageWithoutIrradiance; + return image::TextureUsage::createCubeTextureFromImage; case AMBIENT_TEXTURE: if (options.value("generateIrradiance", true).toBool()) { - return image::TextureUsage::createCubeTextureFromImage; + return image::TextureUsage::createCubeTextureAndIrradianceFromImage; } else { - return image::TextureUsage::createCubeTextureFromImageWithoutIrradiance; + return image::TextureUsage::createCubeTextureFromImage; } case BUMP_TEXTURE: return image::TextureUsage::createNormalTextureFromBumpImage; @@ -177,14 +178,24 @@ gpu::TexturePointer TextureUsage::createMetallicTextureFromImage(QImage&& srcIma return process2DTextureGrayscaleFromImage(std::move(srcImage), srcImageName, compress, target, false, abortProcessing); } -gpu::TexturePointer TextureUsage::createCubeTextureFromImage(QImage&& srcImage, const std::string& srcImageName, +gpu::TexturePointer TextureUsage::createCubeTextureAndIrradianceFromImage(QImage&& srcImage, const std::string& srcImageName, bool compress, BackendTarget target, const std::atomic& abortProcessing) { - return processCubeTextureColorFromImage(std::move(srcImage), srcImageName, compress, target, true, abortProcessing); + return processCubeTextureColorFromImage(std::move(srcImage), srcImageName, compress, target, CUBE_GENERATE_IRRADIANCE, abortProcessing); } -gpu::TexturePointer TextureUsage::createCubeTextureFromImageWithoutIrradiance(QImage&& srcImage, const std::string& srcImageName, +gpu::TexturePointer TextureUsage::createCubeTextureFromImage(QImage&& srcImage, const std::string& srcImageName, bool compress, BackendTarget target, const std::atomic& abortProcessing) { - return processCubeTextureColorFromImage(std::move(srcImage), srcImageName, compress, target, false, abortProcessing); + return processCubeTextureColorFromImage(std::move(srcImage), srcImageName, compress, target, CUBE_DEFAULT, abortProcessing); +} + +gpu::TexturePointer TextureUsage::createGGXConvolvedCubeTextureFromImage(QImage&& image, const std::string& srcImageName, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing) { + return processCubeTextureColorFromImage(std::move(image), srcImageName, compress, target, CUBE_GGX_CONVOLVE, abortProcessing); +} + +gpu::TexturePointer TextureUsage::createGGXConvolvedCubeTextureAndIrradianceFromImage(QImage&& image, const std::string& srcImageName, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing) { + return processCubeTextureColorFromImage(std::move(image), srcImageName, compress, target, CUBE_GENERATE_IRRADIANCE | CUBE_GGX_CONVOLVE, abortProcessing); } static float denormalize(float value, const float minValue) { @@ -206,11 +217,17 @@ static uint32 packR11G11B10F(const glm::vec3& color) { return glm::packF2x11_1x10(ucolor); } -static std::function getHDRPackingFunction(const gpu::Element& format) { +static uint32 packUnorm4x8(const glm::vec3& color) { + return glm::packUnorm4x8(glm::vec4(color, 1.0f)); +} + +static std::function getPackingFunction(const gpu::Element& format) { if (format == gpu::Element::COLOR_RGB9E5) { return glm::packF3x9_E1x5; } else if (format == gpu::Element::COLOR_R11G11B10) { return packR11G11B10F; + } else if (format == gpu::Element::COLOR_RGBA_32 || format == gpu::Element::COLOR_SRGBA_32 || format == gpu::Element::COLOR_BGRA_32 || format == gpu::Element::COLOR_SBGRA_32) { + return packUnorm4x8; } else { qCWarning(imagelogging) << "Unknown handler format"; Q_UNREACHABLE(); @@ -219,21 +236,27 @@ static std::function getHDRPackingFunction(const gpu:: } std::function getHDRPackingFunction() { - return getHDRPackingFunction(HDR_FORMAT); + return getPackingFunction(HDR_FORMAT); } -std::function getHDRUnpackingFunction() { - if (HDR_FORMAT == gpu::Element::COLOR_RGB9E5) { +std::function getUnpackingFunction(const gpu::Element& format) { + if (format == gpu::Element::COLOR_RGB9E5) { return glm::unpackF3x9_E1x5; - } else if (HDR_FORMAT == gpu::Element::COLOR_R11G11B10) { + } else if (format == gpu::Element::COLOR_R11G11B10) { return glm::unpackF2x11_1x10; + } else if (format == gpu::Element::COLOR_RGBA_32 || format == gpu::Element::COLOR_SRGBA_32 || format == gpu::Element::COLOR_BGRA_32 || format == gpu::Element::COLOR_SBGRA_32) { + return glm::unpackUnorm4x8; } else { - qCWarning(imagelogging) << "Unknown HDR encoding format in QImage"; + qCWarning(imagelogging) << "Unknown handler format"; Q_UNREACHABLE(); return nullptr; } } +std::function getHDRUnpackingFunction() { + return getUnpackingFunction(HDR_FORMAT); +} + QImage processRawImageData(QIODevice& content, const std::string& filename) { // Help the QImage loader by extracting the image file format from the url filename ext. // Some tga are not created properly without it. @@ -440,7 +463,7 @@ struct OutputHandler : public nvtt::OutputHandler { struct PackedFloatOutputHandler : public OutputHandler { PackedFloatOutputHandler(gpu::Texture* texture, int face, gpu::Element format) : OutputHandler(texture, face) { - _packFunc = getHDRPackingFunction(format); + _packFunc = getPackingFunction(format); } virtual void beginImage(int size, int width, int height, int depth, int face, int miplevel) override { @@ -498,6 +521,43 @@ public: } }; +void convertToFloat(const unsigned char* source, int width, int height, int lineStride, gpu::Element sourceFormat, std::vector& output) { + std::vector::iterator outputIt; + auto unpackFunc = getUnpackingFunction(sourceFormat); + + output.resize(width * height); + outputIt = output.begin(); + for (auto lineNb = 0; lineNb < height; lineNb++) { + const uint32* srcPixelIt = reinterpret_cast(source + lineNb * lineStride); + const uint32* srcPixelEnd = srcPixelIt + width; + + while (srcPixelIt < srcPixelEnd) { + *outputIt = glm::vec4(unpackFunc(*srcPixelIt), 1.0f); + ++srcPixelIt; + ++outputIt; + } + } + assert(outputIt == output.end()); +} + +void convertFromFloat(unsigned char* output, int width, int height, int lineStride, gpu::Element outputFormat, const std::vector& source) { + std::vector::const_iterator sourceIt; + auto packFunc = getPackingFunction(outputFormat); + + sourceIt = source.begin(); + for (auto lineNb = 0; lineNb < height; lineNb++) { + uint32* outPixelIt = reinterpret_cast(output + lineNb * lineStride); + uint32* outPixelEnd = outPixelIt + width; + + while (outPixelIt < outPixelEnd) { + *outPixelIt = packFunc(*sourceIt); + ++outPixelIt; + ++sourceIt; + } + } + assert(sourceIt == source.end()); +} + void generateHDRMips(gpu::Texture* texture, QImage&& image, BackendTarget target, const std::atomic& abortProcessing, int face) { // Take a local copy to force move construction // https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f18-for-consume-parameters-pass-by-x-and-stdmove-the-parameter @@ -509,7 +569,6 @@ void generateHDRMips(gpu::Texture* texture, QImage&& image, BackendTarget target std::vector data; std::vector::iterator dataIt; auto mipFormat = texture->getStoredMipFormat(); - std::function unpackFunc = getHDRUnpackingFunction(); nvtt::InputFormat inputFormat = nvtt::InputFormat_RGBA_32F; nvtt::WrapMode wrapMode = nvtt::WrapMode_Mirror; @@ -535,19 +594,7 @@ void generateHDRMips(gpu::Texture* texture, QImage&& image, BackendTarget target return; } - data.resize(width * height); - dataIt = data.begin(); - for (auto lineNb = 0; lineNb < height; lineNb++) { - const uint32* srcPixelIt = reinterpret_cast(localCopy.constScanLine(lineNb)); - const uint32* srcPixelEnd = srcPixelIt + width; - - while (srcPixelIt < srcPixelEnd) { - *dataIt = glm::vec4(unpackFunc(*srcPixelIt), 1.0f); - ++srcPixelIt; - ++dataIt; - } - } - assert(dataIt == data.end()); + convertToFloat(localCopy.bits(), width, height, localCopy.bytesPerLine(), HDR_FORMAT, data); // We're done with the localCopy, free up the memory to avoid bloating the heap localCopy = QImage(); // QImage doesn't have a clear function, so override it with an empty one. @@ -785,22 +832,74 @@ void generateLDRMips(gpu::Texture* texture, QImage&& image, BackendTarget target #endif -void generateMips(gpu::Texture* texture, QImage&& image, BackendTarget target, const std::atomic& abortProcessing = false, int face = -1) { -#if CPU_MIPMAPS - PROFILE_RANGE(resource_parse, "generateMips"); +void generateMips(gpu::Texture* texture, QImage&& image, BackendTarget target, const std::atomic& abortProcessing = false, int face = -1, bool forceCPUBuild = false) { + if (forceCPUBuild || CPU_MIPMAPS) { + PROFILE_RANGE(resource_parse, "generateMips"); - if (target == BackendTarget::GLES32) { - generateLDRMips(texture, std::move(image), target, abortProcessing, face); - } else { - if (image.format() == QIMAGE_HDRFORMAT) { - generateHDRMips(texture, std::move(image), target, abortProcessing, face); - } else { + if (target == BackendTarget::GLES32) { generateLDRMips(texture, std::move(image), target, abortProcessing, face); + } else { + if (image.format() == QIMAGE_HDRFORMAT) { + generateHDRMips(texture, std::move(image), target, abortProcessing, face); + } else { + generateLDRMips(texture, std::move(image), target, abortProcessing, face); + } + } + } else { + texture->setAutoGenerateMips(true); + } +} + +void convolveFaceWithGGX(const CubeMap& source, int face, const std::atomic& abortProcessing) { + +} + +void convolveWithGGX(gpu::Texture* texture, BackendTarget target, const std::atomic& abortProcessing = false) { + PROFILE_RANGE(resource_parse, "convolveWithGGX"); + CubeMap source(texture->getWidth(), texture->getHeight(), texture->getNumMips()); + gpu::uint16 mipLevel; + int face; + const auto textureFormat = texture->getTexelFormat(); + + // Convert all mip data to float as source + for (mipLevel = 0; mipLevel < source.getMipCount(); ++mipLevel) { + auto mipDims = texture->evalMipDimensions(mipLevel); + auto& mip = source.editMip(mipLevel); + + for (face = 0; face < 6; face++) { + auto sourcePixels = texture->accessStoredMipFace(mipLevel, face)->data(); + convertToFloat(sourcePixels, mipDims.x, mipDims.y, sizeof(uint32)*mipDims.x, textureFormat, mip[face]); + if (abortProcessing.load()) { + return; + } } } -#else - texture->setAutoGenerateMips(true); -#endif + + for (face = 0; face < 6; face++) { + convolveFaceWithGGX(source, face, abortProcessing); + } + + if (!abortProcessing) { + // Convert all mip data back from float + unsigned char* convertedPixels = new unsigned char[texture->getWidth() * texture->getHeight() * sizeof(uint32)]; + + for (mipLevel = 0; mipLevel < source.getMipCount(); ++mipLevel) { + auto mipDims = texture->evalMipDimensions(mipLevel); + auto mipSize = texture->evalMipFaceSize(mipLevel); + auto& mip = source.getMip(mipLevel); + + for (face = 0; face < 6; face++) { + convertFromFloat(convertedPixels, mipDims.x, mipDims.y, sizeof(uint32)*mipDims.x, textureFormat, mip[face]); + texture->assignStoredMipFace(mipLevel, face, mipSize, convertedPixels); + if (abortProcessing.load()) { + delete[] convertedPixels; + return; + } + } + } + + delete[] convertedPixels; + } } void processTextureAlpha(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask) { @@ -1407,7 +1506,7 @@ QImage convertToHDRFormat(QImage&& srcImage, gpu::Element format) { } gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcImage, const std::string& srcImageName, - bool compress, BackendTarget target, bool generateIrradiance, + bool compress, BackendTarget target, int options, const std::atomic& abortProcessing) { PROFILE_RANGE(resource_parse, "processCubeTextureColorFromImage"); @@ -1492,7 +1591,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI theTexture->setStoredMipFormat(formatMip); // Generate irradiance while we are at it - if (generateIrradiance) { + if (options & CUBE_GENERATE_IRRADIANCE) { PROFILE_RANGE(resource_parse, "generateIrradiance"); gpu::Element irradianceFormat; // TODO: we could locally compress the irradiance texture on Android, but we don't need to @@ -1516,7 +1615,12 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI } for (uint8 face = 0; face < faces.size(); ++face) { - generateMips(theTexture.get(), std::move(faces[face]), target, abortProcessing, face); + // Force building the mip maps right now on CPU if we are convolving for GGX later on + generateMips(theTexture.get(), std::move(faces[face]), target, abortProcessing, face, (options & CUBE_GGX_CONVOLVE) == CUBE_GGX_CONVOLVE); + } + + if (options & CUBE_GGX_CONVOLVE) { + convolveWithGGX(theTexture.get(), target, abortProcessing); } } diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index b816edac39..9c27b0cf3c 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -72,8 +72,12 @@ gpu::TexturePointer createMetallicTextureFromImage(QImage&& image, const std::st bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); gpu::TexturePointer createCubeTextureFromImage(QImage&& image, const std::string& srcImageName, bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); -gpu::TexturePointer createCubeTextureFromImageWithoutIrradiance(QImage&& image, const std::string& srcImageName, - bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); +gpu::TexturePointer createCubeTextureAndIrradianceFromImage(QImage&& image, const std::string& srcImageName, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); +gpu::TexturePointer createGGXConvolvedCubeTextureFromImage(QImage&& image, const std::string& srcImageName, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); +gpu::TexturePointer createGGXConvolvedCubeTextureAndIrradianceFromImage(QImage&& image, const std::string& srcImageName, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); gpu::TexturePointer createLightmapTextureFromImage(QImage&& image, const std::string& srcImageName, bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); gpu::TexturePointer process2DTextureColorFromImage(QImage&& srcImage, const std::string& srcImageName, bool compress, @@ -82,8 +86,14 @@ gpu::TexturePointer process2DTextureNormalMapFromImage(QImage&& srcImage, const gpu::BackendTarget target, bool isBumpMap, const std::atomic& abortProcessing); gpu::TexturePointer process2DTextureGrayscaleFromImage(QImage&& srcImage, const std::string& srcImageName, bool compress, gpu::BackendTarget target, bool isInvertedPixels, const std::atomic& abortProcessing); + +enum CubeTextureOptions { + CUBE_DEFAULT = 0x0, + CUBE_GENERATE_IRRADIANCE = 0x1, + CUBE_GGX_CONVOLVE = 0x2 +}; gpu::TexturePointer processCubeTextureColorFromImage(QImage&& srcImage, const std::string& srcImageName, bool compress, - gpu::BackendTarget target, bool generateIrradiance, const std::atomic& abortProcessing); + gpu::BackendTarget target, int option, const std::atomic& abortProcessing); } // namespace TextureUsage From 4a2323f3c2323f700be53c8f5ee12b1f99b4f4b0 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Wed, 27 Mar 2019 17:43:26 +0100 Subject: [PATCH 026/286] Just need to write correct textureLod equivalent on CPU cube map --- libraries/image/src/image/CubeMap.cpp | 214 ++++++++++++++++++ libraries/image/src/image/CubeMap.h | 10 + libraries/image/src/image/Image.cpp | 19 +- .../render-utils/src/AntialiasingEffect.cpp | 29 +-- libraries/shared/src/RandomAndNoise.h | 47 ++++ 5 files changed, 280 insertions(+), 39 deletions(-) create mode 100644 libraries/shared/src/RandomAndNoise.h diff --git a/libraries/image/src/image/CubeMap.cpp b/libraries/image/src/image/CubeMap.cpp index 303cb98fe7..acd8d6fb85 100644 --- a/libraries/image/src/image/CubeMap.cpp +++ b/libraries/image/src/image/CubeMap.cpp @@ -10,6 +10,16 @@ // #include "CubeMap.h" +#include +#include +#include + +#include "RandomAndNoise.h" + +#ifndef M_PI +#define M_PI 3.14159265359 +#endif + using namespace image; CubeMap::CubeMap(int width, int height, int mipCount) : @@ -26,3 +36,207 @@ CubeMap::CubeMap(int width, int height, int mipCount) : } } } + +glm::vec4 CubeMap::fetchLod(const glm::vec3& dir, float lod) const { + // TODO + return glm::vec4(0.0f); +} + +static glm::vec3 sampleGGX(const glm::vec2& Xi, const float roughness) { + const float a = roughness * roughness; + + float phi = (float)(2.0 * M_PI * Xi.x); + float cosTheta = (float)(std::sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y))); + float sinTheta = (float)(std::sqrt(1.0 - cosTheta * cosTheta)); + + // from spherical coordinates to cartesian coordinates + glm::vec3 H; + H.x = std::cos(phi) * sinTheta; + H.y = std::sin(phi) * sinTheta; + H.z = cosTheta; + + return H; +} + +static float evaluateGGX(float NdotH, float roughness) { + float alpha = roughness * roughness; + float alphaSquared = alpha * alpha; + float denom = (float)(NdotH * NdotH * (alphaSquared - 1.0) + 1.0); + return alphaSquared / (denom * denom); +} + +struct CubeMap::GGXSamples { + float invTotalWeight; + std::vector points; +}; + +void CubeMap::generateGGXSamples(GGXSamples& data, float roughness, const int resolution) { + glm::vec2 xi; + glm::vec3 L; + glm::vec3 H; + const float saTexel = (float)(4.0 * M_PI / (6.0 * resolution * resolution)); + const float mipBias = 3.0f; + const auto sampleCount = data.points.size(); + const auto hammersleySequenceLength = data.points.size(); + int sampleIndex = 0; + int hammersleySampleIndex = 0; + float NdotL; + + data.invTotalWeight = 0.0f; + + // Do some computation in tangent space + while (sampleIndex < sampleCount) { + if (hammersleySampleIndex < hammersleySequenceLength) { + xi = evaluateHammersley((int)hammersleySampleIndex, (int)hammersleySequenceLength); + H = sampleGGX(xi, roughness); + L = H * (2.0f * H.z) - glm::vec3(0.0f, 0.0f, 1.0f); + NdotL = L.z; + hammersleySampleIndex++; + } else { + NdotL = -1.0f; + } + + while (NdotL <= 0.0f) { + // Create a purely random sample + xi.x = rand() / float(RAND_MAX); + xi.y = rand() / float(RAND_MAX); + H = sampleGGX(xi, roughness); + L = H * (2.0f * H.z) - glm::vec3(0.0f, 0.0f, 1.0f); + NdotL = L.z; + } + + float NdotH = std::max(0.0f, H.z); + float HdotV = NdotH; + float D = evaluateGGX(NdotH, roughness); + float pdf = (D * NdotH / (4.0f * HdotV)) + 0.0001f; + float saSample = 1.0f / (float(sampleCount) * pdf + 0.0001f); + float mipLevel = std::max(0.5f * log2(saSample / saTexel) + mipBias, 0.0f); + + auto& sample = data.points[sampleIndex]; + sample.x = L.x; + sample.y = L.y; + sample.z = L.z; + sample.w = mipLevel; + + data.invTotalWeight += NdotL; + + sampleIndex++; + } + data.invTotalWeight = 1.0f / data.invTotalWeight; +} + +void CubeMap::convolveForGGX(CubeMap& output, const std::atomic& abortProcessing) const { + // This should match fragment.glsl values, too + static const float ROUGHNESS_1_MIP_RESOLUTION = 1.5f; + static const gpu::uint16 MAX_SAMPLE_COUNT = 4000; + + const auto mipCount = getMipCount(); + GGXSamples params; + + params.points.reserve(MAX_SAMPLE_COUNT); + + for (gpu::uint16 mipLevel = 0; mipLevel < mipCount; ++mipLevel) { + // This is the inverse code found in fragment.glsl in evaluateAmbientLighting + float levelAlpha = float(mipLevel) / (mipCount - ROUGHNESS_1_MIP_RESOLUTION); + float mipRoughness = levelAlpha * (1.0f + 2.0f * levelAlpha) / 3.0f; + mipRoughness = std::max(1e-3f, mipRoughness); + mipRoughness = std::min(1.0f, mipRoughness); + + params.points.resize(std::min(MAX_SAMPLE_COUNT, 1U + size_t(4000 * mipRoughness * mipRoughness))); + generateGGXSamples(params, mipRoughness, _width); + + for (int face = 0; face < 6; face++) { + convolveMipFaceForGGX(params, output, mipLevel, face, abortProcessing); + if (abortProcessing.load()) { + return; + } + } + } +} + +void CubeMap::convolveMipFaceForGGX(const GGXSamples& samples, CubeMap& output, gpu::uint16 mipLevel, int face, const std::atomic& abortProcessing) const { + static const glm::vec3 NORMALS[24] = { + // POSITIVE X + glm::vec3(1.0f, 1.0f, 1.0f), + glm::vec3(1.0f, 1.0f, -1.0f), + glm::vec3(1.0f, -1.0f, 1.0f), + glm::vec3(1.0f, -1.0f, -1.0f), + // NEGATIVE X + glm::vec3(-1.0f, 1.0f, -1.0f), + glm::vec3(-1.0f, 1.0f, 1.0f), + glm::vec3(-1.0f, -1.0f, -1.0f), + glm::vec3(-1.0f, -1.0f, 1.0f), + // POSITIVE Y + glm::vec3(-1.0f, 1.0f, -1.0f), + glm::vec3(1.0f, 1.0f, -1.0f), + glm::vec3(-1.0f, 1.0f, 1.0f), + glm::vec3(1.0f, 1.0f, 1.0f), + // NEGATIVE Y + glm::vec3(-1.0f, -1.0f, 1.0f), + glm::vec3(1.0f, -1.0f, 1.0f), + glm::vec3(-1.0f, -1.0f, -1.0f), + glm::vec3(1.0f, -1.0f, -1.0f), + // POSITIVE Z + glm::vec3(-1.0f, 1.0f, 1.0f), + glm::vec3(1.0f, 1.0f, 1.0f), + glm::vec3(-1.0f, -1.0f, 1.0f), + glm::vec3(1.0f, -1.0f, 1.0f), + // NEGATIVE Z + glm::vec3(1.0f, 1.0f, -1.0f), + glm::vec3(-1.0f, 1.0f, -1.0f), + glm::vec3(1.0f, -1.0f, -1.0f), + glm::vec3(-1.0f, -1.0f, -1.0f) + }; + + const glm::vec3* faceNormals = NORMALS + face * 4; + const glm::vec3 deltaXNormalLo = faceNormals[1] - faceNormals[0]; + const glm::vec3 deltaXNormalHi = faceNormals[3] - faceNormals[2]; + auto& outputFace = output._mips[mipLevel][face]; + + tbb::parallel_for(tbb::blocked_range2d(0, _width, 16, 0, _height, 16), [&](const tbb::blocked_range2d& range) { + auto rowRange = range.rows(); + auto colRange = range.cols(); + + for (auto x = rowRange.begin(); x < rowRange.end(); x++) { + const float xAlpha = (x + 0.5f) / _width; + const glm::vec3 normalYLo = faceNormals[0] + deltaXNormalLo * xAlpha; + const glm::vec3 normalYHi = faceNormals[2] + deltaXNormalHi * xAlpha; + const glm::vec3 deltaYNormal = normalYHi - normalYLo; + + for (auto y = colRange.begin(); y < colRange.end(); y++) { + const float yAlpha = (y + 0.5f) / _width; + // Interpolate normal for this pixel + const glm::vec3 normal = glm::normalize(normalYLo + deltaYNormal * yAlpha); + + outputFace[x + y * _width] = computeConvolution(normal, samples); + } + + if (abortProcessing.load()) { + break; + } + } + }); +} + +glm::vec4 CubeMap::computeConvolution(const glm::vec3& N, const GGXSamples& samples) const { + // from tangent-space vector to world-space + glm::vec3 bitangent = abs(N.z) < 0.999 ? glm::vec3(0.0, 0.0, 1.0) : glm::vec3(1.0, 0.0, 0.0); + glm::vec3 tangent = glm::normalize(glm::cross(bitangent, N)); + bitangent = glm::cross(N, tangent); + + const size_t sampleCount = samples.points.size(); + glm::vec4 prefilteredColor = glm::vec4(0.0f); + + for (int i = 0; i < sampleCount; ++i) { + const auto& sample = samples.points[i]; + glm::vec3 L(sample.x, sample.y, sample.z); + float NdotL = L.z; + float mipLevel = sample.w; + // Now back to world space + L = tangent * L.x + bitangent * L.y + N * L.z; + prefilteredColor += fetchLod(L, mipLevel) * NdotL; + } + prefilteredColor = prefilteredColor * samples.invTotalWeight; + prefilteredColor.a = 1.0f; + return prefilteredColor; +} \ No newline at end of file diff --git a/libraries/image/src/image/CubeMap.h b/libraries/image/src/image/CubeMap.h index 05a571cafc..231db7d76f 100644 --- a/libraries/image/src/image/CubeMap.h +++ b/libraries/image/src/image/CubeMap.h @@ -16,6 +16,7 @@ #include #include #include +#include namespace image { @@ -31,11 +32,20 @@ namespace image { Faces& editMip(gpu::uint16 mipLevel) { return _mips[mipLevel]; } const Faces& getMip(gpu::uint16 mipLevel) const { return _mips[mipLevel]; } + void convolveForGGX(CubeMap& output, const std::atomic& abortProcessing) const; + glm::vec4 fetchLod(const glm::vec3& dir, float lod) const; + private: + struct GGXSamples; + int _width; int _height; std::vector _mips; + + static void generateGGXSamples(GGXSamples& data, float roughness, const int resolution); + void convolveMipFaceForGGX(const GGXSamples& samples, CubeMap& output, gpu::uint16 mipLevel, int face, const std::atomic& abortProcessing) const; + glm::vec4 computeConvolution(const glm::vec3& normal, const GGXSamples& samples) const; }; } diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 6e7e08ea89..7131871937 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -850,13 +850,10 @@ void generateMips(gpu::Texture* texture, QImage&& image, BackendTarget target, c } } -void convolveFaceWithGGX(const CubeMap& source, int face, const std::atomic& abortProcessing) { - -} - -void convolveWithGGX(gpu::Texture* texture, BackendTarget target, const std::atomic& abortProcessing = false) { - PROFILE_RANGE(resource_parse, "convolveWithGGX"); +void convolveForGGX(gpu::Texture* texture, BackendTarget target, const std::atomic& abortProcessing = false) { + PROFILE_RANGE(resource_parse, "convolveForGGX"); CubeMap source(texture->getWidth(), texture->getHeight(), texture->getNumMips()); + CubeMap output(texture->getWidth(), texture->getHeight(), texture->getNumMips()); gpu::uint16 mipLevel; int face; const auto textureFormat = texture->getTexelFormat(); @@ -875,18 +872,16 @@ void convolveWithGGX(gpu::Texture* texture, BackendTarget target, const std::ato } } - for (face = 0; face < 6; face++) { - convolveFaceWithGGX(source, face, abortProcessing); - } + source.convolveForGGX(output, abortProcessing); if (!abortProcessing) { // Convert all mip data back from float unsigned char* convertedPixels = new unsigned char[texture->getWidth() * texture->getHeight() * sizeof(uint32)]; - for (mipLevel = 0; mipLevel < source.getMipCount(); ++mipLevel) { + for (mipLevel = 0; mipLevel < output.getMipCount(); ++mipLevel) { auto mipDims = texture->evalMipDimensions(mipLevel); auto mipSize = texture->evalMipFaceSize(mipLevel); - auto& mip = source.getMip(mipLevel); + auto& mip = output.getMip(mipLevel); for (face = 0; face < 6; face++) { convertFromFloat(convertedPixels, mipDims.x, mipDims.y, sizeof(uint32)*mipDims.x, textureFormat, mip[face]); @@ -1620,7 +1615,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI } if (options & CUBE_GGX_CONVOLVE) { - convolveWithGGX(theTexture.get(), target, abortProcessing); + convolveForGGX(theTexture.get(), target, abortProcessing); } } diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 17c13df19a..f30e67a979 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -26,7 +26,7 @@ #include "ViewFrustum.h" #include "GeometryCache.h" #include "FramebufferCache.h" - +#include "RandomAndNoise.h" namespace ru { using render_utils::slot::texture::Texture; @@ -359,36 +359,11 @@ int JitterSampleConfig::play() { return _state; } -template -class Halton { -public: - - float eval(int index) const { - float f = 1.0f; - float r = 0.0f; - float invB = 1.0f / (float)B; - index++; // Indices start at 1, not 0 - - while (index > 0) { - f = f * invB; - r = r + f * (float)(index % B); - index = index / B; - - } - - return r; - } - -}; - - JitterSample::SampleSequence::SampleSequence(){ // Halton sequence (2,3) - Halton<2> genX; - Halton<3> genY; for (int i = 0; i < SEQUENCE_LENGTH; i++) { - offsets[i] = glm::vec2(genX.eval(i), genY.eval(i)); + offsets[i] = glm::vec2(evaluateHalton<2>(i), evaluateHalton<3>(i)); offsets[i] -= vec2(0.5f); } offsets[SEQUENCE_LENGTH] = glm::vec2(0.0f); diff --git a/libraries/shared/src/RandomAndNoise.h b/libraries/shared/src/RandomAndNoise.h new file mode 100644 index 0000000000..c69c186159 --- /dev/null +++ b/libraries/shared/src/RandomAndNoise.h @@ -0,0 +1,47 @@ +// +// RandomAndNoise.h +// +// Created by Olivier Prat on 05/16/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef RANDOM_AND_NOISE_H +#define RANDOM_AND_NOISE_H + +#include + +// Low discrepancy Halton sequence generator +template +float evaluateHalton(int index) { + float f = 1.0f; + float r = 0.0f; + float invB = 1.0f / (float)B; + index++; // Indices start at 1, not 0 + + while (index > 0) { + f = f * invB; + r = r + f * (float)(index % B); + index = index / B; + + } + + return r; +} + +inline float getRadicalInverseVdC(uint32_t bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10f; // / 0x100000000\n" +} + +// Low discrepancy Hammersley 2D sequence generator +inline glm::vec2 evaluateHammersley(int k, const int sequenceLength) { + return glm::vec2(float(k) / float(sequenceLength), getRadicalInverseVdC(k)); +} + +#endif \ No newline at end of file From d71c6c28de460b1a962d2f545038532eb640c4d1 Mon Sep 17 00:00:00 2001 From: Angus Antley Date: Wed, 27 Mar 2019 12:00:50 -0700 Subject: [PATCH 027/286] added the new hand anims to the resource, and added hand reset to initAnimGraph --- .../avatar/animations/emote_heart_left.fbx | Bin 0 -> 463104 bytes .../avatar/animations/emote_heart_right.fbx | Bin 0 -> 455344 bytes .../avatar/animations/emote_horns_left.fbx | Bin 0 -> 463168 bytes .../avatar/animations/emote_horns_right.fbx | Bin 0 -> 455344 bytes .../avatar/animations/emote_okay_left.fbx | Bin 0 -> 460144 bytes .../avatar/animations/emote_okay_right.fbx | Bin 0 -> 452416 bytes .../avatar/animations/emote_open_left.fbx | Bin 0 -> 463152 bytes .../avatar/animations/emote_open_right.fbx | Bin 0 -> 455360 bytes .../avatar/animations/emote_peace_left.fbx | Bin 0 -> 463168 bytes .../avatar/animations/emote_peace_right.fbx | Bin 0 -> 455392 bytes .../avatar/animations/emote_shaka_left.fbx | Bin 0 -> 463136 bytes .../avatar/animations/emote_shaka_right.fbx | Bin 0 -> 455344 bytes libraries/animation/src/Rig.cpp | 14 ++++++++++++++ 13 files changed, 14 insertions(+) create mode 100644 interface/resources/avatar/animations/emote_heart_left.fbx create mode 100644 interface/resources/avatar/animations/emote_heart_right.fbx create mode 100644 interface/resources/avatar/animations/emote_horns_left.fbx create mode 100644 interface/resources/avatar/animations/emote_horns_right.fbx create mode 100644 interface/resources/avatar/animations/emote_okay_left.fbx create mode 100644 interface/resources/avatar/animations/emote_okay_right.fbx create mode 100644 interface/resources/avatar/animations/emote_open_left.fbx create mode 100644 interface/resources/avatar/animations/emote_open_right.fbx create mode 100644 interface/resources/avatar/animations/emote_peace_left.fbx create mode 100644 interface/resources/avatar/animations/emote_peace_right.fbx create mode 100644 interface/resources/avatar/animations/emote_shaka_left.fbx create mode 100644 interface/resources/avatar/animations/emote_shaka_right.fbx diff --git a/interface/resources/avatar/animations/emote_heart_left.fbx b/interface/resources/avatar/animations/emote_heart_left.fbx new file mode 100644 index 0000000000000000000000000000000000000000..a53f1b125ea8eaa04fc4808e383c0c542a950309 GIT binary patch literal 463104 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

hFvs4p+QBz%B9p<;qVj44TYLe+(v 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 Ly>{8!qtX8XGQQJf literal 0 HcmV?d00001 diff --git a/interface/resources/avatar/animations/emote_heart_right.fbx b/interface/resources/avatar/animations/emote_heart_right.fbx new file mode 100644 index 0000000000000000000000000000000000000000..20d3dd1d182b30eb8b7a366e5c603bded539cf58 GIT binary patch literal 455344 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