From ad99a0f053853514867f8891dfb74ea19aad02c7 Mon Sep 17 00:00:00 2001 From: Sam Cake Date: Sun, 17 May 2015 16:57:32 -0700 Subject: [PATCH 001/192] Improving the quality of the normals with a better packing algorithm in th edeferred --- .../render-utils/src/DeferredBufferWrite.slh | 22 ++++++++++++++++--- .../render-utils/src/DeferredGlobalLight.slh | 9 +++++--- libraries/render-utils/src/Model.cpp | 7 ++++++ libraries/render-utils/src/Model.h | 1 + libraries/render-utils/src/TextureCache.cpp | 7 ++++++ libraries/render-utils/src/TextureCache.h | 4 ++++ 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index a7f4055bba..0538b01fac 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -17,6 +17,23 @@ uniform float glowIntensity; // the alpha threshold uniform float alphaThreshold; +uniform sampler2D normalFittingScaleMap; + +vec3 bestFitNormal(vec3 normal) { + vec3 absNorm = abs(normal); + float maxNAbs = max(absNorm.z, max(absNorm.x, absNorm.y)); + + vec2 texcoord = (absNorm.z < maxNAbs ? + (absNorm.y < maxNAbs ? absNorm.yz : absNorm.xz) : + absNorm.xy); + texcoord = (texcoord.x < texcoord.y ? texcoord.yx : texcoord.xy); + texcoord.y /= texcoord.x; + vec3 cN = normal / maxNAbs; + float fittingScale = texture2D(normalFittingScaleMap, texcoord).a; + cN *= fittingScale; + return (cN * 0.5 + 0.5); +} + float evalOpaqueFinalAlpha(float alpha, float mapAlpha) { return mix(alpha * glowIntensity, 1.0 - alpha * glowIntensity, step(mapAlpha, alphaThreshold)); } @@ -26,7 +43,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 diffuse, vec3 specular, discard; } gl_FragData[0] = vec4(diffuse.rgb, alpha); - gl_FragData[1] = vec4(normal, 0.0) * 0.5 + vec4(0.5, 0.5, 0.5, 1.0); + gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); gl_FragData[2] = vec4(specular, shininess / 128.0); } @@ -36,8 +53,7 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 diffuse, vec3 s } gl_FragData[0] = vec4(diffuse.rgb, alpha); - //gl_FragData[1] = vec4(normal, 0.0) * 0.5 + vec4(0.5, 0.5, 0.5, 1.0); - gl_FragData[1] = vec4(normal, 0.0) * 0.5 + vec4(0.5, 0.5, 0.5, 0.5); + gl_FragData[1] = vec4(bestFitNormal(normal), 0.5); gl_FragData[2] = vec4(emissive, shininess / 128.0); } diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 2f312e42d8..debad16e9b 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -111,12 +111,15 @@ vec3 evalSkyboxGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec4 fragEyeVector = invViewMat * vec4(-position, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - vec3 color = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); - + vec3 ambient = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); + vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, specular, gloss); - color += vec3(diffuse + shading.rgb) * shading.w * shadowAttenuation * getLightColor(light) * getLightIntensity(light); + vec3 reflectedDir = reflect(-fragEyeDir, fragNormal); + vec3 skyTexel = evalSkyboxLight(reflectedDir, 1 - gloss).xyz; + vec3 color = ambient + vec3(diffuse + shading.rgb) * shading.w * shadowAttenuation * skyTexel * getLightColor(light) * getLightIntensity(light); + return color; } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 080b63370a..84be730977 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -111,6 +111,8 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, slotBindings.insert(gpu::Shader::Binding(std::string("specularMap"), 2)); slotBindings.insert(gpu::Shader::Binding(std::string("emissiveMap"), 3)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingScaleMap"), 4)); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vertexShader, pixelShader)); gpu::Shader::makeProgram(*program, slotBindings); @@ -186,6 +188,7 @@ void Model::RenderPipelineLib::initLocations(gpu::ShaderPointer& program, Model: locations.texcoordMatrices = program->getUniforms().findLocation("texcoordMatrices"); locations.emissiveParams = program->getUniforms().findLocation("emissiveParams"); locations.glowIntensity = program->getUniforms().findLocation("glowIntensity"); + locations.normalFittingScaleMapUnit = program->getTextures().findLocation("normalFittingScaleMap"); locations.specularTextureUnit = program->getTextures().findLocation("specularMap"); locations.emissiveTextureUnit = program->getTextures().findLocation("emissiveMap"); @@ -2149,6 +2152,10 @@ void Model::pickPrograms(gpu::Batch& batch, RenderMode mode, bool translucent, f if ((locations->glowIntensity > -1) && (mode != RenderArgs::SHADOW_RENDER_MODE)) { GLBATCH(glUniform1f)(locations->glowIntensity, DependencyManager::get()->getIntensity()); } + + if ((locations->normalFittingScaleMapUnit > -1)) { + batch.setUniformTexture(locations->normalFittingScaleMapUnit, DependencyManager::get()->getNormalFittingScaleTexture()); + } } int Model::renderMeshesForModelsInScene(gpu::Batch& batch, RenderMode mode, bool translucent, float alphaThreshold, diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 126e8ad4d1..aa065039e0 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -336,6 +336,7 @@ private: int emissiveTextureUnit; int emissiveParams; int glowIntensity; + int normalFittingScaleMapUnit; int materialBufferUnit; int clusterMatrices; int clusterIndices; diff --git a/libraries/render-utils/src/TextureCache.cpp b/libraries/render-utils/src/TextureCache.cpp index c1817169c9..def6a78a8a 100644 --- a/libraries/render-utils/src/TextureCache.cpp +++ b/libraries/render-utils/src/TextureCache.cpp @@ -148,6 +148,13 @@ const gpu::TexturePointer& TextureCache::getBlueTexture() { return _blueTexture; } +const gpu::TexturePointer& TextureCache::getNormalFittingScaleTexture() { + if (!_NFSTexture) { + _NFSTexture = getTexture(QUrl("http://advances.realtimerendering.com/s2010/Kaplanyan-CryEngine3(SIGGRAPH%202010%20Advanced%20RealTime%20Rendering%20Course)-NormalsFittingTexture.dds")); + } + return _NFSTexture->getGPUTexture(); +} + /// Extra data for creating textures. class TextureExtra { public: diff --git a/libraries/render-utils/src/TextureCache.h b/libraries/render-utils/src/TextureCache.h index 381359ef80..cb14baa9b0 100644 --- a/libraries/render-utils/src/TextureCache.h +++ b/libraries/render-utils/src/TextureCache.h @@ -52,6 +52,9 @@ public: /// Returns the a pale blue texture (useful for a normal map). const gpu::TexturePointer& getBlueTexture(); + // Returns a map used to compress the normals through a fitting scale algorithm + const gpu::TexturePointer& getNormalFittingScaleTexture(); + /// Returns a texture version of an image file gpu::TexturePointer getImageTexture(const QString & path); @@ -109,6 +112,7 @@ private: gpu::TexturePointer _permutationNormalTexture; gpu::TexturePointer _whiteTexture; gpu::TexturePointer _blueTexture; + NetworkTexturePointer _NFSTexture; QHash > _dilatableNetworkTextures; From 0d3f56e5f2e800b4b4b5b835aafec86488d38961 Mon Sep 17 00:00:00 2001 From: Sam Cake Date: Mon, 18 May 2015 12:30:56 -0700 Subject: [PATCH 002/192] playing with the shader --- libraries/render-utils/src/DeferredBufferWrite.slh | 8 ++++++-- libraries/render-utils/src/DeferredGlobalLight.slh | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 0538b01fac..ea14830de2 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -43,8 +43,12 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 diffuse, vec3 specular, discard; } gl_FragData[0] = vec4(diffuse.rgb, alpha); - gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); - gl_FragData[2] = vec4(specular, shininess / 128.0); + if ( gl_FragCoord.x > 1024) { + gl_FragData[1] = vec4(normal * 0.5 + vec3(0.5), 1.0); + } else { + gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); + } + gl_FragData[2] = vec4(specular, shininess / 100.0); } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 diffuse, vec3 specular, float shininess, vec3 emissive) { diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index debad16e9b..ab423bc449 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -118,7 +118,10 @@ vec3 evalSkyboxGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 reflectedDir = reflect(-fragEyeDir, fragNormal); vec3 skyTexel = evalSkyboxLight(reflectedDir, 1 - gloss).xyz; - vec3 color = ambient + vec3(diffuse + shading.rgb) * shading.w * shadowAttenuation * skyTexel * getLightColor(light) * getLightIntensity(light); + // vec3 lightIrradiance = (skyTexel * (gloss) + (1 - gloss) * getLightColor(light)) * getLightIntensity(light); + vec3 lightIrradiance = (getLightColor(light)) * getLightIntensity(light); + + vec3 color = ambient + vec3(diffuse + shading.rgb * skyTexel) * shading.w * shadowAttenuation * lightIrradiance; return color; } From 1e3a55d9d238624817f752e825e950e280793ad1 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 18 May 2015 15:58:08 -0700 Subject: [PATCH 003/192] Stable status regarding normal and specular shading --- libraries/render-utils/src/DeferredBuffer.slh | 2 +- libraries/render-utils/src/DeferredBufferWrite.slh | 2 +- libraries/render-utils/src/DeferredGlobalLight.slh | 13 ++++++++----- libraries/render-utils/src/DeferredLighting.slh | 6 +++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/libraries/render-utils/src/DeferredBuffer.slh b/libraries/render-utils/src/DeferredBuffer.slh index 885fa96543..fc32e81b9d 100755 --- a/libraries/render-utils/src/DeferredBuffer.slh +++ b/libraries/render-utils/src/DeferredBuffer.slh @@ -65,7 +65,7 @@ DeferredFragment unpackDeferredFragment(vec2 texcoord) { frag.diffuse = frag.diffuseVal.xyz; frag.opacity = frag.diffuseVal.w; frag.specular = frag.specularVal.xyz; - frag.gloss = frag.specularVal.w; + frag.gloss = frag.specularVal.w * 128.0; // bring back gloss to [0, 128] return frag; } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index ea14830de2..15432f59c0 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -48,7 +48,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 diffuse, vec3 specular, } else { gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); } - gl_FragData[2] = vec4(specular, shininess / 100.0); + gl_FragData[2] = vec4(specular, shininess / 128.0); } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 diffuse, vec3 specular, float shininess, vec3 emissive) { diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index ab423bc449..6cf9e085ae 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -111,18 +111,21 @@ vec3 evalSkyboxGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec4 fragEyeVector = invViewMat * vec4(-position, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - vec3 ambient = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); + vec3 ambientLight = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, specular, gloss); vec3 reflectedDir = reflect(-fragEyeDir, fragNormal); - vec3 skyTexel = evalSkyboxLight(reflectedDir, 1 - gloss).xyz; + vec3 skyTexel = evalSkyboxLight(reflectedDir, 1 - gloss/128).xyz; - // vec3 lightIrradiance = (skyTexel * (gloss) + (1 - gloss) * getLightColor(light)) * getLightIntensity(light); vec3 lightIrradiance = (getLightColor(light)) * getLightIntensity(light); - vec3 color = ambient + vec3(diffuse + shading.rgb * skyTexel) * shading.w * shadowAttenuation * lightIrradiance; - + vec3 diffuseLight = diffuse * shading.w * shadowAttenuation * lightIrradiance; + + vec3 specularLight = shading.rgb * skyTexel * shading.w * shadowAttenuation * lightIrradiance; + + vec3 color = ambientLight + diffuseLight + specularLight; + return color; } diff --git a/libraries/render-utils/src/DeferredLighting.slh b/libraries/render-utils/src/DeferredLighting.slh index bb37a9e3e8..8c99f8ed6d 100755 --- a/libraries/render-utils/src/DeferredLighting.slh +++ b/libraries/render-utils/src/DeferredLighting.slh @@ -22,8 +22,8 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, vec3 sp vec3 halfDir = normalize(fragEyeDir + fragLightDir); // float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss * 128.0); - float specularPower = pow(max(0.0, dot(halfDir, fragNormal)), gloss * 128.0); - specularPower *= (gloss * 128.0 * 0.125 + 0.25); + float specularPower = pow(max(0.0, dot(halfDir, fragNormal)), gloss); + specularPower *= (gloss * 0.125 + 0.25); float shlickPower = (1.0 - dot(fragLightDir,halfDir)); float shlickPower2 = shlickPower * shlickPower; @@ -43,7 +43,7 @@ vec4 evalBlinnShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, vec3 // Specular Lighting depends on the half vector and the gloss vec3 halfDir = normalize(fragEyeDir + fragLightDir); - float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss * 128.0); + float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss); vec3 reflect = specularPower * specular; return vec4(reflect, diffuse); From a649d9edb06d1719c217d1c3b7005abac8f8b647 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Tue, 19 May 2015 18:02:20 -0700 Subject: [PATCH 004/192] more fooling around --- libraries/render-utils/src/DeferredBuffer.slh | 2 +- .../render-utils/src/DeferredBufferWrite.slh | 2 +- .../render-utils/src/DeferredGlobalLight.slh | 16 ++++++++++------ libraries/render-utils/src/DeferredLighting.slh | 7 ++++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/libraries/render-utils/src/DeferredBuffer.slh b/libraries/render-utils/src/DeferredBuffer.slh index fc32e81b9d..885fa96543 100755 --- a/libraries/render-utils/src/DeferredBuffer.slh +++ b/libraries/render-utils/src/DeferredBuffer.slh @@ -65,7 +65,7 @@ DeferredFragment unpackDeferredFragment(vec2 texcoord) { frag.diffuse = frag.diffuseVal.xyz; frag.opacity = frag.diffuseVal.w; frag.specular = frag.specularVal.xyz; - frag.gloss = frag.specularVal.w * 128.0; // bring back gloss to [0, 128] + frag.gloss = frag.specularVal.w; return frag; } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 15432f59c0..ea14830de2 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -48,7 +48,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 diffuse, vec3 specular, } else { gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); } - gl_FragData[2] = vec4(specular, shininess / 128.0); + gl_FragData[2] = vec4(specular, shininess / 100.0); } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 diffuse, vec3 specular, float shininess, vec3 emissive) { diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 6cf9e085ae..601f38d616 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -92,14 +92,18 @@ vec3 evalAmbienSphereGlobalColor(float shadowAttenuation, vec3 position, vec3 no vec3 fragNormal = normalize(vec3(invViewMat * vec4(normal, 0.0))); vec4 fragEyeVector = invViewMat * vec4(-position, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - - vec3 ambientNormal = fragNormal.xyz; - vec3 color = diffuse.rgb * evalSphericalLight(ambientSphere, ambientNormal).xyz * getLightAmbientIntensity(light); + + vec3 ambientLight = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, specular, gloss); - color += vec3(diffuse + shading.rgb) * shading.w * shadowAttenuation * getLightColor(light) * getLightIntensity(light); + vec3 lightIrradiance = (getLightColor(light)) * getLightIntensity(light); + vec3 diffuseLight = (diffuse * (vec3(1.0) - specular) + specular) * shading.w * shadowAttenuation * lightIrradiance; + + vec3 specularLight = shading.rgb * shading.w * shadowAttenuation * lightIrradiance; + + vec3 color = ambientLight + diffuseLight + specularLight; return color; } @@ -116,11 +120,11 @@ vec3 evalSkyboxGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, specular, gloss); vec3 reflectedDir = reflect(-fragEyeDir, fragNormal); - vec3 skyTexel = evalSkyboxLight(reflectedDir, 1 - gloss/128).xyz; + vec3 skyTexel = evalSkyboxLight(reflectedDir, 1 - gloss).xyz; vec3 lightIrradiance = (getLightColor(light)) * getLightIntensity(light); - vec3 diffuseLight = diffuse * shading.w * shadowAttenuation * lightIrradiance; + vec3 diffuseLight = (diffuse * (vec3(1.0) - specular) + skyTexel * specular) * shading.w * shadowAttenuation * lightIrradiance; vec3 specularLight = shading.rgb * skyTexel * shading.w * shadowAttenuation * lightIrradiance; diff --git a/libraries/render-utils/src/DeferredLighting.slh b/libraries/render-utils/src/DeferredLighting.slh index 8c99f8ed6d..721a3ff9c7 100755 --- a/libraries/render-utils/src/DeferredLighting.slh +++ b/libraries/render-utils/src/DeferredLighting.slh @@ -22,13 +22,14 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, vec3 sp vec3 halfDir = normalize(fragEyeDir + fragLightDir); // float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss * 128.0); - float specularPower = pow(max(0.0, dot(halfDir, fragNormal)), gloss); - specularPower *= (gloss * 0.125 + 0.25); + float thegloss = pow(2.0, 13.0 * gloss); + float specularPower = pow(max(0.0, dot(halfDir, fragNormal)), thegloss); + specularPower *= (thegloss * 0.125 + 0.25); float shlickPower = (1.0 - dot(fragLightDir,halfDir)); float shlickPower2 = shlickPower * shlickPower; float shlickPower5 = shlickPower2 * shlickPower2 * shlickPower; - vec3 schlick = specular * (1.0 - shlickPower5) + vec3(shlickPower5); + vec3 schlick = specular + (vec3(1.0) - specular) * shlickPower5 / (4.0 - 3.0 * gloss); vec3 reflect = specularPower * schlick; return vec4(reflect, diffuse); From 3ad7bdc79e145f0dc447b75adcb8211c112e7445 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 20 May 2015 14:34:02 -0700 Subject: [PATCH 005/192] maybe ready for master --- interface/src/Application.cpp | 2 +- libraries/render-utils/src/DeferredBufferWrite.slh | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 37ee116f40..fd64054a7e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3302,7 +3302,7 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, RenderArgs skybox = skyStage->getSkybox(); if (skybox) { gpu::Batch batch; - model::Skybox::render(batch, _viewFrustum, *skybox); + model::Skybox::render(batch, *getDisplayViewFrustum(), *skybox); gpu::GLBackend::renderBatch(batch); glUseProgram(0); diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index ea14830de2..0538b01fac 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -43,12 +43,8 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 diffuse, vec3 specular, discard; } gl_FragData[0] = vec4(diffuse.rgb, alpha); - if ( gl_FragCoord.x > 1024) { - gl_FragData[1] = vec4(normal * 0.5 + vec3(0.5), 1.0); - } else { - gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); - } - gl_FragData[2] = vec4(specular, shininess / 100.0); + gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); + gl_FragData[2] = vec4(specular, shininess / 128.0); } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 diffuse, vec3 specular, float shininess, vec3 emissive) { From 1f1fd1362a3efd3f63f74616964ea8e7c08b3673 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 20 May 2015 16:16:22 -0700 Subject: [PATCH 006/192] INcluding the texture for normal best fit --- .../images/NormalsFittingTexture.dds | Bin 0 -> 1398229 bytes libraries/render-utils/src/TextureCache.cpp | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 interface/resources/images/NormalsFittingTexture.dds diff --git a/interface/resources/images/NormalsFittingTexture.dds b/interface/resources/images/NormalsFittingTexture.dds new file mode 100644 index 0000000000000000000000000000000000000000..8207ee09816ea5867e564c6de12dadbc3dea4920 GIT binary patch literal 1398229 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󷐾_50$=jlX+vHq?^WnpLDKbK5R$a<)+03jK2vy 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%0Aov-d~)fqTc0 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 z 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

V6Fz{wAb~w{r1L!huWJeX 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ÎiV{BvFwr$(S)>_+IYpu1`T5GM9 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ⅆ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 +#include "PathUtils.h" + TextureCache::TextureCache() : _permutationNormalTexture(0), _whiteTexture(0), @@ -150,7 +152,8 @@ const gpu::TexturePointer& TextureCache::getBlueTexture() { const gpu::TexturePointer& TextureCache::getNormalFittingScaleTexture() { if (!_NFSTexture) { - _NFSTexture = getTexture(QUrl("http://advances.realtimerendering.com/s2010/Kaplanyan-CryEngine3(SIGGRAPH%202010%20Advanced%20RealTime%20Rendering%20Course)-NormalsFittingTexture.dds")); + // _NFSTexture = getTexture(QUrl("http://advances.realtimerendering.com/s2010/Kaplanyan-CryEngine3(SIGGRAPH%202010%20Advanced%20RealTime%20Rendering%20Course)-NormalsFittingTexture.dds")); + _NFSTexture = getTexture(QUrl::fromLocalFile(PathUtils::resourcesPath() + "images/NormalsFittingTexture.dds")); } return _NFSTexture->getGPUTexture(); } From a388ba7b005aacd85a538ff9ed755925f421d5fa Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 21 Aug 2015 16:45:52 -0700 Subject: [PATCH 007/192] fix conflicts --- interface/src/Application.cpp | 9 ----- libraries/render-utils/src/Model.cpp | 39 --------------------- libraries/render-utils/src/Model.h | 4 --- libraries/render-utils/src/TextureCache.cpp | 20 ----------- libraries/render-utils/src/TextureCache.h | 9 ----- 5 files changed, 81 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a7765b37aa..50d60b4cb3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3419,21 +3419,12 @@ namespace render { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... atmosphere..."); -<<<<<<< HEAD - } - } else if (skyStage->getBackgroundMode() == model::SunSkyStage::SKY_BOX) { - skybox = skyStage->getSkybox(); - if (skybox) { - gpu::Batch batch; - model::Skybox::render(batch, *getDisplayViewFrustum(), *skybox); -======= background->_environment->renderAtmospheres(batch, *(args->_viewFrustum)); } } } else if (skyStage->getBackgroundMode() == model::SunSkyStage::SKY_BOX) { PerformanceTimer perfTimer("skybox"); ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 skybox = skyStage->getSkybox(); if (skybox) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index b2604ec90a..b642e5f955 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -186,12 +186,7 @@ void Model::RenderPipelineLib::initLocations(gpu::ShaderPointer& program, Model: locations.texcoordMatrices = program->getUniforms().findLocation("texcoordMatrices"); locations.emissiveParams = program->getUniforms().findLocation("emissiveParams"); locations.glowIntensity = program->getUniforms().findLocation("glowIntensity"); -<<<<<<< HEAD - locations.normalFittingScaleMapUnit = program->getTextures().findLocation("normalFittingScaleMap"); - -======= locations.normalFittingMapUnit = program->getTextures().findLocation("normalFittingMap"); ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 locations.specularTextureUnit = program->getTextures().findLocation("specularMap"); locations.emissiveTextureUnit = program->getTextures().findLocation("emissiveMap"); locations.materialBufferUnit = program->getBuffers().findLocation("materialBuffer"); @@ -1783,42 +1778,8 @@ void Model::pickPrograms(gpu::Batch& batch, RenderMode mode, bool translucent, f } if ((locations->glowIntensity > -1) && (mode != RenderArgs::SHADOW_RENDER_MODE)) { -<<<<<<< HEAD - GLBATCH(glUniform1f)(locations->glowIntensity, DependencyManager::get()->getIntensity()); - } - - if ((locations->normalFittingScaleMapUnit > -1)) { - batch.setUniformTexture(locations->normalFittingScaleMapUnit, DependencyManager::get()->getNormalFittingScaleTexture()); - } -} - -int Model::renderMeshesForModelsInScene(gpu::Batch& batch, RenderMode mode, bool translucent, float alphaThreshold, - bool hasLightmap, bool hasTangents, bool hasSpecular, bool isSkinned, bool isWireframe, RenderArgs* args) { - - PROFILE_RANGE(__FUNCTION__); - int meshPartsRendered = 0; - - bool pickProgramsNeeded = true; - Locations* locations = nullptr; - - foreach(Model* model, _modelsInScene) { - QVector* whichList = model->pickMeshList(translucent, alphaThreshold, hasLightmap, hasTangents, hasSpecular, isSkinned, isWireframe); - if (whichList) { - QVector& list = *whichList; - if (list.size() > 0) { - if (pickProgramsNeeded) { - pickPrograms(batch, mode, translucent, alphaThreshold, hasLightmap, hasTangents, hasSpecular, isSkinned, isWireframe, args, locations); - pickProgramsNeeded = false; - } - - model->setupBatchTransform(batch, args); - meshPartsRendered += model->renderMeshesFromList(list, batch, mode, translucent, alphaThreshold, args, locations); - } - } -======= const float DEFAULT_GLOW_INTENSITY = 1.0f; // FIXME - glow is removed batch._glUniform1f(locations->glowIntensity, DEFAULT_GLOW_INTENSITY); ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 } if ((locations->normalFittingMapUnit > -1)) { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 1dd6c6b0e8..e55bff6aca 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -337,11 +337,7 @@ private: int emissiveTextureUnit; int emissiveParams; int glowIntensity; -<<<<<<< HEAD - int normalFittingScaleMapUnit; -======= int normalFittingMapUnit; ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 int materialBufferUnit; int clusterMatrices; int clusterIndices; diff --git a/libraries/render-utils/src/TextureCache.cpp b/libraries/render-utils/src/TextureCache.cpp index bcf6818669..deeec58f49 100644 --- a/libraries/render-utils/src/TextureCache.cpp +++ b/libraries/render-utils/src/TextureCache.cpp @@ -29,18 +29,7 @@ #include "RenderUtilsLogging.h" -<<<<<<< HEAD -#include "PathUtils.h" - -TextureCache::TextureCache() : - _permutationNormalTexture(0), - _whiteTexture(0), - _blueTexture(0), - _frameBufferSize(100, 100) -{ -======= TextureCache::TextureCache() { ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 const qint64 TEXTURE_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; setUnusedResourceCacheSize(TEXTURE_DEFAULT_UNUSED_MAX_SIZE); } @@ -133,14 +122,6 @@ const gpu::TexturePointer& TextureCache::getBlueTexture() { return _blueTexture; } -<<<<<<< HEAD -const gpu::TexturePointer& TextureCache::getNormalFittingScaleTexture() { - if (!_NFSTexture) { - // _NFSTexture = getTexture(QUrl("http://advances.realtimerendering.com/s2010/Kaplanyan-CryEngine3(SIGGRAPH%202010%20Advanced%20RealTime%20Rendering%20Course)-NormalsFittingTexture.dds")); - _NFSTexture = getTexture(QUrl::fromLocalFile(PathUtils::resourcesPath() + "images/NormalsFittingTexture.dds")); - } - return _NFSTexture->getGPUTexture(); -======= const gpu::TexturePointer& TextureCache::getBlackTexture() { if (!_blackTexture) { _blackTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC4, gpu::UINT8, gpu::RGBA), 1, 1)); @@ -155,7 +136,6 @@ const gpu::TexturePointer& TextureCache::getNormalFittingTexture() { _normalFittingTexture = getImageTexture(PathUtils::resourcesPath() + "images/normalFittingScale.dds"); } return _normalFittingTexture; ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 } /// Extra data for creating textures. diff --git a/libraries/render-utils/src/TextureCache.h b/libraries/render-utils/src/TextureCache.h index 95ea9a7469..eeb17f07b9 100644 --- a/libraries/render-utils/src/TextureCache.h +++ b/libraries/render-utils/src/TextureCache.h @@ -51,16 +51,11 @@ public: /// Returns the a pale blue texture (useful for a normal map). const gpu::TexturePointer& getBlueTexture(); -<<<<<<< HEAD - // Returns a map used to compress the normals through a fitting scale algorithm - const gpu::TexturePointer& getNormalFittingScaleTexture(); -======= /// Returns the a black texture (useful for a default). const gpu::TexturePointer& getBlackTexture(); // Returns a map used to compress the normals through a fitting scale algorithm const gpu::TexturePointer& getNormalFittingTexture(); ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 /// Returns a texture version of an image file static gpu::TexturePointer getImageTexture(const QString& path); @@ -83,12 +78,8 @@ private: gpu::TexturePointer _whiteTexture; gpu::TexturePointer _grayTexture; gpu::TexturePointer _blueTexture; -<<<<<<< HEAD - NetworkTexturePointer _NFSTexture; -======= gpu::TexturePointer _blackTexture; gpu::TexturePointer _normalFittingTexture; ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 QHash > _dilatableNetworkTextures; }; From 4fabc25b3e2893b5fd1cdc5d30d1fe2092eb2447 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 21 Aug 2015 16:49:01 -0700 Subject: [PATCH 008/192] Fixing conflicts in shaders --- .../render-utils/src/DeferredBufferWrite.slh | 22 ++----------------- libraries/render-utils/src/Model.cpp | 3 --- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index deea3f6cb9..1c1330f0c0 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -21,11 +21,7 @@ uniform float glowIntensity; // the alpha threshold uniform float alphaThreshold; -<<<<<<< HEAD -uniform sampler2D normalFittingScaleMap; -======= uniform sampler2D normalFittingMap; ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 vec3 bestFitNormal(vec3 normal) { vec3 absNorm = abs(normal); @@ -37,11 +33,8 @@ vec3 bestFitNormal(vec3 normal) { texcoord = (texcoord.x < texcoord.y ? texcoord.yx : texcoord.xy); texcoord.y /= texcoord.x; vec3 cN = normal / maxNAbs; -<<<<<<< HEAD - float fittingScale = texture2D(normalFittingScaleMap, texcoord).a; -======= + float fittingScale = texture(normalFittingMap, texcoord).a; ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 cN *= fittingScale; return (cN * 0.5 + 0.5); } @@ -57,15 +50,10 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 diffuse, vec3 specular, if (alpha != glowIntensity) { discard; } -<<<<<<< HEAD - gl_FragData[0] = vec4(diffuse.rgb, alpha); - gl_FragData[1] = vec4(bestFitNormal(normal), 1.0); - gl_FragData[2] = vec4(specular, shininess / 128.0); -======= + _fragColor0 = vec4(diffuse.rgb, alpha); _fragColor1 = vec4(bestFitNormal(normal), 1.0); _fragColor2 = vec4(specular, shininess / 128.0); ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 diffuse, vec3 specular, float shininess, vec3 emissive) { @@ -73,16 +61,10 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 diffuse, vec3 s discard; } -<<<<<<< HEAD - gl_FragData[0] = vec4(diffuse.rgb, alpha); - gl_FragData[1] = vec4(bestFitNormal(normal), 0.5); - gl_FragData[2] = vec4(emissive, shininess / 128.0); -======= _fragColor0 = vec4(diffuse.rgb, alpha); //_fragColor1 = vec4(normal, 0.0) * 0.5 + vec4(0.5, 0.5, 0.5, 1.0); _fragColor1 = vec4(bestFitNormal(normal), 0.5); _fragColor2 = vec4(emissive, shininess / 128.0); ->>>>>>> 518cf3be1504234eb0dc22906876e292b2186f57 } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 diffuse, vec3 specular, float shininess) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index b642e5f955..8a704486b1 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -110,9 +110,6 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), 4)); slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), DeferredLightingEffect::NORMAL_FITTING_MAP_SLOT)); - - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingScaleMap"), 4)); - gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vertexShader, pixelShader)); gpu::Shader::makeProgram(*program, slotBindings); From 01f48072392768b495a78db6a59d030aea7d1dd9 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 21 Aug 2015 16:51:57 -0700 Subject: [PATCH 009/192] Merging and getting back original files --- .../render-utils/src/DeferredGlobalLight.slh | 27 +++++-------------- .../render-utils/src/DeferredLighting.slh | 9 +++---- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 5329fc8cb5..fe8e74361a 100755 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -89,18 +89,14 @@ vec3 evalAmbienSphereGlobalColor(float shadowAttenuation, vec3 position, vec3 no vec3 fragNormal = normalize(vec3(invViewMat * vec4(normal, 0.0))); vec4 fragEyeVector = invViewMat * vec4(-position, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - - vec3 ambientLight = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); + + vec3 ambientNormal = fragNormal.xyz; + vec3 color = diffuse.rgb * evalSphericalLight(ambientSphere, ambientNormal).xyz * getLightAmbientIntensity(light); vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, specular, gloss); - vec3 lightIrradiance = (getLightColor(light)) * getLightIntensity(light); + color += vec3(diffuse + shading.rgb) * shading.w * shadowAttenuation * getLightColor(light) * getLightIntensity(light); - vec3 diffuseLight = (diffuse * (vec3(1.0) - specular) + specular) * shading.w * shadowAttenuation * lightIrradiance; - - vec3 specularLight = shading.rgb * shading.w * shadowAttenuation * lightIrradiance; - - vec3 color = ambientLight + diffuseLight + specularLight; return color; } @@ -112,20 +108,11 @@ vec3 evalSkyboxGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec4 fragEyeVector = invViewMat * vec4(-position, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - vec3 ambientLight = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); - + vec3 color = diffuse.rgb * evalSphericalLight(ambientSphere, fragNormal).xyz * getLightAmbientIntensity(light); + vec4 shading = evalFragShading(fragNormal, -getLightDirection(light), fragEyeDir, specular, gloss); - vec3 reflectedDir = reflect(-fragEyeDir, fragNormal); - vec3 skyTexel = evalSkyboxLight(reflectedDir, 1 - gloss).xyz; - - vec3 lightIrradiance = (getLightColor(light)) * getLightIntensity(light); - - vec3 diffuseLight = (diffuse * (vec3(1.0) - specular) + skyTexel * specular) * shading.w * shadowAttenuation * lightIrradiance; - - vec3 specularLight = shading.rgb * skyTexel * shading.w * shadowAttenuation * lightIrradiance; - - vec3 color = ambientLight + diffuseLight + specularLight; + color += vec3(diffuse + shading.rgb) * shading.w * shadowAttenuation * getLightColor(light) * getLightIntensity(light); return color; } diff --git a/libraries/render-utils/src/DeferredLighting.slh b/libraries/render-utils/src/DeferredLighting.slh index 721a3ff9c7..bb37a9e3e8 100755 --- a/libraries/render-utils/src/DeferredLighting.slh +++ b/libraries/render-utils/src/DeferredLighting.slh @@ -22,14 +22,13 @@ vec4 evalPBRShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, vec3 sp vec3 halfDir = normalize(fragEyeDir + fragLightDir); // float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss * 128.0); - float thegloss = pow(2.0, 13.0 * gloss); - float specularPower = pow(max(0.0, dot(halfDir, fragNormal)), thegloss); - specularPower *= (thegloss * 0.125 + 0.25); + float specularPower = pow(max(0.0, dot(halfDir, fragNormal)), gloss * 128.0); + specularPower *= (gloss * 128.0 * 0.125 + 0.25); float shlickPower = (1.0 - dot(fragLightDir,halfDir)); float shlickPower2 = shlickPower * shlickPower; float shlickPower5 = shlickPower2 * shlickPower2 * shlickPower; - vec3 schlick = specular + (vec3(1.0) - specular) * shlickPower5 / (4.0 - 3.0 * gloss); + vec3 schlick = specular * (1.0 - shlickPower5) + vec3(shlickPower5); vec3 reflect = specularPower * schlick; return vec4(reflect, diffuse); @@ -44,7 +43,7 @@ vec4 evalBlinnShading(vec3 fragNormal, vec3 fragLightDir, vec3 fragEyeDir, vec3 // Specular Lighting depends on the half vector and the gloss vec3 halfDir = normalize(fragEyeDir + fragLightDir); - float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss); + float specularPower = pow(facingLight * max(0.0, dot(halfDir, fragNormal)), gloss * 128.0); vec3 reflect = specularPower * specular; return vec4(reflect, diffuse); From 799a3cae551fcd5f45a124075c7fd36a1bbf23e9 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 21 Aug 2015 20:14:06 -0700 Subject: [PATCH 010/192] adding the top model Asset --- libraries/model/src/model/Asset.cpp | 20 ++++++++++++++++++++ libraries/model/src/model/Asset.h | 27 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 libraries/model/src/model/Asset.cpp create mode 100644 libraries/model/src/model/Asset.h diff --git a/libraries/model/src/model/Asset.cpp b/libraries/model/src/model/Asset.cpp new file mode 100644 index 0000000000..d839dad809 --- /dev/null +++ b/libraries/model/src/model/Asset.cpp @@ -0,0 +1,20 @@ +// +// Asset.cpp +// libraries/model/src/model +// +// Created by Sam Gateau on 08/21/2015. +// Copyright 2015 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 "Asset.h" + +using namespace model; + +Asset::Asset() { +} + +Asset::~Asset() { +} + diff --git a/libraries/model/src/model/Asset.h b/libraries/model/src/model/Asset.h new file mode 100644 index 0000000000..a18cabba28 --- /dev/null +++ b/libraries/model/src/model/Asset.h @@ -0,0 +1,27 @@ +// +// Asset.h +// libraries/model/src/model +// +// Created by Sam Gateau on 08/21/2015. +// Copyright 2015 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_model_Asset_h +#define hifi_model_Asset_h + +#include +#include + + +namespace model { + +class Asset { +public: +}; + +typedef std::shared_ptr< Asset > AssetPointer; + +}; +#endif \ No newline at end of file From d9e326258f9312b51693bf09a4e9cf05e97de2fc Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 21 Aug 2015 23:24:33 -0700 Subject: [PATCH 011/192] Take 0 --- libraries/model/src/model/Asset.h | 66 ++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/libraries/model/src/model/Asset.h b/libraries/model/src/model/Asset.h index a18cabba28..f9cc5ee4f4 100644 --- a/libraries/model/src/model/Asset.h +++ b/libraries/model/src/model/Asset.h @@ -11,14 +11,76 @@ #ifndef hifi_model_Asset_h #define hifi_model_Asset_h -#include -#include +#include +#include +#include "Material.h" +#include "Geometry.h" namespace model { +template +class Table { +public: + typedef std::vector< T > Vector; + typedef int ID; + + const ID INVALID_ID = 0; + + enum Version { + DRAFT = 0, + FINAL, + NUM_VERSIONS, + }; + + Table() { + for (auto e : _elements) { + e.resize(0); + } + } + ~Table() {} + + ID add(Version v, const T& element) { + switch (v) { + case DRAFT: { + _elements[DRAFT].push_back(element); + return ID(-(_elements[DRAFT].size() - 1)); + break; + } + case FINAL: { + _elements[FINAL].push_back(element); + return ID(_elements[FINAL].size() - 1); + break; + } + } + return INVALID_ID; + } + +protected: + Vector _elements[NUM_VERSIONS]; +}; + +typedef Table< MaterialPointer > MaterialTable; +typedef Table< MeshPointer > MeshTable; + class Asset { public: + + + Asset(); + ~Asset(); + + MeshTable& editMeshes() { return _meshes; } + const MeshTable& getMeshes() const { return _meshes; } + + MaterialTable& editMaterials() { return _materials; } + const MaterialTable& getMaterials() const { return _materials; } + +protected: + + MeshTable _meshes; + MaterialTable _materials; + }; typedef std::shared_ptr< Asset > AssetPointer; From c9ddaf78a127fa6381be938d0886411e87092180 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 24 Aug 2015 10:22:32 -0700 Subject: [PATCH 012/192] Very simple draft of the Asset concept in model library --- libraries/model/src/model/Asset.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model/src/model/Asset.h b/libraries/model/src/model/Asset.h index f9cc5ee4f4..28e1caf4aa 100644 --- a/libraries/model/src/model/Asset.h +++ b/libraries/model/src/model/Asset.h @@ -40,7 +40,7 @@ public: } ~Table() {} - ID add(Version v, const T& element) { + ID add(const T& element, Version v = FINAL) { switch (v) { case DRAFT: { _elements[DRAFT].push_back(element); From a6db88f2571488621602c75b9be3a566cb231d41 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 24 Aug 2015 14:00:48 -0700 Subject: [PATCH 013/192] converting material and mesh from srgb to linear in pipeline --- libraries/model/src/model/Material.cpp | 12 ++++++++++ libraries/model/src/model/Material.h | 4 ++++ libraries/model/src/model/Material.slh | 32 +++++++++++++++++++++++++- libraries/render-utils/src/model.slf | 4 +++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index 7e52212fd1..c422d75454 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -13,6 +13,18 @@ using namespace model; using namespace gpu; +float componentSRGBToLinear(float cs) { + if (cs > 0.04045) { + return pow(((cs + 0.055)/1.055), 2.4); + } else { + return cs / 12.92; + } +} + +glm::vec3 convertSRGBToLinear(const glm::vec3& srgb) { + return glm::vec3(componentSRGBToLinear(srgb.x), componentSRGBToLinear(srgb.y), componentSRGBToLinear(srgb.z)); +} + Material::Material() : _key(0), _schemaBuffer(), diff --git a/libraries/model/src/model/Material.h b/libraries/model/src/model/Material.h index a1a17d29e9..e729eac603 100755 --- a/libraries/model/src/model/Material.h +++ b/libraries/model/src/model/Material.h @@ -23,6 +23,10 @@ namespace model { +static glm::vec3 convertSRGBToLinear(const glm::vec3& srgb); + + + // Material Key is a coarse trait description of a material used to classify the materials class MaterialKey { public: diff --git a/libraries/model/src/model/Material.slh b/libraries/model/src/model/Material.slh index 61b433b432..f2fa0a2a25 100644 --- a/libraries/model/src/model/Material.slh +++ b/libraries/model/src/model/Material.slh @@ -26,8 +26,38 @@ Material getMaterial() { return _mat; } +float componentSRGBToLinear(float cs) { + /* sRGB to linear conversion + { cs / 12.92, cs <= 0.04045 + cl = { + { ((cs + 0.055)/1.055)^2.4, cs > 0.04045 + + constants: + T = 0.04045 + A = 1 / 1.055 = 0.94786729857 + B = 0.055 * A = 0.05213270142 + C = 1 / 12.92 = 0.0773993808 + G = 2.4 + */ + const float T = 0.04045; + const float A = 0.947867; + const float B = 0.052132; + const float C = 0.077399; + const float G = 2.4; + + if (cs > T) { + return pow((cs * A + B), G); + } else { + return cs * C; + } +} + +vec3 SRGBToLinear(vec3 srgb) { + return vec3(componentSRGBToLinear(srgb.x),componentSRGBToLinear(srgb.y),componentSRGBToLinear(srgb.z)); +} + float getMaterialOpacity(Material m) { return m._diffuse.a; } -vec3 getMaterialDiffuse(Material m) { return m._diffuse.rgb; } +vec3 getMaterialDiffuse(Material m) { return (gl_FragCoord.x > 800 ? SRGBToLinear(m._diffuse.rgb) : m._diffuse.rgb); } vec3 getMaterialSpecular(Material m) { return m._specular.rgb; } float getMaterialShininess(Material m) { return m._specular.a; } diff --git a/libraries/render-utils/src/model.slf b/libraries/render-utils/src/model.slf index f455030f6f..93a4a29988 100755 --- a/libraries/render-utils/src/model.slf +++ b/libraries/render-utils/src/model.slf @@ -28,12 +28,14 @@ void main(void) { // Fetch diffuse map vec4 diffuse = texture(diffuseMap, _texCoord0); + vec3 vertexColor = (gl_FragCoord.y > 400 ? SRGBToLinear(_color.rgb) : _color.rgb); + Material mat = getMaterial(); packDeferredFragment( normalize(_normal.xyz), evalOpaqueFinalAlpha(getMaterialOpacity(mat), diffuse.a), - getMaterialDiffuse(mat) * diffuse.rgb * _color, + getMaterialDiffuse(mat) * diffuse.rgb * vertexColor, getMaterialSpecular(mat), getMaterialShininess(mat)); } From 753d44cb4cb68a41f92dcaa83e0acda6c7698250 Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Tue, 25 Aug 2015 12:53:46 -0700 Subject: [PATCH 014/192] platform.js --- examples/example/entities/platform.js | 1057 +++++++++++++++++++++++++ 1 file changed, 1057 insertions(+) create mode 100644 examples/example/entities/platform.js diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js new file mode 100644 index 0000000000..4f20614f65 --- /dev/null +++ b/examples/example/entities/platform.js @@ -0,0 +1,1057 @@ +// +// platform.js +// +// Created by Seiji Emery on 8/19/15 +// Copyright 2015 High Fidelity, Inc. +// +// Entity stress test / procedural demo. +// Spawns a platform under your avatar made up of random cubes or spheres. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var SCRIPT_NAME = "platform.js"; + +Script.include("../../libraries/uiwidgets.js"); +// Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); + +// Platform script +(function () { + +var SCRIPT_NAME = "platform.js"; +var USE_DEBUG_LOG = false; +var LOG_ENTITY_CREATION_MESSAGES = false; +var LOG_UPDATE_STATUS_MESSAGES = false; + +var MAX_UPDATE_INTERVAL = 0.2; // restrict to 5 updates / sec + +var AVATAR_HEIGHT_OFFSET = 1.5; + +// Initial state +var NUM_PLATFORM_ENTITIES = 400; +var RADIUS = 5.0; + +// Limits for the user controls on platform radius, density, and dimensions (width, height, depth). +// Color limits are hardcoded at [0, 255]. +var UI_RADIUS_RANGE = [ 1.0, 15.0 ]; +var UI_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (actually, just don't go above 20k entities... >.>) +var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; + +// Used to detect teleportation. If user moves faster than this over one time step (times dt), +// then we trigger a complete rebuild at their new height. +var MAX_ACCELERATION_THRESHOLD = 20.0; + +// Utils +(function () { + if (typeof(Math.randRange) === 'undefined') { + Math.randRange = function (min, max) { + return Math.random() * (max - min) + min; + } + } + if (typeof(Math.randInt) === 'undefined') { + Math.randInt = function (n) { + return Math.floor(Math.random() * n) | 0; + } + } + function fromComponents (r, g, b, a) { + this.red = r; + this.green = g; + this.blue = b; + this.alpha = a || 1.0; + } + function fromHex (c) { + this.red = parseInt(c[1] + c[2], 16); + this.green = parseInt(c[3] + c[4], 16); + this.blue = parseInt(c[5] + c[6], 16); + } + var Color = this.Color = function () { + if (arguments.length >= 3) { + fromComponents.apply(this, arguments); + } else if (arguments.length == 1 && arguments[0].length == 7 && arguments[0][0] == '#') { + fromHex.apply(this, arguments); + } else { + throw new Error("Invalid arguments to new Color(): " + JSON.stringify(arguments)); + } + } + Color.prototype.toString = function () { + return "[Color: " + JSON.stringify(this) + "]"; + } +})(); + +// RNG models +(function () { + /// Encapsulates a simple color model that generates colors using a linear, pseudo-random color distribution. + var RandomColorModel = this.RandomColorModel = function () { + this.shadeRange = 0; // = 200; + this.minColor = 55; // = 100; + this.redRange = 255; // = 200; + this.greenRange = 0; // = 10; + this.blueRange = 0; // = 0; + }; + /// Generates 4 numbers in [0, 1] corresponding to each color attribute (uniform shade and additive red, green, blue). + /// This is done in a separate step from actually generating the colors, since it allows us to either A) completely + /// rebuild / re-randomize the color values, or B) reuse the RNG values but with different color parameters, which + /// enables us to do realtime color editing on the same visuals (awesome!). + RandomColorModel.prototype.generateSeed = function () { + return [ Math.random(), Math.random(), Math.random(), Math.random() ]; + }; + /// Takes a random 'seed' (4 floats from this.generateSeed()) and calculates a pseudo-random + /// color by combining that with the color model's current parameters. + RandomColorModel.prototype.getRandom = function (r) { + // logMessage("color seed values " + JSON.stringify(r)); + var shade = Math.min(255, this.minColor + r[0] * this.shadeRange); + + // No clamping on the color components, so they may overflow. + // However, this creates some pretty interesting visuals, so we're not "fixing" this. + var color = { + red: shade + r[1] * this.redRange, + green: shade + r[2] * this.greenRange, + blue: shade + r[3] * this.blueRange + }; + // logMessage("this: " + JSON.stringify(this)); + // logMessage("color: " + JSON.stringify(color), COLORS.RED); + return color; + }; + /// Custom property iterator used to setup UI (sliders, etc) + RandomColorModel.prototype.setupUI = function (callback) { + var _this = this; + [ + ['shadeRange', 'shade range'], + ['minColor', 'shade min'], + ['redRange', 'red (additive)'], + ['greenRange', 'green (additive)'], + ['blueRange', 'blue (additive)'] + ].forEach(function (v) { + // name, value, min, max, onValueChanged + callback(v[1], _this[v[0]], 0, 255, function (value) { _this[v[0]] = value }); + }); + } + + /// Generates pseudo-random dimensions for our cubes / shapes. + var RandomShapeModel = this.RandomShapeModel = function () { + this.widthRange = [ 0.3, 0.7 ]; + this.depthRange = [ 0.5, 0.8 ]; + this.heightRange = [ 0.01, 0.08 ]; + }; + /// Generates 3 seed numbers in [0, 1] + RandomShapeModel.prototype.generateSeed = function () { + return [ Math.random(), Math.random(), Math.random() ]; + } + /// Combines seed values with width/height/depth ranges to produce vec3 dimensions for a cube / sphere. + RandomShapeModel.prototype.getRandom = function (r) { + return { + x: r[0] * (this.widthRange[1] - this.widthRange[0]) + this.widthRange[0], + y: r[1] * (this.heightRange[1] - this.heightRange[0]) + this.heightRange[0], + z: r[2] * (this.depthRange[1] - this.depthRange[0]) + this.depthRange[0] + }; + } + /// Custom property iterator used to setup UI (sliders, etc) + RandomShapeModel.prototype.setupUI = function (callback) { + var _this = this; + var dimensionsMin = UI_SHAPE_DIMENSIONS_RANGE[0]; + var dimensionsMax = UI_SHAPE_DIMENSIONS_RANGE[1]; + [ + ['widthRange', 'width'], + ['depthRange', 'depth'], + ['heightRange', 'height'] + ].forEach(function (v) { + // name, value, min, max, onValueChanged + callback(v[1], _this[v[0]], dimensionsMin, dimensionsMax, function (value) { _this[v[0]] = value }); + }); + } + + /// Combines color + shape PRNG models and hides their implementation details. + var RandomAttribModel = this.RandomAttribModel = function () { + this.colorModel = new RandomColorModel(); + this.shapeModel = new RandomShapeModel(); + } + /// Completely re-randomizes obj's `color` and `dimensions` parameters based on the current model params. + RandomAttribModel.prototype.randomizeShapeAndColor = function (obj) { + // logMessage("randomizing " + JSON.stringify(obj)); + obj._colorSeed = this.colorModel.generateSeed(); + obj._shapeSeed = this.shapeModel.generateSeed(); + this.updateShapeAndColor(obj); + // logMessage("color seed: " + JSON.stringify(obj._colorSeed), COLORS.RED); + // logMessage("randomized color: " + JSON.stringify(obj.color), COLORS.RED); + // logMessage("randomized: " + JSON.stringify(obj)); + return obj; + } + /// Updates obj's `color` and `dimensions` params to use the current model params. + /// Reuses hidden seed attribs; _must_ have called randomizeShapeAndColor(obj) at some point before + /// calling this. + RandomAttribModel.prototype.updateShapeAndColor = function (obj) { + try { + // logMessage("update shape and color: " + this.colorModel); + obj.color = this.colorModel.getRandom(obj._colorSeed); + obj.dimensions = this.shapeModel.getRandom(obj._shapeSeed); + } catch (e) { + logMessage("update shape / color failed", COLORS.RED); + logMessage('' + e, COLORS.RED); + logMessage("obj._colorSeed = " + JSON.stringify(obj._colorSeed)); + logMessage("obj._shapeSeed = " + JSON.stringify(obj._shapeSeed)); + // logMessage("obj = " + JSON.stringify(obj)); + throw e; + } + return obj; + } +})(); + +// Status / logging UI (ignore this) +(function () { + var COLORS = this.COLORS = { + 'GREEN': new Color("#2D870C"), + 'RED': new Color("#AF1E07"), + 'LIGHT_GRAY': new Color("#CCCCCC"), + 'DARK_GRAY': new Color("#4E4E4E") + }; + function buildDebugLog () { + var LINE_WIDTH = 400; + var LINE_HEIGHT = 20; + + var lines = []; + var lineIndex = 0; + for (var i = 0; i < 20; ++i) { + lines.push(new UI.Label({ + text: " ", visible: false, + width: LINE_WIDTH, height: LINE_HEIGHT, + })); + } + var title = new UI.Label({ + text: SCRIPT_NAME, visible: true, + width: LINE_WIDTH, height: LINE_HEIGHT, + }); + + var overlay = new UI.Box({ + visible: true, + width: LINE_WIDTH, height: 0, + backgroundColor: COLORS.DARK_GRAY, + backgroundAlpha: 0.3 + }); + overlay.setPosition(280, 10); + relayoutFrom(0); + UI.updateLayout(); + + function relayoutFrom (n) { + var layoutPos = { + x: overlay.position.x, + y: overlay.position.y + }; + + title.setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + + // for (var i = n; i >= 0; --i) { + for (var i = n + 1; i < lines.length; ++i) { + if (lines[i].visible) { + lines[i].setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + } + } + // for (var i = lines.length - 1; i > n; --i) { + for (var i = 0; i <= n; ++i) { + if (lines[i].visible) { + lines[i].setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + } + } + overlay.height = (layoutPos.y - overlay.position.y + 10); + overlay.getOverlay().update({ + height: overlay.height + }); + } + this.logMessage = function (text, color, alpha) { + lines[lineIndex].setVisible(true); + relayoutFrom(lineIndex); + + lines[lineIndex].getOverlay().update({ + text: text, + visible: true, + color: color || COLORS.LIGHT_GRAY, + alpha: alpha !== undefined ? alpha : 1.0, + x: lines[lineIndex].position.x, + y: lines[lineIndex].position.y + }); + lineIndex = (lineIndex + 1) % lines.length; + UI.updateLayout(); + } + } + if (USE_DEBUG_LOG) { + buildDebugLog(); + } else { + this.logMessage = function (msg) { + print(SCRIPT_NAME + ": " + msg); + } + } +})(); + +// Utils (ignore) +(function () { + // Utility function + this.withDefaults = function (properties, defaults) { + // logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults)); + properties = properties || {}; + if (defaults) { + for (var k in defaults) { + properties[k] = defaults[k]; + } + } + return properties; + } + + // Math utils + if (typeof(Math.randRange) === 'undefined') { + Math.randRange = function (min, max) { + return Math.random() * (max - min) + min; + } + } + if (typeof(Math.randInt) === 'undefined') { + Math.randInt = function (n) { + return Math.floor(Math.random() * n) | 0; + } + } + + /// Random distrib: Get a random point within a circle on the xz plane with radius r, center p. + this.randomCirclePoint = function (r, pos) { + var a = Math.random(), b = Math.random(); + if (b < a) { + var tmp = b; + b = a; + a = tmp; + } + var point = { + x: pos.x + b * r * Math.cos(2 * Math.PI * a / b), + y: pos.y, + z: pos.z + b * r * Math.sin(2 * Math.PI * a / b) + }; + if (LOG_ENTITY_CREATION_MESSAGES) { + // logMessage("input params: " + JSON.stringify({ radius: r, position: pos }), COLORS.GREEN); + // logMessage("a = " + a + ", b = " + b); + logMessage("generated point: " + JSON.stringify(point), COLORS.RED); + } + return point; + } + + // Entity utils. NOT using overlayManager for... reasons >.> + var makeEntity = this.makeEntity = function (properties) { + if (LOG_ENTITY_CREATION_MESSAGES) { + logMessage("Creating entity: " + JSON.stringify(properties)); + } + var entity = Entities.addEntity(properties); + return { + update: function (properties) { + Entities.editEntity(entity, properties); + }, + destroy: function () { + Entities.deleteEntity(entity) + } + }; + } + // this.makeLight = function (properties) { + // return makeEntity(withDefaults(properties, { + // type: "Light", + // isSpotlight: false, + // diffuseColor: { red: 255, green: 100, blue: 100 }, + // ambientColor: { red: 200, green: 80, blue: 80 } + // })); + // } + this.makeBox = function (properties) { + // logMessage("Creating box: " + JSON.stringify(properties)); + return makeEntity(withDefaults(properties, { + type: "Box" + })); + } +})(); + +// Platform +(function () { + /// Encapsulates a platform 'piece'. Owns an entity (`box`), and handles destruction and some other state. + var PlatformComponent = this.PlatformComponent = function (properties) { + // logMessage("Platform component initialized with " + Object.keys(properties), COLORS.GREEN); + this.position = properties.position || null; + this.color = properties.color || null; + this.dimensions = properties.dimensions || null; + + if (properties._colorSeed) + this._colorSeed = properties._colorSeed; + if (properties._shapeSeed) + this._shapeSeed = properties._shapeSeed; + + // logMessage("dimensions: " + JSON.stringify(this.dimensions)); + // logMessage("color: " + JSON.stringify(this.color)); + this.box = makeBox({ + dimensions: this.dimensions, + color: this.color, + position: this.position, + alpha: 0.5 + }); + }; + /// Updates platform to be at position p, and calls .update() with the current + /// position, color, and dimensions parameters. + PlatformComponent.prototype.update = function (position) { + if (position) + this.position = position; + // logMessage("updating with " + JSON.stringify(this)); + this.box.update(this); + } + function swap (a, b) { + var tmp = a; + a = b; + b = tmp; + } + /// Swap state with another component + PlatformComponent.prototype.swap = function (other) { + swap(this.position, other.position); + swap(this.dimensions, other.dimensions); + swap(this.color, other.color); + } + PlatformComponent.prototype.destroy = function () { + if (this.box) { + this.box.destroy(); + this.box = null; + } + } + + // util + function inRange (p1, p2, radius) { + return Vec3.distance(p1, p2) < Math.abs(radius); + } + + /// Encapsulates a moving platform that follows the avatar around (mostly). + /// Owns a _large_ amount of entities, + var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) { + this.position = position; + this.radius = radius; + this.randomizer = new RandomAttribModel(); + + var boxes = this.boxes = []; + // logMessage("Spawning " + n + " entities", COLORS.GREEN); + while (n > 0) { + var properties = { position: this.randomPoint() }; + this.randomizer.randomizeShapeAndColor(properties); + // logMessage("properties: " + JSON.stringify(properties)); + boxes.push(new PlatformComponent(properties)); + --n; + } + this.targetDensity = this.getEntityDensity(); + this.pendingUpdates = {}; + this.updateTimer = 0.0; + + this.platformHeight = position.y; + this.oldPos = { x: position.x, y: position.y, z: position.z }; + this.oldRadius = radius; + } + DynamicPlatform.prototype.toString = function () { + return "[DynamicPlatform (" + this.boxes.length + " entities)]"; + } + DynamicPlatform.prototype.updateEntityAttribs = function () { + var _this = this; + this.setPendingUpdate('updateEntityAttribs', function () { + // logMessage("updating model", COLORS.GREEN); + _this.boxes.forEach(function (box) { + this.randomizer.updateShapeAndColor(box); + box.update(); + }, _this); + }); + } + + /// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms). + /// This is to prevent UI code from running full entity updates every 10 ms (or whatever). + DynamicPlatform.prototype.setPendingUpdate = function (name, callback) { + if (!this.pendingUpdates[name]) { + this.pendingUpdates[name] = { + callback: callback, + timer: 0.0, + skippedUpdates: 0 + } + } else { + this.pendingUpdates[name].callback = callback; + this.pendingUpdates[name].skippedUpdates++; + // logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds"); + } + } + /// Runs all queued updates as soon as they can execute (each one has a cooldown timer). + DynamicPlatform.prototype.processPendingUpdates = function (dt) { + for (var k in this.pendingUpdates) { + if (this.pendingUpdates[k].timer >= 0.0) + this.pendingUpdates[k].timer -= dt; + + if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) { + // logMessage("Running update for \"" + k + "\" (skipped " + this.pendingUpdates[k].skippedUpdates + ")"); + try { + this.pendingUpdates[k].callback(); + } catch (e) { + logMessage("update for \"" + k + "\" failed: " + e, COLORS.RED); + } + this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL; + this.pendingUpdates[k].skippedUpdates = 0; + this.pendingUpdates[k].callback = null; + } + } + } + + /// Updates the platform based on the avatar's current position (spawning / despawning entities as needed), + /// and calls processPendingUpdates() once this is done. + /// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full + /// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a + /// normal walking / flying speed. + DynamicPlatform.prototype.update = function (dt, position) { + // logMessage("updating " + this); + position.y = this.platformHeight; + this.position = position; + + var toUpdate = []; + this.boxes.forEach(function (box, i) { + // if (Math.abs(box.position.y - position.y) > HEIGHT_TOLERANCE || !inRange(box, position, radius)) { + if (!inRange(box.position, this.position, this.radius)) { + toUpdate.push(i); + } + }, this); + + var MAX_TRIES = toUpdate.length * 8; + var tries = MAX_TRIES; + var moved = 0; + var recalcs = 0; + toUpdate.forEach(function (index) { + if ((index % 2 == 0) || tries > 0) { + do { + var randomPoint = this.randomPoint(this.position, this.radius); + ++recalcs + } while (--tries > 0 && inRange(randomPoint, this.oldPos, this.oldRadiuss)); + + if (LOG_UPDATE_STATUS_MESSAGES && tries <= 0) { + logMessage("updatePlatform() gave up after " + MAX_TRIES + " iterations (" + moved + " / " + toUpdate.length + " successful updates)", COLORS.RED); + logMessage("old pos: " + JSON.stringify(this.oldPos) + ", old radius: " + this.oldRadius); + logMessage("new pos: " + JSON.stringify(this.position) + ", new radius: " + this.radius); + } + } else { + var randomPoint = this.randomPoint(position, this.radius); + } + + this.randomizer.randomizeShapeAndColor(this.boxes[index]); + this.boxes[index].update(randomPoint); + // this.boxes[index].setValues({ + // position: randomPoint, + // // dimensions: this.randomDimensions(), + // // color: this.randomColor() + // }); + ++moved; + }, this); + recalcs = recalcs - toUpdate.length; + + this.oldPos = position; + this.oldRadius = this.radius; + if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) { + logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs"); + } + + this.processPendingUpdates(dt); + } + DynamicPlatform.prototype.getEntityCount = function () { + return this.boxes.length; + } + + /// Sets the entity count to n. Don't call this directly -- use setRadius / density instead. + DynamicPlatform.prototype.setEntityCount = function (n) { + if (n > this.boxes.length) { + // logMessage("Setting entity count to " + n + " (adding " + (n - this.boxes.length) + " entities)", COLORS.GREEN); + + // Spawn new boxes + n = n - this.boxes.length; + for (; n > 0; --n) { + var properties = { position: this.randomPoint() }; + this.randomizer.randomizeShapeAndColor(properties); + this.boxes.push(new PlatformComponent(properties)); + } + } else if (n < this.boxes.length) { + // logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN); + + // Destroy random boxes (technically, the most recent ones, but it should be sorta random) + n = this.boxes.length - n; + for (; n > 0; --n) { + this.boxes.pop().destroy(); + } + } + } + /// Calculate the entity density based on radial surface area. + DynamicPlatform.prototype.getEntityDensity = function () { + return (this.boxes.length * 1.0) / (Math.PI * this.radius * this.radius); + } + /// Queues a setDensity update. This is expensive, so we don't call it directly from UI. + DynamicPlatform.prototype.setDensityOnNextUpdate = function (density) { + var _this = this; + this.targetDensity = density; + this.setPendingUpdate('density', function () { + _this.updateEntityDensity(density); + }); + } + DynamicPlatform.prototype.updateEntityDensity = function (density) { + this.setEntityCount(Math.floor(density * Math.PI * this.radius * this.radius)); + } + DynamicPlatform.prototype.getRadius = function () { + return this.radius; + } + /// Queues a setRadius update. This is expensive, so we don't call it directly from UI. + DynamicPlatform.prototype.setRadiusOnNextUpdate = function (radius) { + var _this = this; + this.setPendingUpdate('radius', function () { + _this.setRadius(radius); + }); + } + var DEBUG_RADIUS_RECALC = false; + DynamicPlatform.prototype.setRadius = function (radius) { + if (radius < this.radius) { // Reduce case + // logMessage("Setting radius to " + radius + " (shrink by " + (this.radius - radius) + ")", COLORS.GREEN ); + this.radius = radius; + + // Remove all entities outside of current bounds. Requires swapping, since we want to maintain a contiguous array. + // Algorithm: two pointers at front and back. We traverse fwd and back, swapping elems so that all entities in bounds + // are at the front of the array, and all entities out of bounds are at the back. We then pop + destroy all entities + // at the back to reduce the entity count. + var count = this.boxes.length; + var toDelete = 0; + var swapList = []; + if (DEBUG_RADIUS_RECALC) { + logMessage("starting at i = 0, j = " + (count - 1)); + } + for (var i = 0, j = count - 1; i < j; ) { + // Find first elem outside of bounds that we can move to the back + while (inRange(this.boxes[i].position, this.position, this.radius) && i < j) { + ++i; + } + // Find first elem in bounds that we can move to the front + while (!inRange(this.boxes[j].position, this.position, this.radius) && i < j) { + --j; ++toDelete; + } + if (i < j) { + // swapList.push([i, j]); + if (DEBUG_RADIUS_RECALC) { + logMessage("swapping " + i + ", " + j); + } + this.boxes[i].swap(this.boxes[j]); + ++i, --j; ++toDelete; + } else { + if (DEBUG_RADIUS_RECALC) { + logMessage("terminated at i = " + i + ", j = " + j, COLORS.RED); + } + } + } + if (DEBUG_RADIUS_RECALC) { + logMessage("toDelete = " + toDelete, COLORS.RED); + } + // Sanity check + if (toDelete > this.boxes.length) { + logMessage("Error: toDelete " + toDelete + " > entity count " + this.boxes.length + " (setRadius algorithm)", COLORS.RED); + toDelete = this.boxes.length; + } + if (toDelete > 0) { + // logMessage("Deleting " + toDelete + " entities as part of radius resize", COLORS.GREEN); + } + // Delete cleared boxes + for (; toDelete > 0; --toDelete) { + this.boxes.pop().destroy(); + } + // fix entity density (just in case -- we may have uneven entity distribution) + this.updateEntityDensity(this.targetDensity); + } else if (radius > this.radius) { + // Grow case (much simpler) + // logMessage("Setting radius to " + radius + " (grow by " + (radius - this.radius) + ")", COLORS.GREEN); + + // Add entities based on entity density + // var density = this.getEntityDensity(); + var density = this.targetDensity; + var oldArea = Math.PI * this.radius * this.radius; + var n = Math.floor(density * Math.PI * (radius * radius - this.radius * this.radius)); + + if (n > 0) { + // logMessage("Adding " + n + " entities", COLORS.GREEN); + + // Add entities (we use a slightly different algorithm to place them in the area between two concentric circles. + // This is *slightly* less uniform (the reason we're not using this everywhere is entities would be tightly clustered + // at the platform center and become spread out as the radius increases), but the use-case here is just incremental + // radius resizes and the user's not likely to notice the difference). + for (; n > 0; --n) { + var theta = Math.randRange(0.0, Math.PI * 2.0); + var r = Math.randRange(this.radius, radius); + // logMessage("theta = " + theta + ", r = " + r); + var pos = { + x: Math.cos(theta) * r + this.position.x, + y: this.position.y, + z: Math.sin(theta) * r + this.position.y + }; + + var properties = { position: pos }; + this.randomizer.randomizeShapeAndColor(properties); + this.boxes.push(new PlatformComponent(properties)); + } + } + this.radius = radius; + } + } + DynamicPlatform.prototype.updateHeight = function (height) { + this.platformHeight = height; + + // Invalidate current boxes to trigger a rebuild + this.boxes.forEach(function (box) { + box.position.x += this.oldRadius * 100; + }); + // this.update(dt, position, radius); + } + /// Gets a random point within the platform bounds. + /// Should maybe get moved to the RandomAttribModel (would be much cleaner), but this works for now. + DynamicPlatform.prototype.randomPoint = function (position, radius) { + position = position || this.position; + radius = radius !== undefined ? radius : this.radius; + return randomCirclePoint(radius, position); + } + /// Old. The RandomAttribModel replaces this and enables realtime editing of the *****_RANGE params. + // DynamicPlatform.prototype.randomDimensions = function () { + // return { + // x: Math.randRange(WIDTH_RANGE[0], WIDTH_RANGE[1]), + // y: Math.randRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]), + // z: Math.randRange(DEPTH_RANGE[0], DEPTH_RANGE[1]) + // }; + // } + // DynamicPlatform.prototype.randomColor = function () { + // var shade = Math.randRange(SHADE_RANGE[0], SHADE_RANGE[1]); + // // var h = HUE_RANGE; + // return { + // red: shade + Math.randRange(RED_RANGE[0], RED_RANGE[1]) | 0, + // green: shade + Math.randRange(GREEN_RANGE[0], GREEN_RANGE[1]) | 0, + // blue: shade + Math.randRange(BLUE_RANGE[0], BLUE_RANGE[1]) | 0 + // } + // // return COLORS[Math.randInt(COLORS.length)] + // } + + /// Cleanup. + DynamicPlatform.prototype.destroy = function () { + this.boxes.forEach(function (box) { + box.destroy(); + }); + this.boxes = []; + } +})(); + +// UI +(function () { + var CATCH_SETUP_ERRORS = true; + + // Util functions for setting up widgets (the widget library is intended to be used like this) + function makePanel (dir, properties) { + return new UI.WidgetStack(withDefaults(properties, { + dir: dir + })); + } + function addSpacing (parent, width, height) { + parent.add(new UI.Box({ + backgroundAlpha: 0.0, + width: width, height: height + })); + } + function addLabel (parent, text) { + return parent.add(new UI.Label({ + text: text, + width: 200, + height: 20 + })); + } + function addSlider (parent, label, min, max, getValue, onValueChanged) { + try { + var layout = parent.add(new UI.WidgetStack({ dir: "+x" })); + var textLabel = layout.add(new UI.Label({ + text: label, + width: 130, + height: 20 + })); + var valueLabel = layout.add(new UI.Label({ + text: "" + (+getValue().toFixed(1)), + width: 60, + height: 20 + })); + var slider = layout.add(new UI.Slider({ + value: getValue(), minValue: min, maxValue: max, + width: 300, height: 20, + slider: { + width: 30, + height: 18 + }, + onValueChanged: function (value) { + valueLabel.setText("" + (+value.toFixed(1))); + onValueChanged(value, slider); + UI.updateLayout(); + } + })); + return slider; + } catch (e) { + logMessage("" + e, COLORS.RED); + logMessage("parent: " + parent, COLORS.RED); + logMessage("label: " + label, COLORS.RED); + logMessage("min: " + min, COLORS.RED); + logMessage("max: " + max, COLORS.RED); + logMessage("getValue: " + getValue, COLORS.RED); + logMessage("onValueChanged: " + onValueChanged, COLORS.RED); + throw e; + } + } + function addPushButton (parent, label, onClicked, setActive) { + var button = parent.add(new UI.Box({ + text: label, + width: 120, + height: 20 + })); + } + function moveToBottomLeftScreenCorner (widget) { + var border = 5; + var pos = { + x: border, + y: Controller.getViewportDimensions().y - widget.getHeight() - border + }; + if (widget.position.x != pos.x || widget.position.y != pos.y) { + widget.setPosition(pos.x, pos.y); + UI.updateLayout(); + } + } + var _export = this; + + /// Setup the UI. Creates a bunch of sliders for setting the platform radius, density, and entity color / shape properties. + /// The entityCount slider is readonly. + function _setupUI (platform) { + var layoutContainer = makePanel("+y", { visible: true }); + // layoutContainer.setPosition(10, 280); + // makeDraggable(layoutContainer); + _export.onScreenResize = function () { + moveToBottomLeftScreenCorner(layoutContainer); + } + var topSection = layoutContainer.add(makePanel("+x")); addSpacing(layoutContainer, 1, 5); + var btmSection = layoutContainer.add(makePanel("+x")); + + var controls = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); + var buttons = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); + + var colorControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); + var shapeControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); + + // Top controls + addLabel(controls, "Platform (platform.js)"); + controls.radiusSlider = addSlider(controls, "radius", UI_RADIUS_RANGE[0], UI_RADIUS_RANGE[1], function () { return platform.getRadius() }, + function (value) { + platform.setRadiusOnNextUpdate(value); + controls.entityCountSlider.setValue(platform.getEntityCount()); + }); + addSpacing(controls, 1, 2); + controls.densitySlider = addSlider(controls, "entity density", UI_DENSITY_RANGE[0], UI_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, + function (value) { + platform.setDensityOnNextUpdate(value); + controls.entityCountSlider.setValue(platform.getEntityCount()); + }); + addSpacing(controls, 1, 2); + + var minEntities = Math.PI * UI_RADIUS_RANGE[0] * UI_RADIUS_RANGE[0] * UI_DENSITY_RANGE[0]; + var maxEntities = Math.PI * UI_RADIUS_RANGE[1] * UI_RADIUS_RANGE[1] * UI_DENSITY_RANGE[1]; + controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() }, + function (value) {}); + controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions) + controls.entityCountSlider.slider.actions = {}; + + // Bottom controls + + // Iterate over controls (making sliders) for the RNG shape / dimensions model + platform.randomizer.shapeModel.setupUI(function (name, value, min, max, setValue) { + // logMessage("platform.randomizer.shapeModel." + name + " = " + value); + var internal = { + avg: (value[0] + value[1]) * 0.5, + range: Math.abs(value[0] - value[1]) + }; + // logMessage(JSON.stringify(internal), COLORS.GREEN); + addSlider(shapeControls, name + ' avg', min, max, function () { return internal.avg; }, function (value) { + internal.avg = value; + setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); + platform.updateEntityAttribs(); + }); + addSpacing(shapeControls, 1, 2); + addSlider(shapeControls, name + ' range', min, max, function () { return internal.range }, function (value) { + internal.range = value; + setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); + platform.updateEntityAttribs(); + }); + addSpacing(shapeControls, 1, 2); + }); + // Do the same for the color model + platform.randomizer.colorModel.setupUI(function (name, value, min, max, setValue) { + // logMessage("platform.randomizer.colorModel." + name + " = " + value); + addSlider(colorControls, name, min, max, function () { return value; }, function (value) { + setValue(value); + platform.updateEntityAttribs(); + }); + addSpacing(shapeControls, 1, 2); + }); + + moveToBottomLeftScreenCorner(layoutContainer); + layoutContainer.setVisible(true); + } + this.setupUI = function (platform) { + if (CATCH_SETUP_ERRORS) { + try { + _setupUI(platform); + } catch (e) { + logMessage("Error setting up ui: " + e, COLORS.RED); + } + } else { + _setupUI(platform); + } + } +})(); + +// Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog; bad +// for detailed debugging since you lose the file and line num even if the error gets rethrown. + +// Catch errors from init +var CATCH_INIT_ERRORS = true; + +// Catch errors from everything (technically, Script and Controller signals that runs platform / ui code) +var CATCH_ERRORS_FROM_EVENT_UPDATES = false; + +// Setup everything +(function () { + var doLater = null; + if (CATCH_ERRORS_FROM_EVENT_UPDATES) { + function catchErrors (fcn) { + return function () { + try { + fcn.apply(this, arguments); + } catch (e) { + logMessage('' + e, COLORS.RED); + logMessage("while calling " + fcn); + logMessage("Called by: " + arguments.callee.caller); + } + } + } + // We need to do this after the functions are actually registered... + doLater = function () { + // Intercept errors from functions called by Script.update and Script.ScriptEnding. + [ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) { + this[fcn] = catchErrors(this[fcn]); + }); + }; + // These need to be wrapped first though: + + // Intercept errors from UI functions called by Controller.****Event. + [ 'handleMousePress', 'handleMouseMove', 'handleMouseRelease' ].forEach(function (fcn) { + UI[fcn] = catchErrors(UI[fcn]); + }); + } + + function getTargetPlatformPosition () { + var pos = MyAvatar.position; + pos.y -= AVATAR_HEIGHT_OFFSET; + return pos; + } + + var platform = this.platform = null; + var lastHeight = null; + + this.initPlatform = function () { + platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS); + lastHeight = getTargetPlatformPosition().y; + } + + // this.init = function () { + // function _init () { + + // platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS); + // lastHeight = getTargetPlatformPosition().y; + // // Script.update.connect(update); + // // setupUI(platform); + // } + // if (CATCH_INIT_ERRORS) { + // try { + // _init(); + // } catch (e) { + // logMessage("error while initializing: " + e, COLORS.RED); + // } + // } else { + // _init(); + // } + // } + var lastDimensions = Controller.getViewportDimensions(); + function checkScreenDimensions () { + var dimensions = Controller.getViewportDimensions(); + if (dimensions.x != lastDimensions.x || dimensions.y != lastDimensions.y) { + onScreenResize(dimensions.x, dimensions.y); + } + lastDimensions = dimensions; + } + + this.update = function (dt) { + try { + checkScreenDimensions(); + + var pos = getTargetPlatformPosition(); + if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) { + // User likely teleported + logMessage("Height rebuild (" + + "(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" + + " > " + MAX_ACCELERATION_THRESHOLD + ")"); + platform.updateHeight(pos.y); + } + platform.update(dt, getTargetPlatformPosition(), platform.getRadius()); + lastHeight = pos.y; + } catch (e) { + logMessage("" + e, COLORS.RED); + } + } + this.teardown = function () { + try { + platform.destroy(); + UI.teardown(); + + Controller.mousePressEvent.disconnect(UI.handleMousePress); + Controller.mouseMoveEvent.disconnect(UI.handleMouseMove); + Controller.mouseReleaseEvent.disconnect(UI.handleMouseRelease); + } catch (e) { + logMessage("" + e, COLORS.RED); + } + } + + if (doLater) { + doLater(); + } + this.startup = function () { + if (Entities.canAdjustLocks() && Entities.canRez()) { + Script.update.disconnect(this.startup); + + function init () { + logMessage("initializing..."); + + this.initPlatform(); + + Script.update.connect(this.update); + Script.scriptEnding.connect(this.teardown); + + this.setupUI(platform); + + logMessage("finished initializing.", COLORS.GREEN); + } + if (CATCH_INIT_ERRORS) { + try { + init(); + } catch (error) { + logMessage("" + error, COLORS.RED); + } + } else { + init(); + } + + Controller.mousePressEvent.connect(UI.handleMousePress); + Controller.mouseMoveEvent.connect(UI.handleMouseMove); + Controller.mouseReleaseEvent.connect(UI.handleMouseRelease); + } + } +})(); +Script.update.connect(startup); + +})(); + + + + From 1e9fce2a612bd4b031484e6871e74a0226b5cc5c Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Tue, 25 Aug 2015 22:12:51 -0700 Subject: [PATCH 015/192] Drafting the materials for FBXReader --- libraries/fbx/src/FBXReader.cpp | 617 ++++++------------- libraries/fbx/src/FBXReader.h | 121 +++- libraries/fbx/src/FBXReader_Node.cpp | 342 ++++++++++ libraries/fbx/src/OBJReader.cpp | 19 +- libraries/model/src/model/Asset.h | 68 +- libraries/model/src/model/Material.h | 9 + libraries/model/src/model/Material.slh | 29 +- libraries/networking/src/ResourceCache.cpp | 3 +- libraries/render-utils/src/GeometryCache.cpp | 9 +- libraries/render-utils/src/GeometryCache.h | 18 + libraries/render-utils/src/Model.cpp | 10 + libraries/render-utils/src/model.slf | 4 +- libraries/render-utils/src/model.slv | 34 +- 13 files changed, 816 insertions(+), 467 deletions(-) create mode 100644 libraries/fbx/src/FBXReader_Node.cpp diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index f0d13f8792..5d87b5f316 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -38,7 +38,7 @@ //#define DEBUG_FBXREADER using namespace std; - +/* struct TextureParam { glm::vec2 UVTranslation; glm::vec2 UVScaling; @@ -80,22 +80,27 @@ struct TextureParam { {} }; - +*/ bool FBXMesh::hasSpecularTexture() const { - foreach (const FBXMeshPart& part, parts) { +// TODO fix that in model.cpp / payload + /* foreach (const FBXMeshPart& part, parts) { if (!part.specularTexture.filename.isEmpty()) { return true; } } + */ return false; } bool FBXMesh::hasEmissiveTexture() const { + // TODO Fix that in model cpp / payload + /* foreach (const FBXMeshPart& part, parts) { if (!part.emissiveTexture.filename.isEmpty()) { return true; } } + */ return false; } @@ -184,325 +189,7 @@ static int fbxGeometryMetaTypeId = qRegisterMetaType(); static int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); static int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); -template int streamSize() { - return sizeof(T); -} -template int streamSize() { - return 1; -} - -template QVariant readBinaryArray(QDataStream& in, int& position) { - quint32 arrayLength; - quint32 encoding; - quint32 compressedLength; - - in >> arrayLength; - in >> encoding; - in >> compressedLength; - position += sizeof(quint32) * 3; - - QVector values; - const unsigned int DEFLATE_ENCODING = 1; - if (encoding == DEFLATE_ENCODING) { - // preface encoded data with uncompressed length - QByteArray compressed(sizeof(quint32) + compressedLength, 0); - *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); - in.readRawData(compressed.data() + sizeof(quint32), compressedLength); - position += compressedLength; - QByteArray uncompressed = qUncompress(compressed); - QDataStream uncompressedIn(uncompressed); - uncompressedIn.setByteOrder(QDataStream::LittleEndian); - uncompressedIn.setVersion(QDataStream::Qt_4_5); // for single/double precision switch - for (quint32 i = 0; i < arrayLength; i++) { - T value; - uncompressedIn >> value; - values.append(value); - } - } else { - for (quint32 i = 0; i < arrayLength; i++) { - T value; - in >> value; - position += streamSize(); - values.append(value); - } - } - return QVariant::fromValue(values); -} - -QVariant parseBinaryFBXProperty(QDataStream& in, int& position) { - char ch; - in.device()->getChar(&ch); - position++; - switch (ch) { - case 'Y': { - qint16 value; - in >> value; - position += sizeof(qint16); - return QVariant::fromValue(value); - } - case 'C': { - bool value; - in >> value; - position++; - return QVariant::fromValue(value); - } - case 'I': { - qint32 value; - in >> value; - position += sizeof(qint32); - return QVariant::fromValue(value); - } - case 'F': { - float value; - in >> value; - position += sizeof(float); - return QVariant::fromValue(value); - } - case 'D': { - double value; - in >> value; - position += sizeof(double); - return QVariant::fromValue(value); - } - case 'L': { - qint64 value; - in >> value; - position += sizeof(qint64); - return QVariant::fromValue(value); - } - case 'f': { - return readBinaryArray(in, position); - } - case 'd': { - return readBinaryArray(in, position); - } - case 'l': { - return readBinaryArray(in, position); - } - case 'i': { - return readBinaryArray(in, position); - } - case 'b': { - return readBinaryArray(in, position); - } - case 'S': - case 'R': { - quint32 length; - in >> length; - position += sizeof(quint32) + length; - return QVariant::fromValue(in.device()->read(length)); - } - default: - throw QString("Unknown property type: ") + ch; - } -} - -FBXNode parseBinaryFBXNode(QDataStream& in, int& position) { - qint32 endOffset; - quint32 propertyCount; - quint32 propertyListLength; - quint8 nameLength; - - in >> endOffset; - in >> propertyCount; - in >> propertyListLength; - in >> nameLength; - position += sizeof(quint32) * 3 + sizeof(quint8); - - FBXNode node; - const int MIN_VALID_OFFSET = 40; - if (endOffset < MIN_VALID_OFFSET || nameLength == 0) { - // use a null name to indicate a null node - return node; - } - node.name = in.device()->read(nameLength); - position += nameLength; - - for (quint32 i = 0; i < propertyCount; i++) { - node.properties.append(parseBinaryFBXProperty(in, position)); - } - - while (endOffset > position) { - FBXNode child = parseBinaryFBXNode(in, position); - if (child.name.isNull()) { - return node; - - } else { - node.children.append(child); - } - } - - return node; -} - -class Tokenizer { -public: - - Tokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } - - enum SpecialToken { - NO_TOKEN = -1, - NO_PUSHBACKED_TOKEN = -1, - DATUM_TOKEN = 0x100 - }; - - int nextToken(); - const QByteArray& getDatum() const { return _datum; } - - void pushBackToken(int token) { _pushedBackToken = token; } - void ungetChar(char ch) { _device->ungetChar(ch); } - -private: - - QIODevice* _device; - QByteArray _datum; - int _pushedBackToken; -}; - -int Tokenizer::nextToken() { - if (_pushedBackToken != NO_PUSHBACKED_TOKEN) { - int token = _pushedBackToken; - _pushedBackToken = NO_PUSHBACKED_TOKEN; - return token; - } - - char ch; - while (_device->getChar(&ch)) { - if (QChar(ch).isSpace()) { - continue; // skip whitespace - } - switch (ch) { - case ';': - _device->readLine(); // skip the comment - break; - - case ':': - case '{': - case '}': - case ',': - return ch; // special punctuation - - case '\"': - _datum = ""; - while (_device->getChar(&ch)) { - if (ch == '\"') { // end on closing quote - break; - } - if (ch == '\\') { // handle escaped quotes - if (_device->getChar(&ch) && ch != '\"') { - _datum.append('\\'); - } - } - _datum.append(ch); - } - return DATUM_TOKEN; - - default: - _datum = ""; - _datum.append(ch); - while (_device->getChar(&ch)) { - if (QChar(ch).isSpace() || ch == ';' || ch == ':' || ch == '{' || ch == '}' || ch == ',' || ch == '\"') { - ungetChar(ch); // read until we encounter a special character, then replace it - break; - } - _datum.append(ch); - } - return DATUM_TOKEN; - } - } - return NO_TOKEN; -} - -FBXNode parseTextFBXNode(Tokenizer& tokenizer) { - FBXNode node; - - if (tokenizer.nextToken() != Tokenizer::DATUM_TOKEN) { - return node; - } - node.name = tokenizer.getDatum(); - - if (tokenizer.nextToken() != ':') { - return node; - } - - int token; - bool expectingDatum = true; - while ((token = tokenizer.nextToken()) != Tokenizer::NO_TOKEN) { - if (token == '{') { - for (FBXNode child = parseTextFBXNode(tokenizer); !child.name.isNull(); child = parseTextFBXNode(tokenizer)) { - node.children.append(child); - } - return node; - } - if (token == ',') { - expectingDatum = true; - - } else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) { - QByteArray datum = tokenizer.getDatum(); - if ((token = tokenizer.nextToken()) == ':') { - tokenizer.ungetChar(':'); - tokenizer.pushBackToken(Tokenizer::DATUM_TOKEN); - return node; - - } else { - tokenizer.pushBackToken(token); - node.properties.append(datum); - expectingDatum = false; - } - } else { - tokenizer.pushBackToken(token); - return node; - } - } - - return node; -} - -FBXNode parseFBX(QIODevice* device) { - // verify the prolog - const QByteArray BINARY_PROLOG = "Kaydara FBX Binary "; - if (device->peek(BINARY_PROLOG.size()) != BINARY_PROLOG) { - // parse as a text file - FBXNode top; - Tokenizer tokenizer(device); - while (device->bytesAvailable()) { - FBXNode next = parseTextFBXNode(tokenizer); - if (next.name.isNull()) { - return top; - - } else { - top.children.append(next); - } - } - return top; - } - QDataStream in(device); - in.setByteOrder(QDataStream::LittleEndian); - in.setVersion(QDataStream::Qt_4_5); // for single/double precision switch - - // see http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ for an explanation - // of the FBX binary format - - // skip the rest of the header - const int HEADER_SIZE = 27; - in.skipRawData(HEADER_SIZE); - int position = HEADER_SIZE; - - // parse the top-level node - FBXNode top; - while (device->bytesAvailable()) { - FBXNode next = parseBinaryFBXNode(in, position); - if (next.name.isNull()) { - return top; - - } else { - top.children.append(next); - } - } - - return top; -} QVector createVec4Vector(const QVector& doubleVector) { QVector values; @@ -693,7 +380,7 @@ public: glm::vec3 rotationMax; // radians }; -glm::mat4 getGlobalTransform(const QMultiHash& parentMap, +glm::mat4 getGlobalTransform(const QMultiHash& _connectionParentMap, const QHash& models, QString nodeID, bool mixamoHack) { glm::mat4 globalTransform; while (!nodeID.isNull()) { @@ -704,7 +391,7 @@ glm::mat4 getGlobalTransform(const QMultiHash& parentMap, // there's something weird about the models from Mixamo Fuse; they don't skin right with the full transform return globalTransform; } - QList parentIDs = parentMap.values(nodeID); + QList parentIDs = _connectionParentMap.values(nodeID); nodeID = QString(); foreach (const QString& parentID, parentIDs) { if (models.contains(parentID)) { @@ -738,17 +425,6 @@ void printNode(const FBXNode& node, int indentLevel) { } } -class Material { -public: - glm::vec3 diffuse; - glm::vec3 specular; - glm::vec3 emissive; - float shininess; - float opacity; - QString id; - model::MaterialPointer _material; -}; - class Cluster { public: QVector indices; @@ -756,19 +432,19 @@ public: glm::mat4 transformLink; }; -void appendModelIDs(const QString& parentID, const QMultiHash& childMap, +void appendModelIDs(const QString& parentID, const QMultiHash& _connectionChildMap, QHash& models, QSet& remainingModels, QVector& modelIDs) { if (remainingModels.contains(parentID)) { modelIDs.append(parentID); remainingModels.remove(parentID); } int parentIndex = modelIDs.size() - 1; - foreach (const QString& childID, childMap.values(parentID)) { + foreach (const QString& childID, _connectionChildMap.values(parentID)) { if (remainingModels.contains(childID)) { FBXModel& model = models[childID]; if (model.parentIndex == -1) { model.parentIndex = parentIndex; - appendModelIDs(childID, childMap, models, remainingModels, modelIDs); + appendModelIDs(childID, _connectionChildMap, models, remainingModels, modelIDs); } } } @@ -1235,11 +911,11 @@ void addBlendshapes(const ExtractedBlendshape& extracted, const QList& parentMap, +QString getTopModelID(const QMultiHash& _connectionParentMap, const QHash& models, const QString& modelID) { QString topID = modelID; forever { - foreach (const QString& parentID, parentMap.values(topID)) { + foreach (const QString& parentID, _connectionParentMap.values(topID)) { if (models.contains(parentID)) { topID = parentID; goto outerContinue; @@ -1303,10 +979,10 @@ FBXTexture getTexture(const QString& textureID, return texture; } -bool checkMaterialsHaveTextures(const QHash& materials, - const QHash& textureFilenames, const QMultiHash& childMap) { +bool checkMaterialsHaveTextures(const QHash& materials, + const QHash& textureFilenames, const QMultiHash& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { - foreach (const QString& childID, childMap.values(materialID)) { + foreach (const QString& childID, _connectionChildMap.values(materialID)) { if (textureFilenames.contains(childID)) { return true; } @@ -1530,29 +1206,21 @@ QByteArray fileOnUrl(const QByteArray& filenameString, const QString& url) { return filename; } -FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) { + const FBXNode& node = _fbxNode; QHash meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; QHash ooChildToParent; QVector blendshapes; - QMultiHash parentMap; - QMultiHash childMap; + QHash models; QHash clusters; QHash animationCurves; - QHash textureNames; - QHash textureFilenames; - QHash textureParams; - QHash textureContent; - QHash materials; + QHash typeFlags; - QHash diffuseTextures; - QHash bumpTextures; - QHash specularTextures; - QHash emissiveTextures; - QHash ambientTextures; + QHash localRotations; QHash xComponents; QHash yComponents; @@ -1580,8 +1248,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping QString jointLeftToeID; QString jointRightToeID; - float lightmapOffset = 0.0f; - + QVector humanIKJointNames; for (int i = 0;; i++) { QByteArray jointName = HUMANIK_JOINTS[i]; @@ -1618,6 +1285,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping FBXGeometry* geometryPtr = new FBXGeometry; FBXGeometry& geometry = *geometryPtr; + geometry._asset.reset(new model::Asset()); + float unitScaleFactor = 1.0f; glm::vec3 ambientColor; QString hifiGlobalNodeID; @@ -1942,8 +1611,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping textureContent.insert(filename, content); } } else if (object.name == "Material") { - Material material = { glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(), 96.0f, 1.0f, - QString(""), model::MaterialPointer(NULL)}; + FBXMaterial material = { glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(), glm::vec2(0.f, 1.0f), 96.0f, 1.0f, + QString(""), model::MaterialTable::INVALID_ID}; foreach (const FBXNode& subobject, object.children) { bool properties = false; QByteArray propertyName; @@ -1962,13 +1631,13 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping foreach (const FBXNode& property, subobject.children) { if (property.name == propertyName) { if (property.properties.at(0) == "DiffuseColor") { - material.diffuse = getVec3(property.properties, index); + material.diffuseColor = getVec3(property.properties, index); } else if (property.properties.at(0) == "SpecularColor") { - material.specular = getVec3(property.properties, index); + material.specularColor = getVec3(property.properties, index); } else if (property.properties.at(0) == "Emissive") { - material.emissive = getVec3(property.properties, index); + material.emissiveColor = getVec3(property.properties, index); } else if (property.properties.at(0) == "Shininess") { material.shininess = property.properties.at(index).value(); @@ -1999,25 +1668,9 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } #endif } - material.id = getID(object.properties); + material.materialID = getID(object.properties); - material._material = make_shared(); - material._material->setEmissive(material.emissive); - if (glm::all(glm::equal(material.diffuse, glm::vec3(0.0f)))) { - material._material->setDiffuse(material.diffuse); - } else { - material._material->setDiffuse(material.diffuse); - } - material._material->setMetallic(glm::length(material.specular)); - material._material->setGloss(material.shininess); - - if (material.opacity <= 0.0f) { - material._material->setOpacity(1.0f); - } else { - material._material->setOpacity(material.opacity); - } - - materials.insert(material.id, material); + _fbxMaterials.insert(material.materialID, material); } else if (object.name == "NodeAttribute") { #if defined(DEBUG_FBXREADER) @@ -2106,11 +1759,11 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping if (!hifiGlobalNodeID.isEmpty() && (parentID == hifiGlobalNodeID)) { std::map< QString, FBXLight >::iterator lit = lights.find(childID); if (lit != lights.end()) { - lightmapLevel = (*lit).second.intensity; - if (lightmapLevel <= 0.0f) { - loadLightmaps = false; + _lightmapLevel = (*lit).second.intensity; + if (_lightmapLevel <= 0.0f) { + _loadLightmaps = false; } - lightmapOffset = glm::clamp((*lit).second.color.x, 0.f, 1.f); + _lightmapOffset = glm::clamp((*lit).second.color.x, 0.f, 1.f); } } } @@ -2144,18 +1797,18 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } else if (type.contains("shininess")) { counter++; - } else if (loadLightmaps && type.contains("emissive")) { + } else if (_loadLightmaps && type.contains("emissive")) { emissiveTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); - } else if (loadLightmaps && type.contains("ambient")) { + } else if (_loadLightmaps && type.contains("ambient")) { ambientTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else { QString typenam = type.data(); counter++; } } - parentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); - childMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } } } @@ -2184,15 +1837,15 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { std::map< QString, FBXLight >::iterator l = lights.begin(); - lightmapLevel = (*l).second.intensity; + _lightmapLevel = (*l).second.intensity; } } // assign the blendshapes to their corresponding meshes foreach (const ExtractedBlendshape& extracted, blendshapes) { - QString blendshapeChannelID = parentMap.value(extracted.id); - QString blendshapeID = parentMap.value(blendshapeChannelID); - QString meshID = parentMap.value(blendshapeID); + QString blendshapeChannelID = _connectionParentMap.value(extracted.id); + QString blendshapeID = _connectionParentMap.value(blendshapeChannelID); + QString meshID = _connectionParentMap.value(blendshapeID); addBlendshapes(extracted, blendshapeChannelIndices.values(blendshapeChannelID), meshes[meshID]); } @@ -2209,23 +1862,23 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping QSet remainingModels; for (QHash::const_iterator model = models.constBegin(); model != models.constEnd(); model++) { // models with clusters must be parented to the cluster top - foreach (const QString& deformerID, childMap.values(model.key())) { - foreach (const QString& clusterID, childMap.values(deformerID)) { + foreach (const QString& deformerID, _connectionChildMap.values(model.key())) { + foreach (const QString& clusterID, _connectionChildMap.values(deformerID)) { if (!clusters.contains(clusterID)) { continue; } - QString topID = getTopModelID(parentMap, models, childMap.value(clusterID)); - childMap.remove(parentMap.take(model.key()), model.key()); - parentMap.insert(model.key(), topID); + QString topID = getTopModelID(_connectionParentMap, models, _connectionChildMap.value(clusterID)); + _connectionChildMap.remove(_connectionParentMap.take(model.key()), model.key()); + _connectionParentMap.insert(model.key(), topID); goto outerBreak; } } outerBreak: // make sure the parent is in the child map - QString parent = parentMap.value(model.key()); - if (!childMap.contains(parent, model.key())) { - childMap.insert(parent, model.key()); + QString parent = _connectionParentMap.value(model.key()); + if (!_connectionChildMap.contains(parent, model.key())) { + _connectionChildMap.insert(parent, model.key()); } remainingModels.insert(model.key()); } @@ -2236,8 +1889,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping first = id; } } - QString topID = getTopModelID(parentMap, models, first); - appendModelIDs(parentMap.value(topID), childMap, models, remainingModels, modelIDs); + QString topID = getTopModelID(_connectionParentMap, models, first); + appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, models, remainingModels, modelIDs); } // figure the number of animation frames from the curves @@ -2298,7 +1951,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = model.name; - foreach (const QString& childID, childMap.values(modelID)) { + foreach (const QString& childID, _connectionChildMap.values(modelID)) { QString type = typeFlags.value(childID); if (!type.isEmpty()) { geometry.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); @@ -2350,8 +2003,11 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping geometry.bindExtents.reset(); geometry.meshExtents.reset(); + // Create the Material Library + consolidateFBXMaterials(); + // see if any materials have texture children - bool materialsHaveTextures = checkMaterialsHaveTextures(materials, textureFilenames, childMap); + bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, textureFilenames, _connectionChildMap); for (QHash::iterator it = meshes.begin(); it != meshes.end(); it++) { ExtractedMesh& extracted = it.value(); @@ -2359,8 +2015,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping extracted.mesh.meshExtents.reset(); // accumulate local transforms - QString modelID = models.contains(it.key()) ? it.key() : parentMap.value(it.key()); - glm::mat4 modelTransform = getGlobalTransform(parentMap, models, modelID, geometry.applicationName == "mixamo.com"); + QString modelID = models.contains(it.key()) ? it.key() : _connectionParentMap.value(it.key()); + glm::mat4 modelTransform = getGlobalTransform(_connectionParentMap, models, modelID, geometry.applicationName == "mixamo.com"); // compute the mesh extents from the transformed vertices foreach (const glm::vec3& vertex, extracted.mesh.vertices) { @@ -2374,14 +2030,19 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } // look for textures, material properties + // allocate the Part material library int materialIndex = 0; int textureIndex = 0; bool generateTangents = false; - QList children = childMap.values(modelID); + QList children = _connectionChildMap.values(modelID); for (int i = children.size() - 1; i >= 0; i--) { + const QString& childID = children.at(i); - if (materials.contains(childID)) { - Material material = materials.value(childID); + if (_fbxMaterials.contains(childID)) { + // the pure material associated with this part + FBXMaterial material = _fbxMaterials.value(childID); + + bool detectDifferentUVs = false; FBXTexture diffuseTexture; QString diffuseTextureID = diffuseTextures.value(childID); @@ -2389,7 +2050,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); // FBX files generated by 3DSMax have an intermediate texture parent, apparently - foreach (const QString& childTextureID, childMap.values(diffuseTextureID)) { + foreach (const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { if (textureFilenames.contains(childTextureID)) { diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); } @@ -2420,12 +2081,12 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping FBXTexture emissiveTexture; glm::vec2 emissiveParams(0.f, 1.f); - emissiveParams.x = lightmapOffset; - emissiveParams.y = lightmapLevel; + emissiveParams.x = _lightmapOffset; + emissiveParams.y = _lightmapLevel; QString emissiveTextureID = emissiveTextures.value(childID); QString ambientTextureID = ambientTextures.value(childID); - if (loadLightmaps && (!emissiveTextureID.isNull() || !ambientTextureID.isNull())) { + if (_loadLightmaps && (!emissiveTextureID.isNull() || !ambientTextureID.isNull())) { if (!emissiveTextureID.isNull()) { emissiveTexture = getTexture(emissiveTextureID, textureNames, textureFilenames, textureContent, textureParams); @@ -2447,10 +2108,10 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping if (extracted.partMaterialTextures.at(j).first == materialIndex) { FBXMeshPart& part = extracted.mesh.parts[j]; - part._material = material._material; - part.diffuseColor = material.diffuse; - part.specularColor = material.specular; - part.emissiveColor = material.emissive; + /* part._material = material._material; + part.diffuseColor = material.diffuseColor; + part.specularColor = material.specularColor; + part.emissiveColor = material.emissiveColor; part.shininess = material.shininess; part.opacity = material.opacity; if (!diffuseTexture.filename.isNull()) { @@ -2466,8 +2127,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping part.emissiveTexture = emissiveTexture; } part.emissiveParams = emissiveParams; - - part.materialID = material.id; + */ + part.materialID = material.materialID; } } materialIndex++; @@ -2477,7 +2138,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) { - extracted.mesh.parts[j].diffuseTexture = texture; + // extracted.mesh.parts[j].diffuseTexture = texture; } } textureIndex++; @@ -2509,8 +2170,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping // find the clusters with which the mesh is associated QVector clusterIDs; - foreach (const QString& childID, childMap.values(it.key())) { - foreach (const QString& clusterID, childMap.values(childID)) { + foreach (const QString& childID, _connectionChildMap.values(it.key())) { + foreach (const QString& clusterID, _connectionChildMap.values(childID)) { if (!clusters.contains(clusterID)) { continue; } @@ -2520,7 +2181,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping // see http://stackoverflow.com/questions/13566608/loading-skinning-information-from-fbx for a discussion // of skinning information in FBX - QString jointID = childMap.value(clusterID); + QString jointID = _connectionChildMap.value(clusterID); fbxCluster.jointIndex = modelIDs.indexOf(jointID); if (fbxCluster.jointIndex == -1) { qCDebug(modelformat) << "Joint not in model list: " << jointID; @@ -2773,5 +2434,113 @@ FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const } FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { - return extractFBXGeometry(parseFBX(device), mapping, url, loadLightmaps, lightmapLevel); + FBXReader reader; + reader._fbxNode = FBXReader::parseFBX(device); + reader._loadLightmaps = loadLightmaps; + reader._lightmapLevel = lightmapLevel; + + return reader.extractFBXGeometry(mapping, url); +} + + +bool FBXMaterial::needTangentSpace() const { + return !normalTexture.isNull(); +} + + +void FBXReader::consolidateFBXMaterials() { + + // foreach (const QString& materialID, materials) { + for (QHash::iterator it = _fbxMaterials.begin(); it != _fbxMaterials.end(); it++) { + FBXMaterial& material = (*it); + // the pure material associated with this part + bool detectDifferentUVs = false; + FBXTexture diffuseTexture; + QString diffuseTextureID = diffuseTextures.value(material.materialID); + if (!diffuseTextureID.isNull()) { + diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); + + // FBX files generated by 3DSMax have an intermediate texture parent, apparently + foreach (const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { + if (textureFilenames.contains(childTextureID)) { + diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); + } + } + + // TODO associate this per part + //diffuseTexture.texcoordSet = matchTextureUVSetToAttributeChannel(diffuseTexture.texcoordSetName, extracted.texcoordSetMap); + + material.diffuseTexture = diffuseTexture; + + detectDifferentUVs = (diffuseTexture.texcoordSet != 0) || (!diffuseTexture.transform.isIdentity()); + } + + FBXTexture normalTexture; + QString bumpTextureID = bumpTextures.value(material.materialID); + if (!bumpTextureID.isNull()) { + normalTexture = getTexture(bumpTextureID, textureNames, textureFilenames, textureContent, textureParams); + + // TODO Need to generate tangent space at association per part + //generateTangents = true; + + // TODO at per part association time + // normalTexture.texcoordSet = matchTextureUVSetToAttributeChannel(normalTexture.texcoordSetName, extracted.texcoordSetMap); + + material.normalTexture = normalTexture; + + detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); + } + + FBXTexture specularTexture; + QString specularTextureID = specularTextures.value(material.materialID); + if (!specularTextureID.isNull()) { + specularTexture = getTexture(specularTextureID, textureNames, textureFilenames, textureContent, textureParams); + // TODO at per part association time + // specularTexture.texcoordSet = matchTextureUVSetToAttributeChannel(specularTexture.texcoordSetName, extracted.texcoordSetMap); + detectDifferentUVs |= (specularTexture.texcoordSet != 0) || (!specularTexture.transform.isIdentity()); + } + + FBXTexture emissiveTexture; + glm::vec2 emissiveParams(0.f, 1.f); + emissiveParams.x = _lightmapOffset; + emissiveParams.y = _lightmapLevel; + + QString emissiveTextureID = emissiveTextures.value(material.materialID); + QString ambientTextureID = ambientTextures.value(material.materialID); + if (_loadLightmaps && (!emissiveTextureID.isNull() || !ambientTextureID.isNull())) { + + if (!emissiveTextureID.isNull()) { + emissiveTexture = getTexture(emissiveTextureID, textureNames, textureFilenames, textureContent, textureParams); + emissiveParams.y = 4.0f; + } else if (!ambientTextureID.isNull()) { + emissiveTexture = getTexture(ambientTextureID, textureNames, textureFilenames, textureContent, textureParams); + } + + // TODO : do this at per part association + //emissiveTexture.texcoordSet = matchTextureUVSetToAttributeChannel(emissiveTexture.texcoordSetName, extracted.texcoordSetMap); + + material.emissiveParams = emissiveParams; + material.emissiveTexture = emissiveTexture; + + + detectDifferentUVs |= (emissiveTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); + } + + // Finally create the true material representation + material._material = make_shared(); + material._material->setEmissive(material.emissiveColor); + if (glm::all(glm::equal(material.diffuseColor, glm::vec3(0.0f)))) { + material._material->setDiffuse(material.diffuseColor); + } else { + material._material->setDiffuse(material.diffuseColor); + } + material._material->setMetallic(glm::length(material.specularColor)); + material._material->setGloss(material.shininess); + + if (material.opacity <= 0.0f) { + material._material->setOpacity(1.0f); + } else { + material._material->setOpacity(material.opacity); + } + } } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 471a9c1777..26ded3e9f5 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -28,6 +28,7 @@ #include #include +#include class QIODevice; class FBXNode; @@ -89,6 +90,15 @@ public: glm::mat4 inverseBindMatrix; }; + +// The true texture image which can be used for different textures +class FBXTextureImage { +public: + QString name; + QByteArray filename; + QByteArray content; +}; + /// A texture map in an FBX document. class FBXTexture { public: @@ -99,6 +109,8 @@ public: Transform transform; int texcoordSet; QString texcoordSetName; + + bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); } }; /// A single part of a mesh (with the same material). @@ -109,7 +121,7 @@ public: QVector triangleIndices; // original indices from the FBX mesh mutable gpu::BufferPointer quadsAsTrianglesIndicesBuffer; - glm::vec3 diffuseColor; + /* glm::vec3 diffuseColor; glm::vec3 specularColor; glm::vec3 emissiveColor; glm::vec2 emissiveParams; @@ -120,15 +132,38 @@ public: FBXTexture normalTexture; FBXTexture specularTexture; FBXTexture emissiveTexture; - +*/ QString materialID; - model::MaterialPointer _material; + // model::MaterialPointer _material; mutable bool trianglesForQuadsAvailable = false; mutable int trianglesForQuadsIndicesCount = 0; gpu::BufferPointer getTrianglesForQuads() const; }; +class FBXMaterial { +public: + glm::vec3 diffuseColor; + glm::vec3 specularColor; + glm::vec3 emissiveColor; + glm::vec2 emissiveParams; + float shininess; + float opacity; + + QString materialID; + model::MaterialTable::ID _modelMaterialID; + model::MaterialPointer _material; + + FBXTexture diffuseTexture; + FBXTexture opacityTexture; + FBXTexture normalTexture; + FBXTexture specularTexture; + FBXTexture emissiveTexture; + + bool needTangentSpace() const; + +}; + /// A single mesh (with optional blendshapes) extracted from an FBX document. class FBXMesh { public: @@ -220,7 +255,7 @@ public: bool hasSkeletonJoints; QVector meshes; - + glm::mat4 offset; int leftEyeJointIndex = -1; @@ -266,6 +301,8 @@ public: QString getModelNameOfMesh(int meshIndex) const; QList blendshapeChannelNames; + + model::AssetPointer _asset; }; Q_DECLARE_METATYPE(FBXGeometry) @@ -278,4 +315,80 @@ FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const /// \exception QString if an error occurs in parsing FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +struct TextureParam { + glm::vec2 UVTranslation; + glm::vec2 UVScaling; + glm::vec4 cropping; + QString UVSet; + + glm::vec3 translation; + glm::vec3 rotation; + glm::vec3 scaling; + uint8_t alphaSource; + uint8_t currentTextureBlendMode; + bool useMaterial; + + template + bool assign(T& ref, const T& v) { + if (ref == v) { + return false; + } else { + ref = v; + isDefault = false; + return true; + } + } + + bool isDefault; + + TextureParam() : + UVTranslation(0.0f), + UVScaling(1.0f), + cropping(0.0f), + UVSet("map1"), + translation(0.0f), + rotation(0.0f), + scaling(1.0f), + alphaSource(0), + currentTextureBlendMode(0), + useMaterial(true), + isDefault(true) + {} +}; + +class FBXReader { +public: + FBXGeometry* _fbxGeometry; + + FBXNode _fbxNode; + static FBXNode parseFBX(QIODevice* device); + + FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url); + + QHash _textureImages; + + QHash textureNames; + QHash textureFilenames; + QHash textureContent; + QHash textureParams; + + + QHash diffuseTextures; + QHash bumpTextures; + QHash specularTextures; + QHash emissiveTextures; + QHash ambientTextures; + + QHash _fbxMaterials; + + void consolidateFBXMaterials(); + + bool _loadLightmaps = true; + float _lightmapOffset = 0.0f; + float _lightmapLevel; + + QMultiHash _connectionParentMap; + QMultiHash _connectionChildMap; +}; + #endif // hifi_FBXReader_h diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp new file mode 100644 index 0000000000..32c3595075 --- /dev/null +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -0,0 +1,342 @@ +// +// FBXReader.cpp +// interface/src/renderer +// +// Created by Andrzej Kapolka on 9/18/13. +// Copyright 2013 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include "FBXReader.h" + +template int streamSize() { + return sizeof(T); +} + +template int streamSize() { + return 1; +} + +template QVariant readBinaryArray(QDataStream& in, int& position) { + quint32 arrayLength; + quint32 encoding; + quint32 compressedLength; + + in >> arrayLength; + in >> encoding; + in >> compressedLength; + position += sizeof(quint32) * 3; + + QVector values; + const unsigned int DEFLATE_ENCODING = 1; + if (encoding == DEFLATE_ENCODING) { + // preface encoded data with uncompressed length + QByteArray compressed(sizeof(quint32) + compressedLength, 0); + *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); + in.readRawData(compressed.data() + sizeof(quint32), compressedLength); + position += compressedLength; + QByteArray uncompressed = qUncompress(compressed); + QDataStream uncompressedIn(uncompressed); + uncompressedIn.setByteOrder(QDataStream::LittleEndian); + uncompressedIn.setVersion(QDataStream::Qt_4_5); // for single/double precision switch + for (quint32 i = 0; i < arrayLength; i++) { + T value; + uncompressedIn >> value; + values.append(value); + } + } else { + for (quint32 i = 0; i < arrayLength; i++) { + T value; + in >> value; + position += streamSize(); + values.append(value); + } + } + return QVariant::fromValue(values); +} + +QVariant parseBinaryFBXProperty(QDataStream& in, int& position) { + char ch; + in.device()->getChar(&ch); + position++; + switch (ch) { + case 'Y': { + qint16 value; + in >> value; + position += sizeof(qint16); + return QVariant::fromValue(value); + } + case 'C': { + bool value; + in >> value; + position++; + return QVariant::fromValue(value); + } + case 'I': { + qint32 value; + in >> value; + position += sizeof(qint32); + return QVariant::fromValue(value); + } + case 'F': { + float value; + in >> value; + position += sizeof(float); + return QVariant::fromValue(value); + } + case 'D': { + double value; + in >> value; + position += sizeof(double); + return QVariant::fromValue(value); + } + case 'L': { + qint64 value; + in >> value; + position += sizeof(qint64); + return QVariant::fromValue(value); + } + case 'f': { + return readBinaryArray(in, position); + } + case 'd': { + return readBinaryArray(in, position); + } + case 'l': { + return readBinaryArray(in, position); + } + case 'i': { + return readBinaryArray(in, position); + } + case 'b': { + return readBinaryArray(in, position); + } + case 'S': + case 'R': { + quint32 length; + in >> length; + position += sizeof(quint32) + length; + return QVariant::fromValue(in.device()->read(length)); + } + default: + throw QString("Unknown property type: ") + ch; + } +} + +FBXNode parseBinaryFBXNode(QDataStream& in, int& position) { + qint32 endOffset; + quint32 propertyCount; + quint32 propertyListLength; + quint8 nameLength; + + in >> endOffset; + in >> propertyCount; + in >> propertyListLength; + in >> nameLength; + position += sizeof(quint32) * 3 + sizeof(quint8); + + FBXNode node; + const int MIN_VALID_OFFSET = 40; + if (endOffset < MIN_VALID_OFFSET || nameLength == 0) { + // use a null name to indicate a null node + return node; + } + node.name = in.device()->read(nameLength); + position += nameLength; + + for (quint32 i = 0; i < propertyCount; i++) { + node.properties.append(parseBinaryFBXProperty(in, position)); + } + + while (endOffset > position) { + FBXNode child = parseBinaryFBXNode(in, position); + if (child.name.isNull()) { + return node; + + } else { + node.children.append(child); + } + } + + return node; +} + +class Tokenizer { +public: + + Tokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } + + enum SpecialToken { + NO_TOKEN = -1, + NO_PUSHBACKED_TOKEN = -1, + DATUM_TOKEN = 0x100 + }; + + int nextToken(); + const QByteArray& getDatum() const { return _datum; } + + void pushBackToken(int token) { _pushedBackToken = token; } + void ungetChar(char ch) { _device->ungetChar(ch); } + +private: + + QIODevice* _device; + QByteArray _datum; + int _pushedBackToken; +}; + +int Tokenizer::nextToken() { + if (_pushedBackToken != NO_PUSHBACKED_TOKEN) { + int token = _pushedBackToken; + _pushedBackToken = NO_PUSHBACKED_TOKEN; + return token; + } + + char ch; + while (_device->getChar(&ch)) { + if (QChar(ch).isSpace()) { + continue; // skip whitespace + } + switch (ch) { + case ';': + _device->readLine(); // skip the comment + break; + + case ':': + case '{': + case '}': + case ',': + return ch; // special punctuation + + case '\"': + _datum = ""; + while (_device->getChar(&ch)) { + if (ch == '\"') { // end on closing quote + break; + } + if (ch == '\\') { // handle escaped quotes + if (_device->getChar(&ch) && ch != '\"') { + _datum.append('\\'); + } + } + _datum.append(ch); + } + return DATUM_TOKEN; + + default: + _datum = ""; + _datum.append(ch); + while (_device->getChar(&ch)) { + if (QChar(ch).isSpace() || ch == ';' || ch == ':' || ch == '{' || ch == '}' || ch == ',' || ch == '\"') { + ungetChar(ch); // read until we encounter a special character, then replace it + break; + } + _datum.append(ch); + } + return DATUM_TOKEN; + } + } + return NO_TOKEN; +} + +FBXNode parseTextFBXNode(Tokenizer& tokenizer) { + FBXNode node; + + if (tokenizer.nextToken() != Tokenizer::DATUM_TOKEN) { + return node; + } + node.name = tokenizer.getDatum(); + + if (tokenizer.nextToken() != ':') { + return node; + } + + int token; + bool expectingDatum = true; + while ((token = tokenizer.nextToken()) != Tokenizer::NO_TOKEN) { + if (token == '{') { + for (FBXNode child = parseTextFBXNode(tokenizer); !child.name.isNull(); child = parseTextFBXNode(tokenizer)) { + node.children.append(child); + } + return node; + } + if (token == ',') { + expectingDatum = true; + + } else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) { + QByteArray datum = tokenizer.getDatum(); + if ((token = tokenizer.nextToken()) == ':') { + tokenizer.ungetChar(':'); + tokenizer.pushBackToken(Tokenizer::DATUM_TOKEN); + return node; + + } else { + tokenizer.pushBackToken(token); + node.properties.append(datum); + expectingDatum = false; + } + } else { + tokenizer.pushBackToken(token); + return node; + } + } + + return node; +} + +FBXNode FBXReader::parseFBX(QIODevice* device) { + // verify the prolog + const QByteArray BINARY_PROLOG = "Kaydara FBX Binary "; + if (device->peek(BINARY_PROLOG.size()) != BINARY_PROLOG) { + // parse as a text file + FBXNode top; + Tokenizer tokenizer(device); + while (device->bytesAvailable()) { + FBXNode next = parseTextFBXNode(tokenizer); + if (next.name.isNull()) { + return top; + + } else { + top.children.append(next); + } + } + return top; + } + QDataStream in(device); + in.setByteOrder(QDataStream::LittleEndian); + in.setVersion(QDataStream::Qt_4_5); // for single/double precision switch + + // see http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ for an explanation + // of the FBX binary format + + // skip the rest of the header + const int HEADER_SIZE = 27; + in.skipRawData(HEADER_SIZE); + int position = HEADER_SIZE; + + // parse the top-level node + FBXNode top; + while (device->bytesAvailable()) { + FBXNode next = parseBinaryFBXNode(in, position); + if (next.name.isNull()) { + return top; + + } else { + top.children.append(next); + } + } + + return top; +} + diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 841fdcfad9..148d103aeb 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -121,21 +121,21 @@ glm::vec2 OBJTokenizer::getVec2() { void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { - meshPart.diffuseColor = glm::vec3(1, 1, 1); + /* meshPart.diffuseColor = glm::vec3(1, 1, 1); meshPart.specularColor = glm::vec3(1, 1, 1); meshPart.emissiveColor = glm::vec3(0, 0, 0); meshPart.emissiveParams = glm::vec2(0, 1); meshPart.shininess = 40; - meshPart.opacity = 1; + meshPart.opacity = 1;*/ meshPart.materialID = materialID; - meshPart.opacity = 1.0; + /* meshPart.opacity = 1.0; meshPart._material = std::make_shared(); meshPart._material->setDiffuse(glm::vec3(1.0, 1.0, 1.0)); meshPart._material->setOpacity(1.0); meshPart._material->setMetallic(0.0); meshPart._material->setGloss(96.0); - meshPart._material->setEmissive(glm::vec3(0.0, 0.0, 0.0)); + meshPart._material->setEmissive(glm::vec3(0.0, 0.0, 0.0));*/ } // OBJFace @@ -486,7 +486,10 @@ FBXGeometry* OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, } if (!groupMaterialName.isEmpty()) { OBJMaterial* material = &materials[groupMaterialName]; - // The code behind this is in transition. Some things are set directly in the FXBMeshPart... + + // TODO Fix this once the transision is understood + + /*// The code behind this is in transition. Some things are set directly in the FXBMeshPart... meshPart.materialID = groupMaterialName; meshPart.diffuseTexture.filename = material->diffuseTextureFilename; meshPart.specularTexture.filename = material->specularTextureFilename; @@ -495,6 +498,7 @@ FBXGeometry* OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, meshPart._material->setMetallic(glm::length(material->specularColor)); meshPart._material->setGloss(material->shininess); meshPart._material->setOpacity(material->opacity); + */ } // qCDebug(modelformat) << "OBJ Reader part:" << meshPartCount << "name:" << leadFace.groupName << "material:" << groupMaterialName << "diffuse:" << meshPart._material->getDiffuse() << "faces:" << faceGroup.count() << "triangle indices will start with:" << mesh.vertices.count(); foreach(OBJFace face, faceGroup) { @@ -576,15 +580,18 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { foreach (FBXMeshPart meshPart, mesh.parts) { qCDebug(modelformat) << " quadIndices.count() =" << meshPart.quadIndices.count(); qCDebug(modelformat) << " triangleIndices.count() =" << meshPart.triangleIndices.count(); + /* qCDebug(modelformat) << " diffuseColor =" << meshPart.diffuseColor << "mat =" << meshPart._material->getDiffuse(); qCDebug(modelformat) << " specularColor =" << meshPart.specularColor << "mat =" << meshPart._material->getMetallic(); qCDebug(modelformat) << " emissiveColor =" << meshPart.emissiveColor << "mat =" << meshPart._material->getEmissive(); qCDebug(modelformat) << " emissiveParams =" << meshPart.emissiveParams; qCDebug(modelformat) << " gloss =" << meshPart.shininess << "mat =" << meshPart._material->getGloss(); qCDebug(modelformat) << " opacity =" << meshPart.opacity << "mat =" << meshPart._material->getOpacity(); + */ qCDebug(modelformat) << " materialID =" << meshPart.materialID; - qCDebug(modelformat) << " diffuse texture =" << meshPart.diffuseTexture.filename; + /* qCDebug(modelformat) << " diffuse texture =" << meshPart.diffuseTexture.filename; qCDebug(modelformat) << " specular texture =" << meshPart.specularTexture.filename; + */ } qCDebug(modelformat) << " clusters.count() =" << mesh.clusters.count(); foreach (FBXCluster cluster, mesh.clusters) { diff --git a/libraries/model/src/model/Asset.h b/libraries/model/src/model/Asset.h index 28e1caf4aa..1d5919d008 100644 --- a/libraries/model/src/model/Asset.h +++ b/libraries/model/src/model/Asset.h @@ -25,14 +25,29 @@ public: typedef std::vector< T > Vector; typedef int ID; - const ID INVALID_ID = 0; + static const ID INVALID_ID = 0; + typedef size_t Index; enum Version { DRAFT = 0, FINAL, NUM_VERSIONS, }; + static Version evalVersionFromID(ID id) { + if (ID <= 0) { + return DRAFT; + } else (ID > 0) { + return FINAL; + } + } + static Index evalIndexFromID(ID id) { + return Index(id < 0 ? -id : id) - 1; + } + static ID evalID(Index index, Version version) { + return (version == DRAFT ? -int(index + 1) : int(index + 1)); + } + Table() { for (auto e : _elements) { e.resize(0); @@ -40,29 +55,54 @@ public: } ~Table() {} - ID add(const T& element, Version v = FINAL) { - switch (v) { - case DRAFT: { - _elements[DRAFT].push_back(element); - return ID(-(_elements[DRAFT].size() - 1)); - break; + Index getNumElements() const { + return _elements[DRAFT].size(); + } + + ID add(const T& element) { + for (auto e : _elements) { + e.push_back(element); } - case FINAL: { - _elements[FINAL].push_back(element); - return ID(_elements[FINAL].size() - 1); - break; + return evalID(_elements[DRAFT].size(), DRAFT); + } + + void set(ID id, const T& element) { + Index index = evalIndexFromID(id); + if (index < getNumElements()) { + _elements[DRAFT][index] = element; } + } + + const T& get(ID id, const T& element) const { + Index index = evalIndexFromID(id); + if (index < getNumElements()) { + return _elements[DRAFT][index]; } - return INVALID_ID; + return _default; } protected: Vector _elements[NUM_VERSIONS]; + T _default; }; typedef Table< MaterialPointer > MaterialTable; +typedef Table< TextureChannelPointer > TextureChannelTable; + typedef Table< MeshPointer > MeshTable; + +class Shape { +public: + + MeshTable::ID _meshID{ MeshTable::INVALID_ID }; + int _partID = 0; + + MaterialTable::ID _materialID{ MaterialTable::INVALID_ID }; +}; + +typedef Table< Shape > ShapeTable; + class Asset { public: @@ -76,10 +116,14 @@ public: MaterialTable& editMaterials() { return _materials; } const MaterialTable& getMaterials() const { return _materials; } + ShapeTable& editShapes() { return _shapes; } + const ShapeTable& getShapes() const { return _shapes; } + protected: MeshTable _meshes; MaterialTable _materials; + ShapeTable _shapes; }; diff --git a/libraries/model/src/model/Material.h b/libraries/model/src/model/Material.h index e729eac603..8d548ad641 100755 --- a/libraries/model/src/model/Material.h +++ b/libraries/model/src/model/Material.h @@ -198,6 +198,15 @@ public: }; }; +class TextureChannel { +public: + TextureChannel() {} + + gpu::TextureView _texture; +}; +typedef std::shared_ptr< TextureChannel > TextureChannelPointer; + + class Material { public: typedef gpu::BufferView UniformBufferView; diff --git a/libraries/model/src/model/Material.slh b/libraries/model/src/model/Material.slh index f2fa0a2a25..2f75c2b41e 100644 --- a/libraries/model/src/model/Material.slh +++ b/libraries/model/src/model/Material.slh @@ -25,20 +25,18 @@ uniform materialBuffer { Material getMaterial() { return _mat; } - +/* float componentSRGBToLinear(float cs) { - /* sRGB to linear conversion - { cs / 12.92, cs <= 0.04045 - cl = { - { ((cs + 0.055)/1.055)^2.4, cs > 0.04045 - - constants: - T = 0.04045 - A = 1 / 1.055 = 0.94786729857 - B = 0.055 * A = 0.05213270142 - C = 1 / 12.92 = 0.0773993808 - G = 2.4 - */ + // sRGB to linear conversion + // { cs / 12.92, cs <= 0.04045 + // cl = { + // { ((cs + 0.055)/1.055)^2.4, cs > 0.04045 + // constants: + // T = 0.04045 + // A = 1 / 1.055 = 0.94786729857 + // B = 0.055 * A = 0.05213270142 + // C = 1 / 12.92 = 0.0773993808 + // G = 2.4 const float T = 0.04045; const float A = 0.947867; const float B = 0.052132; @@ -55,9 +53,10 @@ float componentSRGBToLinear(float cs) { vec3 SRGBToLinear(vec3 srgb) { return vec3(componentSRGBToLinear(srgb.x),componentSRGBToLinear(srgb.y),componentSRGBToLinear(srgb.z)); } - +vec3 getMaterialDiffuse(Material m) { return (gl_FragCoord.x < 800 ? SRGBToLinear(m._diffuse.rgb) : m._diffuse.rgb); } +*/ float getMaterialOpacity(Material m) { return m._diffuse.a; } -vec3 getMaterialDiffuse(Material m) { return (gl_FragCoord.x > 800 ? SRGBToLinear(m._diffuse.rgb) : m._diffuse.rgb); } +vec3 getMaterialDiffuse(Material m) { return m._diffuse.rgb; } vec3 getMaterialSpecular(Material m) { return m._specular.rgb; } float getMaterialShininess(Material m) { return m._specular.a; } diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 75028abe93..389466069a 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -412,8 +412,9 @@ void Resource::handleReplyFinished() { ResourceCache::requestCompleted(this); finishedLoading(true); - emit loaded(*reply); downloadFinished(reply); + // Signal the VERY end of loading AND processing a resource, at this point even the specialized class is finalized + emit loaded(*reply); } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 2f81fe8b84..ec1f037f7e 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1723,7 +1723,8 @@ void GeometryReader::run() { NetworkGeometry::NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl) : _url(url), _mapping(mapping), - _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url) { + _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url), + _asset() { if (delayLoad) { _state = DelayState; @@ -1910,6 +1911,9 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas int totalIndices = 0; bool checkForTexcoordLightmap = false; + // process material parts + foreach (const FBXMaterial& mat, ) { + // process network parts foreach (const FBXMeshPart& part, mesh.parts) { NetworkMeshPart* networkPart = new NetworkMeshPart(); @@ -2051,11 +2055,14 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas void NetworkGeometry::modelParseSuccess(FBXGeometry* geometry) { // assume owner ship of geometry pointer _geometry.reset(geometry); + _asset = _geometry->_asset; foreach(const FBXMesh& mesh, _geometry->meshes) { _meshes.emplace_back(buildNetworkMesh(mesh, _textureBaseUrl)); } + foreach(const FBXMaterial& material, _geometry-> + _state = SuccessState; emit onSuccess(*this, *_geometry.get()); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 71fa35c054..c83c884aaa 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -25,9 +25,12 @@ #include +#include + class NetworkGeometry; class NetworkMesh; class NetworkTexture; +class NetworkMaterial; typedef glm::vec3 Vec3Key; @@ -329,6 +332,7 @@ public: // WARNING: only valid when isLoaded returns true. const FBXGeometry& getFBXGeometry() const { return *_geometry; } const std::vector>& getMeshes() const { return _meshes; } + const model::AssetPointer getAsset() const { return _asset; } void setTextureWithNameToURL(const QString& name, const QUrl& url); QStringList getTextureNames() const; @@ -357,6 +361,8 @@ protected slots: void modelParseSuccess(FBXGeometry* geometry); void modelParseError(int error, QString str); + void oneTextureLoaded(); + protected: void attemptRequestInternal(); void requestMapping(const QUrl& url); @@ -378,6 +384,9 @@ protected: std::unique_ptr _geometry; std::vector> _meshes; + // The model asset created from this NetworkGeometry + model::AssetPointer _asset; + // cache for isLoadedWithTextures() mutable bool _isLoadedWithTextures = false; }; @@ -413,6 +422,15 @@ public: bool isTranslucent() const; }; +class NetworkMaterial { +public: + model::MaterialTable::ID _materialID; + + typedef std::map TextureChannelIDs; + TextureChannelIDs _textureChannelIDs; + +}; + /// The state associated with a single mesh. class NetworkMesh { public: diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 8a704486b1..d05320c24c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -768,6 +768,16 @@ public: QUrl url; int meshIndex; int partIndex; + + // Core definition of a Shape = transform + model/mesh/part + material + model::AssetPointer _asset; + model::ShapeTable::ID _shapeID; + + Transform _transform; + model::MeshPointer _mesh; + int _part; + model::MaterialPointer _material; + }; namespace render { diff --git a/libraries/render-utils/src/model.slf b/libraries/render-utils/src/model.slf index 93a4a29988..f455030f6f 100755 --- a/libraries/render-utils/src/model.slf +++ b/libraries/render-utils/src/model.slf @@ -28,14 +28,12 @@ void main(void) { // Fetch diffuse map vec4 diffuse = texture(diffuseMap, _texCoord0); - vec3 vertexColor = (gl_FragCoord.y > 400 ? SRGBToLinear(_color.rgb) : _color.rgb); - Material mat = getMaterial(); packDeferredFragment( normalize(_normal.xyz), evalOpaqueFinalAlpha(getMaterialOpacity(mat), diffuse.a), - getMaterialDiffuse(mat) * diffuse.rgb * vertexColor, + getMaterialDiffuse(mat) * diffuse.rgb * _color, getMaterialSpecular(mat), getMaterialShininess(mat)); } diff --git a/libraries/render-utils/src/model.slv b/libraries/render-utils/src/model.slv index 4319e9f9e0..75237b52b7 100755 --- a/libraries/render-utils/src/model.slv +++ b/libraries/render-utils/src/model.slv @@ -17,6 +17,36 @@ <$declareStandardTransform()$> + +/* +float componentSRGBToLinear(float cs) { + // sRGB to linear conversion + // { cs / 12.92, cs <= 0.04045 + // cl = { + // { ((cs + 0.055)/1.055)^2.4, cs > 0.04045 + // constants: + // T = 0.04045 + // A = 1 / 1.055 = 0.94786729857 + // B = 0.055 * A = 0.05213270142 + // C = 1 / 12.92 = 0.0773993808 + // G = 2.4 + const float T = 0.04045; + const float A = 0.947867; + const float B = 0.052132; + const float C = 0.077399; + const float G = 2.4; + + if (cs > T) { + return pow((cs * A + B), G); + } else { + return cs * C; + } +} + +vec3 SRGBToLinear(vec3 srgb) { + return vec3(componentSRGBToLinear(srgb.x),componentSRGBToLinear(srgb.y),componentSRGBToLinear(srgb.z)); +} +*/ const int MAX_TEXCOORDS = 2; uniform mat4 texcoordMatrices[MAX_TEXCOORDS]; @@ -30,7 +60,9 @@ void main(void) { // pass along the diffuse color _color = inColor.xyz; - + // _color = SRGBToLinear(inColor.xyz); + + // and the texture coordinates _texCoord0 = (texcoordMatrices[0] * vec4(inTexCoord0.st, 0.0, 1.0)).st; From f6953f8e02a1efabcb42ac76d9ded6bc0c536a7f Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 27 Aug 2015 09:52:07 -0700 Subject: [PATCH 016/192] REfining th eTextureMap design but still with the issue of the lightmapped model --- interface/src/ModelPackager.cpp | 22 ++++- libraries/fbx/src/FBXReader.cpp | 27 +----- libraries/fbx/src/FBXReader.h | 16 +--- libraries/model/src/model/Asset.h | 1 - libraries/model/src/model/Material.cpp | 21 +++-- libraries/model/src/model/Material.h | 27 ++---- libraries/model/src/model/TextureStorage.cpp | 22 +++++ libraries/model/src/model/TextureStorage.h | 15 ++++ libraries/render-utils/src/GeometryCache.cpp | 89 ++++++++++++++++++-- libraries/render-utils/src/GeometryCache.h | 49 ++++++++--- libraries/render-utils/src/Model.cpp | 66 ++++++++++----- libraries/render-utils/src/Model.h | 3 +- 12 files changed, 256 insertions(+), 102 deletions(-) diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 0b564f3574..e90ac74073 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -340,7 +340,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename void ModelPackager::listTextures() { _textures.clear(); - foreach (FBXMesh mesh, _geometry->meshes) { + /* foreach (FBXMesh mesh, _geometry->meshes) { foreach (FBXMeshPart part, mesh.parts) { if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty() && !_textures.contains(part.diffuseTexture.filename)) { @@ -360,7 +360,27 @@ void ModelPackager::listTextures() { _textures << part.emissiveTexture.filename; } } + } */ + foreach (FBXMaterial mat, _geometry->materials) { + if (!mat.diffuseTexture.filename.isEmpty() && mat.diffuseTexture.content.isEmpty() && + !_textures.contains(mat.diffuseTexture.filename)) { + _textures << mat.diffuseTexture.filename; + } + if (!mat.normalTexture.filename.isEmpty() && mat.normalTexture.content.isEmpty() && + !_textures.contains(mat.normalTexture.filename)) { + + _textures << mat.normalTexture.filename; + } + if (!mat.specularTexture.filename.isEmpty() && mat.specularTexture.content.isEmpty() && + !_textures.contains(mat.specularTexture.filename)) { + _textures << mat.specularTexture.filename; + } + if (!mat.emissiveTexture.filename.isEmpty() && mat.emissiveTexture.content.isEmpty() && + !_textures.contains(mat.emissiveTexture.filename)) { + _textures << mat.emissiveTexture.filename; + } } + } bool ModelPackager::copyTextures(const QString& oldDir, const QDir& newDir) { diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 5d87b5f316..0f65d2ac1c 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -2005,6 +2005,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // Create the Material Library consolidateFBXMaterials(); + geometry.materials = _fbxMaterials; // see if any materials have texture children bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, textureFilenames, _connectionChildMap); @@ -2044,7 +2045,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS bool detectDifferentUVs = false; - FBXTexture diffuseTexture; + /* FBXTexture diffuseTexture; QString diffuseTextureID = diffuseTextures.value(childID); if (!diffuseTextureID.isNull()) { diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); @@ -2099,7 +2100,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS detectDifferentUVs |= (emissiveTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); } - + */ if (detectDifferentUVs) { detectDifferentUVs = false; } @@ -2107,28 +2108,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { if (extracted.partMaterialTextures.at(j).first == materialIndex) { FBXMeshPart& part = extracted.mesh.parts[j]; - - /* part._material = material._material; - part.diffuseColor = material.diffuseColor; - part.specularColor = material.specularColor; - part.emissiveColor = material.emissiveColor; - part.shininess = material.shininess; - part.opacity = material.opacity; - if (!diffuseTexture.filename.isNull()) { - part.diffuseTexture = diffuseTexture; - } - if (!normalTexture.filename.isNull()) { - part.normalTexture = normalTexture; - } - if (!specularTexture.filename.isNull()) { - part.specularTexture = specularTexture; - } - if (!emissiveTexture.filename.isNull()) { - part.emissiveTexture = emissiveTexture; - } - part.emissiveParams = emissiveParams; - */ part.materialID = material.materialID; + generateTangents = material.needTangentSpace(); } } materialIndex++; diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 26ded3e9f5..4fdf166c42 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -121,20 +121,8 @@ public: QVector triangleIndices; // original indices from the FBX mesh mutable gpu::BufferPointer quadsAsTrianglesIndicesBuffer; - /* glm::vec3 diffuseColor; - glm::vec3 specularColor; - glm::vec3 emissiveColor; - glm::vec2 emissiveParams; - float shininess; - float opacity; - - FBXTexture diffuseTexture; - FBXTexture normalTexture; - FBXTexture specularTexture; - FBXTexture emissiveTexture; -*/ QString materialID; - // model::MaterialPointer _material; + mutable bool trianglesForQuadsAvailable = false; mutable int trianglesForQuadsIndicesCount = 0; @@ -256,6 +244,8 @@ public: QVector meshes; + QHash materials; + glm::mat4 offset; int leftEyeJointIndex = -1; diff --git a/libraries/model/src/model/Asset.h b/libraries/model/src/model/Asset.h index 1d5919d008..a7b06b1239 100644 --- a/libraries/model/src/model/Asset.h +++ b/libraries/model/src/model/Asset.h @@ -87,7 +87,6 @@ protected: }; typedef Table< MaterialPointer > MaterialTable; -typedef Table< TextureChannelPointer > TextureChannelTable; typedef Table< MeshPointer > MeshTable; diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index c422d75454..a013fcd845 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -10,6 +10,8 @@ // #include "Material.h" +#include "TextureStorage.h" + using namespace model; using namespace gpu; @@ -28,7 +30,7 @@ glm::vec3 convertSRGBToLinear(const glm::vec3& srgb) { Material::Material() : _key(0), _schemaBuffer(), - _textureMap() { + _textureMaps() { // only if created from nothing shall we create the Buffer to store the properties Schema schema; @@ -40,13 +42,13 @@ Material::Material() : Material::Material(const Material& material) : _key(material._key), _schemaBuffer(material._schemaBuffer), - _textureMap(material._textureMap) { + _textureMaps(material._textureMaps) { } Material& Material::operator= (const Material& material) { _key = (material._key); _schemaBuffer = (material._schemaBuffer); - _textureMap = (material._textureMap); + _textureMaps = (material._textureMaps); return (*this); } @@ -79,8 +81,15 @@ void Material::setOpacity(float opacity) { _schemaBuffer.edit()._opacity = opacity; } -void Material::setTextureView(MapChannel channel, const gpu::TextureView& view) { - _key.setMapChannel(channel, (view.isValid())); - _textureMap[channel] = view; +void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textureMap) { + if (textureMap && !textureMap->isNull()) { + _key.setMapChannel(channel, (true)); + _textureMaps[channel] = textureMap; + } else { + _key.setMapChannel(channel, (false)); + _textureMaps.erase(channel); + } } + + diff --git a/libraries/model/src/model/Material.h b/libraries/model/src/model/Material.h index 8d548ad641..0bb79fa7e1 100755 --- a/libraries/model/src/model/Material.h +++ b/libraries/model/src/model/Material.h @@ -13,19 +13,17 @@ #include #include -#include #include -#include "gpu/Resource.h" -#include "gpu/Texture.h" - +#include namespace model { static glm::vec3 convertSRGBToLinear(const glm::vec3& srgb); - +class TextureMap; +typedef std::shared_ptr< TextureMap > TextureMapPointer; // Material Key is a coarse trait description of a material used to classify the materials class MaterialKey { @@ -198,24 +196,14 @@ public: }; }; -class TextureChannel { -public: - TextureChannel() {} - - gpu::TextureView _texture; -}; -typedef std::shared_ptr< TextureChannel > TextureChannelPointer; - - class Material { public: typedef gpu::BufferView UniformBufferView; - typedef gpu::TextureView TextureView; typedef glm::vec3 Color; typedef MaterialKey::MapChannel MapChannel; - typedef std::map TextureMap; + typedef std::map TextureMaps; typedef std::bitset MapFlags; Material(); @@ -254,14 +242,15 @@ public: const UniformBufferView& getSchemaBuffer() const { return _schemaBuffer; } - void setTextureView(MapChannel channel, const TextureView& texture); - const TextureMap& getTextureMap() const { return _textureMap; } + // The texture map to channel association + void setTextureMap(MapChannel channel, const TextureMapPointer& textureMap); + const TextureMaps& getTextureMaps() const { return _textureMaps; } protected: MaterialKey _key; UniformBufferView _schemaBuffer; - TextureMap _textureMap; + TextureMaps _textureMaps; }; typedef std::shared_ptr< Material > MaterialPointer; diff --git a/libraries/model/src/model/TextureStorage.cpp b/libraries/model/src/model/TextureStorage.cpp index 499054c34b..e0f3923248 100755 --- a/libraries/model/src/model/TextureStorage.cpp +++ b/libraries/model/src/model/TextureStorage.cpp @@ -26,3 +26,25 @@ void TextureStorage::reset(const QUrl& url, const TextureUsage& usage) { _usage = usage; } + + + +void TextureMap::setTextureStorage(TextureStoragePointer& texStorage) { + _textureStorage = texStorage; +} + +bool TextureMap::isNull() const { + if (_textureStorage) { + return _textureStorage->isMipAvailable(0); + } else { + return false; + } +} + +gpu::TextureView TextureMap::getTextureView() const { + if (_textureStorage) { + return gpu::TextureView(_textureStorage->getGPUTexture(), 0); + } else { + return gpu::TextureView(); + } +} \ No newline at end of file diff --git a/libraries/model/src/model/TextureStorage.h b/libraries/model/src/model/TextureStorage.h index a6752d21b2..e96b5c5af2 100755 --- a/libraries/model/src/model/TextureStorage.h +++ b/libraries/model/src/model/TextureStorage.h @@ -51,6 +51,21 @@ protected: }; typedef std::shared_ptr< TextureStorage > TextureStoragePointer; +class TextureMap { +public: + TextureMap() {} + + void setTextureStorage(TextureStoragePointer& texStorage); + + bool isNull() const; + + gpu::TextureView getTextureView() const; + +protected: + TextureStoragePointer _textureStorage; +}; +typedef std::shared_ptr< TextureMap > TextureMapPointer; + }; #endif // hifi_model_TextureStorage_h diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index ec1f037f7e..6a1283d73d 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1911,13 +1911,12 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas int totalIndices = 0; bool checkForTexcoordLightmap = false; - // process material parts - foreach (const FBXMaterial& mat, ) { + // process network parts foreach (const FBXMeshPart& part, mesh.parts) { NetworkMeshPart* networkPart = new NetworkMeshPart(); - +/* if (!part.diffuseTexture.filename.isEmpty()) { networkPart->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(part.diffuseTexture.filename)), DEFAULT_TEXTURE, mesh.isEye, part.diffuseTexture.content); @@ -1938,7 +1937,7 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas false, part.emissiveTexture.content); networkPart->emissiveTextureName = part.emissiveTexture.name; checkForTexcoordLightmap = true; - } + }*/ networkMesh->_parts.emplace_back(networkPart); totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); } @@ -2007,7 +2006,8 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); if (mesh.texCoords1.size()) { networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); - } else if (checkForTexcoordLightmap && mesh.texCoords.size()) { + // } else if (checkForTexcoordLightmap && mesh.texCoords.size()) { + } else if (mesh.texCoords.size()) { // need lightmap texcoord UV but doesn't have uv#1 so just reuse the same channel networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum - 1, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); } @@ -2052,16 +2052,75 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas return networkMesh; } +static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) { + auto textureCache = DependencyManager::get(); + NetworkMaterial* networkMaterial = new NetworkMaterial(); + + int totalIndices = 0; + bool checkForTexcoordLightmap = false; + + networkMaterial->_material = material._material; + + if (!material.diffuseTexture.filename.isEmpty()) { + networkMaterial->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.diffuseTexture.filename)), DEFAULT_TEXTURE, + /* mesh.isEye*/ false, material.diffuseTexture.content); + networkMaterial->diffuseTextureName = material.diffuseTexture.name; + networkMaterial->_diffuseTexTransform = material.diffuseTexture.transform; + } + if (!material.normalTexture.filename.isEmpty()) { + networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), NORMAL_TEXTURE, + false, material.normalTexture.content); + networkMaterial->normalTextureName = material.normalTexture.name; + } + if (!material.specularTexture.filename.isEmpty()) { + networkMaterial->specularTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.specularTexture.filename)), SPECULAR_TEXTURE, + false, material.specularTexture.content); + networkMaterial->specularTextureName = material.specularTexture.name; + } + if (!material.emissiveTexture.filename.isEmpty()) { + networkMaterial->emissiveTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.emissiveTexture.filename)), EMISSIVE_TEXTURE, + false, material.emissiveTexture.content); + networkMaterial->emissiveTextureName = material.emissiveTexture.name; + networkMaterial->_emissiveTexTransform = material.emissiveTexture.transform; + networkMaterial->_emissiveParams = material.emissiveParams; + checkForTexcoordLightmap = true; + } + + return networkMaterial; +} + + void NetworkGeometry::modelParseSuccess(FBXGeometry* geometry) { // assume owner ship of geometry pointer _geometry.reset(geometry); _asset = _geometry->_asset; + + foreach(const FBXMesh& mesh, _geometry->meshes) { _meshes.emplace_back(buildNetworkMesh(mesh, _textureBaseUrl)); } - foreach(const FBXMaterial& material, _geometry-> + QHash fbxMatIDToMatID; + foreach(const FBXMaterial& material, _geometry->materials) { + fbxMatIDToMatID[material.materialID] = _materials.size(); + _materials.emplace_back(buildNetworkMaterial(material, _textureBaseUrl)); + } + + + int meshID = 0; + foreach(const FBXMesh& mesh, _geometry->meshes) { + int partID = 0; + foreach (const FBXMeshPart& part, mesh.parts) { + NetworkShape* networkShape = new NetworkShape(); + networkShape->_meshID = meshID; + networkShape->_partID = partID; + networkShape->_materialID = fbxMatIDToMatID[part.materialID]; + _shapes.emplace_back(networkShape); + partID++; + } + meshID++; + } _state = SuccessState; emit onSuccess(*this, *_geometry.get()); @@ -2078,6 +2137,20 @@ void NetworkGeometry::modelParseError(int error, QString str) { _resource = nullptr; } + +const NetworkMaterial* NetworkGeometry::getShapeMaterial(int shapeID) { + if ((shapeID >= 0) && (shapeID < _shapes.size())) { + int materialID = _shapes[shapeID]->_materialID; + if ((materialID >= 0) && (materialID < _materials.size())) { + return _materials[materialID].get(); + } else { + return 0; + } + } else { + return 0; + } +} + bool NetworkMeshPart::isTranslucent() const { return diffuseTexture && diffuseTexture->isTranslucent(); } @@ -2085,7 +2158,9 @@ bool NetworkMeshPart::isTranslucent() const { bool NetworkMesh::isPartTranslucent(const FBXMesh& fbxMesh, int partIndex) const { assert(partIndex >= 0); assert((size_t)partIndex < _parts.size()); - return (_parts.at(partIndex)->isTranslucent() || fbxMesh.parts.at(partIndex).opacity != 1.0f); + // return (_parts.at(partIndex)->isTranslucent() || fbxMesh.parts.at(partIndex).opacity != 1.0f); + // TODO FIX That + return (_parts.at(partIndex)->isTranslucent()); } int NetworkMesh::getTranslucentPartCount(const FBXMesh& fbxMesh) const { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index c83c884aaa..9a4c36d2bf 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -31,6 +31,7 @@ class NetworkGeometry; class NetworkMesh; class NetworkTexture; class NetworkMaterial; +class NetworkShape; typedef glm::vec3 Vec3Key; @@ -334,6 +335,14 @@ public: const std::vector>& getMeshes() const { return _meshes; } const model::AssetPointer getAsset() const { return _asset; } + // model::MeshPointer getShapeMesh(int shapeID); + // int getShapePart(int shapeID); + + // THis would be the finale verison + // model::MaterialPointer getShapeMaterial(int shapeID); + const NetworkMaterial* getShapeMaterial(int shapeID); + + void setTextureWithNameToURL(const QString& name, const QUrl& url); QStringList getTextureNames() const; @@ -361,8 +370,6 @@ protected slots: void modelParseSuccess(FBXGeometry* geometry); void modelParseError(int error, QString str); - void oneTextureLoaded(); - protected: void attemptRequestInternal(); void requestMapping(const QUrl& url); @@ -383,6 +390,9 @@ protected: Resource* _resource = nullptr; std::unique_ptr _geometry; std::vector> _meshes; + std::vector> _materials; + std::vector> _shapes; + // The model asset created from this NetworkGeometry model::AssetPointer _asset; @@ -406,6 +416,32 @@ private: QVariantHash _mapping; }; + +class NetworkShape { +public: + int _meshID{ -1 }; + int _partID{ -1 }; + int _materialID{ -1 }; +}; + +class NetworkMaterial { +public: + model::MaterialPointer _material; + QString diffuseTextureName; + QSharedPointer diffuseTexture; + QString normalTextureName; + QSharedPointer normalTexture; + QString specularTextureName; + QSharedPointer specularTexture; + QString emissiveTextureName; + QSharedPointer emissiveTexture; + + Transform _diffuseTexTransform; + Transform _emissiveTexTransform; + + glm::vec2 _emissiveParams; +}; + /// The state associated with a single mesh part. class NetworkMeshPart { public: @@ -422,15 +458,6 @@ public: bool isTranslucent() const; }; -class NetworkMaterial { -public: - model::MaterialTable::ID _materialID; - - typedef std::map TextureChannelIDs; - TextureChannelIDs _textureChannelIDs; - -}; - /// The state associated with a single mesh. class NetworkMesh { public: diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index d05320c24c..388256b122 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -758,8 +758,12 @@ void Model::renderSetup(RenderArgs* args) { class MeshPartPayload { public: - MeshPartPayload(bool transparent, Model* model, int meshIndex, int partIndex) : + /* MeshPartPayload(bool transparent, Model* model, int meshIndex, int partIndex) : transparent(transparent), model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex) { } + */ + MeshPartPayload(bool transparent, Model* model, int meshIndex, int partIndex, int shapeIndex) : + transparent(transparent), model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex), _shapeID(shapeIndex) { } + typedef render::Payload Payload; typedef Payload::DataPointer Pointer; @@ -771,13 +775,7 @@ public: // Core definition of a Shape = transform + model/mesh/part + material model::AssetPointer _asset; - model::ShapeTable::ID _shapeID; - - Transform _transform; - model::MeshPointer _mesh; - int _part; - model::MaterialPointer _material; - + int _shapeID; }; namespace render { @@ -796,7 +794,8 @@ namespace render { } template <> void payloadRender(const MeshPartPayload::Pointer& payload, RenderArgs* args) { if (args) { - return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->transparent); + // return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->transparent); + return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->transparent, payload->_shapeID); } } @@ -1457,7 +1456,8 @@ AABox Model::getPartBounds(int meshIndex, int partIndex) { return AABox(); } -void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent) { +void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent, int shapeID) { +//void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent) { // PROFILE_RANGE(__FUNCTION__); PerformanceTimer perfTimer("Model::renderPart"); if (!_readyWhenAdded) { @@ -1486,6 +1486,15 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran const FBXGeometry& geometry = _geometry->getFBXGeometry(); const std::vector>& networkMeshes = _geometry->getMeshes(); + auto drawMaterial = _geometry->getShapeMaterial(shapeID); + if (!drawMaterial) { + return; + }; + + // Not yet + // auto drawMesh = _geometry->getShapeMesh(shapeID); + // auto drawPart = _geometry->getShapePart(shapeID); + // guard against partially loaded meshes if (meshIndex >= (int)networkMeshes.size() || meshIndex >= (int)geometry.meshes.size() || meshIndex >= (int)_meshStates.size() ) { return; @@ -1495,10 +1504,13 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran const FBXMesh& mesh = geometry.meshes.at(meshIndex); const MeshState& state = _meshStates.at(meshIndex); - bool translucentMesh = translucent; // networkMesh.getTranslucentPartCount(mesh) == networkMesh.parts.size(); + auto drawMaterialKey = drawMaterial->_material->getKey(); + bool translucentMesh = drawMaterialKey.isTransparent(); + +// bool translucentMesh = translucent; // networkMesh.getTranslucentPartCount(mesh) == networkMesh.parts.size(); bool hasTangents = !mesh.tangents.isEmpty(); - bool hasSpecular = mesh.hasSpecularTexture(); - bool hasLightmap = mesh.hasEmissiveTexture(); + bool hasSpecular = !drawMaterial->specularTextureName.isEmpty(); //mesh.hasSpecularTexture(); + bool hasLightmap = !drawMaterial->emissiveTextureName.isEmpty(); //mesh.hasEmissiveTexture(); bool isSkinned = state.clusterMatrices.size() > 1; bool wireframe = isWireframe(); @@ -1601,7 +1613,9 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran const NetworkMeshPart& networkPart = *(networkMesh._parts.at(partIndex).get()); const FBXMeshPart& part = mesh.parts.at(partIndex); - model::MaterialPointer material = part._material; + + //model::MaterialPointer material = part._material; + auto material = drawMaterial->_material; #ifdef WANT_DEBUG if (material == nullptr) { @@ -1623,7 +1637,8 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran batch.setUniformBuffer(locations->materialBufferUnit, material->getSchemaBuffer()); } - Texture* diffuseMap = networkPart.diffuseTexture.data(); + //Texture* diffuseMap = networkPart.diffuseTexture.data(); + Texture* diffuseMap = drawMaterial->diffuseTexture.data(); if (mesh.isEye && diffuseMap) { // FIXME - guard against out of bounds here if (meshIndex < _dilatedTextures.size()) { @@ -1641,12 +1656,19 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran if (locations->texcoordMatrices >= 0) { glm::mat4 texcoordTransform[2]; - if (!part.diffuseTexture.transform.isIdentity()) { + /* if (!part.diffuseTexture.transform.isIdentity()) { part.diffuseTexture.transform.getMatrix(texcoordTransform[0]); } if (!part.emissiveTexture.transform.isIdentity()) { part.emissiveTexture.transform.getMatrix(texcoordTransform[1]); + }*/ + if (!drawMaterial->_diffuseTexTransform.isIdentity()) { + drawMaterial->_diffuseTexTransform.getMatrix(texcoordTransform[0]); } + if (!drawMaterial->_emissiveTexTransform.isIdentity()) { + drawMaterial->_emissiveTexTransform.getMatrix(texcoordTransform[1]); + } + batch._glUniformMatrix4fv(locations->texcoordMatrices, 2, false, (const float*) &texcoordTransform); } @@ -1670,8 +1692,10 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran // drawcall with an emissive, so let's do it for now. if (locations->emissiveTextureUnit >= 0) { // assert(locations->emissiveParams >= 0); // we should have the emissiveParams defined in the shader - float emissiveOffset = part.emissiveParams.x; - float emissiveScale = part.emissiveParams.y; + //float emissiveOffset = part.emissiveParams.x; + //float emissiveScale = part.emissiveParams.y; + float emissiveOffset = drawMaterial->_emissiveParams.x; + float emissiveScale = drawMaterial->_emissiveParams.y; batch._glUniform2f(locations->emissiveParams, emissiveOffset, emissiveScale); NetworkTexture* emissiveMap = networkPart.emissiveTexture.data(); @@ -1729,6 +1753,7 @@ void Model::segregateMeshGroups() { _opaqueRenderItems.clear(); // Run through all of the meshes, and place them into their segregated, but unsorted buckets + int shapeID = 0; for (int i = 0; i < (int)networkMeshes.size(); i++) { const NetworkMesh& networkMesh = *(networkMeshes.at(i).get()); const FBXMesh& mesh = geometry.meshes.at(i); @@ -1749,10 +1774,11 @@ void Model::segregateMeshGroups() { int totalParts = mesh.parts.size(); for (int partIndex = 0; partIndex < totalParts; partIndex++) { if (networkMesh.isPartTranslucent(mesh, partIndex)) { - _transparentRenderItems << std::make_shared(true, this, i, partIndex); + _transparentRenderItems << std::make_shared(true, this, i, partIndex, shapeID); } else { - _opaqueRenderItems << std::make_shared(false, this, i, partIndex); + _opaqueRenderItems << std::make_shared(false, this, i, partIndex, shapeID); } + shapeID++; } } _meshGroupsKnown = true; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index e55bff6aca..69e7dc24f4 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -90,7 +90,8 @@ public: bool isVisible() const { return _isVisible; } AABox getPartBounds(int meshIndex, int partIndex); - void renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent); + void renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent, int shapeID); + // void renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent); bool maybeStartBlender(); From d57c7de316d82967b079a79bf1273a603baba144 Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Thu, 27 Aug 2015 11:51:36 -0700 Subject: [PATCH 017/192] Added rebuild + toggle entity type --- examples/example/entities/platform.js | 258 +++++++++++++++++--------- 1 file changed, 171 insertions(+), 87 deletions(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index 4f20614f65..66438daf9e 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -5,42 +5,35 @@ // Copyright 2015 High Fidelity, Inc. // // Entity stress test / procedural demo. -// Spawns a platform under your avatar made up of random cubes or spheres. +// Spawns a platform under your avatar made up of random cubes or spheres. The platform follows your avatar +// around, and has a // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var SCRIPT_NAME = "platform.js"; - Script.include("../../libraries/uiwidgets.js"); -// Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); // Platform script (function () { - var SCRIPT_NAME = "platform.js"; -var USE_DEBUG_LOG = false; -var LOG_ENTITY_CREATION_MESSAGES = false; -var LOG_UPDATE_STATUS_MESSAGES = false; +var USE_DEBUG_LOG = true; // Turns on the 2dOverlay-based debug log. If false, just redirects to print. +var NUM_DEBUG_LOG_LINES = 10; +var LOG_ENTITY_CREATION_MESSAGES = false; // detailed debugging (init) +var LOG_UPDATE_STATUS_MESSAGES = false; // detailed debugging (startup) var MAX_UPDATE_INTERVAL = 0.2; // restrict to 5 updates / sec - -var AVATAR_HEIGHT_OFFSET = 1.5; +var AVATAR_HEIGHT_OFFSET = 1.5; // offset to make the platform spawn under your feet. Might need to be adjusted for unusually proportioned avatars. // Initial state var NUM_PLATFORM_ENTITIES = 400; var RADIUS = 5.0; -// Limits for the user controls on platform radius, density, and dimensions (width, height, depth). +// Defines min/max for onscreen platform radius, density, and entity width/height/depth sliders. // Color limits are hardcoded at [0, 255]. var UI_RADIUS_RANGE = [ 1.0, 15.0 ]; -var UI_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (actually, just don't go above 20k entities... >.>) -var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; - -// Used to detect teleportation. If user moves faster than this over one time step (times dt), -// then we trigger a complete rebuild at their new height. -var MAX_ACCELERATION_THRESHOLD = 20.0; +var UI_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density. +var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits // Utils (function () { @@ -211,7 +204,7 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; var lines = []; var lineIndex = 0; - for (var i = 0; i < 20; ++i) { + for (var i = 0; i < NUM_DEBUG_LOG_LINES; ++i) { lines.push(new UI.Label({ text: " ", visible: false, width: LINE_WIDTH, height: LINE_HEIGHT, @@ -288,7 +281,7 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; // Utils (ignore) (function () { // Utility function - this.withDefaults = function (properties, defaults) { + var withDefaults = this.withDefaults = function (properties, defaults) { // logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults)); properties = properties || {}; if (defaults) { @@ -298,6 +291,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; } return properties; } + var withReadonlyProp = this.withReadonlyProp = function (propname, value, obj) { + Object.defineProperty(obj, propname, { + value: value, + writable: false + }); + return obj; + } // Math utils if (typeof(Math.randRange) === 'undefined') { @@ -338,14 +338,14 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; logMessage("Creating entity: " + JSON.stringify(properties)); } var entity = Entities.addEntity(properties); - return { + return withReadonlyProp("type", properties.type, { update: function (properties) { Entities.editEntity(entity, properties); }, destroy: function () { Entities.deleteEntity(entity) - } - }; + }, + }); } // this.makeLight = function (properties) { // return makeEntity(withDefaults(properties, { @@ -371,6 +371,9 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; this.position = properties.position || null; this.color = properties.color || null; this.dimensions = properties.dimensions || null; + this.entityType = properties.type || "Box"; + + // logMessage("Spawning with type: '" + this.entityType + "' (properties.type = '" + properties.type + "')", COLORS.GREEN); if (properties._colorSeed) this._colorSeed = properties._colorSeed; @@ -379,10 +382,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; // logMessage("dimensions: " + JSON.stringify(this.dimensions)); // logMessage("color: " + JSON.stringify(this.color)); - this.box = makeBox({ + + this.cachedEntity = null; + this.activeEntity = makeEntity({ + type: this.entityType, + position: this.position, dimensions: this.dimensions, color: this.color, - position: this.position, alpha: 0.5 }); }; @@ -392,23 +398,65 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; if (position) this.position = position; // logMessage("updating with " + JSON.stringify(this)); - this.box.update(this); + this.activeEntity.update(this); } function swap (a, b) { var tmp = a; a = b; b = tmp; } + PlatformComponent.prototype.swapEntityType = function (newType) { + if (this.entityType !== newType) { + this.entityType = newType; + + // logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')"); + + // if (this.activeEntity) { + // this.activeEntity.destroy(); + // } + // this.activeEntity = makeEntity({ + // type: this.entityType, + // position: this.position, + // dimensions: this.dimensions, + // color: this.color, + // alpha: 0.5 + // }); + if (this.cachedEntity && this.cachedEntity.type == newType) { + this.cachedEntity.update({ visible: true }); + this.activeEntity.update({ visible: false }); + swap(this.cachedEntity, this.activeEntity); + this.update(this.position); + } else { + this.activeEntity.update({ visible: false }); + this.cachedEntity = this.activeEntity; + this.activeEntity = makeEntity({ + type: newType, + dimensions: this.dimensions, + color: this.color, + position: this.position, + alpha: 0.5 + }); + } + } + } /// Swap state with another component PlatformComponent.prototype.swap = function (other) { swap(this.position, other.position); swap(this.dimensions, other.dimensions); swap(this.color, other.color); + swap(this.entityType, other.entityType); + swap(this.activeEntity, other.activeEntity); + swap(this._colorSeed, other._colorSeed); + swap(this._shapeSeed, other._shapeSeed); } PlatformComponent.prototype.destroy = function () { - if (this.box) { - this.box.destroy(); - this.box = null; + if (this.activeEntity) { + this.activeEntity.destroy(); + this.activeEntity = null; + } + if (this.cachedEntity) { + this.cachedEntity.destroy(); + this.cachedEntity = null; } } @@ -423,14 +471,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; this.position = position; this.radius = radius; this.randomizer = new RandomAttribModel(); + this.boxType = "Sphere"; + this.boxTypes = [ "Box", "Sphere" ]; - var boxes = this.boxes = []; // logMessage("Spawning " + n + " entities", COLORS.GREEN); + var boxes = this.boxes = []; while (n > 0) { - var properties = { position: this.randomPoint() }; - this.randomizer.randomizeShapeAndColor(properties); - // logMessage("properties: " + JSON.stringify(properties)); - boxes.push(new PlatformComponent(properties)); + boxes.push(this.spawnEntity()); --n; } this.targetDensity = this.getEntityDensity(); @@ -444,6 +491,12 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; DynamicPlatform.prototype.toString = function () { return "[DynamicPlatform (" + this.boxes.length + " entities)]"; } + DynamicPlatform.prototype.spawnEntity = function () { + // logMessage("Called spawn entity. this.boxType = '" + this.boxType + "'") + var properties = { position: this.randomPoint(), type: this.boxType }; + this.randomizer.randomizeShapeAndColor(properties); + return new PlatformComponent(properties); + } DynamicPlatform.prototype.updateEntityAttribs = function () { var _this = this; this.setPendingUpdate('updateEntityAttribs', function () { @@ -454,6 +507,26 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; }, _this); }); } + DynamicPlatform.prototype.toggleBoxType = function () { + var _this = this; + this.setPendingUpdate('toggleBoxType', function () { + // Swap / cycle through types: find index of current type and set next type to idx+1 + for (var idx = 0; idx < _this.boxTypes.length; ++idx) { + if (_this.boxTypes[idx] === _this.boxType) { + var nextIndex = (idx + 1) % _this.boxTypes.length; + logMessage("swapping box type from '" + _this.boxType + "' to '" + _this.boxTypes[nextIndex] + "'", COLORS.GREEN); + _this.boxType = _this.boxTypes[nextIndex]; + break; + } + } + _this.boxes.forEach(function (box) { + box.swapEntityType(_this.boxType); + }, _this); + }); + } + DynamicPlatform.prototype.getBoxType = function () { + return this.boxType; + } /// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms). /// This is to prevent UI code from running full entity updates every 10 ms (or whatever). @@ -550,6 +623,18 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; DynamicPlatform.prototype.getEntityCount = function () { return this.boxes.length; } + DynamicPlatform.prototype.getEntityCountWithRadius = function (radius) { + var est = Math.floor((radius * radius) / (this.radius * this.radius) * this.getEntityCount()); + var actual = Math.floor(Math.PI * radius * radius * this.getEntityDensity()); + + if (est != actual) { + logMessage("assert failed: getEntityCountWithRadius() -- est " + est + " != actual " + actual); + } + return est; + } + DynamicPlatform.prototype.getEntityCountWithDensity = function (density) { + return Math.floor(Math.PI * this.radius * this.radius * density); + } /// Sets the entity count to n. Don't call this directly -- use setRadius / density instead. DynamicPlatform.prototype.setEntityCount = function (n) { @@ -559,9 +644,10 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; // Spawn new boxes n = n - this.boxes.length; for (; n > 0; --n) { - var properties = { position: this.randomPoint() }; - this.randomizer.randomizeShapeAndColor(properties); - this.boxes.push(new PlatformComponent(properties)); + // var properties = { position: this.randomPoint() }; + // this.randomizer.randomizeShapeAndColor(properties); + // this.boxes.push(new PlatformComponent(properties)); + this.boxes.push(this.spawnEntity()); } } else if (n < this.boxes.length) { // logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN); @@ -680,15 +766,17 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; z: Math.sin(theta) * r + this.position.y }; - var properties = { position: pos }; - this.randomizer.randomizeShapeAndColor(properties); - this.boxes.push(new PlatformComponent(properties)); + // var properties = { position: pos }; + // this.randomizer.randomizeShapeAndColor(properties); + // this.boxes.push(new PlatformComponent(properties)); + this.boxes.push(this.spawnEntity()); } } this.radius = radius; } } DynamicPlatform.prototype.updateHeight = function (height) { + logMessage("Setting platform height to " + height); this.platformHeight = height; // Invalidate current boxes to trigger a rebuild @@ -730,7 +818,7 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; }); this.boxes = []; } -})(); +}).call(this); // UI (function () { @@ -793,12 +881,16 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; throw e; } } - function addPushButton (parent, label, onClicked, setActive) { + function addButton (parent, label, onClicked) { var button = parent.add(new UI.Box({ text: label, - width: 120, - height: 20 + width: 160, + height: 26, + leftMargin: 8, + topMargin: 3 })); + button.addAction('onClick', onClicked); + return button; } function moveToBottomLeftScreenCorner (widget) { var border = 5; @@ -836,13 +928,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; controls.radiusSlider = addSlider(controls, "radius", UI_RADIUS_RANGE[0], UI_RADIUS_RANGE[1], function () { return platform.getRadius() }, function (value) { platform.setRadiusOnNextUpdate(value); - controls.entityCountSlider.setValue(platform.getEntityCount()); + controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value)); }); addSpacing(controls, 1, 2); controls.densitySlider = addSlider(controls, "entity density", UI_DENSITY_RANGE[0], UI_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, function (value) { platform.setDensityOnNextUpdate(value); - controls.entityCountSlider.setValue(platform.getEntityCount()); + controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value)); }); addSpacing(controls, 1, 2); @@ -852,6 +944,16 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; function (value) {}); controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions) controls.entityCountSlider.slider.actions = {}; + + // Buttons + addSpacing(buttons, 1, 22); + addButton(buttons, 'rebuild', function () { + platform.updateHeight(MyAvatar.position.y - AVATAR_HEIGHT_OFFSET); + }); + addSpacing(buttons, 1, 2); + addButton(buttons, 'toggle entity type', function () { + platform.toggleBoxType(); + }); // Bottom controls @@ -902,8 +1004,8 @@ var MAX_ACCELERATION_THRESHOLD = 20.0; } })(); -// Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog; bad -// for detailed debugging since you lose the file and line num even if the error gets rethrown. +// Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog +// (if it's enabled); bad for detailed debugging since you lose the file and line num even if the error gets rethrown. // Catch errors from init var CATCH_INIT_ERRORS = true; @@ -915,6 +1017,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; (function () { var doLater = null; if (CATCH_ERRORS_FROM_EVENT_UPDATES) { + // Decorates a function w/ explicit error catching + printing to the debug log. function catchErrors (fcn) { return function () { try { @@ -926,7 +1029,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; } } } - // We need to do this after the functions are actually registered... + // We need to do this after the functions are registered... doLater = function () { // Intercept errors from functions called by Script.update and Script.ScriptEnding. [ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) { @@ -947,32 +1050,17 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; return pos; } + // Program state var platform = this.platform = null; var lastHeight = null; + // Init this.initPlatform = function () { platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS); lastHeight = getTargetPlatformPosition().y; } - // this.init = function () { - // function _init () { - - // platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS); - // lastHeight = getTargetPlatformPosition().y; - // // Script.update.connect(update); - // // setupUI(platform); - // } - // if (CATCH_INIT_ERRORS) { - // try { - // _init(); - // } catch (e) { - // logMessage("error while initializing: " + e, COLORS.RED); - // } - // } else { - // _init(); - // } - // } + // Handle relative screen positioning (UI) var lastDimensions = Controller.getViewportDimensions(); function checkScreenDimensions () { var dimensions = Controller.getViewportDimensions(); @@ -982,24 +1070,22 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; lastDimensions = dimensions; } + // Update this.update = function (dt) { - try { - checkScreenDimensions(); + checkScreenDimensions(); - var pos = getTargetPlatformPosition(); - if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) { - // User likely teleported - logMessage("Height rebuild (" + - "(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" + - " > " + MAX_ACCELERATION_THRESHOLD + ")"); - platform.updateHeight(pos.y); - } - platform.update(dt, getTargetPlatformPosition(), platform.getRadius()); - lastHeight = pos.y; - } catch (e) { - logMessage("" + e, COLORS.RED); - } + var pos = getTargetPlatformPosition(); + // if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) { + // // User likely teleported + // logMessage("Height rebuild (" + + // "(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" + + // " > " + MAX_ACCELERATION_THRESHOLD + ")"); + // platform.updateHeight(pos.y); + // } + platform.update(dt, getTargetPlatformPosition(), platform.getRadius()); } + + // Teardown this.teardown = function () { try { platform.destroy(); @@ -1016,6 +1102,8 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; if (doLater) { doLater(); } + + // Delays startup until / if entities can be spawned. this.startup = function () { if (Entities.canAdjustLocks() && Entities.canRez()) { Script.update.disconnect(this.startup); @@ -1047,11 +1135,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; Controller.mouseReleaseEvent.connect(UI.handleMouseRelease); } } -})(); -Script.update.connect(startup); - + Script.update.connect(this.startup); })(); - - - +})(); From 62a6a9d45efead58813935d7c955621353fd393e Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Thu, 27 Aug 2015 11:54:47 -0700 Subject: [PATCH 018/192] Description --- examples/example/entities/platform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index 66438daf9e..ac41dc593b 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -5,8 +5,8 @@ // Copyright 2015 High Fidelity, Inc. // // Entity stress test / procedural demo. -// Spawns a platform under your avatar made up of random cubes or spheres. The platform follows your avatar -// around, and has a +// Spawns a platform under your avatar made up of randomly sized and colored boxes or spheres. The platform follows your avatar +// around, and comes with a UI to update the platform's properties (radius, entity density, color distribution, etc) in real time. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html From fbf6d0efe8b21947307d1014595e142da52161c3 Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Thu, 27 Aug 2015 11:57:53 -0700 Subject: [PATCH 019/192] Added failsafe so dependencies don't break --- examples/example/entities/platform.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index ac41dc593b..9cd6687a10 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -12,7 +12,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + +// UI and debug console implemented using uiwidgets / 2d overlays Script.include("../../libraries/uiwidgets.js"); +if (typeof('UI') === 'undefined') { // backup link in case the user downloaded this somewhere + Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); +} // Platform script (function () { From 6583ea179136416c6654e1b920673322cdb2b8e7 Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Thu, 27 Aug 2015 13:08:19 -0700 Subject: [PATCH 020/192] Reverted default entity type back to spheres --- examples/example/entities/platform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index 9cd6687a10..0aeb80e98f 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -476,7 +476,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension this.position = position; this.radius = radius; this.randomizer = new RandomAttribModel(); - this.boxType = "Sphere"; + this.boxType = "Box"; this.boxTypes = [ "Box", "Sphere" ]; // logMessage("Spawning " + n + " entities", COLORS.GREEN); From 2bea436e12a7a32282a693de412ae135c93ea58d Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Thu, 27 Aug 2015 18:34:31 -0700 Subject: [PATCH 021/192] Added timeouts if USE_ENTITY_TIMEOUTS is enabled, entities will now clean up after themselves if not touched within X seconds (ie. if the script and/or client crashes and scriptEnding isn't called then the entities will timeout; otherwise, they get cleaned up normally when the script ends). --- examples/example/entities/platform.js | 139 +++++++++++++++++++------- 1 file changed, 103 insertions(+), 36 deletions(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index 0aeb80e98f..ea9d2fd85d 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -30,6 +30,10 @@ var LOG_UPDATE_STATUS_MESSAGES = false; // detailed debugging (startup) var MAX_UPDATE_INTERVAL = 0.2; // restrict to 5 updates / sec var AVATAR_HEIGHT_OFFSET = 1.5; // offset to make the platform spawn under your feet. Might need to be adjusted for unusually proportioned avatars. +var USE_ENTITY_TIMEOUTS = true; +var ENTITY_TIMEOUT_DURATION = 30.0; // kill entities in 30 secs if they don't get any updates +var ENTITY_REFRESH_INTERVAL = 10.0; // poke the entities every 10s so they don't die until we're done with them + // Initial state var NUM_PLATFORM_ENTITIES = 400; var RADIUS = 5.0; @@ -350,6 +354,9 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension destroy: function () { Entities.deleteEntity(entity) }, + getId: function () { + return entity; + } }); } // this.makeLight = function (properties) { @@ -389,14 +396,27 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension // logMessage("color: " + JSON.stringify(this.color)); this.cachedEntity = null; - this.activeEntity = makeEntity({ - type: this.entityType, - position: this.position, + this.activeEntity = this.spawnEntity(this.entityType); + }; + PlatformComponent.prototype.spawnEntity = function (type) { + return makeEntity({ + type: type, + position: this.position, dimensions: this.dimensions, color: this.color, + lifetime: USE_ENTITY_TIMEOUTS ? ENTITY_TIMEOUT_DURATION : -1.0, alpha: 0.5 }); - }; + } + if (USE_ENTITY_TIMEOUTS) { + PlatformComponent.prototype.pokeEntity = function () { + // Kinda inefficient, but there's no way to get around this :/ + var age = Entities.getEntityProperties(this.activeEntity.getId()).age; + this.activeEntity.update({ lifetime: ENTITY_TIMEOUT_DURATION + age }); + } + } else { + PlatformComponent.prototype.pokeEntity = function () {} + } /// Updates platform to be at position p, and calls .update() with the current /// position, color, and dimensions parameters. PlatformComponent.prototype.update = function (position) { @@ -409,39 +429,25 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension var tmp = a; a = b; b = tmp; - } + } PlatformComponent.prototype.swapEntityType = function (newType) { if (this.entityType !== newType) { this.entityType = newType; - // logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')"); - - // if (this.activeEntity) { - // this.activeEntity.destroy(); - // } - // this.activeEntity = makeEntity({ - // type: this.entityType, - // position: this.position, - // dimensions: this.dimensions, - // color: this.color, - // alpha: 0.5 - // }); - if (this.cachedEntity && this.cachedEntity.type == newType) { - this.cachedEntity.update({ visible: true }); - this.activeEntity.update({ visible: false }); - swap(this.cachedEntity, this.activeEntity); - this.update(this.position); - } else { - this.activeEntity.update({ visible: false }); - this.cachedEntity = this.activeEntity; - this.activeEntity = makeEntity({ - type: newType, - dimensions: this.dimensions, - color: this.color, - position: this.position, - alpha: 0.5 - }); + if (this.activeEntity) { + this.activeEntity.destroy(); } + this.activeEntity = spawnEntity(newType); + // if (this.cachedEntity && this.cachedEntity.type == newType) { + // this.cachedEntity.update({ visible: true }); + // this.activeEntity.update({ visible: false }); + // swap(this.cachedEntity, this.activeEntity); + // this.update(this.position); + // } else { + // this.activeEntity.update({ visible: false }); + // this.cachedEntity = this.activeEntity; + // this.activeEntity = spawnEntity(newType); + // } } } /// Swap state with another component @@ -471,7 +477,6 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension } /// Encapsulates a moving platform that follows the avatar around (mostly). - /// Owns a _large_ amount of entities, var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) { this.position = position; this.radius = radius; @@ -479,7 +484,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension this.boxType = "Box"; this.boxTypes = [ "Box", "Sphere" ]; - // logMessage("Spawning " + n + " entities", COLORS.GREEN); + logMessage("Spawning " + n + " entities", COLORS.GREEN); var boxes = this.boxes = []; while (n > 0) { boxes.push(this.spawnEntity()); @@ -492,6 +497,8 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension this.platformHeight = position.y; this.oldPos = { x: position.x, y: position.y, z: position.z }; this.oldRadius = radius; + + // this.sendPokes(); } DynamicPlatform.prototype.toString = function () { return "[DynamicPlatform (" + this.boxes.length + " entities)]"; @@ -533,16 +540,45 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension return this.boxType; } + // if (USE_ENTITY_TIMEOUTS) { + // DynamicPlatform.prototype.sendPokes = function () { + // var _this = this; + // function poke () { + // logMessage("Poking entities so they don't die", COLORS.GREEN); + // _this.boxes.forEach(function (box) { + // box.pokeEntity(); + // }, _this); + + + // if (_this.pendingUpdates['keepalive']) { + // logMessage("previous timer: " + _this.pendingUpdates['keepalive'].timer + "; new timer: " + ENTITY_REFRESH_INTERVAL) + // } + // _this.pendingUpdates['keepalive'] = { + // callback: poke, + // timer: ENTITY_REFRESH_INTERVAL, + // skippedUpdates: 0 + // }; + // // _this.setPendingUpdate('keepalive', poke); + // // _this.pendingUpdates['keepalive'].timer = ENTITY_REFRESH_INTERVAL; + // } + // poke(); + // } + // } else { + // DynamicPlatform.prototype.sendPokes = function () {}; + // } + /// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms). /// This is to prevent UI code from running full entity updates every 10 ms (or whatever). DynamicPlatform.prototype.setPendingUpdate = function (name, callback) { if (!this.pendingUpdates[name]) { + // logMessage("Queued update for " + name, COLORS.GREEN); this.pendingUpdates[name] = { callback: callback, timer: 0.0, skippedUpdates: 0 } } else { + // logMessage("Deferred update for " + name, COLORS.GREEN); this.pendingUpdates[name].callback = callback; this.pendingUpdates[name].skippedUpdates++; // logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds"); @@ -555,7 +591,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension this.pendingUpdates[k].timer -= dt; if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) { - // logMessage("Running update for \"" + k + "\" (skipped " + this.pendingUpdates[k].skippedUpdates + ")"); + // logMessage("Dispatching update for " + k); try { this.pendingUpdates[k].callback(); } catch (e) { @@ -564,6 +600,8 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL; this.pendingUpdates[k].skippedUpdates = 0; this.pendingUpdates[k].callback = null; + } else { + // logMessage("Deferred update for " + k + " for " + this.pendingUpdates[k].timer + " seconds"); } } } @@ -573,7 +611,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension /// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full /// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a /// normal walking / flying speed. - DynamicPlatform.prototype.update = function (dt, position) { + DynamicPlatform.prototype.updatePosition = function (dt, position) { // logMessage("updating " + this); position.y = this.platformHeight; this.position = position; @@ -622,8 +660,37 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) { logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs"); } + } + DynamicPlatform.prototype.update = function (dt, position) { + this.updatePosition(dt, position); this.processPendingUpdates(dt); + this.sendPokes(dt); + } + + if (USE_ENTITY_TIMEOUTS) { + DynamicPlatform.prototype.sendPokes = function (dt) { + logMessage("starting keepalive", COLORS.GREEN); + // logMessage("dt = " + dt, COLORS.RED); + // var original = this.sendPokes; + var pokeTimer = 0.0; + this.sendPokes = function (dt) { + // logMessage("dt = " + dt); + if ((pokeTimer -= dt) < 0.0) { + // logMessage("Poking entities so they don't die", COLORS.GREEN); + this.boxes.forEach(function (box) { + box.pokeEntity(); + }, this); + pokeTimer = ENTITY_REFRESH_INTERVAL; + } else { + // logMessage("Poking entities in " + pokeTimer + " seconds"); + } + } + // logMessage("this.sendPokes === past this.sendPokes? " + (this.sendPokes === original), COLORS.GREEN); + this.sendPokes(dt); + } + } else { + DynamicPlatform.prototype.sendPokes = function () {}; } DynamicPlatform.prototype.getEntityCount = function () { return this.boxes.length; From c12f192c3ab405d10a6b11ddfdd6aa1c7f597f91 Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Thu, 27 Aug 2015 21:44:55 -0700 Subject: [PATCH 022/192] bugfix... Apparently I didn't test that -_- --- examples/example/entities/platform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index ea9d2fd85d..3d6744b364 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -15,7 +15,7 @@ // UI and debug console implemented using uiwidgets / 2d overlays Script.include("../../libraries/uiwidgets.js"); -if (typeof('UI') === 'undefined') { // backup link in case the user downloaded this somewhere +if (typeof(UI) === 'undefined') { // backup link in case the user downloaded this somewhere Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); } From 85f230bf17b52828387fa3d29f3e20ddab9be380 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 28 Aug 2015 08:12:50 -0700 Subject: [PATCH 023/192] Getting rid of the NetworkMEshPart and simplifying the thinking around the MOdel regarding its RenderItems --- libraries/render-utils/src/GeometryCache.cpp | 86 +++++++++-------- libraries/render-utils/src/GeometryCache.h | 19 +--- libraries/render-utils/src/Model.cpp | 97 ++++++-------------- libraries/render-utils/src/Model.h | 6 +- 4 files changed, 82 insertions(+), 126 deletions(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 6a1283d73d..01dc619e06 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1761,15 +1761,14 @@ bool NetworkGeometry::isLoadedWithTextures() const { if (!isLoaded()) { return false; } + if (!_isLoadedWithTextures) { - for (auto&& mesh : _meshes) { - for (auto && part : mesh->_parts) { - if ((part->diffuseTexture && !part->diffuseTexture->isLoaded()) || - (part->normalTexture && !part->normalTexture->isLoaded()) || - (part->specularTexture && !part->specularTexture->isLoaded()) || - (part->emissiveTexture && !part->emissiveTexture->isLoaded())) { - return false; - } + for (auto&& material : _materials) { + if ((material->diffuseTexture && !material->diffuseTexture->isLoaded()) || + (material->normalTexture && !material->normalTexture->isLoaded()) || + (material->specularTexture && !material->specularTexture->isLoaded()) || + (material->emissiveTexture && !material->emissiveTexture->isLoaded())) { + return false; } } _isLoadedWithTextures = true; @@ -1778,8 +1777,23 @@ bool NetworkGeometry::isLoadedWithTextures() const { } void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& url) { + + if (_meshes.size() > 0) { auto textureCache = DependencyManager::get(); + for (auto&& material : _materials) { + QSharedPointer matchingTexture = QSharedPointer(); + if (material->diffuseTextureName == name) { + material->diffuseTexture = textureCache->getTexture(url, DEFAULT_TEXTURE, /* _geometry->meshes[i].isEye*/ false); + } else if (material->normalTextureName == name) { + material->normalTexture = textureCache->getTexture(url); + } else if (material->specularTextureName == name) { + material->specularTexture = textureCache->getTexture(url); + } else if (material->emissiveTextureName == name) { + material->emissiveTexture = textureCache->getTexture(url); + } + } +/* for (size_t i = 0; i < _meshes.size(); i++) { NetworkMesh& mesh = *(_meshes[i].get()); for (size_t j = 0; j < mesh._parts.size(); j++) { @@ -1795,7 +1809,7 @@ void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& u part.emissiveTexture = textureCache->getTexture(url); } } - } + }*/ } else { qCWarning(renderutils) << "Ignoring setTextureWirthNameToURL() geometry not ready." << name << url; } @@ -1804,7 +1818,28 @@ void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& u QStringList NetworkGeometry::getTextureNames() const { QStringList result; - for (size_t i = 0; i < _meshes.size(); i++) { + for (auto&& material : _materials) { + if (!material->diffuseTextureName.isEmpty() && material->diffuseTexture) { + QString textureURL = material->diffuseTexture->getURL().toString(); + result << material->diffuseTextureName + ":" + textureURL; + } + + if (!material->normalTextureName.isEmpty() && material->normalTexture) { + QString textureURL = material->normalTexture->getURL().toString(); + result << material->normalTextureName + ":" + textureURL; + } + + if (!material->specularTextureName.isEmpty() && material->specularTexture) { + QString textureURL = material->specularTexture->getURL().toString(); + result << material->specularTextureName + ":" + textureURL; + } + + if (!material->emissiveTextureName.isEmpty() && material->emissiveTexture) { + QString textureURL = material->emissiveTexture->getURL().toString(); + result << material->emissiveTextureName + ":" + textureURL; + } + } + /* for (size_t i = 0; i < _meshes.size(); i++) { const NetworkMesh& mesh = *(_meshes[i].get()); for (size_t j = 0; j < mesh._parts.size(); j++) { const NetworkMeshPart& part = *(mesh._parts[j].get()); @@ -1829,7 +1864,7 @@ QStringList NetworkGeometry::getTextureNames() const { result << part.emissiveTextureName + ":" + textureURL; } } - } + }*/ return result; } @@ -1915,8 +1950,8 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas // process network parts foreach (const FBXMeshPart& part, mesh.parts) { - NetworkMeshPart* networkPart = new NetworkMeshPart(); -/* + /* NetworkMeshPart* networkPart = new NetworkMeshPart(); + if (!part.diffuseTexture.filename.isEmpty()) { networkPart->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(part.diffuseTexture.filename)), DEFAULT_TEXTURE, mesh.isEye, part.diffuseTexture.content); @@ -1937,8 +1972,9 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas false, part.emissiveTexture.content); networkPart->emissiveTextureName = part.emissiveTexture.name; checkForTexcoordLightmap = true; - }*/ + } networkMesh->_parts.emplace_back(networkPart); + */ totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); } @@ -2151,25 +2187,3 @@ const NetworkMaterial* NetworkGeometry::getShapeMaterial(int shapeID) { } } -bool NetworkMeshPart::isTranslucent() const { - return diffuseTexture && diffuseTexture->isTranslucent(); -} - -bool NetworkMesh::isPartTranslucent(const FBXMesh& fbxMesh, int partIndex) const { - assert(partIndex >= 0); - assert((size_t)partIndex < _parts.size()); - // return (_parts.at(partIndex)->isTranslucent() || fbxMesh.parts.at(partIndex).opacity != 1.0f); - // TODO FIX That - return (_parts.at(partIndex)->isTranslucent()); -} - -int NetworkMesh::getTranslucentPartCount(const FBXMesh& fbxMesh) const { - int count = 0; - - for (size_t i = 0; i < _parts.size(); i++) { - if (isPartTranslucent(fbxMesh, i)) { - count++; - } - } - return count; -} diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 9a4c36d2bf..a8e6adbcf4 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -338,7 +338,7 @@ public: // model::MeshPointer getShapeMesh(int shapeID); // int getShapePart(int shapeID); - // THis would be the finale verison + // This would be the final verison // model::MaterialPointer getShapeMaterial(int shapeID); const NetworkMaterial* getShapeMaterial(int shapeID); @@ -442,21 +442,6 @@ public: glm::vec2 _emissiveParams; }; -/// The state associated with a single mesh part. -class NetworkMeshPart { -public: - - QString diffuseTextureName; - QSharedPointer diffuseTexture; - QString normalTextureName; - QSharedPointer normalTexture; - QString specularTextureName; - QSharedPointer specularTexture; - QString emissiveTextureName; - QSharedPointer emissiveTexture; - - bool isTranslucent() const; -}; /// The state associated with a single mesh. class NetworkMesh { @@ -468,8 +453,6 @@ public: gpu::Stream::FormatPointer _vertexFormat; - std::vector> _parts; - int getTranslucentPartCount(const FBXMesh& fbxMesh) const; bool isPartTranslucent(const FBXMesh& fbxMesh, int partIndex) const; }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 388256b122..83df978d34 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -761,13 +761,12 @@ public: /* MeshPartPayload(bool transparent, Model* model, int meshIndex, int partIndex) : transparent(transparent), model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex) { } */ - MeshPartPayload(bool transparent, Model* model, int meshIndex, int partIndex, int shapeIndex) : - transparent(transparent), model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex), _shapeID(shapeIndex) { } + MeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex) : + model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex), _shapeID(shapeIndex) { } typedef render::Payload Payload; typedef Payload::DataPointer Pointer; - bool transparent; Model* model; QUrl url; int meshIndex; @@ -783,7 +782,21 @@ namespace render { if (!payload->model->isVisible()) { return ItemKey::Builder().withInvisible().build(); } - return payload->transparent ? ItemKey::Builder::transparentShape() : ItemKey::Builder::opaqueShape(); + auto geometry = payload->model->getGeometry(); + if (!geometry.isNull()) { + auto drawMaterial = geometry->getShapeMaterial(payload->_shapeID); + if (drawMaterial) { + auto matKey = drawMaterial->_material->getKey(); + if (matKey.isTransparent() || matKey.isTransparentMap()) { + return ItemKey::Builder::transparentShape(); + } else { + return ItemKey::Builder::opaqueShape(); + } + } + } + + // Return opaque for lack of a better idea + return ItemKey::Builder::opaqueShape(); } template <> const Item::Bound payloadGetBound(const MeshPartPayload::Pointer& payload) { @@ -794,8 +807,7 @@ namespace render { } template <> void payloadRender(const MeshPartPayload::Pointer& payload, RenderArgs* args) { if (args) { - // return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->transparent); - return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->transparent, payload->_shapeID); + return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->_shapeID); } } @@ -824,16 +836,7 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan bool somethingAdded = false; - foreach (auto renderItem, _transparentRenderItems) { - auto item = scene->allocateID(); - auto renderData = MeshPartPayload::Pointer(renderItem); - auto renderPayload = std::make_shared(renderData); - pendingChanges.resetItem(item, renderPayload); - _renderItems.insert(item, renderPayload); - somethingAdded = true; - } - - foreach (auto renderItem, _opaqueRenderItems) { + foreach (auto renderItem, _renderItemsSet) { auto item = scene->allocateID(); auto renderData = MeshPartPayload::Pointer(renderItem); auto renderPayload = std::make_shared(renderData); @@ -854,17 +857,7 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan bool somethingAdded = false; - foreach (auto renderItem, _transparentRenderItems) { - auto item = scene->allocateID(); - auto renderData = MeshPartPayload::Pointer(renderItem); - auto renderPayload = std::make_shared(renderData); - renderPayload->addStatusGetters(statusGetters); - pendingChanges.resetItem(item, renderPayload); - _renderItems.insert(item, renderPayload); - somethingAdded = true; - } - - foreach (auto renderItem, _opaqueRenderItems) { + foreach (auto renderItem, _renderItemsSet) { auto item = scene->allocateID(); auto renderData = MeshPartPayload::Pointer(renderItem); auto renderPayload = std::make_shared(renderData); @@ -1456,7 +1449,7 @@ AABox Model::getPartBounds(int meshIndex, int partIndex) { return AABox(); } -void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent, int shapeID) { +void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shapeID) { //void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent) { // PROFILE_RANGE(__FUNCTION__); PerformanceTimer perfTimer("Model::renderPart"); @@ -1607,11 +1600,10 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran } // guard against partially loaded meshes - if (partIndex >= (int)networkMesh._parts.size() || partIndex >= mesh.parts.size()) { + if (/*partIndex >= (int)networkMesh._parts.size() ||*/ partIndex >= mesh.parts.size()) { return; } - const NetworkMeshPart& networkPart = *(networkMesh._parts.at(partIndex).get()); const FBXMeshPart& part = mesh.parts.at(partIndex); //model::MaterialPointer material = part._material; @@ -1637,7 +1629,6 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran batch.setUniformBuffer(locations->materialBufferUnit, material->getSchemaBuffer()); } - //Texture* diffuseMap = networkPart.diffuseTexture.data(); Texture* diffuseMap = drawMaterial->diffuseTexture.data(); if (mesh.isEye && diffuseMap) { // FIXME - guard against out of bounds here @@ -1656,12 +1647,6 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran if (locations->texcoordMatrices >= 0) { glm::mat4 texcoordTransform[2]; - /* if (!part.diffuseTexture.transform.isIdentity()) { - part.diffuseTexture.transform.getMatrix(texcoordTransform[0]); - } - if (!part.emissiveTexture.transform.isIdentity()) { - part.emissiveTexture.transform.getMatrix(texcoordTransform[1]); - }*/ if (!drawMaterial->_diffuseTexTransform.isIdentity()) { drawMaterial->_diffuseTexTransform.getMatrix(texcoordTransform[0]); } @@ -1673,13 +1658,13 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran } if (!mesh.tangents.isEmpty()) { - NetworkTexture* normalMap = networkPart.normalTexture.data(); + NetworkTexture* normalMap = drawMaterial->normalTexture.data(); batch.setResourceTexture(1, (!normalMap || !normalMap->isLoaded()) ? textureCache->getBlueTexture() : normalMap->getGPUTexture()); } if (locations->specularTextureUnit >= 0) { - NetworkTexture* specularMap = networkPart.specularTexture.data(); + NetworkTexture* specularMap = drawMaterial->specularTexture.data(); batch.setResourceTexture(locations->specularTextureUnit, (!specularMap || !specularMap->isLoaded()) ? textureCache->getBlackTexture() : specularMap->getGPUTexture()); } @@ -1698,12 +1683,12 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran float emissiveScale = drawMaterial->_emissiveParams.y; batch._glUniform2f(locations->emissiveParams, emissiveOffset, emissiveScale); - NetworkTexture* emissiveMap = networkPart.emissiveTexture.data(); + NetworkTexture* emissiveMap = drawMaterial->emissiveTexture.data(); batch.setResourceTexture(locations->emissiveTextureUnit, (!emissiveMap || !emissiveMap->isLoaded()) ? textureCache->getGrayTexture() : emissiveMap->getGPUTexture()); } - if (translucent && locations->lightBufferUnit >= 0) { + if (translucentMesh && locations->lightBufferUnit >= 0) { DependencyManager::get()->setupTransparent(args, locations->lightBufferUnit); } } @@ -1749,8 +1734,7 @@ void Model::segregateMeshGroups() { return; } - _transparentRenderItems.clear(); - _opaqueRenderItems.clear(); + _renderItemsSet.clear(); // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; @@ -1759,25 +1743,10 @@ void Model::segregateMeshGroups() { const FBXMesh& mesh = geometry.meshes.at(i); const MeshState& state = _meshStates.at(i); - bool translucentMesh = networkMesh.getTranslucentPartCount(mesh) == (int)networkMesh._parts.size(); - bool hasTangents = !mesh.tangents.isEmpty(); - bool hasSpecular = mesh.hasSpecularTexture(); - bool hasLightmap = mesh.hasEmissiveTexture(); - bool isSkinned = state.clusterMatrices.size() > 1; - bool wireframe = isWireframe(); - - if (wireframe) { - translucentMesh = hasTangents = hasSpecular = hasLightmap = isSkinned = false; - } - // Create the render payloads int totalParts = mesh.parts.size(); for (int partIndex = 0; partIndex < totalParts; partIndex++) { - if (networkMesh.isPartTranslucent(mesh, partIndex)) { - _transparentRenderItems << std::make_shared(true, this, i, partIndex, shapeID); - } else { - _opaqueRenderItems << std::make_shared(false, this, i, partIndex, shapeID); - } + _renderItemsSet << std::make_shared(this, i, partIndex, shapeID); shapeID++; } } @@ -1827,15 +1796,7 @@ bool Model::initWhenReady(render::ScenePointer scene) { render::PendingChanges pendingChanges; - foreach (auto renderItem, _transparentRenderItems) { - auto item = scene->allocateID(); - auto renderData = MeshPartPayload::Pointer(renderItem); - auto renderPayload = std::make_shared(renderData); - _renderItems.insert(item, renderPayload); - pendingChanges.resetItem(item, renderPayload); - } - - foreach (auto renderItem, _opaqueRenderItems) { + foreach (auto renderItem, _renderItemsSet) { auto item = scene->allocateID(); auto renderData = MeshPartPayload::Pointer(renderItem); auto renderPayload = std::make_shared(renderData); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 69e7dc24f4..7c4a329ea8 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -90,8 +90,7 @@ public: bool isVisible() const { return _isVisible; } AABox getPartBounds(int meshIndex, int partIndex); - void renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent, int shapeID); - // void renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent); + void renderPart(RenderArgs* args, int meshIndex, int partIndex, int shapeID); bool maybeStartBlender(); @@ -487,8 +486,7 @@ private: bool _renderCollisionHull; - QSet> _transparentRenderItems; - QSet> _opaqueRenderItems; + QSet> _renderItemsSet; QMap _renderItems; bool _readyWhenAdded = false; bool _needsReload = true; From e2fce1271348158710bf6750388854473f361cf8 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 28 Aug 2015 09:51:03 -0700 Subject: [PATCH 024/192] Splitting the FBXREader.cpp file into sub files --- libraries/fbx/src/FBXReader.cpp | 807 +-------------------- libraries/fbx/src/FBXReader.h | 29 + libraries/fbx/src/FBXReader_Material.cpp | 146 ++++ libraries/fbx/src/FBXReader_Mesh.cpp | 509 +++++++++++++ libraries/fbx/src/FBXReader_Node.cpp | 129 +++- libraries/render-utils/src/GeometryCache.h | 6 +- 6 files changed, 819 insertions(+), 807 deletions(-) create mode 100644 libraries/fbx/src/FBXReader_Material.cpp create mode 100644 libraries/fbx/src/FBXReader_Mesh.cpp diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 1e5b77f33f..a443b9d295 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -190,127 +190,6 @@ static int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); static int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); - -QVector createVec4Vector(const QVector& doubleVector) { - QVector values; - for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 4) * 4); it != end; ) { - float x = *it++; - float y = *it++; - float z = *it++; - float w = *it++; - values.append(glm::vec4(x, y, z, w)); - } - return values; -} - - -QVector createVec4VectorRGBA(const QVector& doubleVector, glm::vec4& average) { - QVector values; - for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 4) * 4); it != end; ) { - float x = *it++; - float y = *it++; - float z = *it++; - float w = *it++; - auto val = glm::vec4(x, y, z, w); - values.append(val); - average += val; - } - if (!values.isEmpty()) { - average *= (1.0f / float(values.size())); - } - return values; -} - -QVector createVec3Vector(const QVector& doubleVector) { - QVector values; - for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 3) * 3); it != end; ) { - float x = *it++; - float y = *it++; - float z = *it++; - values.append(glm::vec3(x, y, z)); - } - return values; -} - -QVector createVec2Vector(const QVector& doubleVector) { - QVector values; - for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 2) * 2); it != end; ) { - float s = *it++; - float t = *it++; - values.append(glm::vec2(s, -t)); - } - return values; -} - -glm::mat4 createMat4(const QVector& doubleVector) { - return glm::mat4(doubleVector.at(0), doubleVector.at(1), doubleVector.at(2), doubleVector.at(3), - doubleVector.at(4), doubleVector.at(5), doubleVector.at(6), doubleVector.at(7), - doubleVector.at(8), doubleVector.at(9), doubleVector.at(10), doubleVector.at(11), - doubleVector.at(12), doubleVector.at(13), doubleVector.at(14), doubleVector.at(15)); -} - -QVector getIntVector(const FBXNode& node) { - foreach (const FBXNode& child, node.children) { - if (child.name == "a") { - return getIntVector(child); - } - } - if (node.properties.isEmpty()) { - return QVector(); - } - QVector vector = node.properties.at(0).value >(); - if (!vector.isEmpty()) { - return vector; - } - for (int i = 0; i < node.properties.size(); i++) { - vector.append(node.properties.at(i).toInt()); - } - return vector; -} - -QVector getFloatVector(const FBXNode& node) { - foreach (const FBXNode& child, node.children) { - if (child.name == "a") { - return getFloatVector(child); - } - } - if (node.properties.isEmpty()) { - return QVector(); - } - QVector vector = node.properties.at(0).value >(); - if (!vector.isEmpty()) { - return vector; - } - for (int i = 0; i < node.properties.size(); i++) { - vector.append(node.properties.at(i).toFloat()); - } - return vector; -} - -QVector getDoubleVector(const FBXNode& node) { - foreach (const FBXNode& child, node.children) { - if (child.name == "a") { - return getDoubleVector(child); - } - } - if (node.properties.isEmpty()) { - return QVector(); - } - QVector vector = node.properties.at(0).value >(); - if (!vector.isEmpty()) { - return vector; - } - for (int i = 0; i < node.properties.size(); i++) { - vector.append(node.properties.at(i).toDouble()); - } - return vector; -} - -glm::vec3 getVec3(const QVariantList& properties, int index) { - return glm::vec3(properties.at(index).value(), properties.at(index + 1).value(), - properties.at(index + 2).value()); -} - glm::vec3 parseVec3(const QString& string) { QStringList elements = string.split(','); if (elements.isEmpty()) { @@ -450,60 +329,6 @@ void appendModelIDs(const QString& parentID, const QMultiHash& } } -class Vertex { -public: - int originalIndex; - glm::vec2 texCoord; - glm::vec2 texCoord1; -}; - -uint qHash(const Vertex& vertex, uint seed = 0) { - return qHash(vertex.originalIndex, seed); -} - -bool operator==(const Vertex& v1, const Vertex& v2) { - return v1.originalIndex == v2.originalIndex && v1.texCoord == v2.texCoord && v1.texCoord1 == v2.texCoord1; -} - -class ExtractedMesh { -public: - FBXMesh mesh; - QMultiHash newIndices; - QVector > blendshapeIndexMaps; - QVector > partMaterialTextures; - QHash texcoordSetMap; - std::map texcoordSetMap2; -}; - -class AttributeData { -public: - QVector texCoords; - QVector texCoordIndices; - QString name; - int index; -}; - -class MeshData { -public: - ExtractedMesh extracted; - QVector vertices; - QVector polygonIndices; - bool normalsByVertex; - QVector normals; - QVector normalIndices; - - bool colorsByVertex; - glm::vec4 averageColor{1.0f, 1.0f, 1.0f, 1.0f}; - QVector colors; - QVector colorIndices; - - QVector texCoords; - QVector texCoordIndices; - - QHash indices; - - std::vector attributes; -}; gpu::BufferPointer FBXMeshPart::getTrianglesForQuads() const { // if we've been asked for our triangulation of the original quads, but we don't yet have them @@ -555,306 +380,18 @@ gpu::BufferPointer FBXMeshPart::getTrianglesForQuads() const { return quadsAsTrianglesIndicesBuffer; } -void appendIndex(MeshData& data, QVector& indices, int index) { - if (index >= data.polygonIndices.size()) { - return; - } - int vertexIndex = data.polygonIndices.at(index); - if (vertexIndex < 0) { - vertexIndex = -vertexIndex - 1; - } - Vertex vertex; - vertex.originalIndex = vertexIndex; - - glm::vec3 position; - if (vertexIndex < data.vertices.size()) { - position = data.vertices.at(vertexIndex); - } - - glm::vec3 normal; - int normalIndex = data.normalsByVertex ? vertexIndex : index; - if (data.normalIndices.isEmpty()) { - if (normalIndex < data.normals.size()) { - normal = data.normals.at(normalIndex); - } - } else if (normalIndex < data.normalIndices.size()) { - normalIndex = data.normalIndices.at(normalIndex); - if (normalIndex >= 0 && normalIndex < data.normals.size()) { - normal = data.normals.at(normalIndex); - } - } - - - glm::vec4 color; - bool hasColors = (data.colors.size() > 1); - if (hasColors) { - int colorIndex = data.colorsByVertex ? vertexIndex : index; - if (data.colorIndices.isEmpty()) { - if (colorIndex < data.colors.size()) { - color = data.colors.at(colorIndex); - } - } else if (colorIndex < data.colorIndices.size()) { - colorIndex = data.colorIndices.at(colorIndex); - if (colorIndex >= 0 && colorIndex < data.colors.size()) { - color = data.colors.at(colorIndex); - } - } - } - - if (data.texCoordIndices.isEmpty()) { - if (index < data.texCoords.size()) { - vertex.texCoord = data.texCoords.at(index); - } - } else if (index < data.texCoordIndices.size()) { - int texCoordIndex = data.texCoordIndices.at(index); - if (texCoordIndex >= 0 && texCoordIndex < data.texCoords.size()) { - vertex.texCoord = data.texCoords.at(texCoordIndex); - } - } - - bool hasMoreTexcoords = (data.attributes.size() > 1); - if (hasMoreTexcoords) { - if (data.attributes[1].texCoordIndices.empty()) { - if (index < data.attributes[1].texCoords.size()) { - vertex.texCoord1 = data.attributes[1].texCoords.at(index); - } - } else if (index < data.attributes[1].texCoordIndices.size()) { - int texCoordIndex = data.attributes[1].texCoordIndices.at(index); - if (texCoordIndex >= 0 && texCoordIndex < data.attributes[1].texCoords.size()) { - vertex.texCoord1 = data.attributes[1].texCoords.at(texCoordIndex); - } - } - } - - QHash::const_iterator it = data.indices.find(vertex); - if (it == data.indices.constEnd()) { - int newIndex = data.extracted.mesh.vertices.size(); - indices.append(newIndex); - data.indices.insert(vertex, newIndex); - data.extracted.newIndices.insert(vertexIndex, newIndex); - data.extracted.mesh.vertices.append(position); - data.extracted.mesh.normals.append(normal); - data.extracted.mesh.texCoords.append(vertex.texCoord); - if (hasColors) { - data.extracted.mesh.colors.append(glm::vec3(color)); - } - if (hasMoreTexcoords) { - data.extracted.mesh.texCoords1.append(vertex.texCoord1); - } - } else { - indices.append(*it); - data.extracted.mesh.normals[*it] += normal; - } -} - -ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex) { - MeshData data; - data.extracted.mesh.meshIndex = meshIndex++; - QVector materials; - QVector textures; - bool isMaterialPerPolygon = false; - - foreach (const FBXNode& child, object.children) { - if (child.name == "Vertices") { - data.vertices = createVec3Vector(getDoubleVector(child)); - - } else if (child.name == "PolygonVertexIndex") { - data.polygonIndices = getIntVector(child); - - } else if (child.name == "LayerElementNormal") { - data.normalsByVertex = false; - bool indexToDirect = false; - foreach (const FBXNode& subdata, child.children) { - if (subdata.name == "Normals") { - data.normals = createVec3Vector(getDoubleVector(subdata)); - - } else if (subdata.name == "NormalsIndex") { - data.normalIndices = getIntVector(subdata); - - } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0) == "ByVertice") { - data.normalsByVertex = true; - - } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0) == "IndexToDirect") { - indexToDirect = true; - } - } - if (indexToDirect && data.normalIndices.isEmpty()) { - // hack to work around wacky Makehuman exports - data.normalsByVertex = true; - } - } else if (child.name == "LayerElementColor") { - data.colorsByVertex = false; - bool indexToDirect = false; - foreach (const FBXNode& subdata, child.children) { - if (subdata.name == "Colors") { - data.colors = createVec4VectorRGBA(getDoubleVector(subdata), data.averageColor); - } else if (subdata.name == "ColorsIndex") { - data.colorIndices = getIntVector(subdata); - - } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0) == "ByVertice") { - data.colorsByVertex = true; - - } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0) == "IndexToDirect") { - indexToDirect = true; - } - } - if (indexToDirect && data.normalIndices.isEmpty()) { - // hack to work around wacky Makehuman exports - data.colorsByVertex = true; - } - -#if defined(FBXREADER_KILL_BLACK_COLOR_ATTRIBUTE) - // Potential feature where we decide to kill the color attribute is to dark? - // Tested with the model: - // https://hifi-public.s3.amazonaws.com/ryan/gardenLight2.fbx - // let's check if we did have true data ? - if (glm::all(glm::lessThanEqual(data.averageColor, glm::vec4(0.09f)))) { - data.colors.clear(); - data.colorIndices.clear(); - data.colorsByVertex = false; - qCDebug(modelformat) << "LayerElementColor has an average value of 0.0f... let's forget it."; - } -#endif - - } else if (child.name == "LayerElementUV") { - if (child.properties.at(0).toInt() == 0) { - AttributeData attrib; - attrib.index = child.properties.at(0).toInt(); - foreach (const FBXNode& subdata, child.children) { - if (subdata.name == "UV") { - data.texCoords = createVec2Vector(getDoubleVector(subdata)); - attrib.texCoords = createVec2Vector(getDoubleVector(subdata)); - } else if (subdata.name == "UVIndex") { - data.texCoordIndices = getIntVector(subdata); - attrib.texCoordIndices = getIntVector(subdata); - } else if (subdata.name == "Name") { - attrib.name = subdata.properties.at(0).toString(); - } -#if defined(DEBUG_FBXREADER) - else { - int unknown = 0; - QString subname = subdata.name.data(); - if ( (subdata.name == "Version") - || (subdata.name == "MappingInformationType") - || (subdata.name == "ReferenceInformationType") ) { - } else { - unknown++; - } - } -#endif - } - data.extracted.texcoordSetMap.insert(attrib.name, data.attributes.size()); - data.attributes.push_back(attrib); - } else { - AttributeData attrib; - attrib.index = child.properties.at(0).toInt(); - foreach (const FBXNode& subdata, child.children) { - if (subdata.name == "UV") { - attrib.texCoords = createVec2Vector(getDoubleVector(subdata)); - } else if (subdata.name == "UVIndex") { - attrib.texCoordIndices = getIntVector(subdata); - } else if (subdata.name == "Name") { - attrib.name = subdata.properties.at(0).toString(); - } -#if defined(DEBUG_FBXREADER) - else { - int unknown = 0; - QString subname = subdata.name.data(); - if ( (subdata.name == "Version") - || (subdata.name == "MappingInformationType") - || (subdata.name == "ReferenceInformationType") ) { - } else { - unknown++; - } - } -#endif - } - - QHash::iterator it = data.extracted.texcoordSetMap.find(attrib.name); - if (it == data.extracted.texcoordSetMap.end()) { - data.extracted.texcoordSetMap.insert(attrib.name, data.attributes.size()); - data.attributes.push_back(attrib); - } else { - // WTF same names for different UVs? - qCDebug(modelformat) << "LayerElementUV #" << attrib.index << " is reusing the same name as #" << (*it) << ". Skip this texcoord attribute."; - } - } - } else if (child.name == "LayerElementMaterial") { - foreach (const FBXNode& subdata, child.children) { - if (subdata.name == "Materials") { - materials = getIntVector(subdata); - } else if (subdata.name == "MappingInformationType") { - if (subdata.properties.at(0) == "ByPolygon") - isMaterialPerPolygon = true; - } else { - isMaterialPerPolygon = false; - } - } - - - } else if (child.name == "LayerElementTexture") { - foreach (const FBXNode& subdata, child.children) { - if (subdata.name == "TextureId") { - textures = getIntVector(subdata); - } - } - } - } - - bool isMultiMaterial = false; - if (isMaterialPerPolygon) { - isMultiMaterial = true; - } - - // convert the polygons to quads and triangles - int polygonIndex = 0; - QHash, int> materialTextureParts; - for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) { - int endIndex = beginIndex; - while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0); - - QPair materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0, - (polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0); - int& partIndex = materialTextureParts[materialTexture]; - if (partIndex == 0) { - data.extracted.partMaterialTextures.append(materialTexture); - data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); - partIndex = data.extracted.mesh.parts.size(); - } - FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; - - if (endIndex - beginIndex == 4) { - appendIndex(data, part.quadIndices, beginIndex++); - appendIndex(data, part.quadIndices, beginIndex++); - appendIndex(data, part.quadIndices, beginIndex++); - appendIndex(data, part.quadIndices, beginIndex++); - } else { - for (int nextIndex = beginIndex + 1;; ) { - appendIndex(data, part.triangleIndices, beginIndex); - appendIndex(data, part.triangleIndices, nextIndex++); - appendIndex(data, part.triangleIndices, nextIndex); - if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) { - break; - } - } - beginIndex = endIndex; - } - } - - return data.extracted; -} FBXBlendshape extractBlendshape(const FBXNode& object) { FBXBlendshape blendshape; foreach (const FBXNode& data, object.children) { if (data.name == "Indexes") { - blendshape.indices = getIntVector(data); + blendshape.indices = FBXReader::getIntVector(data); } else if (data.name == "Vertices") { - blendshape.vertices = createVec3Vector(getDoubleVector(data)); + blendshape.vertices = FBXReader::createVec3Vector(FBXReader::getDoubleVector(data)); } else if (data.name == "Normals") { - blendshape.normals = createVec3Vector(getDoubleVector(data)); + blendshape.normals = FBXReader::createVec3Vector(FBXReader::getDoubleVector(data)); } } return blendshape; @@ -954,31 +491,6 @@ public: QVector values; }; -FBXTexture getTexture(const QString& textureID, - const QHash& textureNames, - const QHash& textureFilenames, - const QHash& textureContent, - const QHash& textureParams) { - FBXTexture texture; - texture.filename = textureFilenames.value(textureID); - texture.name = textureNames.value(textureID); - texture.content = textureContent.value(texture.filename); - texture.transform.setIdentity(); - texture.texcoordSet = 0; - QHash::const_iterator it = textureParams.constFind(textureID); - if (it != textureParams.end()) { - const TextureParam& p = (*it); - texture.transform.setTranslation(p.translation); - texture.transform.setRotation(glm::quat(glm::radians(p.rotation))); - texture.transform.setScale(p.scaling); - if ((p.UVSet != "map1") && (p.UVSet != "UVSet0")) { - texture.texcoordSet = 1; - } - texture.texcoordSetName = p.UVSet; - } - return texture; -} - bool checkMaterialsHaveTextures(const QHash& materials, const QHash& textureFilenames, const QMultiHash& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { @@ -1022,7 +534,7 @@ FBXLight extractLight(const FBXNode& object) { if (propname == "Intensity") { light.intensity = 0.01f * property.properties.at(valIndex).value(); } else if (propname == "Color") { - light.color = getVec3(property.properties, valIndex); + light.color = FBXReader::getVec3(property.properties, valIndex); } } } @@ -1048,149 +560,6 @@ FBXLight extractLight(const FBXNode& object) { } -#if USE_MODEL_MESH -void buildModelMesh(ExtractedMesh& extracted, const QString& url) { - static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex("buildModelMesh failed -- .*"); - - if (extracted.mesh.vertices.size() == 0) { - extracted.mesh._mesh = model::Mesh(); - qCDebug(modelformat) << "buildModelMesh failed -- no vertices, url = " << url; - return; - } - FBXMesh& fbxMesh = extracted.mesh; - model::Mesh mesh; - - // Grab the vertices in a buffer - auto vb = make_shared(); - vb->setData(extracted.mesh.vertices.size() * sizeof(glm::vec3), - (const gpu::Byte*) extracted.mesh.vertices.data()); - gpu::BufferView vbv(vb, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - mesh.setVertexBuffer(vbv); - - // evaluate all attribute channels sizes - int normalsSize = fbxMesh.normals.size() * sizeof(glm::vec3); - int tangentsSize = fbxMesh.tangents.size() * sizeof(glm::vec3); - int colorsSize = fbxMesh.colors.size() * sizeof(glm::vec3); - int texCoordsSize = fbxMesh.texCoords.size() * sizeof(glm::vec2); - int texCoords1Size = fbxMesh.texCoords1.size() * sizeof(glm::vec2); - int clusterIndicesSize = fbxMesh.clusterIndices.size() * sizeof(glm::vec4); - int clusterWeightsSize = fbxMesh.clusterWeights.size() * sizeof(glm::vec4); - - int normalsOffset = 0; - int tangentsOffset = normalsOffset + normalsSize; - int colorsOffset = tangentsOffset + tangentsSize; - int texCoordsOffset = colorsOffset + colorsSize; - int texCoords1Offset = texCoordsOffset + texCoordsSize; - int clusterIndicesOffset = texCoords1Offset + texCoords1Size; - int clusterWeightsOffset = clusterIndicesOffset + clusterIndicesSize; - int totalAttributeSize = clusterWeightsOffset + clusterWeightsSize; - - // Copy all attribute data in a single attribute buffer - auto attribBuffer = make_shared(); - attribBuffer->resize(totalAttributeSize); - attribBuffer->setSubData(normalsOffset, normalsSize, (gpu::Byte*) fbxMesh.normals.constData()); - attribBuffer->setSubData(tangentsOffset, tangentsSize, (gpu::Byte*) fbxMesh.tangents.constData()); - attribBuffer->setSubData(colorsOffset, colorsSize, (gpu::Byte*) fbxMesh.colors.constData()); - attribBuffer->setSubData(texCoordsOffset, texCoordsSize, (gpu::Byte*) fbxMesh.texCoords.constData()); - attribBuffer->setSubData(texCoords1Offset, texCoords1Size, (gpu::Byte*) fbxMesh.texCoords1.constData()); - attribBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (gpu::Byte*) fbxMesh.clusterIndices.constData()); - attribBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (gpu::Byte*) fbxMesh.clusterWeights.constData()); - - if (normalsSize) { - mesh.addAttribute(gpu::Stream::NORMAL, - model::BufferView(attribBuffer, normalsOffset, normalsSize, - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ))); - } - if (tangentsSize) { - mesh.addAttribute(gpu::Stream::TANGENT, - model::BufferView(attribBuffer, tangentsOffset, tangentsSize, - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ))); - } - if (colorsSize) { - mesh.addAttribute(gpu::Stream::COLOR, - model::BufferView(attribBuffer, colorsOffset, colorsSize, - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB))); - } - if (texCoordsSize) { - mesh.addAttribute(gpu::Stream::TEXCOORD, - model::BufferView( attribBuffer, texCoordsOffset, texCoordsSize, - gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV))); - } - if (texCoords1Size) { - mesh.addAttribute(gpu::Stream::TEXCOORD1, - model::BufferView(attribBuffer, texCoords1Offset, texCoords1Size, - gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV))); - } - if (clusterIndicesSize) { - mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, - model::BufferView(attribBuffer, clusterIndicesOffset, clusterIndicesSize, - gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW))); - } - if (clusterWeightsSize) { - mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, - model::BufferView(attribBuffer, clusterWeightsOffset, clusterWeightsSize, - gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW))); - } - - - unsigned int totalIndices = 0; - - foreach(const FBXMeshPart& part, extracted.mesh.parts) { - totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); - } - - if (! totalIndices) { - extracted.mesh._mesh = model::Mesh(); - qCDebug(modelformat) << "buildModelMesh failed -- no indices, url = " << url; - return; - } - - auto ib = make_shared(); - ib->resize(totalIndices * sizeof(int)); - - int indexNum = 0; - int offset = 0; - - std::vector< model::Mesh::Part > parts; - - foreach(const FBXMeshPart& part, extracted.mesh.parts) { - model::Mesh::Part quadPart(indexNum, part.quadIndices.size(), 0, model::Mesh::QUADS); - if (quadPart._numIndices) { - parts.push_back(quadPart); - ib->setSubData(offset, part.quadIndices.size() * sizeof(int), - (gpu::Byte*) part.quadIndices.constData()); - offset += part.quadIndices.size() * sizeof(int); - indexNum += part.quadIndices.size(); - } - model::Mesh::Part triPart(indexNum, part.triangleIndices.size(), 0, model::Mesh::TRIANGLES); - if (triPart._numIndices) { - ib->setSubData(offset, part.triangleIndices.size() * sizeof(int), - (gpu::Byte*) part.triangleIndices.constData()); - offset += part.triangleIndices.size() * sizeof(int); - indexNum += part.triangleIndices.size(); - } - } - - gpu::BufferView ibv(ib, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::XYZ)); - mesh.setIndexBuffer(ibv); - - if (parts.size()) { - auto pb = make_shared(); - pb->setData(parts.size() * sizeof(model::Mesh::Part), (const gpu::Byte*) parts.data()); - gpu::BufferView pbv(pb, gpu::Element(gpu::VEC4, gpu::UINT32, gpu::XYZW)); - mesh.setPartBuffer(pbv); - } else { - extracted.mesh._mesh = model::Mesh(); - qCDebug(modelformat) << "buildModelMesh failed -- no parts, url = " << url; - return; - } - - // model::Box box = - mesh.evalPartBound(0); - - extracted.mesh._mesh = mesh; -} -#endif // USE_MODEL_MESH QByteArray fileOnUrl(const QByteArray& filenameString, const QString& url) { QString path = QFileInfo(url).path(); @@ -2042,80 +1411,19 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (_fbxMaterials.contains(childID)) { // the pure material associated with this part FBXMaterial material = _fbxMaterials.value(childID); - - - bool detectDifferentUVs = false; - /* FBXTexture diffuseTexture; - QString diffuseTextureID = diffuseTextures.value(childID); - if (!diffuseTextureID.isNull()) { - diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); - - // FBX files generated by 3DSMax have an intermediate texture parent, apparently - foreach (const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { - if (textureFilenames.contains(childTextureID)) { - diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); - } - } - diffuseTexture.texcoordSet = matchTextureUVSetToAttributeChannel(diffuseTexture.texcoordSetName, extracted.texcoordSetMap); - - detectDifferentUVs = (diffuseTexture.texcoordSet != 0) || (!diffuseTexture.transform.isIdentity()); - } - - FBXTexture normalTexture; - QString bumpTextureID = bumpTextures.value(childID); - if (!bumpTextureID.isNull()) { - normalTexture = getTexture(bumpTextureID, textureNames, textureFilenames, textureContent, textureParams); - generateTangents = true; - - normalTexture.texcoordSet = matchTextureUVSetToAttributeChannel(normalTexture.texcoordSetName, extracted.texcoordSetMap); - - detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); - } - - FBXTexture specularTexture; - QString specularTextureID = specularTextures.value(childID); - if (!specularTextureID.isNull()) { - specularTexture = getTexture(specularTextureID, textureNames, textureFilenames, textureContent, textureParams); - specularTexture.texcoordSet = matchTextureUVSetToAttributeChannel(specularTexture.texcoordSetName, extracted.texcoordSetMap); - detectDifferentUVs |= (specularTexture.texcoordSet != 0) || (!specularTexture.transform.isIdentity()); - } - - FBXTexture emissiveTexture; - glm::vec2 emissiveParams(0.f, 1.f); - emissiveParams.x = _lightmapOffset; - emissiveParams.y = _lightmapLevel; - - QString emissiveTextureID = emissiveTextures.value(childID); - QString ambientTextureID = ambientTextures.value(childID); - if (_loadLightmaps && (!emissiveTextureID.isNull() || !ambientTextureID.isNull())) { - - if (!emissiveTextureID.isNull()) { - emissiveTexture = getTexture(emissiveTextureID, textureNames, textureFilenames, textureContent, textureParams); - emissiveParams.y = 4.0f; - } else if (!ambientTextureID.isNull()) { - emissiveTexture = getTexture(ambientTextureID, textureNames, textureFilenames, textureContent, textureParams); - } - - emissiveTexture.texcoordSet = matchTextureUVSetToAttributeChannel(emissiveTexture.texcoordSetName, extracted.texcoordSetMap); - - detectDifferentUVs |= (emissiveTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); - } - */ - if (detectDifferentUVs) { - detectDifferentUVs = false; - } for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { if (extracted.partMaterialTextures.at(j).first == materialIndex) { FBXMeshPart& part = extracted.mesh.parts[j]; part.materialID = material.materialID; - generateTangents = material.needTangentSpace(); + generateTangents |= material.needTangentSpace(); } } + materialIndex++; } else if (textureFilenames.contains(childID)) { - FBXTexture texture = getTexture(childID, textureNames, textureFilenames, textureContent, textureParams); + FBXTexture texture = getTexture(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) { @@ -2424,104 +1732,3 @@ FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri } -bool FBXMaterial::needTangentSpace() const { - return !normalTexture.isNull(); -} - - -void FBXReader::consolidateFBXMaterials() { - - // foreach (const QString& materialID, materials) { - for (QHash::iterator it = _fbxMaterials.begin(); it != _fbxMaterials.end(); it++) { - FBXMaterial& material = (*it); - // the pure material associated with this part - bool detectDifferentUVs = false; - FBXTexture diffuseTexture; - QString diffuseTextureID = diffuseTextures.value(material.materialID); - if (!diffuseTextureID.isNull()) { - diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); - - // FBX files generated by 3DSMax have an intermediate texture parent, apparently - foreach (const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { - if (textureFilenames.contains(childTextureID)) { - diffuseTexture = getTexture(diffuseTextureID, textureNames, textureFilenames, textureContent, textureParams); - } - } - - // TODO associate this per part - //diffuseTexture.texcoordSet = matchTextureUVSetToAttributeChannel(diffuseTexture.texcoordSetName, extracted.texcoordSetMap); - - material.diffuseTexture = diffuseTexture; - - detectDifferentUVs = (diffuseTexture.texcoordSet != 0) || (!diffuseTexture.transform.isIdentity()); - } - - FBXTexture normalTexture; - QString bumpTextureID = bumpTextures.value(material.materialID); - if (!bumpTextureID.isNull()) { - normalTexture = getTexture(bumpTextureID, textureNames, textureFilenames, textureContent, textureParams); - - // TODO Need to generate tangent space at association per part - //generateTangents = true; - - // TODO at per part association time - // normalTexture.texcoordSet = matchTextureUVSetToAttributeChannel(normalTexture.texcoordSetName, extracted.texcoordSetMap); - - material.normalTexture = normalTexture; - - detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); - } - - FBXTexture specularTexture; - QString specularTextureID = specularTextures.value(material.materialID); - if (!specularTextureID.isNull()) { - specularTexture = getTexture(specularTextureID, textureNames, textureFilenames, textureContent, textureParams); - // TODO at per part association time - // specularTexture.texcoordSet = matchTextureUVSetToAttributeChannel(specularTexture.texcoordSetName, extracted.texcoordSetMap); - detectDifferentUVs |= (specularTexture.texcoordSet != 0) || (!specularTexture.transform.isIdentity()); - } - - FBXTexture emissiveTexture; - glm::vec2 emissiveParams(0.f, 1.f); - emissiveParams.x = _lightmapOffset; - emissiveParams.y = _lightmapLevel; - - QString emissiveTextureID = emissiveTextures.value(material.materialID); - QString ambientTextureID = ambientTextures.value(material.materialID); - if (_loadLightmaps && (!emissiveTextureID.isNull() || !ambientTextureID.isNull())) { - - if (!emissiveTextureID.isNull()) { - emissiveTexture = getTexture(emissiveTextureID, textureNames, textureFilenames, textureContent, textureParams); - emissiveParams.y = 4.0f; - } else if (!ambientTextureID.isNull()) { - emissiveTexture = getTexture(ambientTextureID, textureNames, textureFilenames, textureContent, textureParams); - } - - // TODO : do this at per part association - //emissiveTexture.texcoordSet = matchTextureUVSetToAttributeChannel(emissiveTexture.texcoordSetName, extracted.texcoordSetMap); - - material.emissiveParams = emissiveParams; - material.emissiveTexture = emissiveTexture; - - - detectDifferentUVs |= (emissiveTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); - } - - // Finally create the true material representation - material._material = make_shared(); - material._material->setEmissive(material.emissiveColor); - if (glm::all(glm::equal(material.diffuseColor, glm::vec3(0.0f)))) { - material._material->setDiffuse(material.diffuseColor); - } else { - material._material->setDiffuse(material.diffuseColor); - } - material._material->setMetallic(glm::length(material.specularColor)); - material._material->setGloss(material.shininess); - - if (material.opacity <= 0.0f) { - material._material->setOpacity(1.0f); - } else { - material._material->setOpacity(material.opacity); - } - } -} diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 4fdf166c42..c3c9a77b18 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -185,6 +185,16 @@ public: # endif }; +class ExtractedMesh { +public: + FBXMesh mesh; + QMultiHash newIndices; + QVector > blendshapeIndexMaps; + QVector > partMaterialTextures; + QHash texcoordSetMap; + std::map texcoordSetMap2; +}; + /// A single animation frame extracted from an FBX document. class FBXAnimationFrame { public: @@ -346,6 +356,8 @@ struct TextureParam { {} }; +class ExtractedMesh; + class FBXReader { public: FBXGeometry* _fbxGeometry; @@ -355,6 +367,12 @@ public: FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url); + ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex); + QHash meshes; + void buildModelMesh(ExtractedMesh& extracted, const QString& url); + + FBXTexture getTexture(const QString& textureID); + QHash _textureImages; QHash textureNames; @@ -379,6 +397,17 @@ public: QMultiHash _connectionParentMap; QMultiHash _connectionChildMap; + + static glm::vec3 getVec3(const QVariantList& properties, int index); + static QVector createVec4Vector(const QVector& doubleVector); + static QVector createVec4VectorRGBA(const QVector& doubleVector, glm::vec4& average); + static QVector createVec3Vector(const QVector& doubleVector); + static QVector createVec2Vector(const QVector& doubleVector); + static glm::mat4 createMat4(const QVector& doubleVector); + + static QVector getIntVector(const FBXNode& node); + static QVector getFloatVector(const FBXNode& node); + static QVector getDoubleVector(const FBXNode& node); }; #endif // hifi_FBXReader_h diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp new file mode 100644 index 0000000000..b634886079 --- /dev/null +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -0,0 +1,146 @@ +// +// FBXReader_Material.cpp +// interface/src/fbx +// +// Created by Sam Gateau on 8/27/2015. +// Copyright 2015 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include "FBXReader.h" + +#include + +bool FBXMaterial::needTangentSpace() const { + return !normalTexture.isNull(); +} + +FBXTexture FBXReader::getTexture(const QString& textureID) { + FBXTexture texture; + texture.filename = textureFilenames.value(textureID); + texture.name = textureNames.value(textureID); + texture.content = textureContent.value(texture.filename); + texture.transform.setIdentity(); + texture.texcoordSet = 0; + QHash::const_iterator it = textureParams.constFind(textureID); + if (it != textureParams.end()) { + const TextureParam& p = (*it); + texture.transform.setTranslation(p.translation); + texture.transform.setRotation(glm::quat(glm::radians(p.rotation))); + texture.transform.setScale(p.scaling); + if ((p.UVSet != "map1") && (p.UVSet != "UVSet0")) { + texture.texcoordSet = 1; + } + texture.texcoordSetName = p.UVSet; + } + return texture; +} + +void FBXReader::consolidateFBXMaterials() { + + // foreach (const QString& materialID, materials) { + for (QHash::iterator it = _fbxMaterials.begin(); it != _fbxMaterials.end(); it++) { + FBXMaterial& material = (*it); + // the pure material associated with this part + bool detectDifferentUVs = false; + FBXTexture diffuseTexture; + QString diffuseTextureID = diffuseTextures.value(material.materialID); + if (!diffuseTextureID.isNull()) { + diffuseTexture = getTexture(diffuseTextureID); + + // FBX files generated by 3DSMax have an intermediate texture parent, apparently + foreach (const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { + if (textureFilenames.contains(childTextureID)) { + diffuseTexture = getTexture(diffuseTextureID); + } + } + + // TODO associate this per part + //diffuseTexture.texcoordSet = matchTextureUVSetToAttributeChannel(diffuseTexture.texcoordSetName, extracted.texcoordSetMap); + + material.diffuseTexture = diffuseTexture; + + detectDifferentUVs = (diffuseTexture.texcoordSet != 0) || (!diffuseTexture.transform.isIdentity()); + } + + FBXTexture normalTexture; + QString bumpTextureID = bumpTextures.value(material.materialID); + if (!bumpTextureID.isNull()) { + normalTexture = getTexture(bumpTextureID); + + // TODO Need to generate tangent space at association per part + //generateTangents = true; + + // TODO at per part association time + // normalTexture.texcoordSet = matchTextureUVSetToAttributeChannel(normalTexture.texcoordSetName, extracted.texcoordSetMap); + + material.normalTexture = normalTexture; + + detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); + } + + FBXTexture specularTexture; + QString specularTextureID = specularTextures.value(material.materialID); + if (!specularTextureID.isNull()) { + specularTexture = getTexture(specularTextureID); + // TODO at per part association time + // specularTexture.texcoordSet = matchTextureUVSetToAttributeChannel(specularTexture.texcoordSetName, extracted.texcoordSetMap); + detectDifferentUVs |= (specularTexture.texcoordSet != 0) || (!specularTexture.transform.isIdentity()); + } + + FBXTexture emissiveTexture; + glm::vec2 emissiveParams(0.f, 1.f); + emissiveParams.x = _lightmapOffset; + emissiveParams.y = _lightmapLevel; + + QString emissiveTextureID = emissiveTextures.value(material.materialID); + QString ambientTextureID = ambientTextures.value(material.materialID); + if (_loadLightmaps && (!emissiveTextureID.isNull() || !ambientTextureID.isNull())) { + + if (!emissiveTextureID.isNull()) { + emissiveTexture = getTexture(emissiveTextureID); + emissiveParams.y = 4.0f; + } else if (!ambientTextureID.isNull()) { + emissiveTexture = getTexture(ambientTextureID); + } + + // TODO : do this at per part association + //emissiveTexture.texcoordSet = matchTextureUVSetToAttributeChannel(emissiveTexture.texcoordSetName, extracted.texcoordSetMap); + + material.emissiveParams = emissiveParams; + material.emissiveTexture = emissiveTexture; + + + detectDifferentUVs |= (emissiveTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); + } + + // Finally create the true material representation + material._material = std::make_shared(); + material._material->setEmissive(material.emissiveColor); + if (glm::all(glm::equal(material.diffuseColor, glm::vec3(0.0f)))) { + material._material->setDiffuse(material.diffuseColor); + } else { + material._material->setDiffuse(material.diffuseColor); + } + material._material->setMetallic(glm::length(material.specularColor)); + material._material->setGloss(material.shininess); + + if (material.opacity <= 0.0f) { + material._material->setOpacity(1.0f); + } else { + material._material->setOpacity(material.opacity); + } + } +} + diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp new file mode 100644 index 0000000000..5cf1c7e27a --- /dev/null +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -0,0 +1,509 @@ +// +// FBXReader_Mesh.cpp +// interface/src/fbx +// +// Created by Sam Gateau on 8/27/2015. +// Copyright 2015 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ModelFormatLogging.h" + +#include "FBXReader.h" + +#include + + +class Vertex { +public: + int originalIndex; + glm::vec2 texCoord; + glm::vec2 texCoord1; +}; + +uint qHash(const Vertex& vertex, uint seed = 0) { + return qHash(vertex.originalIndex, seed); +} + +bool operator==(const Vertex& v1, const Vertex& v2) { + return v1.originalIndex == v2.originalIndex && v1.texCoord == v2.texCoord && v1.texCoord1 == v2.texCoord1; +} + +class AttributeData { +public: + QVector texCoords; + QVector texCoordIndices; + QString name; + int index; +}; + +class MeshData { +public: + ExtractedMesh extracted; + QVector vertices; + QVector polygonIndices; + bool normalsByVertex; + QVector normals; + QVector normalIndices; + + bool colorsByVertex; + glm::vec4 averageColor{1.0f, 1.0f, 1.0f, 1.0f}; + QVector colors; + QVector colorIndices; + + QVector texCoords; + QVector texCoordIndices; + + QHash indices; + + std::vector attributes; +}; + + +void appendIndex(MeshData& data, QVector& indices, int index) { + if (index >= data.polygonIndices.size()) { + return; + } + int vertexIndex = data.polygonIndices.at(index); + if (vertexIndex < 0) { + vertexIndex = -vertexIndex - 1; + } + Vertex vertex; + vertex.originalIndex = vertexIndex; + + glm::vec3 position; + if (vertexIndex < data.vertices.size()) { + position = data.vertices.at(vertexIndex); + } + + glm::vec3 normal; + int normalIndex = data.normalsByVertex ? vertexIndex : index; + if (data.normalIndices.isEmpty()) { + if (normalIndex < data.normals.size()) { + normal = data.normals.at(normalIndex); + } + } else if (normalIndex < data.normalIndices.size()) { + normalIndex = data.normalIndices.at(normalIndex); + if (normalIndex >= 0 && normalIndex < data.normals.size()) { + normal = data.normals.at(normalIndex); + } + } + + + glm::vec4 color; + bool hasColors = (data.colors.size() > 1); + if (hasColors) { + int colorIndex = data.colorsByVertex ? vertexIndex : index; + if (data.colorIndices.isEmpty()) { + if (colorIndex < data.colors.size()) { + color = data.colors.at(colorIndex); + } + } else if (colorIndex < data.colorIndices.size()) { + colorIndex = data.colorIndices.at(colorIndex); + if (colorIndex >= 0 && colorIndex < data.colors.size()) { + color = data.colors.at(colorIndex); + } + } + } + + if (data.texCoordIndices.isEmpty()) { + if (index < data.texCoords.size()) { + vertex.texCoord = data.texCoords.at(index); + } + } else if (index < data.texCoordIndices.size()) { + int texCoordIndex = data.texCoordIndices.at(index); + if (texCoordIndex >= 0 && texCoordIndex < data.texCoords.size()) { + vertex.texCoord = data.texCoords.at(texCoordIndex); + } + } + + bool hasMoreTexcoords = (data.attributes.size() > 1); + if (hasMoreTexcoords) { + if (data.attributes[1].texCoordIndices.empty()) { + if (index < data.attributes[1].texCoords.size()) { + vertex.texCoord1 = data.attributes[1].texCoords.at(index); + } + } else if (index < data.attributes[1].texCoordIndices.size()) { + int texCoordIndex = data.attributes[1].texCoordIndices.at(index); + if (texCoordIndex >= 0 && texCoordIndex < data.attributes[1].texCoords.size()) { + vertex.texCoord1 = data.attributes[1].texCoords.at(texCoordIndex); + } + } + } + + QHash::const_iterator it = data.indices.find(vertex); + if (it == data.indices.constEnd()) { + int newIndex = data.extracted.mesh.vertices.size(); + indices.append(newIndex); + data.indices.insert(vertex, newIndex); + data.extracted.newIndices.insert(vertexIndex, newIndex); + data.extracted.mesh.vertices.append(position); + data.extracted.mesh.normals.append(normal); + data.extracted.mesh.texCoords.append(vertex.texCoord); + if (hasColors) { + data.extracted.mesh.colors.append(glm::vec3(color)); + } + if (hasMoreTexcoords) { + data.extracted.mesh.texCoords1.append(vertex.texCoord1); + } + } else { + indices.append(*it); + data.extracted.mesh.normals[*it] += normal; + } +} + +ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex) { + MeshData data; + data.extracted.mesh.meshIndex = meshIndex++; + QVector materials; + QVector textures; + bool isMaterialPerPolygon = false; + + foreach (const FBXNode& child, object.children) { + if (child.name == "Vertices") { + data.vertices = createVec3Vector(getDoubleVector(child)); + + } else if (child.name == "PolygonVertexIndex") { + data.polygonIndices = getIntVector(child); + + } else if (child.name == "LayerElementNormal") { + data.normalsByVertex = false; + bool indexToDirect = false; + foreach (const FBXNode& subdata, child.children) { + if (subdata.name == "Normals") { + data.normals = createVec3Vector(getDoubleVector(subdata)); + + } else if (subdata.name == "NormalsIndex") { + data.normalIndices = getIntVector(subdata); + + } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0) == "ByVertice") { + data.normalsByVertex = true; + + } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0) == "IndexToDirect") { + indexToDirect = true; + } + } + if (indexToDirect && data.normalIndices.isEmpty()) { + // hack to work around wacky Makehuman exports + data.normalsByVertex = true; + } + } else if (child.name == "LayerElementColor") { + data.colorsByVertex = false; + bool indexToDirect = false; + foreach (const FBXNode& subdata, child.children) { + if (subdata.name == "Colors") { + data.colors = createVec4VectorRGBA(getDoubleVector(subdata), data.averageColor); + } else if (subdata.name == "ColorsIndex") { + data.colorIndices = getIntVector(subdata); + + } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0) == "ByVertice") { + data.colorsByVertex = true; + + } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0) == "IndexToDirect") { + indexToDirect = true; + } + } + if (indexToDirect && data.normalIndices.isEmpty()) { + // hack to work around wacky Makehuman exports + data.colorsByVertex = true; + } + +#if defined(FBXREADER_KILL_BLACK_COLOR_ATTRIBUTE) + // Potential feature where we decide to kill the color attribute is to dark? + // Tested with the model: + // https://hifi-public.s3.amazonaws.com/ryan/gardenLight2.fbx + // let's check if we did have true data ? + if (glm::all(glm::lessThanEqual(data.averageColor, glm::vec4(0.09f)))) { + data.colors.clear(); + data.colorIndices.clear(); + data.colorsByVertex = false; + qCDebug(modelformat) << "LayerElementColor has an average value of 0.0f... let's forget it."; + } +#endif + + } else if (child.name == "LayerElementUV") { + if (child.properties.at(0).toInt() == 0) { + AttributeData attrib; + attrib.index = child.properties.at(0).toInt(); + foreach (const FBXNode& subdata, child.children) { + if (subdata.name == "UV") { + data.texCoords = createVec2Vector(getDoubleVector(subdata)); + attrib.texCoords = createVec2Vector(getDoubleVector(subdata)); + } else if (subdata.name == "UVIndex") { + data.texCoordIndices = getIntVector(subdata); + attrib.texCoordIndices = getIntVector(subdata); + } else if (subdata.name == "Name") { + attrib.name = subdata.properties.at(0).toString(); + } +#if defined(DEBUG_FBXREADER) + else { + int unknown = 0; + QString subname = subdata.name.data(); + if ( (subdata.name == "Version") + || (subdata.name == "MappingInformationType") + || (subdata.name == "ReferenceInformationType") ) { + } else { + unknown++; + } + } +#endif + } + data.extracted.texcoordSetMap.insert(attrib.name, data.attributes.size()); + data.attributes.push_back(attrib); + } else { + AttributeData attrib; + attrib.index = child.properties.at(0).toInt(); + foreach (const FBXNode& subdata, child.children) { + if (subdata.name == "UV") { + attrib.texCoords = createVec2Vector(getDoubleVector(subdata)); + } else if (subdata.name == "UVIndex") { + attrib.texCoordIndices = getIntVector(subdata); + } else if (subdata.name == "Name") { + attrib.name = subdata.properties.at(0).toString(); + } +#if defined(DEBUG_FBXREADER) + else { + int unknown = 0; + QString subname = subdata.name.data(); + if ( (subdata.name == "Version") + || (subdata.name == "MappingInformationType") + || (subdata.name == "ReferenceInformationType") ) { + } else { + unknown++; + } + } +#endif + } + + QHash::iterator it = data.extracted.texcoordSetMap.find(attrib.name); + if (it == data.extracted.texcoordSetMap.end()) { + data.extracted.texcoordSetMap.insert(attrib.name, data.attributes.size()); + data.attributes.push_back(attrib); + } else { + // WTF same names for different UVs? + qCDebug(modelformat) << "LayerElementUV #" << attrib.index << " is reusing the same name as #" << (*it) << ". Skip this texcoord attribute."; + } + } + } else if (child.name == "LayerElementMaterial") { + foreach (const FBXNode& subdata, child.children) { + if (subdata.name == "Materials") { + materials = getIntVector(subdata); + } else if (subdata.name == "MappingInformationType") { + if (subdata.properties.at(0) == "ByPolygon") + isMaterialPerPolygon = true; + } else { + isMaterialPerPolygon = false; + } + } + + + } else if (child.name == "LayerElementTexture") { + foreach (const FBXNode& subdata, child.children) { + if (subdata.name == "TextureId") { + textures = getIntVector(subdata); + } + } + } + } + + bool isMultiMaterial = false; + if (isMaterialPerPolygon) { + isMultiMaterial = true; + } + + // convert the polygons to quads and triangles + int polygonIndex = 0; + QHash, int> materialTextureParts; + for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) { + int endIndex = beginIndex; + while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0); + + QPair materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0, + (polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0); + int& partIndex = materialTextureParts[materialTexture]; + if (partIndex == 0) { + data.extracted.partMaterialTextures.append(materialTexture); + data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); + partIndex = data.extracted.mesh.parts.size(); + } + FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; + + if (endIndex - beginIndex == 4) { + appendIndex(data, part.quadIndices, beginIndex++); + appendIndex(data, part.quadIndices, beginIndex++); + appendIndex(data, part.quadIndices, beginIndex++); + appendIndex(data, part.quadIndices, beginIndex++); + } else { + for (int nextIndex = beginIndex + 1;; ) { + appendIndex(data, part.triangleIndices, beginIndex); + appendIndex(data, part.triangleIndices, nextIndex++); + appendIndex(data, part.triangleIndices, nextIndex); + if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) { + break; + } + } + beginIndex = endIndex; + } + } + + return data.extracted; +} + + +#if USE_MODEL_MESH +void FBXReader::buildModelMesh(ExtractedMesh& extracted, const QString& url) { + static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex("buildModelMesh failed -- .*"); + + if (extracted.mesh.vertices.size() == 0) { + extracted.mesh._mesh = model::Mesh(); + qCDebug(modelformat) << "buildModelMesh failed -- no vertices, url = " << url; + return; + } + FBXMesh& fbxMesh = extracted.mesh; + model::Mesh mesh; + + // Grab the vertices in a buffer + auto vb = std::make_shared(); + vb->setData(extracted.mesh.vertices.size() * sizeof(glm::vec3), + (const gpu::Byte*) extracted.mesh.vertices.data()); + gpu::BufferView vbv(vb, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + mesh.setVertexBuffer(vbv); + + // evaluate all attribute channels sizes + int normalsSize = fbxMesh.normals.size() * sizeof(glm::vec3); + int tangentsSize = fbxMesh.tangents.size() * sizeof(glm::vec3); + int colorsSize = fbxMesh.colors.size() * sizeof(glm::vec3); + int texCoordsSize = fbxMesh.texCoords.size() * sizeof(glm::vec2); + int texCoords1Size = fbxMesh.texCoords1.size() * sizeof(glm::vec2); + int clusterIndicesSize = fbxMesh.clusterIndices.size() * sizeof(glm::vec4); + int clusterWeightsSize = fbxMesh.clusterWeights.size() * sizeof(glm::vec4); + + int normalsOffset = 0; + int tangentsOffset = normalsOffset + normalsSize; + int colorsOffset = tangentsOffset + tangentsSize; + int texCoordsOffset = colorsOffset + colorsSize; + int texCoords1Offset = texCoordsOffset + texCoordsSize; + int clusterIndicesOffset = texCoords1Offset + texCoords1Size; + int clusterWeightsOffset = clusterIndicesOffset + clusterIndicesSize; + int totalAttributeSize = clusterWeightsOffset + clusterWeightsSize; + + // Copy all attribute data in a single attribute buffer + auto attribBuffer = std::make_shared(); + attribBuffer->resize(totalAttributeSize); + attribBuffer->setSubData(normalsOffset, normalsSize, (gpu::Byte*) fbxMesh.normals.constData()); + attribBuffer->setSubData(tangentsOffset, tangentsSize, (gpu::Byte*) fbxMesh.tangents.constData()); + attribBuffer->setSubData(colorsOffset, colorsSize, (gpu::Byte*) fbxMesh.colors.constData()); + attribBuffer->setSubData(texCoordsOffset, texCoordsSize, (gpu::Byte*) fbxMesh.texCoords.constData()); + attribBuffer->setSubData(texCoords1Offset, texCoords1Size, (gpu::Byte*) fbxMesh.texCoords1.constData()); + attribBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (gpu::Byte*) fbxMesh.clusterIndices.constData()); + attribBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (gpu::Byte*) fbxMesh.clusterWeights.constData()); + + if (normalsSize) { + mesh.addAttribute(gpu::Stream::NORMAL, + model::BufferView(attribBuffer, normalsOffset, normalsSize, + gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ))); + } + if (tangentsSize) { + mesh.addAttribute(gpu::Stream::TANGENT, + model::BufferView(attribBuffer, tangentsOffset, tangentsSize, + gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ))); + } + if (colorsSize) { + mesh.addAttribute(gpu::Stream::COLOR, + model::BufferView(attribBuffer, colorsOffset, colorsSize, + gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB))); + } + if (texCoordsSize) { + mesh.addAttribute(gpu::Stream::TEXCOORD, + model::BufferView( attribBuffer, texCoordsOffset, texCoordsSize, + gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV))); + } + if (texCoords1Size) { + mesh.addAttribute(gpu::Stream::TEXCOORD1, + model::BufferView(attribBuffer, texCoords1Offset, texCoords1Size, + gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV))); + } + if (clusterIndicesSize) { + mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, + model::BufferView(attribBuffer, clusterIndicesOffset, clusterIndicesSize, + gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW))); + } + if (clusterWeightsSize) { + mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, + model::BufferView(attribBuffer, clusterWeightsOffset, clusterWeightsSize, + gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW))); + } + + + unsigned int totalIndices = 0; + + foreach(const FBXMeshPart& part, extracted.mesh.parts) { + totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); + } + + if (! totalIndices) { + extracted.mesh._mesh = model::Mesh(); + qCDebug(modelformat) << "buildModelMesh failed -- no indices, url = " << url; + return; + } + + auto ib = std::make_shared(); + ib->resize(totalIndices * sizeof(int)); + + int indexNum = 0; + int offset = 0; + + std::vector< model::Mesh::Part > parts; + + foreach(const FBXMeshPart& part, extracted.mesh.parts) { + model::Mesh::Part quadPart(indexNum, part.quadIndices.size(), 0, model::Mesh::QUADS); + if (quadPart._numIndices) { + parts.push_back(quadPart); + ib->setSubData(offset, part.quadIndices.size() * sizeof(int), + (gpu::Byte*) part.quadIndices.constData()); + offset += part.quadIndices.size() * sizeof(int); + indexNum += part.quadIndices.size(); + } + model::Mesh::Part triPart(indexNum, part.triangleIndices.size(), 0, model::Mesh::TRIANGLES); + if (triPart._numIndices) { + ib->setSubData(offset, part.triangleIndices.size() * sizeof(int), + (gpu::Byte*) part.triangleIndices.constData()); + offset += part.triangleIndices.size() * sizeof(int); + indexNum += part.triangleIndices.size(); + } + } + + gpu::BufferView ibv(ib, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::XYZ)); + mesh.setIndexBuffer(ibv); + + if (parts.size()) { + auto pb = std::make_shared(); + pb->setData(parts.size() * sizeof(model::Mesh::Part), (const gpu::Byte*) parts.data()); + gpu::BufferView pbv(pb, gpu::Element(gpu::VEC4, gpu::UINT32, gpu::XYZW)); + mesh.setPartBuffer(pbv); + } else { + extracted.mesh._mesh = model::Mesh(); + qCDebug(modelformat) << "buildModelMesh failed -- no parts, url = " << url; + return; + } + + // model::Box box = + mesh.evalPartBound(0); + + extracted.mesh._mesh = mesh; +} +#endif // USE_MODEL_MESH + diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp index 32c3595075..d5b606129a 100644 --- a/libraries/fbx/src/FBXReader_Node.cpp +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -1,9 +1,9 @@ // -// FBXReader.cpp -// interface/src/renderer +// FBXReader_Node.cpp +// interface/src/fbx // -// Created by Andrzej Kapolka on 9/18/13. -// Copyright 2013 High Fidelity, Inc. +// Created by Sam Gateau on 8/27/2015. +// Copyright 2015 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 @@ -340,3 +340,124 @@ FBXNode FBXReader::parseFBX(QIODevice* device) { return top; } + +glm::vec3 FBXReader::getVec3(const QVariantList& properties, int index) { + return glm::vec3(properties.at(index).value(), properties.at(index + 1).value(), + properties.at(index + 2).value()); +} + +QVector FBXReader::createVec4Vector(const QVector& doubleVector) { + QVector values; + for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 4) * 4); it != end; ) { + float x = *it++; + float y = *it++; + float z = *it++; + float w = *it++; + values.append(glm::vec4(x, y, z, w)); + } + return values; +} + + +QVector FBXReader::createVec4VectorRGBA(const QVector& doubleVector, glm::vec4& average) { + QVector values; + for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 4) * 4); it != end; ) { + float x = *it++; + float y = *it++; + float z = *it++; + float w = *it++; + auto val = glm::vec4(x, y, z, w); + values.append(val); + average += val; + } + if (!values.isEmpty()) { + average *= (1.0f / float(values.size())); + } + return values; +} + +QVector FBXReader::createVec3Vector(const QVector& doubleVector) { + QVector values; + for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 3) * 3); it != end; ) { + float x = *it++; + float y = *it++; + float z = *it++; + values.append(glm::vec3(x, y, z)); + } + return values; +} + +QVector FBXReader::createVec2Vector(const QVector& doubleVector) { + QVector values; + for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 2) * 2); it != end; ) { + float s = *it++; + float t = *it++; + values.append(glm::vec2(s, -t)); + } + return values; +} + +glm::mat4 FBXReader::createMat4(const QVector& doubleVector) { + return glm::mat4(doubleVector.at(0), doubleVector.at(1), doubleVector.at(2), doubleVector.at(3), + doubleVector.at(4), doubleVector.at(5), doubleVector.at(6), doubleVector.at(7), + doubleVector.at(8), doubleVector.at(9), doubleVector.at(10), doubleVector.at(11), + doubleVector.at(12), doubleVector.at(13), doubleVector.at(14), doubleVector.at(15)); +} + +QVector FBXReader::getIntVector(const FBXNode& node) { + foreach (const FBXNode& child, node.children) { + if (child.name == "a") { + return getIntVector(child); + } + } + if (node.properties.isEmpty()) { + return QVector(); + } + QVector vector = node.properties.at(0).value >(); + if (!vector.isEmpty()) { + return vector; + } + for (int i = 0; i < node.properties.size(); i++) { + vector.append(node.properties.at(i).toInt()); + } + return vector; +} + +QVector FBXReader::getFloatVector(const FBXNode& node) { + foreach (const FBXNode& child, node.children) { + if (child.name == "a") { + return getFloatVector(child); + } + } + if (node.properties.isEmpty()) { + return QVector(); + } + QVector vector = node.properties.at(0).value >(); + if (!vector.isEmpty()) { + return vector; + } + for (int i = 0; i < node.properties.size(); i++) { + vector.append(node.properties.at(i).toFloat()); + } + return vector; +} + +QVector FBXReader::getDoubleVector(const FBXNode& node) { + foreach (const FBXNode& child, node.children) { + if (child.name == "a") { + return getDoubleVector(child); + } + } + if (node.properties.isEmpty()) { + return QVector(); + } + QVector vector = node.properties.at(0).value >(); + if (!vector.isEmpty()) { + return vector; + } + for (int i = 0; i < node.properties.size(); i++) { + vector.append(node.properties.at(i).toDouble()); + } + return vector; +} + diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index a8e6adbcf4..a49d4011d2 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -333,7 +333,7 @@ public: // WARNING: only valid when isLoaded returns true. const FBXGeometry& getFBXGeometry() const { return *_geometry; } const std::vector>& getMeshes() const { return _meshes; } - const model::AssetPointer getAsset() const { return _asset; } + // const model::AssetPointer getAsset() const { return _asset; } // model::MeshPointer getShapeMesh(int shapeID); // int getShapePart(int shapeID); @@ -388,14 +388,14 @@ protected: QUrl _textureBaseUrl; Resource* _resource = nullptr; - std::unique_ptr _geometry; + std::unique_ptr _geometry; // This should go away evenutally once we can put everything we need in the model::AssetPointer std::vector> _meshes; std::vector> _materials; std::vector> _shapes; // The model asset created from this NetworkGeometry - model::AssetPointer _asset; + // model::AssetPointer _asset; // cache for isLoadedWithTextures() mutable bool _isLoadedWithTextures = false; From 202f68be58b3242bfde846786c26ca19903a8288 Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Fri, 28 Aug 2015 11:38:59 -0700 Subject: [PATCH 025/192] Fixed sphere entities + renamed user settings --- examples/example/entities/platform.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index 3d6744b364..bee04d8950 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -40,9 +40,9 @@ var RADIUS = 5.0; // Defines min/max for onscreen platform radius, density, and entity width/height/depth sliders. // Color limits are hardcoded at [0, 255]. -var UI_RADIUS_RANGE = [ 1.0, 15.0 ]; -var UI_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density. -var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits +var PLATFORM_RADIUS_RANGE = [ 1.0, 15.0 ]; +var PLATFORM_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density. +var PLATFORM_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits // Utils (function () { @@ -151,8 +151,8 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension /// Custom property iterator used to setup UI (sliders, etc) RandomShapeModel.prototype.setupUI = function (callback) { var _this = this; - var dimensionsMin = UI_SHAPE_DIMENSIONS_RANGE[0]; - var dimensionsMax = UI_SHAPE_DIMENSIONS_RANGE[1]; + var dimensionsMin = PLATFORM_SHAPE_DIMENSIONS_RANGE[0]; + var dimensionsMax = PLATFORM_SHAPE_DIMENSIONS_RANGE[1]; [ ['widthRange', 'width'], ['depthRange', 'depth'], @@ -437,7 +437,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension if (this.activeEntity) { this.activeEntity.destroy(); } - this.activeEntity = spawnEntity(newType); + this.activeEntity = this.spawnEntity(newType); // if (this.cachedEntity && this.cachedEntity.type == newType) { // this.cachedEntity.update({ visible: true }); // this.activeEntity.update({ visible: false }); @@ -997,21 +997,21 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension // Top controls addLabel(controls, "Platform (platform.js)"); - controls.radiusSlider = addSlider(controls, "radius", UI_RADIUS_RANGE[0], UI_RADIUS_RANGE[1], function () { return platform.getRadius() }, + controls.radiusSlider = addSlider(controls, "radius", PLATFORM_RADIUS_RANGE[0], PLATFORM_RADIUS_RANGE[1], function () { return platform.getRadius() }, function (value) { platform.setRadiusOnNextUpdate(value); controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value)); }); addSpacing(controls, 1, 2); - controls.densitySlider = addSlider(controls, "entity density", UI_DENSITY_RANGE[0], UI_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, + controls.densitySlider = addSlider(controls, "entity density", PLATFORM_DENSITY_RANGE[0], PLATFORM_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, function (value) { platform.setDensityOnNextUpdate(value); controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value)); }); addSpacing(controls, 1, 2); - var minEntities = Math.PI * UI_RADIUS_RANGE[0] * UI_RADIUS_RANGE[0] * UI_DENSITY_RANGE[0]; - var maxEntities = Math.PI * UI_RADIUS_RANGE[1] * UI_RADIUS_RANGE[1] * UI_DENSITY_RANGE[1]; + var minEntities = Math.PI * PLATFORM_RADIUS_RANGE[0] * PLATFORM_RADIUS_RANGE[0] * PLATFORM_DENSITY_RANGE[0]; + var maxEntities = Math.PI * PLATFORM_RADIUS_RANGE[1] * PLATFORM_RADIUS_RANGE[1] * PLATFORM_DENSITY_RANGE[1]; controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() }, function (value) {}); controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions) @@ -1057,7 +1057,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension setValue(value); platform.updateEntityAttribs(); }); - addSpacing(shapeControls, 1, 2); + addSpacing(colorControls, 1, 2); }); moveToBottomLeftScreenCorner(layoutContainer); From d9ea2ae27be1ac76b64adf116fd07c5b6a488ac4 Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Fri, 28 Aug 2015 15:43:55 -0700 Subject: [PATCH 026/192] Fixed sliders --- examples/example/entities/platform.js | 19 +++-- examples/libraries/uiwidgets.js | 99 +++++++++++++++------------ 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index bee04d8950..d4fe7d0f68 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -16,7 +16,12 @@ // UI and debug console implemented using uiwidgets / 2d overlays Script.include("../../libraries/uiwidgets.js"); if (typeof(UI) === 'undefined') { // backup link in case the user downloaded this somewhere + print("Missing library script -- loading from public.highfidelity.io"); Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); + if (typeof(UI) === 'undefined') { + print("Cannot load UIWidgets library -- check your internet connection", COLORS.RED); + throw new Error("Could not load uiwidgets.js"); + } } // Platform script @@ -890,7 +895,7 @@ var PLATFORM_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dim }); this.boxes = []; } -}).call(this); +})(); // UI (function () { @@ -1145,15 +1150,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; // Update this.update = function (dt) { checkScreenDimensions(); - var pos = getTargetPlatformPosition(); - // if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) { - // // User likely teleported - // logMessage("Height rebuild (" + - // "(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" + - // " > " + MAX_ACCELERATION_THRESHOLD + ")"); - // platform.updateHeight(pos.y); - // } platform.update(dt, getTargetPlatformPosition(), platform.getRadius()); } @@ -1182,9 +1179,9 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; function init () { logMessage("initializing..."); - + this.initPlatform(); - + Script.update.connect(this.update); Script.scriptEnding.connect(this.teardown); diff --git a/examples/libraries/uiwidgets.js b/examples/libraries/uiwidgets.js index 70eda8e5f2..479acd2ce8 100644 --- a/examples/libraries/uiwidgets.js +++ b/examples/libraries/uiwidgets.js @@ -28,13 +28,10 @@ if (this.Vec2 == undefined) { return new Vec2(v.x, v.y); } } else if (this.Vec2.clone == undefined) { - print("Vec2 exists; adding Vec2.clone"); this.Vec2.clone = function (v) { return { 'x': v.x || 0.0, 'y': v.y || 0.0 }; } -} else { - print("Vec2...?"); -} +} else {} })(); var Rect = function (xmin, ymin, xmax, ymax) { @@ -566,46 +563,51 @@ var Slider = UI.Slider = function (properties) { this.slider = new Box(properties.slider); this.slider.parent = this; - var updateSliderPos = function (event, widget) { - var rx = Math.max(event.x * 1.0 - widget.position.x - widget.slider.width * 0.5, 0.0); + var clickOffset = { x: 0.0, y: 0.0 }; // offset relative to slider knob + var widget = this; + var updateDrag = function (event) { + var rx = Math.max(event.x * 1.0 - widget.position.x - clickOffset.x, 0.0); var width = Math.max(widget.width - widget.slider.width - widget.padding.x * 2.0, 0.0); var v = Math.min(rx, width) / (width || 1); - widget.value = widget.minValue + ( - widget.maxValue - widget.minValue) * v; + // print("dragging slider: rx = " + rx + ", width = " + width + ", v = " + v); + + widget.value = widget.minValue + (widget.maxValue - widget.minValue) * v; widget.onValueChanged(widget.value); UI.updateLayout(); } + var startDrag = function (event) { + // calculate position of slider knob + var x0 = widget.position.x + widget.padding.x; + var width = (widget.width - widget.slider.width - widget.padding.x * 2.0); + var normalizedValue = (widget.value - widget.minValue) / (widget.maxValue - widget.minValue) - var widget = this; - this.addAction('onMouseDown', function (event) { - sliderRel.x = sliderRel.y = 0.0; - // sliderRel.x = widget.slider.width * 0.5; - // sliderRel.y = widget.slider.height * 0.5; - updateSliderPos(event, widget); + var sliderX = x0 + normalizedValue * width; + var sliderWidth = widget.slider.width; - // hack - ui.clickedWidget = ui.draggedWidget = widget.slider; - }); + if (event.x >= sliderX && event.x <= sliderX + sliderWidth) { + // print("Start drag -- on slider knob"); + clickOffset.x = event.x - sliderX; + } else if (event.x >= x0 && event.x <= x0 + width) { + // print("Start drag -- on slider bar"); + clickOffset.x = sliderWidth * 0.5; + } else { + clickOffset.x = 0.0; + // print("Start drag -- out of bounds!"); + // print("event.x = " + event.x); + // print("x0 = " + x0 + ", x1 = " + (x0 + width) + " (width = " + width + ")"); + // print("s0 = " + sliderX + ", s1 = " + (sliderX + sliderWidth) + "(slider width = " + sliderWidth + ")"); + // print("widget = " + widget); + // print("widget.slider = " + widget.slider); + // print("widget.width = " + widget.width + ", widget.slider.width = " + widget.slider.width); + } + updateDrag(event); + } - var sliderRel = {}; - this.slider.addAction('onMouseDown', function (event) { - sliderRel.x = widget.slider.position.x - event.x; - sliderRel.y = widget.slider.position.y - event.y; - event.x += sliderRel.x; - event.y += sliderRel.y; - updateSliderPos(event, widget); - }); - this.slider.addAction('onDragBegin', function (event) { - event.x += sliderRel.x; - event.y += sliderRel.y; - updateSliderPos(event, widget); - }) - this.slider.addAction('onDragUpdate', function (event) { - event.x += sliderRel.x; - event.y += sliderRel.y; - updateSliderPos(event, widget); - }) + this.addAction('onMouseDown', startDrag); + this.addAction('onDragBegin', updateDrag); + this.addAction('onDragUpdate', updateDrag); + this.slider.actions = this.actions; }; Slider.prototype = new Box(); Slider.prototype.constructor = Slider; @@ -947,16 +949,25 @@ var dispatchEvent = function (action, event, widget) { } } +function hasAction (widget, action) { + // print("widget = " + widget); + // print("action = " + action); + // if (widget) { + // print("widget.actions[] = " + widget.actions[action]); + // print("widget.parent = " + widget.parent); + // } + return widget && (widget.actions[action] || hasAction(widget.parent, action)); +} + UI.handleMouseMove = function (event, canStartDrag) { - if (canStartDrag === undefined) + // if (canStartDrag === undefined) + if (arguments.length < 2) canStartDrag = true; // print("mouse moved x = " + event.x + ", y = " + event.y); var focused = getFocusedWidget(event); - // print("got focus: " + focused); - - if (canStartDrag && !ui.draggedWidget && ui.clickedWidget && ui.clickedWidget.actions['onDragBegin']) { + if (!ui.draggedWidget && ui.clickedWidget && hasAction(ui.clickedWidget, 'onDragBegin')) { ui.draggedWidget = ui.clickedWidget; dispatchEvent('onDragBegin', event, ui.draggedWidget); } else if (ui.draggedWidget) { @@ -980,26 +991,24 @@ UI.handleMousePress = function (event) { } UI.handleMouseDoublePress = function (event) { - // print("DOUBLE CLICK!"); var focused = getFocusedWidget(event); UI.handleMouseMove(event); if (focused) { - // print("dispatched onDoubleClick"); dispatchEvent('onDoubleClick', event, focused); } } UI.handleMouseRelease = function (event) { - // print("Mouse released"); - if (ui.draggedWidget) { dispatchEvent('onDragEnd', event, ui.draggedWidget); } else { - UI.handleMouseMove(event, false); + var clicked = ui.clickedWidget; + ui.clickedWidget = null; + UI.handleMouseMove(event); if (ui.focusedWidget) { dispatchEvent('onMouseUp', event, ui.focusedWidget); - if (ui.clickedWidget == ui.focusedWidget) { + if (clicked == ui.focusedWidget) { dispatchEvent('onClick', event, ui.focusedWidget); } } From b845fcaff9f5b6313a0bc6213f3988bd339c0771 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 28 Aug 2015 16:38:07 -0700 Subject: [PATCH 027/192] Cleanup --- libraries/fbx/src/FBXReader.cpp | 4 +--- libraries/fbx/src/FBXReader.h | 4 ---- libraries/render-utils/src/GeometryCache.cpp | 4 +--- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index a443b9d295..ae0b1d18e6 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -654,8 +654,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS FBXGeometry* geometryPtr = new FBXGeometry; FBXGeometry& geometry = *geometryPtr; - geometry._asset.reset(new model::Asset()); - float unitScaleFactor = 1.0f; glm::vec3 ambientColor; QString hifiGlobalNodeID; @@ -981,7 +979,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } else if (object.name == "Material") { FBXMaterial material = { glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(), glm::vec2(0.f, 1.0f), 96.0f, 1.0f, - QString(""), model::MaterialTable::INVALID_ID}; + QString("")}; foreach (const FBXNode& subobject, object.children) { bool properties = false; QByteArray propertyName; diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index c3c9a77b18..508720d7ec 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -28,7 +28,6 @@ #include #include -#include class QIODevice; class FBXNode; @@ -139,7 +138,6 @@ public: float opacity; QString materialID; - model::MaterialTable::ID _modelMaterialID; model::MaterialPointer _material; FBXTexture diffuseTexture; @@ -301,8 +299,6 @@ public: QString getModelNameOfMesh(int meshIndex) const; QList blendshapeChannelNames; - - model::AssetPointer _asset; }; Q_DECLARE_METATYPE(FBXGeometry) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 01dc619e06..87608b1815 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1723,8 +1723,7 @@ void GeometryReader::run() { NetworkGeometry::NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl) : _url(url), _mapping(mapping), - _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url), - _asset() { + _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url) { if (delayLoad) { _state = DelayState; @@ -2129,7 +2128,6 @@ static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const void NetworkGeometry::modelParseSuccess(FBXGeometry* geometry) { // assume owner ship of geometry pointer _geometry.reset(geometry); - _asset = _geometry->_asset; From 4e944645fe1c3de8715a1a39fcc9604c3870fc49 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 28 Aug 2015 16:58:36 -0700 Subject: [PATCH 028/192] more cleaning for showing a cleaner pr --- interface/src/ModelPackager.cpp | 22 ------- libraries/fbx/src/FBXReader.cpp | 65 ------------------ libraries/fbx/src/FBXReader.h | 14 ---- libraries/fbx/src/OBJReader.cpp | 14 ---- libraries/render-utils/src/GeometryCache.cpp | 69 +------------------- libraries/render-utils/src/Model.cpp | 12 +--- 6 files changed, 4 insertions(+), 192 deletions(-) diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index e90ac74073..5e045d4ca8 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -340,27 +340,6 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename void ModelPackager::listTextures() { _textures.clear(); - /* foreach (FBXMesh mesh, _geometry->meshes) { - foreach (FBXMeshPart part, mesh.parts) { - if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty() && - !_textures.contains(part.diffuseTexture.filename)) { - _textures << part.diffuseTexture.filename; - } - if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty() && - !_textures.contains(part.normalTexture.filename)) { - - _textures << part.normalTexture.filename; - } - if (!part.specularTexture.filename.isEmpty() && part.specularTexture.content.isEmpty() && - !_textures.contains(part.specularTexture.filename)) { - _textures << part.specularTexture.filename; - } - if (!part.emissiveTexture.filename.isEmpty() && part.emissiveTexture.content.isEmpty() && - !_textures.contains(part.emissiveTexture.filename)) { - _textures << part.emissiveTexture.filename; - } - } - } */ foreach (FBXMaterial mat, _geometry->materials) { if (!mat.diffuseTexture.filename.isEmpty() && mat.diffuseTexture.content.isEmpty() && !_textures.contains(mat.diffuseTexture.filename)) { @@ -380,7 +359,6 @@ void ModelPackager::listTextures() { _textures << mat.emissiveTexture.filename; } } - } bool ModelPackager::copyTextures(const QString& oldDir, const QDir& newDir) { diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index ae0b1d18e6..d283689d85 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -38,71 +38,6 @@ //#define DEBUG_FBXREADER using namespace std; -/* -struct TextureParam { - glm::vec2 UVTranslation; - glm::vec2 UVScaling; - glm::vec4 cropping; - QString UVSet; - - glm::vec3 translation; - glm::vec3 rotation; - glm::vec3 scaling; - uint8_t alphaSource; - uint8_t currentTextureBlendMode; - bool useMaterial; - - template - bool assign(T& ref, const T& v) { - if (ref == v) { - return false; - } else { - ref = v; - isDefault = false; - return true; - } - } - - bool isDefault; - - TextureParam() : - UVTranslation(0.0f), - UVScaling(1.0f), - cropping(0.0f), - UVSet("map1"), - translation(0.0f), - rotation(0.0f), - scaling(1.0f), - alphaSource(0), - currentTextureBlendMode(0), - useMaterial(true), - isDefault(true) - {} -}; - -*/ -bool FBXMesh::hasSpecularTexture() const { -// TODO fix that in model.cpp / payload - /* foreach (const FBXMeshPart& part, parts) { - if (!part.specularTexture.filename.isEmpty()) { - return true; - } - } - */ - return false; -} - -bool FBXMesh::hasEmissiveTexture() const { - // TODO Fix that in model cpp / payload - /* - foreach (const FBXMeshPart& part, parts) { - if (!part.emissiveTexture.filename.isEmpty()) { - return true; - } - } - */ - return false; -} QStringList FBXGeometry::getJointNames() const { QStringList names; diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 508720d7ec..19f424c8e5 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -89,15 +89,6 @@ public: glm::mat4 inverseBindMatrix; }; - -// The true texture image which can be used for different textures -class FBXTextureImage { -public: - QString name; - QByteArray filename; - QByteArray content; -}; - /// A texture map in an FBX document. class FBXTexture { public: @@ -173,9 +164,6 @@ public: bool isEye; QVector blendshapes; - - bool hasSpecularTexture() const; - bool hasEmissiveTexture() const; unsigned int meshIndex; // the order the meshes appeared in the object file # if USE_MODEL_MESH @@ -369,8 +357,6 @@ public: FBXTexture getTexture(const QString& textureID); - QHash _textureImages; - QHash textureNames; QHash textureFilenames; QHash textureContent; diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 043f9c70f4..ad70d67cfe 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -121,21 +121,7 @@ glm::vec2 OBJTokenizer::getVec2() { void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { - /* meshPart.diffuseColor = glm::vec3(1, 1, 1); - meshPart.specularColor = glm::vec3(1, 1, 1); - meshPart.emissiveColor = glm::vec3(0, 0, 0); - meshPart.emissiveParams = glm::vec2(0, 1); - meshPart.shininess = 40; - meshPart.opacity = 1;*/ - meshPart.materialID = materialID; - /* meshPart.opacity = 1.0; - meshPart._material = std::make_shared(); - meshPart._material->setDiffuse(glm::vec3(1.0, 1.0, 1.0)); - meshPart._material->setOpacity(1.0); - meshPart._material->setMetallic(0.0); - meshPart._material->setGloss(96.0); - meshPart._material->setEmissive(glm::vec3(0.0, 0.0, 0.0));*/ } // OBJFace diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 87608b1815..da5341a994 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1783,6 +1783,7 @@ void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& u for (auto&& material : _materials) { QSharedPointer matchingTexture = QSharedPointer(); if (material->diffuseTextureName == name) { + // TODO: Find a solution to the eye case material->diffuseTexture = textureCache->getTexture(url, DEFAULT_TEXTURE, /* _geometry->meshes[i].isEye*/ false); } else if (material->normalTextureName == name) { material->normalTexture = textureCache->getTexture(url); @@ -1792,23 +1793,6 @@ void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& u material->emissiveTexture = textureCache->getTexture(url); } } -/* - for (size_t i = 0; i < _meshes.size(); i++) { - NetworkMesh& mesh = *(_meshes[i].get()); - for (size_t j = 0; j < mesh._parts.size(); j++) { - NetworkMeshPart& part = *(mesh._parts[j].get()); - QSharedPointer matchingTexture = QSharedPointer(); - if (part.diffuseTextureName == name) { - part.diffuseTexture = textureCache->getTexture(url, DEFAULT_TEXTURE, _geometry->meshes[i].isEye); - } else if (part.normalTextureName == name) { - part.normalTexture = textureCache->getTexture(url); - } else if (part.specularTextureName == name) { - part.specularTexture = textureCache->getTexture(url); - } else if (part.emissiveTextureName == name) { - part.emissiveTexture = textureCache->getTexture(url); - } - } - }*/ } else { qCWarning(renderutils) << "Ignoring setTextureWirthNameToURL() geometry not ready." << name << url; } @@ -1838,32 +1822,7 @@ QStringList NetworkGeometry::getTextureNames() const { result << material->emissiveTextureName + ":" + textureURL; } } - /* for (size_t i = 0; i < _meshes.size(); i++) { - const NetworkMesh& mesh = *(_meshes[i].get()); - for (size_t j = 0; j < mesh._parts.size(); j++) { - const NetworkMeshPart& part = *(mesh._parts[j].get()); - if (!part.diffuseTextureName.isEmpty() && part.diffuseTexture) { - QString textureURL = part.diffuseTexture->getURL().toString(); - result << part.diffuseTextureName + ":" + textureURL; - } - - if (!part.normalTextureName.isEmpty() && part.normalTexture) { - QString textureURL = part.normalTexture->getURL().toString(); - result << part.normalTextureName + ":" + textureURL; - } - - if (!part.specularTextureName.isEmpty() && part.specularTexture) { - QString textureURL = part.specularTexture->getURL().toString(); - result << part.specularTextureName + ":" + textureURL; - } - - if (!part.emissiveTextureName.isEmpty() && part.emissiveTexture) { - QString textureURL = part.emissiveTexture->getURL().toString(); - result << part.emissiveTextureName + ":" + textureURL; - } - } - }*/ return result; } @@ -1949,31 +1908,6 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas // process network parts foreach (const FBXMeshPart& part, mesh.parts) { - /* NetworkMeshPart* networkPart = new NetworkMeshPart(); - - if (!part.diffuseTexture.filename.isEmpty()) { - networkPart->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(part.diffuseTexture.filename)), DEFAULT_TEXTURE, - mesh.isEye, part.diffuseTexture.content); - networkPart->diffuseTextureName = part.diffuseTexture.name; - } - if (!part.normalTexture.filename.isEmpty()) { - networkPart->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(part.normalTexture.filename)), NORMAL_TEXTURE, - false, part.normalTexture.content); - networkPart->normalTextureName = part.normalTexture.name; - } - if (!part.specularTexture.filename.isEmpty()) { - networkPart->specularTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(part.specularTexture.filename)), SPECULAR_TEXTURE, - false, part.specularTexture.content); - networkPart->specularTextureName = part.specularTexture.name; - } - if (!part.emissiveTexture.filename.isEmpty()) { - networkPart->emissiveTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(part.emissiveTexture.filename)), EMISSIVE_TEXTURE, - false, part.emissiveTexture.content); - networkPart->emissiveTextureName = part.emissiveTexture.name; - checkForTexcoordLightmap = true; - } - networkMesh->_parts.emplace_back(networkPart); - */ totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); } @@ -2097,6 +2031,7 @@ static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const networkMaterial->_material = material._material; if (!material.diffuseTexture.filename.isEmpty()) { + // TODO: SOlve the eye case networkMaterial->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.diffuseTexture.filename)), DEFAULT_TEXTURE, /* mesh.isEye*/ false, material.diffuseTexture.content); networkMaterial->diffuseTextureName = material.diffuseTexture.name; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 83df978d34..26f9749b61 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -758,9 +758,6 @@ void Model::renderSetup(RenderArgs* args) { class MeshPartPayload { public: - /* MeshPartPayload(bool transparent, Model* model, int meshIndex, int partIndex) : - transparent(transparent), model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex) { } - */ MeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex) : model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex), _shapeID(shapeIndex) { } @@ -771,9 +768,6 @@ public: QUrl url; int meshIndex; int partIndex; - - // Core definition of a Shape = transform + model/mesh/part + material - model::AssetPointer _asset; int _shapeID; }; @@ -1450,7 +1444,6 @@ AABox Model::getPartBounds(int meshIndex, int partIndex) { } void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shapeID) { -//void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent) { // PROFILE_RANGE(__FUNCTION__); PerformanceTimer perfTimer("Model::renderPart"); if (!_readyWhenAdded) { @@ -1484,7 +1477,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape return; }; - // Not yet + // TODO: Not yet // auto drawMesh = _geometry->getShapeMesh(shapeID); // auto drawPart = _geometry->getShapePart(shapeID); @@ -1498,9 +1491,8 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape const MeshState& state = _meshStates.at(meshIndex); auto drawMaterialKey = drawMaterial->_material->getKey(); - bool translucentMesh = drawMaterialKey.isTransparent(); + bool translucentMesh = drawMaterialKey.isTransparent() || drawMaterialKey.isTransparentMap(); -// bool translucentMesh = translucent; // networkMesh.getTranslucentPartCount(mesh) == networkMesh.parts.size(); bool hasTangents = !mesh.tangents.isEmpty(); bool hasSpecular = !drawMaterial->specularTextureName.isEmpty(); //mesh.hasSpecularTexture(); bool hasLightmap = !drawMaterial->emissiveTextureName.isEmpty(); //mesh.hasEmissiveTexture(); From 0ac4da285c49c1c62500f3c96729e36406ea993e Mon Sep 17 00:00:00 2001 From: James Pollack Date: Wed, 9 Sep 2015 09:48:39 -0700 Subject: [PATCH 029/192] Update bubblewand to not use overlays by default. --- examples/toys/bubblewand/bubble.js | 148 ++++++++++++++---- examples/toys/bubblewand/createWand.js | 37 +++-- examples/toys/bubblewand/wand.js | 204 ++++++++++++++----------- 3 files changed, 256 insertions(+), 133 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 3cc68fecfa..e5f6d98e28 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -5,48 +5,140 @@ // Copyright 2015 High Fidelity, Inc. // // example of a nested entity. it doesn't do much now besides delete itself if it collides with something (bubbles are fragile! it would be cool if it sometimes merged with other bubbbles it hit) -// todo: play bubble sounds from the bubble itself instead of the wand. +// todo: play bubble sounds & particle bursts from the bubble itself instead of the wand. // blocker: needs some sound fixes and a way to find its own position before unload for spatialization // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - (function() { - // Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); - // Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); + Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); + Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); - //var popSound; + var POP_SOUNDS = [ + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop1.wav"), + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop2.wav"), + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop3.wav") + ] + + BUBBLE_PARTICLE_COLOR = { + red: 0, + green: 40, + blue: 255, + } + + var properties; + var checkPositionInterval; this.preload = function(entityID) { // print('bubble preload') - this.entityID = entityID; - // popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); + // var _t = this; + // _t.entityID = entityID; + // properties = Entities.getEntityProperties(entityID); + // checkPositionInterval = Script.setInterval(function() { + // properties = Entities.getEntityProperties(entityID); + // // print('properties AT CHECK::' + JSON.stringify(properties)); + // }, 200); + + // _t.loadShader(entityID); + }; + + this.loadShader = function(entityID) { + setEntityUserData(entityID, { + "ProceduralEntity": { + "shaderUrl": "http://localhost:8080/shaders/bubble.fs?" + randInt(0, 10000), + } + }) + }; + + + this.leaveEntity = function(entityID) { + print('LEAVE ENTITY:' + entityID) + }; + + this.collisionWithEntity = function(myID, otherID, collision) { + //Entities.deleteEntity(myID); + // Entities.deleteEntity(otherID); + }; + + // this.beforeUnload = function(entityID) { + // print('BEFORE UNLOAD:' + entityID); + // var properties = Entities.getEntityProperties(entityID); + // var position = properties.position; + // print('BEFOREUNLOAD PROPS' + JSON.stringify(position)); + + // }; + + this.unload = function(entityID) { + // Script.clearInterval(checkPositionInterval); + // var position = properties.position; + // this.endOfBubble(position); + var properties = Entities.getEntityProperties(entityID) + var position = properties.position; + //print('UNLOAD PROPS' + JSON.stringify(position)); + }; + + this.endOfBubble = function(position) { + this.burstBubbleSound(position); + this.createBurstParticles(position); + } + + this.burstBubbleSound = function(position) { + var audioOptions = { + volume: 0.5, + position: position + } + Audio.playSound(POP_SOUNDS[randInt(0, 4)], audioOptions); } - this.collisionWithEntity = function(myID, otherID, collision) { - //if(Entites.getEntityProperties(otherID).userData.objectType==='') { merge bubbles?} - // Entities.deleteEntity(myID); - // this.burstBubbleSound(collision.contactPoint) + this.createBurstParticles = function(position) { + var _t = this; + //get the current position of the bubble + var position = properties.position; + //var orientation = properties.orientation; - }; + var animationSettings = JSON.stringify({ + fps: 30, + frameIndex: 0, + running: true, + firstFrame: 0, + lastFrame: 30, + loop: false + }); - this.unload = function(entityID) { - // this.properties = Entities.getEntityProperties(entityID); - //var location = this.properties.position; - //this.burstBubbleSound(); - }; - - - - this.burstBubbleSound = function(location) { - - // var audioOptions = { - // volume: 0.5, - // position: location - // } - - //Audio.playSound(popSound, audioOptions); + var particleBurst = Entities.addEntity({ + type: "ParticleEffect", + animationSettings: animationSettings, + animationIsPlaying: true, + position: position, + lifetime: 1.0, + dimensions: { + x: 1, + y: 1, + z: 1 + }, + emitVelocity: { + x: 0, + y: -1, + z: 0 + }, + velocitySpread: { + x: 1, + y: 0, + z: 1 + }, + emitAcceleration: { + x: 0, + y: -1, + z: 0 + }, + textures: "https://raw.githubusercontent.com/ericrius1/SantasLair/santa/assets/smokeparticle.png", + color: BUBBLE_PARTICLE_COLOR, + lifespan: 1.0, + visible: true, + locked: false + }); } diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 15c347d62a..943ea8fdbb 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -4,7 +4,7 @@ // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. // -// Loads a wand model and attaches the bubble wand behavior. +// Loads a wand model and attaches the bubble wand behavior. // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -13,29 +13,34 @@ Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); -var wandModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx?" + randInt(0, 10000); -var scriptURL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?" + randInt(1, 100500) +var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx?' + randInt(0, 10000); +var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj?' + randInt(0, 10000); +var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); +//for local testing +//var scriptURL = "http://localhost:8080/scripts/wand.js?" + randInt(0, 10000); //create the wand in front of the avatar var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); + var wand = Entities.addEntity({ - type: "Model", - modelURL: wandModel, - position: center, - dimensions: { - x: 0.1, - y: 1, - z: 0.1 - }, - //must be enabled to be grabbable in the physics engine - collisionsWillMove: true, - shapeType: 'box', - script: scriptURL + type: "Model", + modelURL: wandModel, + position: center, + dimensions: { + x: 0.1, + y: 1, + z: 0.1 + }, + //must be enabled to be grabbable in the physics engine + + collisionsWillMove: true, + compoundShapeURL: wandCollisionShape, + script: scriptURL }); function cleanup() { - Entities.deleteEntity(wand); + Entities.deleteEntity(wand); } diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index be1042ab79..d98e0a8f57 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -20,10 +20,22 @@ function convertRange(value, r1, r2) { Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); + var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); + //for local testing + // var bubbleScript = 'http://localhost:8080/scripts/bubble.js?' + randInt(1, 10000); - var TARGET_SIZE = 0.4; + var POP_SOUNDS = [ + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop1.wav"), + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop2.wav"), + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop3.wav") + ] + + var overlays = false; + + //debug overlays for hand position to detect when wand is near avatar head + var TARGET_SIZE = 0.5; var TARGET_COLOR = { red: 128, green: 128, @@ -36,40 +48,46 @@ function convertRange(value, r1, r2) { }; var HAND_SIZE = 0.25; - var leftCubePosition = MyAvatar.getLeftPalmPosition(); - var rightCubePosition = MyAvatar.getRightPalmPosition(); - var leftHand = Overlays.addOverlay("cube", { - position: leftCubePosition, - size: HAND_SIZE, - color: { - red: 0, - green: 0, - blue: 255 - }, - alpha: 1, - solid: false - }); + if (overlays) { - var rightHand = Overlays.addOverlay("cube", { - position: rightCubePosition, - size: HAND_SIZE, - color: { - red: 255, - green: 0, - blue: 0 - }, - alpha: 1, - solid: false - }); - var gustZoneOverlay = Overlays.addOverlay("cube", { - position: getGustDetectorPosition(), - size: TARGET_SIZE, - color: TARGET_COLOR, - alpha: 1, - solid: false - }); + var leftCubePosition = MyAvatar.getLeftPalmPosition(); + var rightCubePosition = MyAvatar.getRightPalmPosition(); + + var leftHand = Overlays.addOverlay("cube", { + position: leftCubePosition, + size: HAND_SIZE, + color: { + red: 0, + green: 0, + blue: 255 + }, + alpha: 1, + solid: false + }); + + var rightHand = Overlays.addOverlay("cube", { + position: rightCubePosition, + size: HAND_SIZE, + color: { + red: 255, + green: 0, + blue: 0 + }, + alpha: 1, + solid: false + }); + + var gustZoneOverlay = Overlays.addOverlay("cube", { + position: getGustDetectorPosition(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + } + function getGustDetectorPosition() { @@ -98,6 +116,11 @@ function convertRange(value, r1, r2) { z: 0 } + var BUBBLE_PARTICLE_COLOR = { + red: 0, + blue: 255, + green: 40 + } var wandEntity = this; @@ -108,9 +131,12 @@ function convertRange(value, r1, r2) { } this.unload = function(entityID) { - Overlays.deleteOverlay(leftHand); - Overlays.deleteOverlay(rightHand); - Overlays.deleteOverlay(gustZoneOverlay) + if (overlays) { + Overlays.deleteOverlay(leftHand); + Overlays.deleteOverlay(rightHand); + Overlays.deleteOverlay(gustZoneOverlay); + } + Entities.editEntity(entityID, { name: "" }); @@ -134,35 +160,45 @@ function convertRange(value, r1, r2) { //get the current position of the wand var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; - - //debug overlays for mouth mode - var leftHandPos = MyAvatar.getLeftPalmPosition(); - var rightHandPos = MyAvatar.getRightPalmPosition(); - - Overlays.editOverlay(leftHand, { - position: leftHandPos - }); - Overlays.editOverlay(rightHand, { - position: rightHandPos - }); - //if the wand is in the gust detector, activate mouth mode and change the overlay color var hitTargetWithWand = findSphereSphereHit(wandPosition, HAND_SIZE / 2, getGustDetectorPosition(), TARGET_SIZE / 2) + var velocity = Vec3.subtract(wandPosition, BubbleWand.lastPosition) + var velocityStrength = Vec3.length(velocity) * 100; + var mouthMode; + if (hitTargetWithWand) { + mouthMode = true; + } else { + mouthMode = false; + } + + + + //debug overlays for mouth mode + if (overlays) { + var leftHandPos = MyAvatar.getLeftPalmPosition(); + var rightHandPos = MyAvatar.getRightPalmPosition(); + + Overlays.editOverlay(leftHand, { + position: leftHandPos + }); + Overlays.editOverlay(rightHand, { + position: rightHandPos + }); + } + + if (mouthMode === true && overlays === true) { Overlays.editOverlay(gustZoneOverlay, { position: getGustDetectorPosition(), color: TARGET_COLOR_HIT }) - mouthMode = true; - - } else { + } else if (overlays) { Overlays.editOverlay(gustZoneOverlay, { position: getGustDetectorPosition(), color: TARGET_COLOR }) - mouthMode = false; } var volumeLevel = MyAvatar.audioAverageLoudness; @@ -170,7 +206,6 @@ function convertRange(value, r1, r2) { var convertedVolume = convertRange(volumeLevel, [0, 5000], [0, 10]); // default is 'wave mode', where waving the object around grows the bubbles - var velocity = Vec3.subtract(wandPosition, BubbleWand.lastPosition) //store the last position of the wand for velocity calculations _t.lastPosition = wandPosition; @@ -184,6 +219,8 @@ function convertRange(value, r1, r2) { //actually grow the bubble var dimensions = Entities.getEntityProperties(_t.currentBubble).dimensions; + var avatarFront = Quat.getFront(MyAvatar.orientation); + var forwardOffset = Vec3.multiply(avatarFront, 0.1); if (velocityStrength > 1 || convertedVolume > 1) { @@ -196,21 +233,17 @@ function convertRange(value, r1, r2) { //bubbles pop after existing for a bit -- so set a random lifetime var lifetime = randInt(3, 8); - //sound is somewhat unstable at the moment so this is commented out. really audio should be played by the bubbles, but there's a blocker. - // Script.setTimeout(function() { - // _t.burstBubbleSound(_t.currentBubble) - // }, lifetime * 1000) - - - //todo: angular velocity without the controller -- forward velocity for mouth mode bubbles // var angularVelocity = Controller.getSpatialControlRawAngularVelocity(hands.leftHand.tip); Entities.editEntity(_t.currentBubble, { - velocity: Vec3.normalize(velocity), + // collisionsWillMove:true, + // ignoreForCollisions:false, + velocity: mouthMode ? avatarFront : velocity, // angularVelocity: Controller.getSpatialControlRawAngularVelocity(hands.leftHand.tip), lifetime: lifetime }); + _t.lastBubble = _t.currentBubble; //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble BubbleWand.spawnBubble(); @@ -244,27 +277,18 @@ function convertRange(value, r1, r2) { }); }, - burstBubbleSound: function(bubble) { - //we want to play the sound at the same location and orientation as the bubble - var position = Entities.getEntityProperties(bubble).position; - var orientation = Entities.getEntityProperties(bubble).orientation; - - //set the options for the audio injector - var audioOptions = { - volume: 0.5, - position: position, - orientation: orientation - } - - - //var audioInjector = Audio.playSound(popSound, audioOptions); - - //remove this bubble from the array to keep things clean - var i = BubbleWand.bubbles.indexOf(bubble); - if (i != -1) { - BubbleWand.bubbles.splice(i, 1); - } - + checkForEntitiesNearBubble: function() { + var _t = this; + var arrayFound = Entities.findEntities(_t.wandTipPosition, 1); + var foundLength = arrayFound.length; + print('found length:::' + foundLength); + }, + enableCollisionsForBubble: function(bubble) { + print('enable bubble collisions:' + bubble) + Entities.editEntity(bubble, { + collisionsWillMove: true, + ignoreForCollisions: false, + }) }, spawnBubble: function() { var _t = this; @@ -275,10 +299,11 @@ function convertRange(value, r1, r2) { var wandPosition = properties.position; var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); - var upOffset = Vec3.multiply(upVector, 0.5); - var forwardOffset = Vec3.multiply(frontVector, 0.1); - var offsetVector = Vec3.sum(upOffset, forwardOffset); - var wandTipPosition = Vec3.sum(wandPosition, offsetVector); + var upOffset = Vec3.multiply(upVector, 0.4); + // var forwardOffset = Vec3.multiply(frontVector, 0.1); + // var offsetVector = Vec3.sum(upOffset, forwardOffset); + // var wandTipPosition = Vec3.sum(wandPosition, offsetVector); + var wandTipPosition = Vec3.sum(wandPosition, upOffset); _t.wandTipPosition = wandTipPosition; //store the position of the tip on spawn for use in velocity calculations @@ -294,10 +319,10 @@ function convertRange(value, r1, r2) { y: 0.01, z: 0.01 }, - collisionsWillMove: false, - ignoreForCollisions: true, + collisionsWillMove: false, //true + ignoreForCollisions: true, //false gravity: BUBBLE_GRAVITY, - // collisionSoundURL:popSound, + collisionSoundURL: POP_SOUNDS[randInt(0, 4)], shapeType: "sphere", script: bubbleScript, }); @@ -309,6 +334,7 @@ function convertRange(value, r1, r2) { init: function() { this.spawnBubble(); Script.update.connect(BubbleWand.update); + } } From da90b7ff087d7daa87c1e59ce0a090ad8839f1fd Mon Sep 17 00:00:00 2001 From: James Pollack Date: Wed, 9 Sep 2015 09:53:47 -0700 Subject: [PATCH 030/192] remove logging around bubble leave entity --- examples/toys/bubblewand/bubble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index e5f6d98e28..fe92ea76c8 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -53,7 +53,7 @@ this.leaveEntity = function(entityID) { - print('LEAVE ENTITY:' + entityID) + // print('LEAVE ENTITY:' + entityID) }; this.collisionWithEntity = function(myID, otherID, collision) { From 8f77c0b61cedda9b8c17ec65db486650b6f3297f Mon Sep 17 00:00:00 2001 From: James Pollack Date: Thu, 10 Sep 2015 09:58:31 -0700 Subject: [PATCH 031/192] Update wand to use relative paths, remove some unused position tracking methods, remove cache busting from model file paths --- examples/toys/bubblewand/bubble.js | 23 +++++------------------ examples/toys/bubblewand/createWand.js | 8 ++++---- examples/toys/bubblewand/wand.js | 4 ++-- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index fe92ea76c8..b7967e81ea 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -12,8 +12,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html (function() { - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); + Script.include("../utilities.js"); + Script.include("../libraries/utils.js"); var POP_SOUNDS = [ SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), @@ -35,25 +35,20 @@ // var _t = this; // _t.entityID = entityID; // properties = Entities.getEntityProperties(entityID); - // checkPositionInterval = Script.setInterval(function() { - // properties = Entities.getEntityProperties(entityID); - // // print('properties AT CHECK::' + JSON.stringify(properties)); - // }, 200); - - // _t.loadShader(entityID); + // _t.loadShader(entityID); }; this.loadShader = function(entityID) { setEntityUserData(entityID, { "ProceduralEntity": { - "shaderUrl": "http://localhost:8080/shaders/bubble.fs?" + randInt(0, 10000), + "shaderUrl": "http://hifi-public.s3.amazonaws.com/james/bubblewand/shaders/quora.fs", } }) }; this.leaveEntity = function(entityID) { - // print('LEAVE ENTITY:' + entityID) + // print('LEAVE ENTITY:' + entityID) }; this.collisionWithEntity = function(myID, otherID, collision) { @@ -61,14 +56,6 @@ // Entities.deleteEntity(otherID); }; - // this.beforeUnload = function(entityID) { - // print('BEFORE UNLOAD:' + entityID); - // var properties = Entities.getEntityProperties(entityID); - // var position = properties.position; - // print('BEFOREUNLOAD PROPS' + JSON.stringify(position)); - - // }; - this.unload = function(entityID) { // Script.clearInterval(checkPositionInterval); // var position = properties.position; diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 943ea8fdbb..2c1217d5ce 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -10,11 +10,11 @@ -Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); -Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); +Script.include("../utilities.js"); +Script.include("../libraries/utils.js"); -var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx?' + randInt(0, 10000); -var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj?' + randInt(0, 10000); +var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx' ; +var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj' ; var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); //for local testing diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index d98e0a8f57..648047fbb8 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -16,8 +16,8 @@ function convertRange(value, r1, r2) { } (function() { - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); + Script.include("../utilities.js"); +Script.include("../libraries/utils.js"); var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); From dfa43e84bf825654a29cf1444edde1c21507dc04 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Thu, 10 Sep 2015 10:31:17 -0700 Subject: [PATCH 032/192] bubble update hook --- examples/toys/bubblewand/bubble.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index b7967e81ea..20020855cb 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -28,6 +28,8 @@ blue: 255, } + var _t = this; + var properties; var checkPositionInterval; this.preload = function(entityID) { @@ -36,8 +38,14 @@ // _t.entityID = entityID; // properties = Entities.getEntityProperties(entityID); // _t.loadShader(entityID); + Script.update.connect(_t.internalUpdate); }; + this.internalUpdate = function() { + + properties = Entities.getEntityProperties(_t.entityID) + } + this.loadShader = function(entityID) { setEntityUserData(entityID, { "ProceduralEntity": { @@ -57,12 +65,13 @@ }; this.unload = function(entityID) { - // Script.clearInterval(checkPositionInterval); - // var position = properties.position; - // this.endOfBubble(position); - var properties = Entities.getEntityProperties(entityID) + + Script.update.disconnect(this.internalUpdate); + properties = Entities.getEntityProperties(entityID) var position = properties.position; - //print('UNLOAD PROPS' + JSON.stringify(position)); + // this.endOfBubble(position); + print('UNLOAD PROPS' + JSON.stringify(position)); + }; this.endOfBubble = function(position) { From 5df483b35f3f250da36ce12340ecce38313ede70 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Thu, 10 Sep 2015 11:32:21 -0700 Subject: [PATCH 033/192] ignore 0,0,0 positions so we have accurate unload for bubbles --- examples/toys/bubblewand/bubble.js | 19 +++++++++---------- examples/toys/bubblewand/createWand.js | 12 ++++++------ examples/toys/bubblewand/wand.js | 13 ++++++------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 20020855cb..edb13c085a 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -12,8 +12,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html (function() { - Script.include("../utilities.js"); - Script.include("../libraries/utils.js"); + Script.include("../../utilities.js"); + Script.include("../../libraries/utils.js"); var POP_SOUNDS = [ SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), @@ -34,16 +34,17 @@ var checkPositionInterval; this.preload = function(entityID) { // print('bubble preload') - // var _t = this; - // _t.entityID = entityID; - // properties = Entities.getEntityProperties(entityID); + _t.entityID = entityID; + //properties = Entities.getEntityProperties(entityID); // _t.loadShader(entityID); Script.update.connect(_t.internalUpdate); }; this.internalUpdate = function() { - - properties = Entities.getEntityProperties(_t.entityID) + var tmpProperties = Entities.getEntityProperties(_t.entityID); + if (tmpProperties.position.x !== 0 && tmpProperties.position.y !== 0 && tmpProperties.position.z !== 0) { + properties = tmpProperties; + } } this.loadShader = function(entityID) { @@ -65,11 +66,9 @@ }; this.unload = function(entityID) { - Script.update.disconnect(this.internalUpdate); - properties = Entities.getEntityProperties(entityID) var position = properties.position; - // this.endOfBubble(position); + _t.endOfBubble(position); print('UNLOAD PROPS' + JSON.stringify(position)); }; diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 2c1217d5ce..df6127cbcf 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -10,15 +10,15 @@ -Script.include("../utilities.js"); -Script.include("../libraries/utils.js"); +Script.include("../../utilities.js"); +Script.include("../../libraries/utils.js"); -var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx' ; -var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj' ; -var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); +var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; +var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; +//var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); //for local testing -//var scriptURL = "http://localhost:8080/scripts/wand.js?" + randInt(0, 10000); +var scriptURL = "http://localhost:8080/wand.js?" + randInt(0, 10000); //create the wand in front of the avatar var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 648047fbb8..3dd6ad5a19 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -16,14 +16,13 @@ function convertRange(value, r1, r2) { } (function() { - Script.include("../utilities.js"); -Script.include("../libraries/utils.js"); - + Script.include("../../utilities.js"); + Script.include("../../libraries/utils.js"); var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); - var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); + //var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); //for local testing - // var bubbleScript = 'http://localhost:8080/scripts/bubble.js?' + randInt(1, 10000); + var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); var POP_SOUNDS = [ SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), @@ -319,8 +318,8 @@ Script.include("../libraries/utils.js"); y: 0.01, z: 0.01 }, - collisionsWillMove: false, //true - ignoreForCollisions: true, //false + collisionsWillMove: true, //true + ignoreForCollisions: false, //false gravity: BUBBLE_GRAVITY, collisionSoundURL: POP_SOUNDS[randInt(0, 4)], shapeType: "sphere", From ee7e25a137f69eddc7b9c9fc08bae711054e790b Mon Sep 17 00:00:00 2001 From: James Pollack Date: Thu, 10 Sep 2015 11:49:12 -0700 Subject: [PATCH 034/192] remove localhost refs --- examples/toys/bubblewand/bubble.js | 4 ++-- examples/toys/bubblewand/createWand.js | 4 ++-- examples/toys/bubblewand/wand.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index edb13c085a..845ad58e17 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -35,12 +35,13 @@ this.preload = function(entityID) { // print('bubble preload') _t.entityID = entityID; - //properties = Entities.getEntityProperties(entityID); + properties = Entities.getEntityProperties(entityID); // _t.loadShader(entityID); Script.update.connect(_t.internalUpdate); }; this.internalUpdate = function() { + // we want the position at unload but for some reason it keeps getting set to 0,0,0 -- so i just exclude that location. sorry origin bubbles. var tmpProperties = Entities.getEntityProperties(_t.entityID); if (tmpProperties.position.x !== 0 && tmpProperties.position.y !== 0 && tmpProperties.position.z !== 0) { properties = tmpProperties; @@ -62,7 +63,6 @@ this.collisionWithEntity = function(myID, otherID, collision) { //Entities.deleteEntity(myID); - // Entities.deleteEntity(otherID); }; this.unload = function(entityID) { diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index df6127cbcf..50af1b251d 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -15,10 +15,10 @@ Script.include("../../libraries/utils.js"); var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; -//var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); +var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); //for local testing -var scriptURL = "http://localhost:8080/wand.js?" + randInt(0, 10000); +//var scriptURL = "http://localhost:8080/wand.js?" + randInt(0, 10000); //create the wand in front of the avatar var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 3dd6ad5a19..e4a9f7231f 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -20,9 +20,9 @@ function convertRange(value, r1, r2) { Script.include("../../libraries/utils.js"); var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); - //var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); + var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); //for local testing - var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); + //var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); var POP_SOUNDS = [ SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), From 387e7b74a96c39f8b6e8ea6f78fa5369824a8691 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Thu, 10 Sep 2015 16:58:39 -0700 Subject: [PATCH 035/192] Return wand to its original position if it hasnt moved for 5 seconds, better particles, wand scaled to human size --- examples/toys/bubblewand/bubble.js | 27 +++++---- examples/toys/bubblewand/createWand.js | 18 +++--- examples/toys/bubblewand/wand.js | 80 +++++++++++++++++++------- 3 files changed, 86 insertions(+), 39 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 845ad58e17..231b1cc7ca 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -12,8 +12,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html (function() { - Script.include("../../utilities.js"); - Script.include("../../libraries/utils.js"); + // Script.include("../../utilities.js"); + // Script.include("../../libraries/utils.js"); + + + Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); + Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); var POP_SOUNDS = [ SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), @@ -69,7 +73,7 @@ Script.update.disconnect(this.internalUpdate); var position = properties.position; _t.endOfBubble(position); - print('UNLOAD PROPS' + JSON.stringify(position)); + // print('UNLOAD PROPS' + JSON.stringify(position)); }; @@ -107,7 +111,7 @@ animationSettings: animationSettings, animationIsPlaying: true, position: position, - lifetime: 1.0, + lifetime: 0.2, dimensions: { x: 1, y: 1, @@ -115,22 +119,25 @@ }, emitVelocity: { x: 0, - y: -1, + y: 0, z: 0 }, velocitySpread: { - x: 1, - y: 0, - z: 1 + x: 0.45, + y: 0.45, + z: 0.45 }, emitAcceleration: { x: 0, - y: -1, + y: -0.1, z: 0 }, + alphaStart: 1.0, + alpha: 1, + alphaFinish: 0.0, textures: "https://raw.githubusercontent.com/ericrius1/SantasLair/santa/assets/smokeparticle.png", color: BUBBLE_PARTICLE_COLOR, - lifespan: 1.0, + lifespan: 0.2, visible: true, locked: false }); diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 50af1b251d..24db08d2c1 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -10,30 +10,32 @@ -Script.include("../../utilities.js"); -Script.include("../../libraries/utils.js"); +// Script.include("../../utilities.js"); +// Script.include("../../libraries/utils.js"); + +Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); +Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; -var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); +var scriptURL = 'http://hifi-public.s3.amazonaws.com/scripts/bubblewand/wand.js?' + randInt(0, 10000); //for local testing //var scriptURL = "http://localhost:8080/wand.js?" + randInt(0, 10000); //create the wand in front of the avatar -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); var wand = Entities.addEntity({ type: "Model", modelURL: wandModel, position: center, dimensions: { - x: 0.1, - y: 1, - z: 0.1 + x: 0.05, + y: 0.5, + z: 0.05 }, //must be enabled to be grabbable in the physics engine - collisionsWillMove: true, compoundShapeURL: wandCollisionShape, script: scriptURL diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index e4a9f7231f..87bdfc6648 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -16,13 +16,20 @@ function convertRange(value, r1, r2) { } (function() { - Script.include("../../utilities.js"); - Script.include("../../libraries/utils.js"); + + Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); + Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); + + + // Script.include("../../utilities.js"); + // Script.include("../../libraries/utils.js"); + + var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); - var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); - //for local testing - //var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); + var bubbleScript = 'http://hifi-public.s3.amazonaws.com/scripts/toys/bubblewand/bubble.js?' + randInt(1, 10000); + + // var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); //for local testing var POP_SOUNDS = [ SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), @@ -127,6 +134,9 @@ function convertRange(value, r1, r2) { // print('PRELOAD') this.entityID = entityID; this.properties = Entities.getEntityProperties(this.entityID); + BubbleWand.originalProperties = this.properties; + print('rotation???' + JSON.stringify(BubbleWand.originalProperties.rotation)); + } this.unload = function(entityID) { @@ -150,11 +160,14 @@ function convertRange(value, r1, r2) { var BubbleWand = { bubbles: [], + timeSinceMoved: 0, + resetAtTime: 5, currentBubble: null, - update: function() { - BubbleWand.internalUpdate(); + update: function(dt) { + BubbleWand.internalUpdate(dt); }, - internalUpdate: function() { + internalUpdate: function(dt) { + var _t = this; //get the current position of the wand var properties = Entities.getEntityProperties(wandEntity.entityID); @@ -162,9 +175,16 @@ function convertRange(value, r1, r2) { //if the wand is in the gust detector, activate mouth mode and change the overlay color var hitTargetWithWand = findSphereSphereHit(wandPosition, HAND_SIZE / 2, getGustDetectorPosition(), TARGET_SIZE / 2) - var velocity = Vec3.subtract(wandPosition, BubbleWand.lastPosition) + var velocity = Vec3.subtract(wandPosition, _t.lastPosition) var velocityStrength = Vec3.length(velocity) * 100; + + var upVector = Quat.getUp(properties.rotation); + var frontVector = Quat.getFront(properties.rotation); + var upOffset = Vec3.multiply(upVector, 0.2); + var wandTipPosition = Vec3.sum(wandPosition, upOffset); + _t.wandTipPosition = wandTipPosition; + var mouthMode; if (hitTargetWithWand) { @@ -172,8 +192,18 @@ function convertRange(value, r1, r2) { } else { mouthMode = false; } + //print('velocityStrength'+velocityStrength) - + //we want to reset the object to its original position if its been a while since it has moved + if (velocityStrength === 0) { + _t.timeSinceMoved = _t.timeSinceMoved + dt; + if (_t.timeSinceMoved > _t.resetAtTime) { + _t.timeSinceMoved = 0; + _t.returnToOriginalLocation(); + } + } else { + _t.timeSinceMoved = 0; + } //debug overlays for mouth mode if (overlays) { @@ -225,7 +255,7 @@ function convertRange(value, r1, r2) { //add some variation in bubble sizes var bubbleSize = randInt(1, 5); - bubbleSize = bubbleSize / 10; + bubbleSize = bubbleSize / 50; //release the bubble if its dimensions are bigger than the bubble size if (dimensions.x > bubbleSize) { @@ -249,14 +279,14 @@ function convertRange(value, r1, r2) { return } else { if (mouthMode) { - dimensions.x += 0.015 * convertedVolume; - dimensions.y += 0.015 * convertedVolume; - dimensions.z += 0.015 * convertedVolume; + dimensions.x += 0.005 * convertedVolume; + dimensions.y += 0.005 * convertedVolume; + dimensions.z += 0.005 * convertedVolume; } else { - dimensions.x += 0.015 * velocityStrength; - dimensions.y += 0.015 * velocityStrength; - dimensions.z += 0.015 * velocityStrength; + dimensions.x += 0.005 * velocityStrength; + dimensions.y += 0.005 * velocityStrength; + dimensions.z += 0.005 * velocityStrength; } } @@ -298,10 +328,7 @@ function convertRange(value, r1, r2) { var wandPosition = properties.position; var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); - var upOffset = Vec3.multiply(upVector, 0.4); - // var forwardOffset = Vec3.multiply(frontVector, 0.1); - // var offsetVector = Vec3.sum(upOffset, forwardOffset); - // var wandTipPosition = Vec3.sum(wandPosition, offsetVector); + var upOffset = Vec3.multiply(upVector, 0.2); var wandTipPosition = Vec3.sum(wandPosition, upOffset); _t.wandTipPosition = wandTipPosition; @@ -329,8 +356,19 @@ function convertRange(value, r1, r2) { //add this bubble to an array of bubbles so we can keep track of them _t.bubbles.push(_t.currentBubble) + }, + returnToOriginalLocation: function() { + var _t = this; + Script.update.disconnect(BubbleWand.update) + _t.currentBubble = null; + Entities.deleteEntity(_t.currentBubble); + Entities.editEntity(wandEntity.entityID, _t.originalProperties) + _t.spawnBubble(); + Script.update.connect(BubbleWand.update); + }, init: function() { + var _t = this; this.spawnBubble(); Script.update.connect(BubbleWand.update); From 1cec61ebbad8b1de8f8dc5ef651f2f1bcdda0231 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Thu, 10 Sep 2015 17:05:24 -0700 Subject: [PATCH 036/192] fire particles before burst sound --- examples/toys/bubblewand/bubble.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 231b1cc7ca..eaabf98e44 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -78,8 +78,9 @@ }; this.endOfBubble = function(position) { - this.burstBubbleSound(position); + this.createBurstParticles(position); + this.burstBubbleSound(position); } this.burstBubbleSound = function(position) { From c3ed2d90643b6edabccfd71ce66b1dafcc514b60 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Thu, 10 Sep 2015 17:10:55 -0700 Subject: [PATCH 037/192] fix file paths --- examples/toys/bubblewand/createWand.js | 2 +- examples/toys/bubblewand/wand.js | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 24db08d2c1..158bcb3b99 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -18,7 +18,7 @@ Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examp var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; -var scriptURL = 'http://hifi-public.s3.amazonaws.com/scripts/bubblewand/wand.js?' + randInt(0, 10000); +var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?' + randInt(0, 10000); //for local testing //var scriptURL = "http://localhost:8080/wand.js?" + randInt(0, 10000); diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 87bdfc6648..020fba9b15 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -27,7 +27,7 @@ function convertRange(value, r1, r2) { var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); - var bubbleScript = 'http://hifi-public.s3.amazonaws.com/scripts/toys/bubblewand/bubble.js?' + randInt(1, 10000); + var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); // var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); //for local testing @@ -306,19 +306,6 @@ function convertRange(value, r1, r2) { }); }, - checkForEntitiesNearBubble: function() { - var _t = this; - var arrayFound = Entities.findEntities(_t.wandTipPosition, 1); - var foundLength = arrayFound.length; - print('found length:::' + foundLength); - }, - enableCollisionsForBubble: function(bubble) { - print('enable bubble collisions:' + bubble) - Entities.editEntity(bubble, { - collisionsWillMove: true, - ignoreForCollisions: false, - }) - }, spawnBubble: function() { var _t = this; //create a new bubble at the tip of the wand From 9074d0d6e12c56cc701f12213d4a0156ea413eb3 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 14 Sep 2015 09:29:46 -0700 Subject: [PATCH 038/192] very first version of the TextureStorage working --- .../entities-renderer/src/EntityTreeRenderer.cpp | 4 ++-- libraries/gpu-networking/CMakeLists.txt | 2 +- .../src/gpu-networking/TextureCache.cpp | 3 +++ .../gpu-networking/src/gpu-networking/TextureCache.h | 6 +++++- libraries/model/CMakeLists.txt | 2 +- libraries/model/src/model/Skybox.cpp | 12 ++++++------ libraries/model/src/model/Skybox.h | 6 +++--- libraries/model/src/model/TextureStorage.cpp | 10 +++++++--- libraries/model/src/model/TextureStorage.h | 8 +++++--- libraries/render-utils/src/GeometryCache.cpp | 6 ++++++ libraries/render-utils/src/GeometryCache.h | 1 + libraries/render-utils/src/Model.cpp | 11 +++++++++-- 12 files changed, 49 insertions(+), 22 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 93a9996740..46a7921be8 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -460,12 +460,12 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrgetUserData()) { userData = zone->getUserData(); - QSharedPointer procedural(new Procedural(userData)); + /* QSharedPointer procedural(new Procedural(userData)); if (procedural->_enabled) { skybox->setProcedural(procedural); } else { skybox->setProcedural(QSharedPointer()); - } + }*/ } if (zone->getSkyboxProperties().getURL().isEmpty()) { skybox->setCubemap(gpu::TexturePointer()); diff --git a/libraries/gpu-networking/CMakeLists.txt b/libraries/gpu-networking/CMakeLists.txt index 836afac371..75e0f61b3e 100644 --- a/libraries/gpu-networking/CMakeLists.txt +++ b/libraries/gpu-networking/CMakeLists.txt @@ -7,5 +7,5 @@ add_dependency_external_projects(glm) find_package(GLM REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) -link_hifi_libraries(shared networking gpu) +link_hifi_libraries(shared networking gpu model) diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp index 6063ff6fa4..8e268d9f04 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp @@ -198,6 +198,8 @@ NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArr _width(0), _height(0) { + _textureStorage.reset(new model::TextureStorage()); + if (!url.isValid()) { _loaded = true; } @@ -566,6 +568,7 @@ void NetworkTexture::setImage(const QImage& image, void* voidTexture, bool trans gpu::Texture* texture = static_cast(voidTexture); // Passing ownership _gpuTexture.reset(texture); + _textureStorage->resetTexture(texture); if (_gpuTexture) { _width = _gpuTexture->getWidth(); diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.h b/libraries/gpu-networking/src/gpu-networking/TextureCache.h index 4e104ab783..ad18550541 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.h +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.h @@ -20,6 +20,7 @@ #include #include +#include namespace gpu { class Batch; @@ -91,11 +92,14 @@ public: Texture(); ~Texture(); - const gpu::TexturePointer& getGPUTexture() const { return _gpuTexture; } + //const gpu::TexturePointer& getGPUTexture() const { return _gpuTexture; } + const gpu::TexturePointer getGPUTexture() const { return _textureStorage->getGPUTexture(); } + model::TextureStoragePointer _textureStorage; protected: gpu::TexturePointer _gpuTexture; + private: }; diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 8acb4b0a71..5e8ebb247c 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -9,4 +9,4 @@ add_dependency_external_projects(glm) find_package(GLM REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) -link_hifi_libraries(shared networking gpu gpu-networking procedural octree) +link_hifi_libraries(shared networking gpu octree) diff --git a/libraries/model/src/model/Skybox.cpp b/libraries/model/src/model/Skybox.cpp index c17bf1df72..7e3af09a1f 100755 --- a/libraries/model/src/model/Skybox.cpp +++ b/libraries/model/src/model/Skybox.cpp @@ -13,7 +13,7 @@ #include #include -#include +// #include #include #include "Skybox_vert.h" @@ -39,7 +39,7 @@ Skybox::Skybox() { void Skybox::setColor(const Color& color) { _color = color; } - +/* void Skybox::setProcedural(QSharedPointer procedural) { _procedural = procedural; if (_procedural) { @@ -48,7 +48,7 @@ void Skybox::setProcedural(QSharedPointer procedural) { // No pipeline state customization } } - +*/ void Skybox::setCubemap(const gpu::TexturePointer& cubemap) { _cubemap = cubemap; } @@ -58,7 +58,7 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky static gpu::BufferPointer theBuffer; static gpu::Stream::FormatPointer theFormat; - if (skybox._procedural || skybox.getCubemap()) { + if (/*skybox._procedural || */skybox.getCubemap()) { if (!theBuffer) { const float CLIP = 1.0f; const glm::vec2 vertices[4] = { { -CLIP, -CLIP }, { CLIP, -CLIP }, { -CLIP, CLIP }, { CLIP, CLIP } }; @@ -78,14 +78,14 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky batch.setInputBuffer(gpu::Stream::POSITION, theBuffer, 0, 8); batch.setInputFormat(theFormat); - if (skybox._procedural && skybox._procedural->_enabled && skybox._procedural->ready()) { + /*if (skybox._procedural && skybox._procedural->_enabled && skybox._procedural->ready()) { if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { batch.setResourceTexture(0, skybox.getCubemap()); } skybox._procedural->prepare(batch, glm::vec3(1)); batch.draw(gpu::TRIANGLE_STRIP, 4); - } else if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { + } else*/ if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { static gpu::BufferPointer theConstants; static gpu::PipelinePointer thePipeline; static int SKYBOX_CONSTANTS_SLOT = 0; // need to be defined by the compilation of the shader diff --git a/libraries/model/src/model/Skybox.h b/libraries/model/src/model/Skybox.h index 809ec7e3b0..887f1ff80b 100755 --- a/libraries/model/src/model/Skybox.h +++ b/libraries/model/src/model/Skybox.h @@ -11,7 +11,7 @@ #ifndef hifi_model_Skybox_h #define hifi_model_Skybox_h -#include +//#include #include #include "Light.h" @@ -36,13 +36,13 @@ public: void setCubemap(const gpu::TexturePointer& cubemap); const gpu::TexturePointer& getCubemap() const { return _cubemap; } - void setProcedural(QSharedPointer procedural); + // void setProcedural(QSharedPointer procedural); static void render(gpu::Batch& batch, const ViewFrustum& frustum, const Skybox& skybox); protected: gpu::TexturePointer _cubemap; - QSharedPointer _procedural; + // QSharedPointer _procedural; Color _color{1.0f, 1.0f, 1.0f}; }; typedef std::shared_ptr< Skybox > SkyboxPointer; diff --git a/libraries/model/src/model/TextureStorage.cpp b/libraries/model/src/model/TextureStorage.cpp index e0f3923248..a0c64dda4a 100755 --- a/libraries/model/src/model/TextureStorage.cpp +++ b/libraries/model/src/model/TextureStorage.cpp @@ -14,18 +14,22 @@ using namespace model; using namespace gpu; // TextureStorage -TextureStorage::TextureStorage() : Texture::Storage(), - _gpuTexture(Texture::createFromStorage(this)) +TextureStorage::TextureStorage() : Texture::Storage()//, + // _gpuTexture(Texture::createFromStorage(this)) {} TextureStorage::~TextureStorage() { } void TextureStorage::reset(const QUrl& url, const TextureUsage& usage) { - _url = url; + _imageUrl = url; _usage = usage; } +void TextureStorage::resetTexture(gpu::Texture* texture) { + _gpuTexture.reset(texture); +} + diff --git a/libraries/model/src/model/TextureStorage.h b/libraries/model/src/model/TextureStorage.h index e96b5c5af2..250217fb0b 100755 --- a/libraries/model/src/model/TextureStorage.h +++ b/libraries/model/src/model/TextureStorage.h @@ -37,17 +37,19 @@ public: TextureStorage(); ~TextureStorage(); - const QUrl& getUrl() const { return _url; } + const QUrl& getUrl() const { return _imageUrl; } gpu::Texture::Type getType() const { return _usage._type; } - const gpu::TexturePointer& getGPUTexture() const { return _gpuTexture; } + const gpu::TexturePointer getGPUTexture() const { return _gpuTexture; } virtual void reset() { Storage::reset(); } void reset(const QUrl& url, const TextureUsage& usage); + void resetTexture(gpu::Texture* texture); + protected: gpu::TexturePointer _gpuTexture; TextureUsage _usage; - QUrl _url; + QUrl _imageUrl; }; typedef std::shared_ptr< TextureStorage > TextureStoragePointer; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index dde3b5bce4..69e08c353c 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -29,6 +29,8 @@ #include "gpu/StandardShaderLib.h" +#include "model/TextureStorage.h" + //#define WANT_DEBUG const int GeometryCache::UNKNOWN_ID = -1; @@ -2038,6 +2040,10 @@ static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const /* mesh.isEye*/ false, material.diffuseTexture.content); networkMaterial->diffuseTextureName = material.diffuseTexture.name; networkMaterial->_diffuseTexTransform = material.diffuseTexture.transform; + + auto diffuseMap = model::TextureMapPointer(new model::TextureMap()); + diffuseMap->setTextureStorage(networkMaterial->diffuseTexture->_textureStorage); + material._material->setTextureMap(model::MaterialKey::DIFFUSE_MAP, diffuseMap); } if (!material.normalTexture.filename.isEmpty()) { networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), NORMAL_TEXTURE, diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index fbead82515..b0e5346261 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -25,6 +25,7 @@ #include +#include #include class NetworkGeometry; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index efdcf6c6bf..2e5a0ec1c2 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1631,6 +1631,9 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape batch.setUniformBuffer(locations->materialBufferUnit, material->getSchemaBuffer()); } + auto textureMaps = drawMaterial->_material->getTextureMaps(); + + auto diffuseMap2 = textureMaps[model::MaterialKey::DIFFUSE_MAP]; Texture* diffuseMap = drawMaterial->diffuseTexture.data(); if (mesh.isEye && diffuseMap) { // FIXME - guard against out of bounds here @@ -1641,8 +1644,12 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape } } } - if (diffuseMap && static_cast(diffuseMap)->isLoaded()) { - batch.setResourceTexture(0, diffuseMap->getGPUTexture()); + //if (diffuseMap && static_cast(diffuseMap)->isLoaded()) { + + if (diffuseMap2 && !diffuseMap2->isNull()) { + + batch.setResourceTexture(0, diffuseMap2->getTextureView()); + // batch.setResourceTexture(0, diffuseMap->getGPUTexture()); } else { batch.setResourceTexture(0, textureCache->getGrayTexture()); } From 9648f560625429f03644b5bde64175ff9ca4a684 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Tue, 15 Sep 2015 15:48:21 -0700 Subject: [PATCH 039/192] some fixups to spray paint script --- examples/entityScripts/sprayPaintCan.js | 20 +++++++------------- examples/sprayPaintSpawner.js | 5 ++--- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/examples/entityScripts/sprayPaintCan.js b/examples/entityScripts/sprayPaintCan.js index 4407140184..e1d8a2535b 100644 --- a/examples/entityScripts/sprayPaintCan.js +++ b/examples/entityScripts/sprayPaintCan.js @@ -1,6 +1,7 @@ (function() { // Script.include("../libraries/utils.js"); //Need absolute path for now, for testing before PR merge and s3 cloning. Will change post-merge + Script.include("https://hifi-public.s3.amazonaws.com/scripts/libraries/utils.js"); GRAB_FRAME_USER_DATA_KEY = "grabFrame"; this.userData = {}; @@ -60,22 +61,16 @@ if (self.activated !== true) { //We were just grabbed, so create a particle system self.grab(); - Entities.editEntity(self.paintStream, { - animationSettings: startSetting - }); } //Move emitter to where entity is always when its activated self.sprayStream(); } else if (self.userData.grabKey && self.userData.grabKey.activated === false && self.activated) { - Entities.editEntity(self.paintStream, { - animationSettings: stopSetting - }); - self.activated = false; + self.letGo(); } } this.grab = function() { - self.activated = true; + this.activated = true; var animationSettings = JSON.stringify({ fps: 30, loop: true, @@ -109,7 +104,7 @@ } this.letGo = function() { - self.activated = false; + this.activated = false; Entities.deleteEntity(this.paintStream); } @@ -123,8 +118,7 @@ } this.sprayStream = function() { - var forwardVec = Quat.getFront(self.properties.rotation); - forwardVec = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, 90, 0), forwardVec); + var forwardVec = Quat.getFront(Quat.multiply(self.properties.rotation , Quat.fromPitchYawRollDegrees(0, 90, 0))); forwardVec = Vec3.normalize(forwardVec); var upVec = Quat.getUp(self.properties.rotation); @@ -132,11 +126,10 @@ position = Vec3.sum(position, Vec3.multiply(upVec, TIP_OFFSET_Y)) Entities.editEntity(self.paintStream, { position: position, - emitVelocity: Vec3.multiply(forwardVec, 4) + emitVelocity: Vec3.multiply(5, forwardVec) }); //Now check for an intersection with an entity - //move forward so ray doesnt intersect with gun var origin = Vec3.sum(position, forwardVec); var pickRay = { @@ -244,6 +237,7 @@ }); + function randFloat(min, max) { return Math.random() * (max - min) + min; } diff --git a/examples/sprayPaintSpawner.js b/examples/sprayPaintSpawner.js index 77b74e6520..6d7243cb45 100644 --- a/examples/sprayPaintSpawner.js +++ b/examples/sprayPaintSpawner.js @@ -11,7 +11,6 @@ //Just temporarily using my own bucket here so others can test the entity. Once PR is tested and merged, then the entity script will appear in its proper place in S3, and I wil switch it var scriptURL = "https://hifi-public.s3.amazonaws.com/eric/scripts/sprayPaintCan.js?=v1"; var modelURL = "https://hifi-public.s3.amazonaws.com/eric/models/paintcan.fbx"; -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); var sprayCan = Entities.addEntity({ type: "Model", @@ -27,8 +26,8 @@ var sprayCan = Entities.addEntity({ collisionsWillMove: true, shapeType: 'box', script: scriptURL, - gravity: {x: 0, y: -0.5, z: 0}, - velocity: {x: 0, y: -1, z: 0} + // gravity: {x: 0, y: -0.5, z: 0}, + // velocity: {x: 0, y: -1, z: 0} }); function cleanup() { From 9992f4b5c5bbb557a05a0c7e09c35bf8127e533f Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 16 Sep 2015 01:16:58 +0200 Subject: [PATCH 040/192] Made stopping scripts by hash (path/url) more tolerable and fixed stopping script functionality in ScriptDiscoveryService: made stopScript(string) to stop script by url or path and stopScriptName(string) to stop by filename --- interface/src/Application.cpp | 15 ++++---- interface/src/Application.h | 4 +-- interface/src/ui/RunningScriptsWidget.cpp | 42 ++++++++++++++--------- interface/src/ui/RunningScriptsWidget.h | 8 ++--- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 89ce392ba0..953c8162fc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -606,7 +606,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _overlays.init(); // do this before scripts load _runningScriptsWidget->setRunningScripts(getRunningScripts()); - connect(_runningScriptsWidget, &RunningScriptsWidget::stopScriptName, this, &Application::stopScript); connect(this, SIGNAL(aboutToQuit()), this, SLOT(saveScripts())); connect(this, SIGNAL(aboutToQuit()), this, SLOT(aboutToQuit())); @@ -4328,17 +4327,18 @@ void Application::stopAllScripts(bool restart) { _myAvatar->clearScriptableSettings(); } -void Application::stopScript(const QString &scriptName, bool restart) { - const QString& scriptURLString = QUrl(scriptName).toString(); - if (_scriptEnginesHash.contains(scriptURLString)) { - ScriptEngine* scriptEngine = _scriptEnginesHash[scriptURLString]; +bool Application::stopScript(const QString& scriptHash, bool restart) { + bool stoppedScript = false; + if (_scriptEnginesHash.contains(scriptHash)) { + ScriptEngine* scriptEngine = _scriptEnginesHash[scriptHash]; if (restart) { auto scriptCache = DependencyManager::get(); - scriptCache->deleteScript(scriptName); + scriptCache->deleteScript(QUrl(scriptHash)); connect(scriptEngine, SIGNAL(finished(const QString&)), SLOT(reloadScript(const QString&))); } scriptEngine->stop(); - qCDebug(interfaceapp) << "stopping script..." << scriptName; + stoppedScript = true; + qCDebug(interfaceapp) << "stopping script..." << scriptHash; // HACK: ATM scripts cannot set/get their animation priorities, so we clear priorities // whenever a script stops in case it happened to have been setting joint rotations. // TODO: expose animation priorities and provide a layered animation control system. @@ -4347,6 +4347,7 @@ void Application::stopScript(const QString &scriptName, bool restart) { if (_scriptEnginesHash.empty()) { _myAvatar->clearScriptableSettings(); } + return stoppedScript; } void Application::reloadAllScripts() { diff --git a/interface/src/Application.h b/interface/src/Application.h index b997fae823..5cb70cf676 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -292,7 +292,7 @@ public: NodeToJurisdictionMap& getEntityServerJurisdictions() { return _entityServerJurisdictions; } QStringList getRunningScripts() { return _scriptEnginesHash.keys(); } - ScriptEngine* getScriptEngine(QString scriptHash) { return _scriptEnginesHash.contains(scriptHash) ? _scriptEnginesHash[scriptHash] : NULL; } + ScriptEngine* getScriptEngine(const QString& scriptHash) { return _scriptEnginesHash.contains(scriptHash) ? _scriptEnginesHash[scriptHash] : NULL; } bool isLookingAtMyAvatar(AvatarSharedPointer avatar); @@ -395,7 +395,7 @@ public slots: void reloadScript(const QString& scriptName, bool isUserLoaded = true); void scriptFinished(const QString& scriptName); void stopAllScripts(bool restart = false); - void stopScript(const QString& scriptName, bool restart = false); + bool stopScript(const QString& scriptHash, bool restart = false); void reloadAllScripts(); void reloadOneScript(const QString& scriptName); void loadDefaultScripts(); diff --git a/interface/src/ui/RunningScriptsWidget.cpp b/interface/src/ui/RunningScriptsWidget.cpp index 1165de7592..61b03bd610 100644 --- a/interface/src/ui/RunningScriptsWidget.cpp +++ b/interface/src/ui/RunningScriptsWidget.cpp @@ -57,16 +57,15 @@ RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) : connect(ui->filterLineEdit, &QLineEdit::textChanged, this, &RunningScriptsWidget::updateFileFilter); connect(ui->scriptTreeView, &QTreeView::doubleClicked, this, &RunningScriptsWidget::loadScriptFromList); - connect(ui->reloadAllButton, &QPushButton::clicked, - Application::getInstance(), &Application::reloadAllScripts); - connect(ui->stopAllButton, &QPushButton::clicked, - this, &RunningScriptsWidget::allScriptsStopped); - connect(ui->loadScriptFromDiskButton, &QPushButton::clicked, - Application::getInstance(), &Application::loadDialog); - connect(ui->loadScriptFromURLButton, &QPushButton::clicked, - Application::getInstance(), &Application::loadScriptURLDialog); - connect(&_reloadSignalMapper, SIGNAL(mapped(QString)), Application::getInstance(), SLOT(reloadOneScript(const QString&))); - connect(&_stopSignalMapper, SIGNAL(mapped(QString)), Application::getInstance(), SLOT(stopScript(const QString&))); + connect(ui->reloadAllButton, &QPushButton::clicked, Application::getInstance(), &Application::reloadAllScripts); + connect(ui->stopAllButton, &QPushButton::clicked, this, &RunningScriptsWidget::allScriptsStopped); + connect(ui->loadScriptFromDiskButton, &QPushButton::clicked, Application::getInstance(), &Application::loadDialog); + connect(ui->loadScriptFromURLButton, &QPushButton::clicked, Application::getInstance(), &Application::loadScriptURLDialog); + connect(&_reloadSignalMapper, static_cast(&QSignalMapper::mapped), + Application::getInstance(), &Application::reloadOneScript); + + connect(&_stopSignalMapper, static_cast(&QSignalMapper::mapped), + [](const QString& script) { Application::getInstance()->stopScript(script); }); UIUtil::scaleWidgetFontSizes(this); } @@ -217,9 +216,6 @@ void RunningScriptsWidget::keyPressEvent(QKeyEvent *keyEvent) { } } -void RunningScriptsWidget::scriptStopped(const QString& scriptName) { -} - void RunningScriptsWidget::allScriptsStopped() { Application::getInstance()->stopAllScripts(); } @@ -227,15 +223,16 @@ void RunningScriptsWidget::allScriptsStopped() { QVariantList RunningScriptsWidget::getRunning() { const int WINDOWS_DRIVE_LETTER_SIZE = 1; QVariantList result; - QStringList runningScripts = Application::getInstance()->getRunningScripts(); - for (int i = 0; i < runningScripts.size(); i++) { - QUrl runningScriptURL = QUrl(runningScripts.at(i)); + foreach(const QString& runningScript, Application::getInstance()->getRunningScripts()) { + QUrl runningScriptURL = QUrl(runningScript); if (runningScriptURL.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { runningScriptURL = QUrl::fromLocalFile(runningScriptURL.toDisplayString(QUrl::FormattingOptions(QUrl::FullyEncoded))); } QVariantMap resultNode; resultNode.insert("name", runningScriptURL.fileName()); resultNode.insert("url", runningScriptURL.toDisplayString(QUrl::FormattingOptions(QUrl::FullyEncoded))); + // The path contains the exact path/URL of the script, which also is used in the stopScript function. + resultNode.insert("path", runningScript); resultNode.insert("local", runningScriptURL.isLocalFile()); result.append(resultNode); } @@ -294,3 +291,16 @@ QVariantList RunningScriptsWidget::getLocal() { } return result; } + +bool RunningScriptsWidget::stopScriptByName(const QString& name) { + foreach (const QString& runningScript, Application::getInstance()->getRunningScripts()) { + if (QUrl(runningScript).fileName().toLower() == name.trimmed().toLower()) { + return Application::getInstance()->stopScript(runningScript, false); + } + } + return false; +} + +bool RunningScriptsWidget::stopScript(const QString& name, bool restart) { + return Application::getInstance()->stopScript(name, restart); +} diff --git a/interface/src/ui/RunningScriptsWidget.h b/interface/src/ui/RunningScriptsWidget.h index c09bce5443..9029b13c56 100644 --- a/interface/src/ui/RunningScriptsWidget.h +++ b/interface/src/ui/RunningScriptsWidget.h @@ -36,7 +36,7 @@ public: const ScriptsModel* getScriptsModel() { return &_scriptsModel; } signals: - void stopScriptName(const QString& name, bool restart = false); + void scriptStopped(const QString& scriptName); protected: virtual bool eventFilter(QObject* sender, QEvent* event); @@ -45,10 +45,11 @@ protected: virtual void showEvent(QShowEvent* event); public slots: - void scriptStopped(const QString& scriptName); QVariantList getRunning(); QVariantList getPublic(); QVariantList getLocal(); + bool stopScript(const QString& name, bool restart = false); + bool stopScriptByName(const QString& name); private slots: void allScriptsStopped(); @@ -63,9 +64,6 @@ private: QSignalMapper _stopSignalMapper; ScriptsModelFilter _scriptsModelFilter; ScriptsModel _scriptsModel; - ScriptsTableWidget* _recentlyLoadedScriptsTable; - QStringList _recentlyLoadedScripts; - QString _lastStoppedScript; QVariantList getPublicChildNodes(TreeNodeFolder* parent); }; From da208526cc4a44df99967d9bed5aa4af100082ec Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Tue, 15 Sep 2015 16:48:28 -0700 Subject: [PATCH 041/192] spray can tweaks --- examples/entityScripts/sprayPaintCan.js | 4 +--- examples/sprayPaintSpawner.js | 10 ++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/entityScripts/sprayPaintCan.js b/examples/entityScripts/sprayPaintCan.js index e1d8a2535b..02ddb91534 100644 --- a/examples/entityScripts/sprayPaintCan.js +++ b/examples/entityScripts/sprayPaintCan.js @@ -98,9 +98,8 @@ green: 20, blue: 150 }, - lifetime: 500, //probably wont be holding longer than this straight + lifetime: 50, //probably wont be holding longer than this straight }); - } this.letGo = function() { @@ -228,7 +227,6 @@ this.unload = function() { Script.update.disconnect(this.update); - Entities.deleteEntity(this.paintStream); this.strokes.forEach(function(stroke) { Entities.deleteEntity(stroke); }); diff --git a/examples/sprayPaintSpawner.js b/examples/sprayPaintSpawner.js index 6d7243cb45..56cf6cccdc 100644 --- a/examples/sprayPaintSpawner.js +++ b/examples/sprayPaintSpawner.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html //Just temporarily using my own bucket here so others can test the entity. Once PR is tested and merged, then the entity script will appear in its proper place in S3, and I wil switch it -var scriptURL = "https://hifi-public.s3.amazonaws.com/eric/scripts/sprayPaintCan.js?=v1"; +var scriptURL = "https://hifi-public.s3.amazonaws.com/eric/scripts/sprayPaintCan.js?=v3"; var modelURL = "https://hifi-public.s3.amazonaws.com/eric/models/paintcan.fbx"; var sprayCan = Entities.addEntity({ @@ -26,12 +26,14 @@ var sprayCan = Entities.addEntity({ collisionsWillMove: true, shapeType: 'box', script: scriptURL, - // gravity: {x: 0, y: -0.5, z: 0}, - // velocity: {x: 0, y: -1, z: 0} + gravity: {x: 0, y: -0.5, z: 0}, + velocity: {x: 0, y: -1, z: 0} }); function cleanup() { - Entities.deleteEntity(sprayCan); + + // Uncomment the below line to delete sprayCan on script reload- for faster iteration during development + // Entities.deleteEntity(sprayCan); } Script.scriptEnding.connect(cleanup); From 37f45b57ff19acd8ddfde71498b411c3c373d041 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Tue, 15 Sep 2015 18:40:54 -0700 Subject: [PATCH 042/192] wand updates --- examples/toys/bubblewand/bubble.js | 7 ++- examples/toys/bubblewand/createWand.js | 10 +++-- examples/toys/bubblewand/wand.js | 60 +++++++++++++++++++------- 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index eaabf98e44..748c946119 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -15,12 +15,11 @@ // Script.include("../../utilities.js"); // Script.include("../../libraries/utils.js"); - - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); +Script.include("http://hifi-public.s3.amazonaws.com/scripts/utilities.js"); +Script.include("http://hifi-public.s3.amazonaws.com/scripts/libraries/utils.js"); var POP_SOUNDS = [ - SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav"), + SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop0.wav") , SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop1.wav"), SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop2.wav"), SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop3.wav") diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 158bcb3b99..49481b6c1a 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -13,8 +13,8 @@ // Script.include("../../utilities.js"); // Script.include("../../libraries/utils.js"); -Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); -Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); +Script.include("http://hifi-public.s3.amazonaws.com/scripts/utilities.js"); +Script.include("http://hifi-public.s3.amazonaws.com/scripts/libraries/utils.js"); var wandModel = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var wandCollisionShape = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; @@ -25,7 +25,11 @@ var scriptURL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wa //create the wand in front of the avatar var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); - +var tablePosition = { + x:546.48, + y:495.63, + z:506.25 +} var wand = Entities.addEntity({ type: "Model", modelURL: wandModel, diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 020fba9b15..afee5fab0c 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -11,22 +11,21 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +'use strict' function convertRange(value, r1, r2) { return (value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0]; } (function() { - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); - + Script.include("http://hifi-public.s3.amazonaws.com/scripts/utilities.js"); + Script.include("http://hifi-public.s3.amazonaws.com/scripts/libraries/utils.js"); // Script.include("../../utilities.js"); // Script.include("../../libraries/utils.js"); var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); // var bubbleScript = 'http://localhost:8080/bubble.js?' + randInt(1, 10000); //for local testing @@ -118,7 +117,7 @@ function convertRange(value, r1, r2) { var BUBBLE_GRAVITY = { x: 0, - y: -0.05, + y: -0.1, z: 0 } @@ -135,8 +134,8 @@ function convertRange(value, r1, r2) { this.entityID = entityID; this.properties = Entities.getEntityProperties(this.entityID); BubbleWand.originalProperties = this.properties; - print('rotation???' + JSON.stringify(BubbleWand.originalProperties.rotation)); - + BubbleWand.init(); + print('initial position' + JSON.stringify(BubbleWand.originalProperties.position)); } this.unload = function(entityID) { @@ -161,8 +160,9 @@ function convertRange(value, r1, r2) { var BubbleWand = { bubbles: [], timeSinceMoved: 0, - resetAtTime: 5, + resetAtTime: 2, currentBubble: null, + atOriginalLocation: true, update: function(dt) { BubbleWand.internalUpdate(dt); }, @@ -176,8 +176,9 @@ function convertRange(value, r1, r2) { var hitTargetWithWand = findSphereSphereHit(wandPosition, HAND_SIZE / 2, getGustDetectorPosition(), TARGET_SIZE / 2) var velocity = Vec3.subtract(wandPosition, _t.lastPosition) - var velocityStrength = Vec3.length(velocity) * 100; + // print('VELOCITY:'+JSON.stringify(velocity)) + var velocityStrength = Vec3.length(velocity) * 100; var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); @@ -194,17 +195,43 @@ function convertRange(value, r1, r2) { } //print('velocityStrength'+velocityStrength) + //we want to reset the object to its original position if its been a while since it has moved + // print('wand position ' + JSON.stringify(wandPosition)) + // print('last position ' + JSON.stringify(_t.lastPosition)) + // print('at original location? ' + _t.atOriginalLocation) + + if (velocityStrength < 0.01) { + velocityStrength = 0 + } + var isMoving; if (velocityStrength === 0) { + isMoving = false; + } else { + isMoving = true; + } + + + if (isMoving === true) { + // print('MOVING') + // print('velocityStrength ' + velocityStrength) + + _t.timeSinceMoved = 0; + _t.atOriginalLocation = false; + } else { _t.timeSinceMoved = _t.timeSinceMoved + dt; + } + + if (isMoving === false && _t.atOriginalLocation === false) { if (_t.timeSinceMoved > _t.resetAtTime) { _t.timeSinceMoved = 0; _t.returnToOriginalLocation(); + } - } else { - _t.timeSinceMoved = 0; } + + //debug overlays for mouth mode if (overlays) { var leftHandPos = MyAvatar.getLeftPalmPosition(); @@ -239,8 +266,7 @@ function convertRange(value, r1, r2) { //store the last position of the wand for velocity calculations _t.lastPosition = wandPosition; - // velocity numbers are pretty small, so lets make them a bit bigger - var velocityStrength = Vec3.length(velocity) * 100; + if (velocityStrength > 10) { velocityStrength = 10 @@ -320,7 +346,7 @@ function convertRange(value, r1, r2) { _t.wandTipPosition = wandTipPosition; //store the position of the tip on spawn for use in velocity calculations - _t.lastPosition = wandTipPosition; + _t.lastPosition = wandPosition; //create a bubble at the wand tip _t.currentBubble = Entities.addEntity({ @@ -340,14 +366,16 @@ function convertRange(value, r1, r2) { script: bubbleScript, }); + //print('spawnbubble position' + JSON.stringify(wandTipPosition)); + //add this bubble to an array of bubbles so we can keep track of them _t.bubbles.push(_t.currentBubble) }, returnToOriginalLocation: function() { var _t = this; + _t.atOriginalLocation = true; Script.update.disconnect(BubbleWand.update) - _t.currentBubble = null; Entities.deleteEntity(_t.currentBubble); Entities.editEntity(wandEntity.entityID, _t.originalProperties) _t.spawnBubble(); @@ -362,6 +390,6 @@ function convertRange(value, r1, r2) { } } - BubbleWand.init(); + }) \ No newline at end of file From efc321bc1053d5bd53cdc48ee199b61d778bd701 Mon Sep 17 00:00:00 2001 From: BOB LONG Date: Wed, 16 Sep 2015 00:40:26 -0700 Subject: [PATCH 043/192] Display face blend coefficients Display the face blend coefficients and update the value in real time. --- examples/faceBlendCoefficients.js | 98 ++++++++++++++++++++++++++++++ interface/src/avatar/MyAvatar.h | 2 + libraries/render-utils/src/Model.h | 5 ++ 3 files changed, 105 insertions(+) create mode 100644 examples/faceBlendCoefficients.js diff --git a/examples/faceBlendCoefficients.js b/examples/faceBlendCoefficients.js new file mode 100644 index 0000000000..56fcadca9d --- /dev/null +++ b/examples/faceBlendCoefficients.js @@ -0,0 +1,98 @@ +// +// coefficients.js +// +// version 2.0 +// +// Created by Bob Long, 9/14/2015 +// A simple panel that can display the blending coefficients of Avatar's face model. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include('utilities/tools/cookies.js') + +var panel; +var coeff; +var interval; +var item = 0; +var DEVELOPER_MENU = "Developer"; +var AVATAR_MENU = DEVELOPER_MENU + " > Avatar"; +var SHOW_FACE_BLEND_COEFFICIENTS = "Show face blend coefficients" + +function MenuConnect(menuItem) { + if (menuItem == SHOW_FACE_BLEND_COEFFICIENTS) { + if(Menu.isOptionChecked(SHOW_FACE_BLEND_COEFFICIENTS)) { + panel.show(); + Overlays.editOverlay(coeff, { visible : true }); + } else { + panel.hide(); + Overlays.editOverlay(coeff, { visible : false }); + } + } +} + +// Add a menu item to show/hide the coefficients +function setupMenu() { + if (!Menu.menuExists(DEVELOPER_MENU)) { + Menu.addMenu(DEVELOPER_MENU); + } + + if (!Menu.menuExists(AVATAR_MENU)) { + Menu.addMenu(AVATAR_MENU); + } + + Menu.addMenuItem({ menuName: AVATAR_MENU, menuItemName: SHOW_FACE_BLEND_COEFFICIENTS, isCheckable: true, isChecked: true }); + Menu.menuItemEvent.connect(MenuConnect); +} + +function setupPanel() { + panel = new Panel(10, 400); + + // Slider to select which coefficient to display + panel.newSlider("Select Coefficient Index", + 0, + 100, + function(value) { item = value.toFixed(0); }, + function() { return item; }, + function(value) { return "index = " + item; } + ); + + // The raw overlay used to show the actual coefficient value + coeff = Overlays.addOverlay("text", { + x: 10, + y: 420, + width: 300, + height: 50, + color: { red: 255, green: 255, blue: 255 }, + alpha: 1.0, + backgroundColor: { red: 127, green: 127, blue: 127 }, + backgroundAlpha: 0.5, + topMargin: 15, + leftMargin: 20, + text: "Coefficient: 0.0" + }); + + // Set up the interval (0.5 sec) to update the coefficient. + interval = Script.setInterval(function() { + Overlays.editOverlay(coeff, { text: "Coefficient: " + MyAvatar.getFaceBlendCoef(item).toFixed(4) }); + }, 500); + + // Mouse event setup + Controller.mouseMoveEvent.connect(function panelMouseMoveEvent(event) { return panel.mouseMoveEvent(event); }); + Controller.mousePressEvent.connect( function panelMousePressEvent(event) { return panel.mousePressEvent(event); }); + Controller.mouseReleaseEvent.connect(function(event) { return panel.mouseReleaseEvent(event); }); +} + +// Clean up +function scriptEnding() { + panel.destroy(); + Overlays.deleteOverlay(coeff); + Script.clearInterval(interval); + + Menu.removeMenuItem(AVATAR_MENU, SHOW_FACE_BLEND_COEFFICIENTS); +} + +setupMenu(); +setupPanel(); +Script.scriptEnding.connect(scriptEnding); \ No newline at end of file diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index bb3c6385f9..5fd5ee4c29 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -110,6 +110,8 @@ public: Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } + Q_INVOKABLE int getFaceBlendCoefNum() const { return getHead()->getFaceModel().getBlendshapeCoefficientsNum(); } + Q_INVOKABLE float getFaceBlendCoef(int index) const { return getHead()->getFaceModel().getBlendshapeCoefficient(index); } Q_INVOKABLE glm::vec3 getEyePosition() const { return getHead()->getEyePosition(); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 348e5cf549..d1aa2901c8 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -191,6 +191,11 @@ public: const std::unordered_set& getCauterizeBoneSet() const { return _cauterizeBoneSet; } void setCauterizeBoneSet(const std::unordered_set& boneSet) { _cauterizeBoneSet = boneSet; } + int getBlendshapeCoefficientsNum() const { return _blendshapeCoefficients.size(); } + float getBlendshapeCoefficient(int index) const { + return index >= _blendshapeCoefficients.size() || index < 0 ? + 0.0f : _blendshapeCoefficients.at(index); } + protected: void setPupilDilation(float dilation) { _pupilDilation = dilation; } From 0b21cc1777356ee8a2b0fa59ca0da36777fd7205 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Wed, 16 Sep 2015 09:59:35 -0700 Subject: [PATCH 044/192] paint can tweaks --- examples/entityScripts/sprayPaintCan.js | 9 ++++++++- examples/sprayPaintSpawner.js | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/entityScripts/sprayPaintCan.js b/examples/entityScripts/sprayPaintCan.js index 02ddb91534..bad13d43d7 100644 --- a/examples/entityScripts/sprayPaintCan.js +++ b/examples/entityScripts/sprayPaintCan.js @@ -57,7 +57,8 @@ timeSinceLastMoved = 0; } - if (self.userData.grabKey && self.userData.grabKey.activated === true) { + //Only activate for the user who grabbed the object + if (self.userData.grabKey && self.userData.grabKey.activated === true && this.userData.grabKey.avatarId == MyAvatar.sessionUUID) { if (self.activated !== true) { //We were just grabbed, so create a particle system self.grab(); @@ -105,6 +106,7 @@ this.letGo = function() { this.activated = false; Entities.deleteEntity(this.paintStream); + this.paintStream = null; } this.reset = function() { @@ -208,6 +210,8 @@ this.entityId = entityId; this.properties = Entities.getEntityProperties(self.entityId); this.getUserData(); + + //Only activate for the avatar who is grabbing the can! if (this.userData.grabKey && this.userData.grabKey.activated) { this.activated = true; } @@ -227,6 +231,9 @@ this.unload = function() { Script.update.disconnect(this.update); + if(this.paintStream) { + Entities.deleteEntity(this.paintStream); + } this.strokes.forEach(function(stroke) { Entities.deleteEntity(stroke); }); diff --git a/examples/sprayPaintSpawner.js b/examples/sprayPaintSpawner.js index 56cf6cccdc..87b96ae1b7 100644 --- a/examples/sprayPaintSpawner.js +++ b/examples/sprayPaintSpawner.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html //Just temporarily using my own bucket here so others can test the entity. Once PR is tested and merged, then the entity script will appear in its proper place in S3, and I wil switch it -var scriptURL = "https://hifi-public.s3.amazonaws.com/eric/scripts/sprayPaintCan.js?=v3"; +var scriptURL = "https://hifi-public.s3.amazonaws.com/eric/scripts/sprayPaintCan.js?=v6"; var modelURL = "https://hifi-public.s3.amazonaws.com/eric/models/paintcan.fbx"; var sprayCan = Entities.addEntity({ @@ -37,3 +37,4 @@ function cleanup() { } Script.scriptEnding.connect(cleanup); + From 500a96ee7ce32f77463db3036f99e1e49152e85d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 16 Sep 2015 12:25:03 -0700 Subject: [PATCH 045/192] guard perpetual AC domain connection if NL thread locked --- libraries/networking/src/ThreadedAssignment.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index aee2805f32..a57bbc847c 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -43,7 +43,9 @@ void ThreadedAssignment::setFinished(bool isFinished) { packetReceiver.setShouldDropPackets(true); if (_domainServerTimer) { - _domainServerTimer->stop(); + // stop the domain-server check in timer by calling deleteLater so it gets cleaned up on NL thread + _domainServerTimer->deleteLater(); + _domainServerTimer = nullptr; } if (_statsTimer) { @@ -65,9 +67,12 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy auto nodeList = DependencyManager::get(); nodeList->setOwnerType(nodeType); - _domainServerTimer = new QTimer(this); + _domainServerTimer = new QTimer(nodeList.data()); connect(_domainServerTimer, SIGNAL(timeout()), this, SLOT(checkInWithDomainServerOrExit())); _domainServerTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + + // move the domain server time to the NL so check-ins fire from there + _domainServerTimer->moveToThread(nodeList->thread()); if (shouldSendStats) { // start sending stats packet once we connect to the domain From cc8ad868b8c36a0b5567c2c5bedb19e0e56ab74e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 16 Sep 2015 12:27:57 -0700 Subject: [PATCH 046/192] don't parent DS timer to object in another thread --- libraries/networking/src/ThreadedAssignment.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index a57bbc847c..5f0db9412c 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -67,7 +67,7 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy auto nodeList = DependencyManager::get(); nodeList->setOwnerType(nodeType); - _domainServerTimer = new QTimer(nodeList.data()); + _domainServerTimer = new QTimer; connect(_domainServerTimer, SIGNAL(timeout()), this, SLOT(checkInWithDomainServerOrExit())); _domainServerTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); From bbf6e8b599ead168d5441d9edfebaafcc890fbe6 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Wed, 16 Sep 2015 13:52:29 -0700 Subject: [PATCH 047/192] using relative paths for entity scripts --- examples/entityScripts/sprayPaintCan.js | 10 +++++----- examples/sprayPaintSpawner.js | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/entityScripts/sprayPaintCan.js b/examples/entityScripts/sprayPaintCan.js index bad13d43d7..aa04e94341 100644 --- a/examples/entityScripts/sprayPaintCan.js +++ b/examples/entityScripts/sprayPaintCan.js @@ -2,7 +2,7 @@ // Script.include("../libraries/utils.js"); //Need absolute path for now, for testing before PR merge and s3 cloning. Will change post-merge - Script.include("https://hifi-public.s3.amazonaws.com/scripts/libraries/utils.js"); + Script.include("../libraries/utils.js"); GRAB_FRAME_USER_DATA_KEY = "grabFrame"; this.userData = {}; @@ -58,7 +58,7 @@ } //Only activate for the user who grabbed the object - if (self.userData.grabKey && self.userData.grabKey.activated === true && this.userData.grabKey.avatarId == MyAvatar.sessionUUID) { + if (self.userData.grabKey && self.userData.grabKey.activated === true && self.userData.grabKey.avatarId == MyAvatar.sessionUUID) { if (self.activated !== true) { //We were just grabbed, so create a particle system self.grab(); @@ -88,9 +88,9 @@ emitVelocity: ZERO_VEC, emitAcceleration: ZERO_VEC, velocitySpread: { - x: .02, - y: .02, - z: 0.02 + x: .1, + y: .1, + z: 0.1 }, emitRate: 100, particleRadius: 0.01, diff --git a/examples/sprayPaintSpawner.js b/examples/sprayPaintSpawner.js index 87b96ae1b7..3b9cee6ef4 100644 --- a/examples/sprayPaintSpawner.js +++ b/examples/sprayPaintSpawner.js @@ -9,7 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html //Just temporarily using my own bucket here so others can test the entity. Once PR is tested and merged, then the entity script will appear in its proper place in S3, and I wil switch it -var scriptURL = "https://hifi-public.s3.amazonaws.com/eric/scripts/sprayPaintCan.js?=v6"; +// var scriptURL = "https://hifi-public.s3.amazonaws.com/eric/scripts/sprayPaintCan.js?=v6 "; +var scriptURL = Script.resolvePath("entityScripts/sprayPaintCan.js?v2"); var modelURL = "https://hifi-public.s3.amazonaws.com/eric/models/paintcan.fbx"; var sprayCan = Entities.addEntity({ From e1cb041576433ffada4d550ac99acc224fc82c18 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 16 Sep 2015 15:00:42 -0700 Subject: [PATCH 048/192] first cut at adding desiredProperties to getProperties --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 4 ++-- .../entities-renderer/src/RenderableModelEntityItem.h | 2 +- libraries/entities/src/BoxEntityItem.cpp | 4 ++-- libraries/entities/src/BoxEntityItem.h | 4 ++-- libraries/entities/src/EntityItem.cpp | 2 +- libraries/entities/src/EntityItem.h | 2 +- libraries/entities/src/EntityScriptingInterface.cpp | 9 +++++++-- libraries/entities/src/EntityScriptingInterface.h | 1 + libraries/entities/src/LightEntityItem.cpp | 4 ++-- libraries/entities/src/LightEntityItem.h | 2 +- libraries/entities/src/LineEntityItem.cpp | 4 ++-- libraries/entities/src/LineEntityItem.h | 2 +- libraries/entities/src/ModelEntityItem.cpp | 4 ++-- libraries/entities/src/ModelEntityItem.h | 2 +- libraries/entities/src/ParticleEffectEntityItem.cpp | 4 ++-- libraries/entities/src/ParticleEffectEntityItem.h | 2 +- libraries/entities/src/PolyLineEntityItem.cpp | 4 ++-- libraries/entities/src/PolyLineEntityItem.h | 2 +- libraries/entities/src/PolyVoxEntityItem.cpp | 4 ++-- libraries/entities/src/PolyVoxEntityItem.h | 2 +- libraries/entities/src/SphereEntityItem.cpp | 4 ++-- libraries/entities/src/SphereEntityItem.h | 2 +- libraries/entities/src/TextEntityItem.cpp | 4 ++-- libraries/entities/src/TextEntityItem.h | 2 +- libraries/entities/src/WebEntityItem.cpp | 4 ++-- libraries/entities/src/WebEntityItem.h | 2 +- libraries/entities/src/ZoneEntityItem.cpp | 4 ++-- libraries/entities/src/ZoneEntityItem.h | 2 +- 28 files changed, 47 insertions(+), 41 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index e0cc5f644f..9f0ce93721 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -358,8 +358,8 @@ bool RenderableModelEntityItem::needsToCallUpdate() const { return _needsInitialSimulation || ModelEntityItem::needsToCallUpdate(); } -EntityItemProperties RenderableModelEntityItem::getProperties() const { - EntityItemProperties properties = ModelEntityItem::getProperties(); // get the properties from our base class +EntityItemProperties RenderableModelEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = ModelEntityItem::getProperties(desiredProperties); // get the properties from our base class if (_originalTexturesRead) { properties.setTextureNames(_originalTextures); } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index d2c9370553..a893cf98f9 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -35,7 +35,7 @@ public: virtual ~RenderableModelEntityItem(); - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, diff --git a/libraries/entities/src/BoxEntityItem.cpp b/libraries/entities/src/BoxEntityItem.cpp index ce05bd93f0..88b4fec9e6 100644 --- a/libraries/entities/src/BoxEntityItem.cpp +++ b/libraries/entities/src/BoxEntityItem.cpp @@ -32,8 +32,8 @@ BoxEntityItem::BoxEntityItem(const EntityItemID& entityItemID, const EntityItemP setProperties(properties); } -EntityItemProperties BoxEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties BoxEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class properties._color = getXColor(); properties._colorChanged = false; diff --git a/libraries/entities/src/BoxEntityItem.h b/libraries/entities/src/BoxEntityItem.h index 49ce67f361..e1bb284980 100644 --- a/libraries/entities/src/BoxEntityItem.h +++ b/libraries/entities/src/BoxEntityItem.h @@ -23,8 +23,8 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; - virtual bool setProperties(const EntityItemProperties& properties); + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 60a3004635..3bebbed527 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1019,7 +1019,7 @@ quint64 EntityItem::getExpiry() const { return _created + (quint64)(_lifetime * (float)USECS_PER_SECOND); } -EntityItemProperties EntityItem::getProperties() const { +EntityItemProperties EntityItem::getProperties(QScriptValue desiredProperties) const { EntityItemProperties properties; properties._id = getID(); properties._idSet = true; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 14f06f9f18..bb736a40a2 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -131,7 +131,7 @@ public: EntityItemID getEntityItemID() const { return EntityItemID(_id); } // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; /// returns true if something changed virtual bool setProperties(const EntityItemProperties& properties); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 4b8b4e2903..5d4934ca7e 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -100,12 +100,17 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) { - EntityItemProperties results; + QScriptValue allProperties; + return getEntityProperties(identity, allProperties); +} + +EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity, QScriptValue desiredProperties) { + EntityItemProperties results; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(identity)); if (entity) { - results = entity->getProperties(); + results = entity->getProperties(desiredProperties); // TODO: improve sitting points and naturalDimensions in the future, // for now we've included the old sitting points model behavior for entity types that are models diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 8d2b0b6892..fe91535e04 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -78,6 +78,7 @@ public slots: /// gets the current model properties for a specific model /// this function will not find return results in script engine contexts which don't have access to models Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid entityID); + Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid identity, QScriptValue desiredProperties); /// edits a model updating only the included properties, will return the identified EntityItemID in case of /// successful edit, if the input entityID is for an unknown model this function will have no effect diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index d67d09e4b1..cb4130ead3 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -56,8 +56,8 @@ void LightEntityItem::setDimensions(const glm::vec3& value) { } -EntityItemProperties LightEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties LightEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(isSpotlight, getIsSpotlight); COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h index 3ed28a252a..fd94e6ef5c 100644 --- a/libraries/entities/src/LightEntityItem.h +++ b/libraries/entities/src/LightEntityItem.h @@ -26,7 +26,7 @@ public: virtual void setDimensions(const glm::vec3& value); // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/LineEntityItem.cpp b/libraries/entities/src/LineEntityItem.cpp index 222aee1a8f..0fb2ea9e01 100644 --- a/libraries/entities/src/LineEntityItem.cpp +++ b/libraries/entities/src/LineEntityItem.cpp @@ -43,9 +43,9 @@ LineEntityItem::LineEntityItem(const EntityItemID& entityItemID, const EntityIte } -EntityItemProperties LineEntityItem::getProperties() const { +EntityItemProperties LineEntityItem::getProperties(QScriptValue desiredProperties) const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class properties._color = getXColor(); diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 3b7590a460..d06e2393b3 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -23,7 +23,7 @@ class LineEntityItem : public EntityItem { ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 4c03f4a7da..deb7db279b 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -43,8 +43,8 @@ ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID, const EntityI _color[0] = _color[1] = _color[2] = 0; } -EntityItemProperties ModelEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties ModelEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelURL, getModelURL); diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 950a95bae2..281487f810 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -25,7 +25,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index b1229b8bb6..f032a988a2 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -147,8 +147,8 @@ void ParticleEffectEntityItem::computeAndUpdateDimensions() { } -EntityItemProperties ParticleEffectEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties ParticleEffectEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 802ff25af3..dea8e0e8c8 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -25,7 +25,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of this entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/PolyLineEntityItem.cpp b/libraries/entities/src/PolyLineEntityItem.cpp index 196fde47fb..0b8593fdf2 100644 --- a/libraries/entities/src/PolyLineEntityItem.cpp +++ b/libraries/entities/src/PolyLineEntityItem.cpp @@ -45,9 +45,9 @@ _strokeWidths(QVector(0.0f)) setProperties(properties); } -EntityItemProperties PolyLineEntityItem::getProperties() const { +EntityItemProperties PolyLineEntityItem::getProperties(QScriptValue desiredProperties) const { QWriteLocker lock(&_quadReadWriteLock); - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class properties._color = getXColor(); diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index e5fdcf9b78..efbadd73b4 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -23,7 +23,7 @@ class PolyLineEntityItem : public EntityItem { ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index 6c53dbfa16..a19430b245 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -104,8 +104,8 @@ const glm::vec3& PolyVoxEntityItem::getVoxelVolumeSize() const { } -EntityItemProperties PolyVoxEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties PolyVoxEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(voxelVolumeSize, getVoxelVolumeSize); COPY_ENTITY_PROPERTY_TO_PROPERTIES(voxelData, getVoxelData); COPY_ENTITY_PROPERTY_TO_PROPERTIES(voxelSurfaceStyle, getVoxelSurfaceStyle); diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 31906a9cc0..e34a99c1e1 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -23,7 +23,7 @@ class PolyVoxEntityItem : public EntityItem { ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/SphereEntityItem.cpp b/libraries/entities/src/SphereEntityItem.cpp index 48eb5e01f7..03d0f223f0 100644 --- a/libraries/entities/src/SphereEntityItem.cpp +++ b/libraries/entities/src/SphereEntityItem.cpp @@ -37,8 +37,8 @@ SphereEntityItem::SphereEntityItem(const EntityItemID& entityItemID, const Entit _volumeMultiplier *= PI / 6.0f; } -EntityItemProperties SphereEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties SphereEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class properties.setColor(getXColor()); return properties; } diff --git a/libraries/entities/src/SphereEntityItem.h b/libraries/entities/src/SphereEntityItem.h index 3b29a3a1f5..af1c87c57e 100644 --- a/libraries/entities/src/SphereEntityItem.h +++ b/libraries/entities/src/SphereEntityItem.h @@ -23,7 +23,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index 61730b208c..f67cc8375b 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -47,8 +47,8 @@ void TextEntityItem::setDimensions(const glm::vec3& value) { EntityItem::setDimensions(glm::vec3(value.x, value.y, TEXT_ENTITY_ITEM_FIXED_DEPTH)); } -EntityItemProperties TextEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties TextEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(text, getText); COPY_ENTITY_PROPERTY_TO_PROPERTIES(lineHeight, getLineHeight); diff --git a/libraries/entities/src/TextEntityItem.h b/libraries/entities/src/TextEntityItem.h index a659f3c39b..1254330dc6 100644 --- a/libraries/entities/src/TextEntityItem.h +++ b/libraries/entities/src/TextEntityItem.h @@ -27,7 +27,7 @@ public: virtual ShapeType getShapeType() const { return SHAPE_TYPE_BOX; } // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 041022a916..10d0101771 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -40,8 +40,8 @@ void WebEntityItem::setDimensions(const glm::vec3& value) { EntityItem::setDimensions(glm::vec3(value.x, value.y, WEB_ENTITY_ITEM_FIXED_DEPTH)); } -EntityItemProperties WebEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties WebEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(sourceUrl, getSourceUrl); return properties; } diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index 24e19e1cb1..58464e8f25 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -26,7 +26,7 @@ public: virtual ShapeType getShapeType() const { return SHAPE_TYPE_BOX; } // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index fed85bf6a5..c7c387d77e 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -73,8 +73,8 @@ EnvironmentData ZoneEntityItem::getEnvironmentData() const { return result; } -EntityItemProperties ZoneEntityItem::getProperties() const { - EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class +EntityItemProperties ZoneEntityItem::getProperties(QScriptValue desiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(keyLightColor, getKeyLightColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(keyLightIntensity, getKeyLightIntensity); diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 41af2f14d8..91a194e2e3 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -27,7 +27,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties() const; + virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time From 455ed27c2bd40d861a2f71183e26398f90fd2382 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 16 Sep 2015 16:26:09 -0700 Subject: [PATCH 049/192] pre-test with eric --- examples/toys/bubblewand/bubble.js | 116 +++-- examples/toys/bubblewand/createWand.js | 43 +- examples/toys/bubblewand/wand.js | 568 ++++++++++++------------- 3 files changed, 388 insertions(+), 339 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 3cc68fecfa..2a0690c921 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -1,54 +1,110 @@ // bubble.js // part of bubblewand // +// Script Type: Entity // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. // // example of a nested entity. it doesn't do much now besides delete itself if it collides with something (bubbles are fragile! it would be cool if it sometimes merged with other bubbbles it hit) -// todo: play bubble sounds from the bubble itself instead of the wand. +// todo: play bubble sounds & particle bursts from the bubble itself instead of the wand. // blocker: needs some sound fixes and a way to find its own position before unload for spatialization // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - (function() { - // Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); - // Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); + Script.include("../../utilities.js"); + Script.include("../../libraries/utils.js"); + + var BUBBLE_PARTICLE_TEXTURE = "https://raw.githubusercontent.com/ericrius1/SantasLair/santa/assets/smokeparticle.png" + + + BUBBLE_PARTICLE_COLOR = { + red: 0, + green: 40, + blue: 255, + }; + + var _this = this; + + var properties; - //var popSound; this.preload = function(entityID) { // print('bubble preload') - this.entityID = entityID; - // popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); - - } - - this.collisionWithEntity = function(myID, otherID, collision) { - //if(Entites.getEntityProperties(otherID).userData.objectType==='') { merge bubbles?} - // Entities.deleteEntity(myID); - // this.burstBubbleSound(collision.contactPoint) + _this.entityID = entityID; + Script.update.connect(_t.internalUpdate); + }; + this.internalUpdate = function() { + // we want the position at unload but for some reason it keeps getting set to 0,0,0 -- so i just exclude that location. sorry origin bubbles. + var tmpProperties = Entities.getEntityProperties(_this.entityID); + if (tmpProperties.position.x !== 0 && tmpProperties.position.y !== 0 && tmpProperties.position.z !== 0) { + properties = tmpProperties; + } }; this.unload = function(entityID) { - // this.properties = Entities.getEntityProperties(entityID); - //var location = this.properties.position; - //this.burstBubbleSound(); + Script.update.disconnect(this.internalUpdate); + var position = properties.position; + _this.endOfBubble(position); + + }; + + this.endOfBubble = function(position) { + this.createBurstParticles(position); + }; + + this.createBurstParticles = function(position) { + //get the current position of the bubble + var position = properties.position; + //var orientation = properties.orientation; + + var animationSettings = JSON.stringify({ + fps: 30, + frameIndex: 0, + running: true, + firstFrame: 0, + lastFrame: 30, + loop: false + }); + + var particleBurst = Entities.addEntity({ + type: "ParticleEffect", + animationSettings: animationSettings, + animationIsPlaying: true, + position: position, + lifetime: 0.2, + dimensions: { + x: 1, + y: 1, + z: 1 + }, + emitVelocity: { + x: 0, + y: 0, + z: 0 + }, + velocitySpread: { + x: 0.45, + y: 0.45, + z: 0.45 + }, + emitAcceleration: { + x: 0, + y: -0.1, + z: 0 + }, + alphaStart: 1.0, + alpha: 1, + alphaFinish: 0.0, + textures: BUBBLE_PARTICLE_TEXTURE, + color: BUBBLE_PARTICLE_COLOR, + lifespan: 0.2, + visible: true, + locked: false + }); + }; - - this.burstBubbleSound = function(location) { - - // var audioOptions = { - // volume: 0.5, - // position: location - // } - - //Audio.playSound(popSound, audioOptions); - - } - - }) \ No newline at end of file diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 15c347d62a..8403ba8516 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -1,41 +1,42 @@ // createWand.js // part of bubblewand // +// Script Type: Entity Spawner // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. // -// Loads a wand model and attaches the bubble wand behavior. +// Loads a wand model and attaches the bubble wand behavior. // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); -Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); - -var wandModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx?" + randInt(0, 10000); -var scriptURL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/wand.js?" + randInt(1, 100500) - +Script.include("../../utilities.js"); +Script.include("../../libraries/utils.js"); +var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; +var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; +var WAND_SCRIPT_URL = Script.resolvePath("wand.js?"+randInt(0,4000)); //create the wand in front of the avatar -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); + var wand = Entities.addEntity({ - type: "Model", - modelURL: wandModel, - position: center, - dimensions: { - x: 0.1, - y: 1, - z: 0.1 - }, - //must be enabled to be grabbable in the physics engine - collisionsWillMove: true, - shapeType: 'box', - script: scriptURL + type: "Model", + modelURL: WAND_MODEL, + position: center, + dimensions: { + x: 0.05, + y: 0.5, + z: 0.05 + }, + //must be enabled to be grabbable in the physics engine + collisionsWillMove: true, + compoundShapeURL: WAND_COLLISION_SHAPE, + script: WAND_SCRIPT_URL }); function cleanup() { - Entities.deleteEntity(wand); + Entities.deleteEntity(wand); } diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index be1042ab79..5edc11d166 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -1,6 +1,7 @@ // wand.js // part of bubblewand // +// Script Type: Entity Script // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. // @@ -11,307 +12,298 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +'use strict'; + function convertRange(value, r1, r2) { - return (value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0]; + return (value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0]; } (function() { - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/utilities.js"); - Script.include("https://raw.githubusercontent.com/highfidelity/hifi/master/examples/libraries/utils.js"); - var bubbleModel = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var bubbleScript = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); - var popSound = SoundCache.getSound("http://hifi-public.s3.amazonaws.com/james/bubblewand/sounds/pop.wav"); - - var TARGET_SIZE = 0.4; - var TARGET_COLOR = { - red: 128, - green: 128, - blue: 128 - }; - var TARGET_COLOR_HIT = { - red: 0, - green: 255, - blue: 0 - }; - - var HAND_SIZE = 0.25; - var leftCubePosition = MyAvatar.getLeftPalmPosition(); - var rightCubePosition = MyAvatar.getRightPalmPosition(); - - var leftHand = Overlays.addOverlay("cube", { - position: leftCubePosition, - size: HAND_SIZE, - color: { - red: 0, - green: 0, - blue: 255 - }, - alpha: 1, - solid: false - }); - - var rightHand = Overlays.addOverlay("cube", { - position: rightCubePosition, - size: HAND_SIZE, - color: { - red: 255, - green: 0, - blue: 0 - }, - alpha: 1, - solid: false - }); - - var gustZoneOverlay = Overlays.addOverlay("cube", { - position: getGustDetectorPosition(), - size: TARGET_SIZE, - color: TARGET_COLOR, - alpha: 1, - solid: false - }); + Script.include("../../utilities.js"); + Script.include("../../libraries/utils.js"); - function getGustDetectorPosition() { - //put the zone in front of your avatar's face - var DISTANCE_IN_FRONT = 0.2; - var DISTANCE_UP = 0.5; - var DISTANCE_TO_SIDE = 0.0; + var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; + var BUBBLE_SCRIPT = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); + Script.resolvePath('bubble.js?' + randInt(0, 5000)); - var up = Quat.getUp(MyAvatar.orientation); - var front = Quat.getFront(MyAvatar.orientation); - var right = Quat.getRight(MyAvatar.orientation); - - var upOffset = Vec3.multiply(up, DISTANCE_UP); - var rightOffset = Vec3.multiply(right, DISTANCE_TO_SIDE); - var frontOffset = Vec3.multiply(front, DISTANCE_IN_FRONT); - - var offset = Vec3.sum(Vec3.sum(rightOffset, frontOffset), upOffset); - var position = Vec3.sum(MyAvatar.position, offset); - return position; - } + var HAND_SIZE = 0.25; + var TARGET_SIZE = 0.04; - var BUBBLE_GRAVITY = { - x: 0, - y: -0.05, - z: 0 - } + + var BUBBLE_GRAVITY = { + x: 0, + y: -0.1, + z: 0 + } + + var MOUTH_MODE_GROWTH_FACTOR = 0.005; + var WAVE_MODE_GROWTH_FACTOR = 0.005; + var SHRINK_LOWER_LIMIT = 0.02; + var SHRINK_FACTOR = 0.001; + var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; + var BUBBLE_DIVISOR = 50; + var BUBBLE_LIFETIME_MIN = 3; + var BUBBLE_LIFETIME_MAX = 8; + + function getGustDetectorPosition() { + //put the zone in front of your avatar's face + var DISTANCE_IN_FRONT = 0.2; + var DISTANCE_UP = 0.5; + var DISTANCE_TO_SIDE = 0.0; + + var up = Quat.getUp(MyAvatar.orientation); + var front = Quat.getFront(MyAvatar.orientation); + var right = Quat.getRight(MyAvatar.orientation); + + var upOffset = Vec3.multiply(up, DISTANCE_UP); + var rightOffset = Vec3.multiply(right, DISTANCE_TO_SIDE); + var frontOffset = Vec3.multiply(front, DISTANCE_IN_FRONT); + + var offset = Vec3.sum(Vec3.sum(rightOffset, frontOffset), upOffset); + var position = Vec3.sum(MyAvatar.position, offset); + return position; + } - var wandEntity = this; - - this.preload = function(entityID) { - // print('PRELOAD') - this.entityID = entityID; - this.properties = Entities.getEntityProperties(this.entityID); - } - - this.unload = function(entityID) { - Overlays.deleteOverlay(leftHand); - Overlays.deleteOverlay(rightHand); - Overlays.deleteOverlay(gustZoneOverlay) - Entities.editEntity(entityID, { - name: "" - }); - Script.update.disconnect(BubbleWand.update); - Entities.deleteEntity(BubbleWand.currentBubble); - while (BubbleWand.bubbles.length > 0) { - Entities.deleteEntity(BubbleWand.bubbles.pop()); - } - - }; - - - var BubbleWand = { - bubbles: [], - currentBubble: null, - update: function() { - BubbleWand.internalUpdate(); - }, - internalUpdate: function() { - var _t = this; - //get the current position of the wand - var properties = Entities.getEntityProperties(wandEntity.entityID); - var wandPosition = properties.position; - - //debug overlays for mouth mode - var leftHandPos = MyAvatar.getLeftPalmPosition(); - var rightHandPos = MyAvatar.getRightPalmPosition(); - - Overlays.editOverlay(leftHand, { - position: leftHandPos - }); - Overlays.editOverlay(rightHand, { - position: rightHandPos - }); - - //if the wand is in the gust detector, activate mouth mode and change the overlay color - var hitTargetWithWand = findSphereSphereHit(wandPosition, HAND_SIZE / 2, getGustDetectorPosition(), TARGET_SIZE / 2) - - var mouthMode; - if (hitTargetWithWand) { - Overlays.editOverlay(gustZoneOverlay, { - position: getGustDetectorPosition(), - color: TARGET_COLOR_HIT - }) - mouthMode = true; - - } else { - Overlays.editOverlay(gustZoneOverlay, { - position: getGustDetectorPosition(), - color: TARGET_COLOR - }) - mouthMode = false; - } - - var volumeLevel = MyAvatar.audioAverageLoudness; - //volume numbers are pretty large, so lets scale them down. - var convertedVolume = convertRange(volumeLevel, [0, 5000], [0, 10]); - - // default is 'wave mode', where waving the object around grows the bubbles - var velocity = Vec3.subtract(wandPosition, BubbleWand.lastPosition) - - //store the last position of the wand for velocity calculations - _t.lastPosition = wandPosition; - - // velocity numbers are pretty small, so lets make them a bit bigger - var velocityStrength = Vec3.length(velocity) * 100; - - if (velocityStrength > 10) { - velocityStrength = 10 - } - - //actually grow the bubble - var dimensions = Entities.getEntityProperties(_t.currentBubble).dimensions; - - if (velocityStrength > 1 || convertedVolume > 1) { - - //add some variation in bubble sizes - var bubbleSize = randInt(1, 5); - bubbleSize = bubbleSize / 10; - - //release the bubble if its dimensions are bigger than the bubble size - if (dimensions.x > bubbleSize) { - //bubbles pop after existing for a bit -- so set a random lifetime - var lifetime = randInt(3, 8); - - //sound is somewhat unstable at the moment so this is commented out. really audio should be played by the bubbles, but there's a blocker. - // Script.setTimeout(function() { - // _t.burstBubbleSound(_t.currentBubble) - // }, lifetime * 1000) - - - //todo: angular velocity without the controller -- forward velocity for mouth mode bubbles - // var angularVelocity = Controller.getSpatialControlRawAngularVelocity(hands.leftHand.tip); - - Entities.editEntity(_t.currentBubble, { - velocity: Vec3.normalize(velocity), - // angularVelocity: Controller.getSpatialControlRawAngularVelocity(hands.leftHand.tip), - lifetime: lifetime - }); - - //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble - BubbleWand.spawnBubble(); - - return - } else { - if (mouthMode) { - dimensions.x += 0.015 * convertedVolume; - dimensions.y += 0.015 * convertedVolume; - dimensions.z += 0.015 * convertedVolume; - - } else { - dimensions.x += 0.015 * velocityStrength; - dimensions.y += 0.015 * velocityStrength; - dimensions.z += 0.015 * velocityStrength; - } - - } - } else { - if (dimensions.x >= 0.02) { - dimensions.x -= 0.001; - dimensions.y -= 0.001; - dimensions.z -= 0.001; - } - - } - - //update the bubble to stay with the wand tip - Entities.editEntity(_t.currentBubble, { - position: _t.wandTipPosition, - dimensions: dimensions - }); - - }, - burstBubbleSound: function(bubble) { - //we want to play the sound at the same location and orientation as the bubble - var position = Entities.getEntityProperties(bubble).position; - var orientation = Entities.getEntityProperties(bubble).orientation; - - //set the options for the audio injector - var audioOptions = { - volume: 0.5, - position: position, - orientation: orientation - } - - - //var audioInjector = Audio.playSound(popSound, audioOptions); - - //remove this bubble from the array to keep things clean - var i = BubbleWand.bubbles.indexOf(bubble); - if (i != -1) { - BubbleWand.bubbles.splice(i, 1); - } - - }, - spawnBubble: function() { - var _t = this; - //create a new bubble at the tip of the wand - //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position - - var properties = Entities.getEntityProperties(wandEntity.entityID); - var wandPosition = properties.position; - var upVector = Quat.getUp(properties.rotation); - var frontVector = Quat.getFront(properties.rotation); - var upOffset = Vec3.multiply(upVector, 0.5); - var forwardOffset = Vec3.multiply(frontVector, 0.1); - var offsetVector = Vec3.sum(upOffset, forwardOffset); - var wandTipPosition = Vec3.sum(wandPosition, offsetVector); - _t.wandTipPosition = wandTipPosition; - - //store the position of the tip on spawn for use in velocity calculations - _t.lastPosition = wandTipPosition; - - //create a bubble at the wand tip - _t.currentBubble = Entities.addEntity({ - type: 'Model', - modelURL: bubbleModel, - position: wandTipPosition, - dimensions: { - x: 0.01, - y: 0.01, - z: 0.01 - }, - collisionsWillMove: false, - ignoreForCollisions: true, - gravity: BUBBLE_GRAVITY, - // collisionSoundURL:popSound, - shapeType: "sphere", - script: bubbleScript, - }); - - //add this bubble to an array of bubbles so we can keep track of them - _t.bubbles.push(_t.currentBubble) - - }, - init: function() { - this.spawnBubble(); - Script.update.connect(BubbleWand.update); - } - } + var wandEntity = this; + this.preload = function(entityID) { + this.entityID = entityID; + this.properties = Entities.getEntityProperties(this.entityID); + BubbleWand.originalProperties = this.properties; BubbleWand.init(); + print('initial position' + JSON.stringify(BubbleWand.originalProperties.position)); + } + + this.unload = function(entityID) { + Entities.editEntity(entityID, { + name: "" + }); + Script.update.disconnect(BubbleWand.update); + Entities.deleteEntity(BubbleWand.currentBubble); + while (BubbleWand.bubbles.length > 0) { + Entities.deleteEntity(BubbleWand.bubbles.pop()); + } + + }; + + + var BubbleWand = { + bubbles: [], + timeSinceMoved: 0, + resetAtTime: 2, + currentBubble: null, + atOriginalLocation: true, + update: function(deltaTime) { + BubbleWand.internalUpdate(deltaTime); + }, + internalUpdate: function(deltaTime) { + var _t = this; + + var GRAB_USER_DATA_KEY = "grabKey"; + var defaultGrabData = { + activated: false, + avatarId: null + }; + + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, wandEntity.entityID, defaultGrabData); + if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { + + // remember we're being grabbed so we can detect being released + _t.beingGrabbed = true; + + // print out that we're being grabbed + // print("I'm being grabbed..."); + _t.handleGrabbedWand(); + + } else if (_t.beingGrabbed) { + + // if we are not being grabbed, and we previously were, then we were just released, remember that + // and print out a message + _t.beingGrabbed = false; + // print("I'm was released..."); + return + } + + }, + handleGrabbedWand: function() { + var _t = this; + + // print('HANDLE GRAB 1') + var properties = Entities.getEntityProperties(wandEntity.entityID); + var wandPosition = properties.position; + //if the wand is in the gust detector, activate mouth mode and change the overlay color + var hitTargetWithWand = findSphereSphereHit(wandPosition, HAND_SIZE / 2, getGustDetectorPosition(), TARGET_SIZE / 2) + + var velocity = Vec3.subtract(wandPosition, _t.lastPosition) + + // print('VELOCITY:' + JSON.stringify(velocity)); + // print('HANDLE GRAB 2') + var velocityStrength = Vec3.length(velocity) * 100; + + var upVector = Quat.getUp(properties.rotation); + var frontVector = Quat.getFront(properties.rotation); + var upOffset = Vec3.multiply(upVector, 0.2); + var wandTipPosition = Vec3.sum(wandPosition, upOffset); + _t.wandTipPosition = wandTipPosition; + + var mouthMode; + + if (hitTargetWithWand) { + mouthMode = true; + } else { + mouthMode = false; + } + + if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { + velocityStrength = 0 + } + + var isMoving; + if (velocityStrength === 0) { + isMoving = false; + } else { + isMoving = true; + } + // print('MOVING?' + isMoving) + if (isMoving === true) { + _t.timeSinceMoved = 0; + _t.atOriginalLocation = false; + } else { + _t.timeSinceMoved = _t.timeSinceMoved + deltaTime; + } + + if (isMoving === false && _t.atOriginalLocation === false) { + if (_t.timeSinceMoved > _t.resetAtTime) { + _t.timeSinceMoved = 0; + _t.returnToOriginalLocation(); + } + } + + var volumeLevel = MyAvatar.audioAverageLoudness; + //volume numbers are pretty large, so lets scale them down. + var convertedVolume = convertRange(volumeLevel, [0, 5000], [0, 10]); + + // default is 'wave mode', where waving the object around grows the bubbles + + //store the last position of the wand for velocity calculations + _t.lastPosition = wandPosition; + + if (velocityStrength > 10) { + velocityStrength = 10 + } + + //actually grow the bubble + var dimensions = Entities.getEntityProperties(_t.currentBubble).dimensions; + var avatarFront = Quat.getFront(MyAvatar.orientation); + var forwardOffset = Vec3.multiply(avatarFront, 0.1); + + if (velocityStrength > 1 || convertedVolume > 1) { + + //add some variation in bubble sizes + var bubbleSize = randInt(1, 5); + bubbleSize = bubbleSize / BUBBLE_DIVISOR; + + //release the bubble if its dimensions are bigger than the bubble size + if (dimensions.x > bubbleSize) { + //bubbles pop after existing for a bit -- so set a random lifetime + var lifetime = randInt(BUBBLE_LIFETIME_MIN, BUBBLE_LIFETIME_MAX); + + Entities.editEntity(_t.currentBubble, { + velocity: mouthMode ? avatarFront : velocity, + lifetime: lifetime + }); + + //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble + BubbleWand.spawnBubble(); + + return + } else { + + if (mouthMode) { + dimensions.x += WAVE_MODE_GROWTH_FACTOR * convertedVolume; + dimensions.y += WAVE_MODE_GROWTH_FACTOR * convertedVolume; + dimensions.z += WAVE_MODE_GROWTH_FACTOR * convertedVolume; + + } else { + dimensions.x += WAVE_MODE_GROWTH_FACTOR * velocityStrength; + dimensions.y += WAVE_MODE_GROWTH_FACTOR * velocityStrength; + dimensions.z += WAVE_MODE_GROWTH_FACTOR * velocityStrength; + } + + } + } else { + if (dimensions.x >= SHRINK_LOWER_LIMIT) { + dimensions.x -= SHRINK_FACTOR; + dimensions.y -= SHRINK_FACTOR; + dimensions.z -= SHRINK_FACTOR; + } + + } + + //update the bubble to stay with the wand tip + Entities.editEntity(_t.currentBubble, { + position: _t.wandTipPosition, + dimensions: dimensions + }); + }, + spawnBubble: function() { + var _t = this; + //create a new bubble at the tip of the wand + //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position + + var properties = Entities.getEntityProperties(wandEntity.entityID); + var wandPosition = properties.position; + var upVector = Quat.getUp(properties.rotation); + var frontVector = Quat.getFront(properties.rotation); + var upOffset = Vec3.multiply(upVector, 0.2); + var wandTipPosition = Vec3.sum(wandPosition, upOffset); + _t.wandTipPosition = wandTipPosition; + + //store the position of the tip on spawn for use in velocity calculations + _t.lastPosition = wandPosition; + + //create a bubble at the wand tip + _t.currentBubble = Entities.addEntity({ + type: 'Model', + modelURL: BUBBLE_MODEL, + position: wandTipPosition, + dimensions: { + x: 0.01, + y: 0.01, + z: 0.01 + }, + collisionsWillMove: true, //true + ignoreForCollisions: false, //false + gravity: BUBBLE_GRAVITY, + shapeType: "sphere", + script: BUBBLE_SCRIPT, + }); + //add this bubble to an array of bubbles so we can keep track of them + _t.bubbles.push(_t.currentBubble) + + }, + returnToOriginalLocation: function() { + var _t = this; + _t.atOriginalLocation = true; + Script.update.disconnect(BubbleWand.update) + Entities.deleteEntity(_t.currentBubble); + Entities.editEntity(wandEntity.entityID, _t.originalProperties) + _t.spawnBubble(); + Script.update.connect(BubbleWand.update); + + }, + init: function() { + var _t = this; + this.spawnBubble(); + Script.update.connect(BubbleWand.update); + + } + } + + }) \ No newline at end of file From 5ab8f192578ce72147cca578bed628cbebc22e69 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 16 Sep 2015 16:42:23 -0700 Subject: [PATCH 050/192] fix merge leftovers --- examples/toys/bubblewand/createWand.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 7386c8b26f..f5cb9625a0 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -22,13 +22,10 @@ var WAND_SCRIPT_URL = Script.resolvePath("wand.js?"+randInt(0,4000)); var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); -var wand = Entities.addEntity({ - type: "Model", - modelURL: WAND_MODEL, var wand = Entities.addEntity({ type: "Model", - modelURL: wandModel, + modelURL: WAND_MODEL, position: center, dimensions: { x: 0.05, From 248107c468521497a0062c7a1dabf1e58d264b77 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 16 Sep 2015 16:45:26 -0700 Subject: [PATCH 051/192] add reload all support for entity scripts --- interface/src/Application.cpp | 2 ++ libraries/entities-renderer/src/EntityTreeRenderer.cpp | 10 ++++++++++ libraries/entities-renderer/src/EntityTreeRenderer.h | 4 ++++ libraries/script-engine/src/ScriptCache.cpp | 7 +++++++ libraries/script-engine/src/ScriptCache.h | 1 + libraries/script-engine/src/ScriptEngine.cpp | 5 ++++- libraries/script-engine/src/ScriptEngine.h | 4 ++-- 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3cb75b55ec..a313308023 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4347,6 +4347,8 @@ void Application::stopScript(const QString &scriptName, bool restart) { } void Application::reloadAllScripts() { + DependencyManager::get()->clearCache(); + getEntities()->reloadEntityScripts(); stopAllScripts(true); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 2fd760bbd3..fa186b15f8 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -93,6 +93,15 @@ void EntityTreeRenderer::clear() { OctreeRenderer::clear(); } +void EntityTreeRenderer::reloadEntityScripts() { + _entitiesScriptEngine->unloadAllEntityScripts(); + foreach(auto entity, _entitiesInScene) { + if (!entity->getScript().isEmpty()) { + _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); + } + } +} + void EntityTreeRenderer::init() { OctreeRenderer::init(); EntityTreePointer entityTree = std::static_pointer_cast(_tree); @@ -769,6 +778,7 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const bool reload) { + qDebug() << "entitySciptChanging() entityID:" << entityID << "reload:" << reload; if (_tree && !_shuttingDown) { _entitiesScriptEngine->unloadEntityScript(entityID); checkAndCallPreload(entityID, reload); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a2f343efd2..6bacb23a3f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -62,6 +62,10 @@ public: /// clears the tree virtual void clear(); + /// reloads the entity scripts, calling unload and preload + void reloadEntityScripts(); + + /// if a renderable entity item needs a model, we will allocate it for them Q_INVOKABLE Model* allocateModel(const QString& url, const QString& collisionUrl); diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index e2c07c05d0..fb0edb1e49 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -27,6 +27,11 @@ ScriptCache::ScriptCache(QObject* parent) { // nothing to do here... } +void ScriptCache::clearCache() { + _scriptCache.clear(); +} + + QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool reload) { QUrl url = ResourceManager::normalizeURL(unnormalizedURL); QString scriptContents; @@ -95,6 +100,8 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable return; } + qCDebug(scriptengine) << "ScriptCache::getScriptContents() scriptOrURL:" << scriptOrURL << " forceDownload:" << forceDownload << " on thread[" << QThread::currentThread() << "] expected thread[" << thread() << "]"; + if (_scriptCache.contains(url) && !forceDownload) { qCDebug(scriptengine) << "Found script in cache:" << url.toString(); #if 1 // def THREAD_DEBUGGING diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 7de14a09f7..b786422b3f 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -28,6 +28,7 @@ class ScriptCache : public QObject, public Dependency { SINGLETON_DEPENDENCY public: + void clearCache(); void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index cd9e9d2e6c..1b341edbb5 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -909,7 +909,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif - DependencyManager::get()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { + + qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() scriptOrURL:" << entityScript << "forceDownload:" << forceRedownload << "on thread[" << QThread::currentThread() << "] expected thread[" << thread() << "]"; + + DependencyManager::get()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 83e65823a5..001f54221b 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -109,8 +109,8 @@ public: // Entity Script Related methods Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload = false); // will call the preload method once loaded Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method - Q_INVOKABLE void unloadAllEntityScripts(); - Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName); + Q_INVOKABLE void unloadAllEntityScripts(); + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision); From 3e7cc7d6eb86e8bd56c97fbe655c59f5d8acf99b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 16 Sep 2015 17:02:52 -0700 Subject: [PATCH 052/192] remove sounds from bubble --- examples/toys/bubblewand/bubble.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 91a72642d3..29d087176b 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -12,7 +12,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -(function() { << << << < HEAD +(function() { Script.include("../../utilities.js"); Script.include("../../libraries/utils.js"); @@ -32,7 +32,7 @@ this.preload = function(entityID) { // print('bubble preload') _this.entityID = entityID; - Script.update.connect(_t.internalUpdate); + Script.update.connect(_this.internalUpdate); }; this.internalUpdate = function() { @@ -47,24 +47,11 @@ this.unload = function(entityID) { Script.update.disconnect(this.internalUpdate); var position = properties.position; - _t.endOfBubble(position); - // print('UNLOAD PROPS' + JSON.stringify(position)); - + _this.endOfBubble(position); }; this.endOfBubble = function(position) { - this.createBurstParticles(position); - this.burstBubbleSound(position); - } - - this.burstBubbleSound = function(position) { - var audioOptions = { - volume: 0.5, - position: position - } - Audio.playSound(POP_SOUNDS[randInt(0, 4)], audioOptions); - } this.createBurstParticles = function(position) { From e1e567b1b5ecadaf80ce689d1a45e062e1d0c5bf Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 16 Sep 2015 17:09:54 -0700 Subject: [PATCH 053/192] remove mouth mode --- examples/toys/bubblewand/bubble.js | 4 +--- examples/toys/bubblewand/wand.js | 38 +++++------------------------- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 29d087176b..e2bbe2b220 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -5,9 +5,7 @@ // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. // -// example of a nested entity. it doesn't do much now besides delete itself if it collides with something (bubbles are fragile! it would be cool if it sometimes merged with other bubbbles it hit) -// todo: play bubble sounds & particle bursts from the bubble itself instead of the wand. -// blocker: needs some sound fixes and a way to find its own position before unload for spatialization +// example of a nested entity. plays a particle burst at the location where its deleted. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 5edc11d166..06f0381277 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -5,7 +5,7 @@ // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. // -// Makes bubbles when you wave the object around, or hold it near your mouth and make noise into the microphone. +// Makes bubbles when you wave the object around. // // For the example, it's attached to a wand -- but you can attach it to whatever entity you want. I dream of BubbleBees :) bzzzz...pop! // @@ -14,16 +14,11 @@ 'use strict'; -function convertRange(value, r1, r2) { - return (value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0]; -} - (function() { Script.include("../../utilities.js"); Script.include("../../libraries/utils.js"); - var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var BUBBLE_SCRIPT = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); Script.resolvePath('bubble.js?' + randInt(0, 5000)); @@ -39,7 +34,6 @@ function convertRange(value, r1, r2) { z: 0 } - var MOUTH_MODE_GROWTH_FACTOR = 0.005; var WAVE_MODE_GROWTH_FACTOR = 0.005; var SHRINK_LOWER_LIMIT = 0.02; var SHRINK_FACTOR = 0.001; @@ -135,9 +129,7 @@ function convertRange(value, r1, r2) { // print('HANDLE GRAB 1') var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; - //if the wand is in the gust detector, activate mouth mode and change the overlay color - var hitTargetWithWand = findSphereSphereHit(wandPosition, HAND_SIZE / 2, getGustDetectorPosition(), TARGET_SIZE / 2) - + var velocity = Vec3.subtract(wandPosition, _t.lastPosition) // print('VELOCITY:' + JSON.stringify(velocity)); @@ -150,14 +142,6 @@ function convertRange(value, r1, r2) { var wandTipPosition = Vec3.sum(wandPosition, upOffset); _t.wandTipPosition = wandTipPosition; - var mouthMode; - - if (hitTargetWithWand) { - mouthMode = true; - } else { - mouthMode = false; - } - if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { velocityStrength = 0 } @@ -183,9 +167,6 @@ function convertRange(value, r1, r2) { } } - var volumeLevel = MyAvatar.audioAverageLoudness; - //volume numbers are pretty large, so lets scale them down. - var convertedVolume = convertRange(volumeLevel, [0, 5000], [0, 10]); // default is 'wave mode', where waving the object around grows the bubbles @@ -198,10 +179,8 @@ function convertRange(value, r1, r2) { //actually grow the bubble var dimensions = Entities.getEntityProperties(_t.currentBubble).dimensions; - var avatarFront = Quat.getFront(MyAvatar.orientation); - var forwardOffset = Vec3.multiply(avatarFront, 0.1); - if (velocityStrength > 1 || convertedVolume > 1) { + if (velocityStrength > 1) { //add some variation in bubble sizes var bubbleSize = randInt(1, 5); @@ -213,7 +192,7 @@ function convertRange(value, r1, r2) { var lifetime = randInt(BUBBLE_LIFETIME_MIN, BUBBLE_LIFETIME_MAX); Entities.editEntity(_t.currentBubble, { - velocity: mouthMode ? avatarFront : velocity, + velocity: velocity, lifetime: lifetime }); @@ -223,16 +202,11 @@ function convertRange(value, r1, r2) { return } else { - if (mouthMode) { - dimensions.x += WAVE_MODE_GROWTH_FACTOR * convertedVolume; - dimensions.y += WAVE_MODE_GROWTH_FACTOR * convertedVolume; - dimensions.z += WAVE_MODE_GROWTH_FACTOR * convertedVolume; - - } else { + dimensions.x += WAVE_MODE_GROWTH_FACTOR * velocityStrength; dimensions.y += WAVE_MODE_GROWTH_FACTOR * velocityStrength; dimensions.z += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - } + } } else { From 9aa2067e3e8149ee541e6d2fc5877a485aa78594 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 16 Sep 2015 17:26:18 -0700 Subject: [PATCH 054/192] cleanup unused reset stuff --- examples/toys/bubblewand/wand.js | 74 ++++++-------------------------- 1 file changed, 12 insertions(+), 62 deletions(-) diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 06f0381277..60e4752a15 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -20,14 +20,11 @@ Script.include("../../libraries/utils.js"); var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var BUBBLE_SCRIPT = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/scripts/bubble.js?' + randInt(1, 10000); - Script.resolvePath('bubble.js?' + randInt(0, 5000)); + var BUBBLE_SCRIPT = Script.resolvePath('bubble.js?' + randInt(0, 5000)); var HAND_SIZE = 0.25; var TARGET_SIZE = 0.04; - - var BUBBLE_GRAVITY = { x: 0, y: -0.1, @@ -38,6 +35,7 @@ var SHRINK_LOWER_LIMIT = 0.02; var SHRINK_FACTOR = 0.001; var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; + var MAX_VELOCITY_STRENGTH = 10; var BUBBLE_DIVISOR = 50; var BUBBLE_LIFETIME_MIN = 3; var BUBBLE_LIFETIME_MAX = 8; @@ -67,9 +65,7 @@ this.preload = function(entityID) { this.entityID = entityID; this.properties = Entities.getEntityProperties(this.entityID); - BubbleWand.originalProperties = this.properties; BubbleWand.init(); - print('initial position' + JSON.stringify(BubbleWand.originalProperties.position)); } this.unload = function(entityID) { @@ -88,29 +84,23 @@ var BubbleWand = { bubbles: [], timeSinceMoved: 0, - resetAtTime: 2, currentBubble: null, - atOriginalLocation: true, update: function(deltaTime) { BubbleWand.internalUpdate(deltaTime); }, internalUpdate: function(deltaTime) { var _t = this; - var GRAB_USER_DATA_KEY = "grabKey"; var defaultGrabData = { activated: false, avatarId: null }; - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, wandEntity.entityID, defaultGrabData); if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { - // remember we're being grabbed so we can detect being released _t.beingGrabbed = true; // print out that we're being grabbed - // print("I'm being grabbed..."); _t.handleGrabbedWand(); } else if (_t.beingGrabbed) { @@ -118,7 +108,7 @@ // if we are not being grabbed, and we previously were, then we were just released, remember that // and print out a message _t.beingGrabbed = false; - // print("I'm was released..."); + return } @@ -126,14 +116,10 @@ handleGrabbedWand: function() { var _t = this; - // print('HANDLE GRAB 1') var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; - - var velocity = Vec3.subtract(wandPosition, _t.lastPosition) - // print('VELOCITY:' + JSON.stringify(velocity)); - // print('HANDLE GRAB 2') + var velocity = Vec3.subtract(wandPosition, _t.lastPosition) var velocityStrength = Vec3.length(velocity) * 100; var upVector = Quat.getUp(properties.rotation); @@ -146,35 +132,11 @@ velocityStrength = 0 } - var isMoving; - if (velocityStrength === 0) { - isMoving = false; - } else { - isMoving = true; - } - // print('MOVING?' + isMoving) - if (isMoving === true) { - _t.timeSinceMoved = 0; - _t.atOriginalLocation = false; - } else { - _t.timeSinceMoved = _t.timeSinceMoved + deltaTime; - } - - if (isMoving === false && _t.atOriginalLocation === false) { - if (_t.timeSinceMoved > _t.resetAtTime) { - _t.timeSinceMoved = 0; - _t.returnToOriginalLocation(); - } - } - - - // default is 'wave mode', where waving the object around grows the bubbles - //store the last position of the wand for velocity calculations _t.lastPosition = wandPosition; - if (velocityStrength > 10) { - velocityStrength = 10 + if (velocityStrength > MAX_VELOCITY_STRENGTH) { + velocityStrength = MAX_VELOCITY_STRENGTH } //actually grow the bubble @@ -202,11 +164,10 @@ return } else { - - dimensions.x += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - dimensions.y += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - dimensions.z += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - + dimensions.x += WAVE_MODE_GROWTH_FACTOR * velocityStrength; + dimensions.y += WAVE_MODE_GROWTH_FACTOR * velocityStrength; + dimensions.z += WAVE_MODE_GROWTH_FACTOR * velocityStrength; + } } else { @@ -226,9 +187,9 @@ }, spawnBubble: function() { var _t = this; + //create a new bubble at the tip of the wand //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position - var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; var upVector = Quat.getUp(properties.rotation); @@ -237,7 +198,7 @@ var wandTipPosition = Vec3.sum(wandPosition, upOffset); _t.wandTipPosition = wandTipPosition; - //store the position of the tip on spawn for use in velocity calculations + //store the position of the tip for use in velocity calculations _t.lastPosition = wandPosition; //create a bubble at the wand tip @@ -259,22 +220,11 @@ //add this bubble to an array of bubbles so we can keep track of them _t.bubbles.push(_t.currentBubble) - }, - returnToOriginalLocation: function() { - var _t = this; - _t.atOriginalLocation = true; - Script.update.disconnect(BubbleWand.update) - Entities.deleteEntity(_t.currentBubble); - Entities.editEntity(wandEntity.entityID, _t.originalProperties) - _t.spawnBubble(); - Script.update.connect(BubbleWand.update); - }, init: function() { var _t = this; this.spawnBubble(); Script.update.connect(BubbleWand.update); - } } From a8fe3b3edd1be1330ba53f570866ccc0536aec63 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 16 Sep 2015 18:08:56 -0700 Subject: [PATCH 055/192] keep the bubble with the wand, make the wand smaller --- examples/toys/bubblewand/createWand.js | 6 ++-- examples/toys/bubblewand/wand.js | 48 ++++++++++++++++---------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index f5cb9625a0..77ac7686c4 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -28,9 +28,9 @@ var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.g modelURL: WAND_MODEL, position: center, dimensions: { - x: 0.05, - y: 0.5, - z: 0.05 + x: 0.025, + y: 0.125, + z: 0.025 }, //must be enabled to be grabbable in the physics engine collisionsWillMove: true, diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 60e4752a15..62970045fb 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -7,12 +7,11 @@ // // Makes bubbles when you wave the object around. // -// For the example, it's attached to a wand -- but you can attach it to whatever entity you want. I dream of BubbleBees :) bzzzz...pop! // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -'use strict'; +// 'use strict'; (function() { @@ -39,6 +38,10 @@ var BUBBLE_DIVISOR = 50; var BUBBLE_LIFETIME_MIN = 3; var BUBBLE_LIFETIME_MAX = 8; + var WAND_TIP_OFFSET = 0.05; + var BUBBLE_SIZE_MIN = 1; + var BUBBLE_SIZE_MAX = 5; + var VELOCITY_THRESHOLD = 1; function getGustDetectorPosition() { //put the zone in front of your avatar's face @@ -89,6 +92,8 @@ BubbleWand.internalUpdate(deltaTime); }, internalUpdate: function(deltaTime) { + var properties = Entities.getEntityProperties(wandEntity.entityID); + var _t = this; var GRAB_USER_DATA_KEY = "grabKey"; var defaultGrabData = { @@ -101,7 +106,7 @@ _t.beingGrabbed = true; // print out that we're being grabbed - _t.handleGrabbedWand(); + _t.handleGrabbedWand(properties); } else if (_t.beingGrabbed) { @@ -111,23 +116,32 @@ return } + var wandTipPosition = _t.getWandTipPosition(properties); + //update the bubble to stay with the wand tip + Entities.editEntity(_t.currentBubble, { + position: wandTipPosition, + }); }, - handleGrabbedWand: function() { + getWandTipPosition: function(properties) { + print('get wand position') + var upVector = Quat.getUp(properties.rotation); + var frontVector = Quat.getFront(properties.rotation); + var upOffset = Vec3.multiply(upVector, WAND_TIP_OFFSET); + var wandTipPosition = Vec3.sum(properties.position, upOffset); + return wandTipPosition + }, + handleGrabbedWand: function(properties) { var _t = this; - var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; + var wandTipPosition = _t.getWandTipPosition(properties) + _t.wandTipPosition = wandTipPosition; + var velocity = Vec3.subtract(wandPosition, _t.lastPosition) var velocityStrength = Vec3.length(velocity) * 100; - var upVector = Quat.getUp(properties.rotation); - var frontVector = Quat.getFront(properties.rotation); - var upOffset = Vec3.multiply(upVector, 0.2); - var wandTipPosition = Vec3.sum(wandPosition, upOffset); - _t.wandTipPosition = wandTipPosition; - if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { velocityStrength = 0 } @@ -142,10 +156,10 @@ //actually grow the bubble var dimensions = Entities.getEntityProperties(_t.currentBubble).dimensions; - if (velocityStrength > 1) { + if (velocityStrength > VELOCITY_THRESHOLD) { //add some variation in bubble sizes - var bubbleSize = randInt(1, 5); + var bubbleSize = randInt(BUBBLE_SIZE_MIN, BUBBLE_SIZE_MAX); bubbleSize = bubbleSize / BUBBLE_DIVISOR; //release the bubble if its dimensions are bigger than the bubble size @@ -163,12 +177,10 @@ return } else { - dimensions.x += WAVE_MODE_GROWTH_FACTOR * velocityStrength; dimensions.y += WAVE_MODE_GROWTH_FACTOR * velocityStrength; dimensions.z += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - } } else { if (dimensions.x >= SHRINK_LOWER_LIMIT) { @@ -192,10 +204,8 @@ //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position var properties = Entities.getEntityProperties(wandEntity.entityID); var wandPosition = properties.position; - var upVector = Quat.getUp(properties.rotation); - var frontVector = Quat.getFront(properties.rotation); - var upOffset = Vec3.multiply(upVector, 0.2); - var wandTipPosition = Vec3.sum(wandPosition, upOffset); + + wandTipPosition = _t.getWandTipPosition(properties); _t.wandTipPosition = wandTipPosition; //store the position of the tip for use in velocity calculations From 75df4e96e8b16bb13523e71d2e6c80b0b59328ce Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 16 Sep 2015 18:48:06 -0700 Subject: [PATCH 056/192] cleanup --- examples/toys/bubblewand/bubble.js | 2 +- examples/toys/bubblewand/createWand.js | 31 ++++++++++++------------- examples/toys/bubblewand/wand.js | 32 ++++++++++++-------------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index e2bbe2b220..2629210c96 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -106,4 +106,4 @@ }; -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 77ac7686c4..7b902e2874 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -17,26 +17,25 @@ Script.include("../../libraries/utils.js"); var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; -var WAND_SCRIPT_URL = Script.resolvePath("wand.js?"+randInt(0,4000)); +var WAND_SCRIPT_URL = Script.resolvePath("wand.js?" + randInt(0, 4000)); //create the wand in front of the avatar var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +var wand = Entities.addEntity({ + type: "Model", + modelURL: WAND_MODEL, + position: center, + dimensions: { + x: 0.025, + y: 0.125, + z: 0.025 + }, + //must be enabled to be grabbable in the physics engine + collisionsWillMove: true, + compoundShapeURL: WAND_COLLISION_SHAPE, + script: WAND_SCRIPT_URL +}); - - var wand = Entities.addEntity({ - type: "Model", - modelURL: WAND_MODEL, - position: center, - dimensions: { - x: 0.025, - y: 0.125, - z: 0.025 - }, - //must be enabled to be grabbable in the physics engine - collisionsWillMove: true, - compoundShapeURL: WAND_COLLISION_SHAPE, - script: WAND_SCRIPT_URL - }); function cleanup() { Entities.deleteEntity(wand); } diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 62970045fb..f43fc0df03 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -11,8 +11,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// 'use strict'; - (function() { Script.include("../../utilities.js"); @@ -28,20 +26,21 @@ x: 0, y: -0.1, z: 0 - } + }; var WAVE_MODE_GROWTH_FACTOR = 0.005; var SHRINK_LOWER_LIMIT = 0.02; var SHRINK_FACTOR = 0.001; - var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; - var MAX_VELOCITY_STRENGTH = 10; var BUBBLE_DIVISOR = 50; var BUBBLE_LIFETIME_MIN = 3; var BUBBLE_LIFETIME_MAX = 8; - var WAND_TIP_OFFSET = 0.05; var BUBBLE_SIZE_MIN = 1; var BUBBLE_SIZE_MAX = 5; - var VELOCITY_THRESHOLD = 1; + var WAND_TIP_OFFSET = 0.05; + var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; + var MAX_VELOCITY_STRENGTH = 10; + var VELOCITY_THRESHOLD = 1; + var VELOCITY_STRENGTH_MULTIPLIER = 100; function getGustDetectorPosition() { //put the zone in front of your avatar's face @@ -60,7 +59,7 @@ var offset = Vec3.sum(Vec3.sum(rightOffset, frontOffset), upOffset); var position = Vec3.sum(MyAvatar.position, offset); return position; - } + }; var wandEntity = this; @@ -93,7 +92,7 @@ }, internalUpdate: function(deltaTime) { var properties = Entities.getEntityProperties(wandEntity.entityID); - + var _t = this; var GRAB_USER_DATA_KEY = "grabKey"; var defaultGrabData = { @@ -116,15 +115,14 @@ return } - var wandTipPosition = _t.getWandTipPosition(properties); - //update the bubble to stay with the wand tip - Entities.editEntity(_t.currentBubble, { - position: wandTipPosition, + var wandTipPosition = _t.getWandTipPosition(properties); + //update the bubble to stay with the wand tip + Entities.editEntity(_t.currentBubble, { + position: wandTipPosition, - }); + }); }, getWandTipPosition: function(properties) { - print('get wand position') var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); var upOffset = Vec3.multiply(upVector, WAND_TIP_OFFSET); @@ -140,7 +138,7 @@ _t.wandTipPosition = wandTipPosition; var velocity = Vec3.subtract(wandPosition, _t.lastPosition) - var velocityStrength = Vec3.length(velocity) * 100; + var velocityStrength = Vec3.length(velocity) * VELOCITY_STRENGTH_MULTIPLIER; if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { velocityStrength = 0 @@ -193,7 +191,7 @@ //update the bubble to stay with the wand tip Entities.editEntity(_t.currentBubble, { - position: _t.wandTipPosition, + dimensions: dimensions }); }, From 5ba6242bdab337c075bba3e6f20226302d834dc9 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 16 Sep 2015 13:22:37 -0700 Subject: [PATCH 057/192] Support a debug oculus plugin for performance testing --- .../src/display-plugins/DisplayPlugin.cpp | 5 + .../oculus/OculusBaseDisplayPlugin.cpp | 151 +++++++++++ .../oculus/OculusBaseDisplayPlugin.h | 79 ++++++ .../oculus/OculusDebugDisplayPlugin.cpp | 40 +++ .../oculus/OculusDebugDisplayPlugin.h | 26 ++ .../oculus/OculusDisplayPlugin.cpp | 251 +----------------- .../oculus/OculusDisplayPlugin.h | 52 +--- 7 files changed, 314 insertions(+), 290 deletions(-) create mode 100644 libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.cpp create mode 100644 libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.h create mode 100644 libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.cpp create mode 100644 libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.h diff --git a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp index 598e78e500..4af45d299b 100644 --- a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp @@ -16,6 +16,7 @@ #include "openvr/OpenVrDisplayPlugin.h" #include "oculus/OculusDisplayPlugin.h" +#include "oculus/OculusDebugDisplayPlugin.h" #include "oculus/OculusLegacyDisplayPlugin.h" const QString& DisplayPlugin::MENU_PATH() { @@ -42,6 +43,10 @@ DisplayPluginList getDisplayPlugins() { // Windows Oculus SDK new OculusDisplayPlugin(), + // Windows Oculus Simulator... uses head tracking and the same rendering + // as the connected hardware, but without using the SDK to display to the + // Rift. Useful for debugging Rift performance with nSight. + new OculusDebugDisplayPlugin(), // Mac/Linux Oculus SDK (0.5) new OculusLegacyDisplayPlugin(), #ifdef Q_OS_WIN diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.cpp new file mode 100644 index 0000000000..fa9d09e392 --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.cpp @@ -0,0 +1,151 @@ +// +// Created by Bradley Austin Davis on 2014/04/13. +// Copyright 2015 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 "OculusBaseDisplayPlugin.h" + +#include + +#include "OculusHelpers.h" + +uvec2 OculusBaseDisplayPlugin::getRecommendedRenderSize() const { + return _desiredFramebufferSize; +} + +void OculusBaseDisplayPlugin::preRender() { +#if (OVR_MAJOR_VERSION >= 6) + ovrFrameTiming ftiming = ovr_GetFrameTiming(_hmd, _frameIndex); + _trackingState = ovr_GetTrackingState(_hmd, ftiming.DisplayMidpointSeconds); + ovr_CalcEyePoses(_trackingState.HeadPose.ThePose, _eyeOffsets, _eyePoses); +#endif +} + +glm::mat4 OculusBaseDisplayPlugin::getProjection(Eye eye, const glm::mat4& baseProjection) const { + return _eyeProjections[eye]; +} + +void OculusBaseDisplayPlugin::resetSensors() { +#if (OVR_MAJOR_VERSION >= 6) + ovr_RecenterPose(_hmd); +#endif +} + +glm::mat4 OculusBaseDisplayPlugin::getEyePose(Eye eye) const { + return toGlm(_eyePoses[eye]); +} + +glm::mat4 OculusBaseDisplayPlugin::getHeadPose() const { + return toGlm(_trackingState.HeadPose.ThePose); +} + +bool OculusBaseDisplayPlugin::isSupported() const { +#if (OVR_MAJOR_VERSION >= 6) + if (!OVR_SUCCESS(ovr_Initialize(nullptr))) { + return false; + } + bool result = false; + if (ovrHmd_Detect() > 0) { + result = true; + } + ovr_Shutdown(); + return result; +#else + return false; +#endif +} + +void OculusBaseDisplayPlugin::init() { +} + +void OculusBaseDisplayPlugin::deinit() { +} + +void OculusBaseDisplayPlugin::activate() { +#if (OVR_MAJOR_VERSION >= 6) + if (!OVR_SUCCESS(ovr_Initialize(nullptr))) { + qFatal("Could not init OVR"); + } + +#if (OVR_MAJOR_VERSION == 6) + if (!OVR_SUCCESS(ovr_Create(0, &_hmd))) { +#elif (OVR_MAJOR_VERSION == 7) + if (!OVR_SUCCESS(ovr_Create(&_hmd, &_luid))) { +#endif + Q_ASSERT(false); + qFatal("Failed to acquire HMD"); + } + + _hmdDesc = ovr_GetHmdDesc(_hmd); + + _ipd = ovr_GetFloat(_hmd, OVR_KEY_IPD, _ipd); + + glm::uvec2 eyeSizes[2]; + ovr_for_each_eye([&](ovrEyeType eye) { + _eyeFovs[eye] = _hmdDesc.DefaultEyeFov[eye]; + ovrEyeRenderDesc& erd = _eyeRenderDescs[eye] = ovr_GetRenderDesc(_hmd, eye, _eyeFovs[eye]); + ovrMatrix4f ovrPerspectiveProjection = + ovrMatrix4f_Projection(erd.Fov, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, ovrProjection_RightHanded); + _eyeProjections[eye] = toGlm(ovrPerspectiveProjection); + + ovrPerspectiveProjection = + ovrMatrix4f_Projection(erd.Fov, 0.001f, 10.0f, ovrProjection_RightHanded); + _compositeEyeProjections[eye] = toGlm(ovrPerspectiveProjection); + + _eyeOffsets[eye] = erd.HmdToEyeViewOffset; + eyeSizes[eye] = toGlm(ovr_GetFovTextureSize(_hmd, eye, erd.Fov, 1.0f)); + }); + ovrFovPort combined = _eyeFovs[Left]; + combined.LeftTan = std::max(_eyeFovs[Left].LeftTan, _eyeFovs[Right].LeftTan); + combined.RightTan = std::max(_eyeFovs[Left].RightTan, _eyeFovs[Right].RightTan); + ovrMatrix4f ovrPerspectiveProjection = + ovrMatrix4f_Projection(combined, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, ovrProjection_RightHanded); + _eyeProjections[Mono] = toGlm(ovrPerspectiveProjection); + + + + _desiredFramebufferSize = uvec2( + eyeSizes[0].x + eyeSizes[1].x, + std::max(eyeSizes[0].y, eyeSizes[1].y)); + + _frameIndex = 0; + + if (!OVR_SUCCESS(ovr_ConfigureTracking(_hmd, + ovrTrackingCap_Orientation | ovrTrackingCap_Position | ovrTrackingCap_MagYawCorrection, 0))) { + qFatal("Could not attach to sensor device"); + } + + // Parent class relies on our _hmd intialization, so it must come after that. + memset(&_sceneLayer, 0, sizeof(ovrLayerEyeFov)); + _sceneLayer.Header.Type = ovrLayerType_EyeFov; + _sceneLayer.Header.Flags = ovrLayerFlag_TextureOriginAtBottomLeft; + ovr_for_each_eye([&](ovrEyeType eye) { + ovrFovPort & fov = _sceneLayer.Fov[eye] = _eyeRenderDescs[eye].Fov; + ovrSizei & size = _sceneLayer.Viewport[eye].Size = ovr_GetFovTextureSize(_hmd, eye, fov, 1.0f); + _sceneLayer.Viewport[eye].Pos = { eye == ovrEye_Left ? 0 : size.w, 0 }; + }); + + if (!OVR_SUCCESS(ovr_ConfigureTracking(_hmd, + ovrTrackingCap_Orientation | ovrTrackingCap_Position | ovrTrackingCap_MagYawCorrection, 0))) { + qFatal("Could not attach to sensor device"); + } +#endif + + WindowOpenGLDisplayPlugin::activate(); +} + +void OculusBaseDisplayPlugin::deactivate() { + WindowOpenGLDisplayPlugin::deactivate(); + +#if (OVR_MAJOR_VERSION >= 6) + ovr_Destroy(_hmd); + _hmd = nullptr; + ovr_Shutdown(); +#endif +} + +void OculusBaseDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSize) { + ++_frameIndex; +} diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.h new file mode 100644 index 0000000000..12023db1ae --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusBaseDisplayPlugin.h @@ -0,0 +1,79 @@ +// +// Created by Bradley Austin Davis on 2015/05/29 +// Copyright 2015 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 +// +#pragma once + +#include "../WindowOpenGLDisplayPlugin.h" + +#include + +#include + +class OculusBaseDisplayPlugin : public WindowOpenGLDisplayPlugin { +public: + virtual bool isSupported() const override; + + virtual void init() override final; + virtual void deinit() override final; + + virtual void activate() override; + virtual void deactivate() override; + + // Stereo specific methods + virtual bool isHmd() const override final { return true; } + virtual glm::mat4 getProjection(Eye eye, const glm::mat4& baseProjection) const override; + virtual glm::uvec2 getRecommendedRenderSize() const override final; + virtual glm::uvec2 getRecommendedUiSize() const override final { return uvec2(1920, 1080); } + virtual void resetSensors() override final; + virtual glm::mat4 getEyePose(Eye eye) const override final; + virtual glm::mat4 getHeadPose() const override final; + +protected: + virtual void preRender() override final; + virtual void display(GLuint finalTexture, const glm::uvec2& sceneSize) override; + +protected: + ovrPosef _eyePoses[2]; + + mat4 _eyeProjections[3]; + mat4 _compositeEyeProjections[2]; + uvec2 _desiredFramebufferSize; + ovrTrackingState _trackingState; + unsigned int _frameIndex{ 0 }; + +#if (OVR_MAJOR_VERSION >= 6) + ovrHmd _hmd; + float _ipd{ OVR_DEFAULT_IPD }; + ovrEyeRenderDesc _eyeRenderDescs[2]; + ovrVector3f _eyeOffsets[2]; + ovrFovPort _eyeFovs[2]; + ovrHmdDesc _hmdDesc; + ovrLayerEyeFov _sceneLayer; +#endif +#if (OVR_MAJOR_VERSION == 7) + ovrGraphicsLuid _luid; +#endif +}; + +#if (OVR_MAJOR_VERSION == 6) +#define ovr_Create ovrHmd_Create +#define ovr_CreateSwapTextureSetGL ovrHmd_CreateSwapTextureSetGL +#define ovr_CreateMirrorTextureGL ovrHmd_CreateMirrorTextureGL +#define ovr_Destroy ovrHmd_Destroy +#define ovr_DestroySwapTextureSet ovrHmd_DestroySwapTextureSet +#define ovr_DestroyMirrorTexture ovrHmd_DestroyMirrorTexture +#define ovr_GetFloat ovrHmd_GetFloat +#define ovr_GetFovTextureSize ovrHmd_GetFovTextureSize +#define ovr_GetFrameTiming ovrHmd_GetFrameTiming +#define ovr_GetTrackingState ovrHmd_GetTrackingState +#define ovr_GetRenderDesc ovrHmd_GetRenderDesc +#define ovr_RecenterPose ovrHmd_RecenterPose +#define ovr_SubmitFrame ovrHmd_SubmitFrame +#define ovr_ConfigureTracking ovrHmd_ConfigureTracking + +#define ovr_GetHmdDesc(X) *X +#endif diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.cpp new file mode 100644 index 0000000000..2021ce1c5a --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.cpp @@ -0,0 +1,40 @@ +// +// Created by Bradley Austin Davis on 2014/04/13. +// Copyright 2015 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 "OculusDebugDisplayPlugin.h" +#include + +const QString OculusDebugDisplayPlugin::NAME("Oculus Rift (Simulator)"); + +const QString & OculusDebugDisplayPlugin::getName() const { + return NAME; +} + +static const QString DEBUG_FLAG("HIFI_DEBUG_OCULUS"); +static bool enableDebugOculus = QProcessEnvironment::systemEnvironment().contains("HIFI_DEBUG_OCULUS"); + +bool OculusDebugDisplayPlugin::isSupported() const { + if (!enableDebugOculus) { + return false; + } + return OculusBaseDisplayPlugin::isSupported(); +} + +void OculusDebugDisplayPlugin::customizeContext() { + WindowOpenGLDisplayPlugin::customizeContext(); + enableVsync(false); +} + +void OculusDebugDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSize) { + WindowOpenGLDisplayPlugin::display(finalTexture, sceneSize); + OculusBaseDisplayPlugin::display(finalTexture, sceneSize); +} + +void OculusDebugDisplayPlugin::finishFrame() { + swapBuffers(); + doneCurrent(); +}; diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.h new file mode 100644 index 0000000000..d23c6ba567 --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusDebugDisplayPlugin.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2015/05/29 +// Copyright 2015 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 +// +#pragma once + +#include "OculusBaseDisplayPlugin.h" + +class OculusDebugDisplayPlugin : public OculusBaseDisplayPlugin { +public: + virtual const QString & getName() const override; + virtual bool isSupported() const override; + +protected: + virtual void display(GLuint finalTexture, const glm::uvec2& sceneSize) override; + virtual void customizeContext() override; + // Do not perform swap in finish + virtual void finishFrame() override; + +private: + static const QString NAME; +}; + diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp index 2f4a9c93cf..58675eab4d 100644 --- a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp @@ -7,59 +7,10 @@ // #include "OculusDisplayPlugin.h" -#include - -#include #include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - - -#if defined(__GNUC__) && !defined(__clang__) - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdouble-promotion" -#endif - -#include - -#if defined(__GNUC__) && !defined(__clang__) - #pragma GCC diagnostic pop -#endif - - -#include -#include -#include #include "OculusHelpers.h" -#if (OVR_MAJOR_VERSION == 6) -#define ovr_Create ovrHmd_Create -#define ovr_CreateSwapTextureSetGL ovrHmd_CreateSwapTextureSetGL -#define ovr_CreateMirrorTextureGL ovrHmd_CreateMirrorTextureGL -#define ovr_Destroy ovrHmd_Destroy -#define ovr_DestroySwapTextureSet ovrHmd_DestroySwapTextureSet -#define ovr_DestroyMirrorTexture ovrHmd_DestroyMirrorTexture -#define ovr_GetFloat ovrHmd_GetFloat -#define ovr_GetFovTextureSize ovrHmd_GetFovTextureSize -#define ovr_GetFrameTiming ovrHmd_GetFrameTiming -#define ovr_GetTrackingState ovrHmd_GetTrackingState -#define ovr_GetRenderDesc ovrHmd_GetRenderDesc -#define ovr_RecenterPose ovrHmd_RecenterPose -#define ovr_SubmitFrame ovrHmd_SubmitFrame -#define ovr_ConfigureTracking ovrHmd_ConfigureTracking - -#define ovr_GetHmdDesc(X) *X -#endif - #if (OVR_MAJOR_VERSION >= 6) // A base class for FBO wrappers that need to use the Oculus C @@ -180,160 +131,21 @@ private: const QString OculusDisplayPlugin::NAME("Oculus Rift"); -uvec2 OculusDisplayPlugin::getRecommendedRenderSize() const { - return _desiredFramebufferSize; -} - -void OculusDisplayPlugin::preRender() { -#if (OVR_MAJOR_VERSION >= 6) - ovrFrameTiming ftiming = ovr_GetFrameTiming(_hmd, _frameIndex); - _trackingState = ovr_GetTrackingState(_hmd, ftiming.DisplayMidpointSeconds); - ovr_CalcEyePoses(_trackingState.HeadPose.ThePose, _eyeOffsets, _eyePoses); -#endif -} - -glm::mat4 OculusDisplayPlugin::getProjection(Eye eye, const glm::mat4& baseProjection) const { - return _eyeProjections[eye]; -} - -void OculusDisplayPlugin::resetSensors() { -#if (OVR_MAJOR_VERSION >= 6) - ovr_RecenterPose(_hmd); -#endif -} - -glm::mat4 OculusDisplayPlugin::getEyePose(Eye eye) const { - return toGlm(_eyePoses[eye]); -} - -glm::mat4 OculusDisplayPlugin::getHeadPose() const { - return toGlm(_trackingState.HeadPose.ThePose); -} - const QString & OculusDisplayPlugin::getName() const { return NAME; } -bool OculusDisplayPlugin::isSupported() const { -#if (OVR_MAJOR_VERSION >= 6) - if (!OVR_SUCCESS(ovr_Initialize(nullptr))) { - return false; - } - bool result = false; - if (ovrHmd_Detect() > 0) { - result = true; - } - ovr_Shutdown(); - return result; -#else - return false; -#endif -} - -void OculusDisplayPlugin::init() { - if (!OVR_SUCCESS(ovr_Initialize(nullptr))) { - qFatal("Could not init OVR"); - } -} - -void OculusDisplayPlugin::deinit() { - ovr_Shutdown(); -} - -#if (OVR_MAJOR_VERSION >= 6) -ovrLayerEyeFov& OculusDisplayPlugin::getSceneLayer() { - return _sceneLayer; -} -#endif - -//static gpu::TexturePointer _texture; - -void OculusDisplayPlugin::activate() { -#if (OVR_MAJOR_VERSION >= 6) - if (!OVR_SUCCESS(ovr_Initialize(nullptr))) { - Q_ASSERT(false); - qFatal("Failed to Initialize SDK"); - } - -// CONTAINER->getPrimarySurface()->makeCurrent(); -#if (OVR_MAJOR_VERSION == 6) - if (!OVR_SUCCESS(ovr_Create(0, &_hmd))) { -#elif (OVR_MAJOR_VERSION == 7) - if (!OVR_SUCCESS(ovr_Create(&_hmd, &_luid))) { -#endif - Q_ASSERT(false); - qFatal("Failed to acquire HMD"); - } - - _hmdDesc = ovr_GetHmdDesc(_hmd); - - _ipd = ovr_GetFloat(_hmd, OVR_KEY_IPD, _ipd); - - glm::uvec2 eyeSizes[2]; - ovr_for_each_eye([&](ovrEyeType eye) { - _eyeFovs[eye] = _hmdDesc.DefaultEyeFov[eye]; - ovrEyeRenderDesc& erd = _eyeRenderDescs[eye] = ovr_GetRenderDesc(_hmd, eye, _eyeFovs[eye]); - ovrMatrix4f ovrPerspectiveProjection = - ovrMatrix4f_Projection(erd.Fov, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, ovrProjection_RightHanded); - _eyeProjections[eye] = toGlm(ovrPerspectiveProjection); - - ovrPerspectiveProjection = - ovrMatrix4f_Projection(erd.Fov, 0.001f, 10.0f, ovrProjection_RightHanded); - _compositeEyeProjections[eye] = toGlm(ovrPerspectiveProjection); - - _eyeOffsets[eye] = erd.HmdToEyeViewOffset; - eyeSizes[eye] = toGlm(ovr_GetFovTextureSize(_hmd, eye, erd.Fov, 1.0f)); - }); - ovrFovPort combined = _eyeFovs[Left]; - combined.LeftTan = std::max(_eyeFovs[Left].LeftTan, _eyeFovs[Right].LeftTan); - combined.RightTan = std::max(_eyeFovs[Left].RightTan, _eyeFovs[Right].RightTan); - ovrMatrix4f ovrPerspectiveProjection = - ovrMatrix4f_Projection(combined, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, ovrProjection_RightHanded); - _eyeProjections[Mono] = toGlm(ovrPerspectiveProjection); - - - - _desiredFramebufferSize = uvec2( - eyeSizes[0].x + eyeSizes[1].x, - std::max(eyeSizes[0].y, eyeSizes[1].y)); - - _frameIndex = 0; - - if (!OVR_SUCCESS(ovr_ConfigureTracking(_hmd, - ovrTrackingCap_Orientation | ovrTrackingCap_Position | ovrTrackingCap_MagYawCorrection, 0))) { - qFatal("Could not attach to sensor device"); - } - - WindowOpenGLDisplayPlugin::activate(); - - // Parent class relies on our _hmd intialization, so it must come after that. - ovrLayerEyeFov& sceneLayer = getSceneLayer(); - memset(&sceneLayer, 0, sizeof(ovrLayerEyeFov)); - sceneLayer.Header.Type = ovrLayerType_EyeFov; - sceneLayer.Header.Flags = ovrLayerFlag_TextureOriginAtBottomLeft; - ovr_for_each_eye([&](ovrEyeType eye) { - ovrFovPort & fov = sceneLayer.Fov[eye] = _eyeRenderDescs[eye].Fov; - ovrSizei & size = sceneLayer.Viewport[eye].Size = ovr_GetFovTextureSize(_hmd, eye, fov, 1.0f); - sceneLayer.Viewport[eye].Pos = { eye == ovrEye_Left ? 0 : size.w, 0 }; - }); - // We're rendering both eyes to the same texture, so only one of the - // pointers is populated - sceneLayer.ColorTexture[0] = _sceneFbo->color; - // not needed since the structure was zeroed on init, but explicit - sceneLayer.ColorTexture[1] = nullptr; - - if (!OVR_SUCCESS(ovr_ConfigureTracking(_hmd, - ovrTrackingCap_Orientation | ovrTrackingCap_Position | ovrTrackingCap_MagYawCorrection, 0))) { - qFatal("Could not attach to sensor device"); - } -#endif -} - void OculusDisplayPlugin::customizeContext() { WindowOpenGLDisplayPlugin::customizeContext(); #if (OVR_MAJOR_VERSION >= 6) _sceneFbo = SwapFboPtr(new SwapFramebufferWrapper(_hmd)); _sceneFbo->Init(getRecommendedRenderSize()); + + // We're rendering both eyes to the same texture, so only one of the + // pointers is populated + _sceneLayer.ColorTexture[0] = _sceneFbo->color; + // not needed since the structure was zeroed on init, but explicit + _sceneLayer.ColorTexture[1] = nullptr; #endif enableVsync(false); // Only enable mirroring if we know vsync is disabled @@ -345,13 +157,9 @@ void OculusDisplayPlugin::deactivate() { makeCurrent(); _sceneFbo.reset(); doneCurrent(); - - WindowOpenGLDisplayPlugin::deactivate(); - - ovr_Destroy(_hmd); - _hmd = nullptr; - ovr_Shutdown(); #endif + + OculusBaseDisplayPlugin::deactivate(); } void OculusDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSize) { @@ -379,9 +187,8 @@ void OculusDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSi drawUnitQuad(); }); - ovrLayerEyeFov& sceneLayer = getSceneLayer(); ovr_for_each_eye([&](ovrEyeType eye) { - sceneLayer.RenderPose[eye] = _eyePoses[eye]; + _sceneLayer.RenderPose[eye] = _eyePoses[eye]; }); auto windowSize = toGlm(_window->size()); @@ -391,7 +198,7 @@ void OculusDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSi viewScaleDesc.HmdToEyeViewOffset[0] = _eyeOffsets[0]; viewScaleDesc.HmdToEyeViewOffset[1] = _eyeOffsets[1]; - ovrLayerHeader* layers = &sceneLayer.Header; + ovrLayerHeader* layers = &_sceneLayer.Header; ovrResult result = ovr_SubmitFrame(_hmd, 0, &viewScaleDesc, &layers, 1); if (!OVR_SUCCESS(result)) { qDebug() << result; @@ -403,11 +210,6 @@ void OculusDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSi #endif } -// Pass input events on to the application -bool OculusDisplayPlugin::eventFilter(QObject* receiver, QEvent* event) { - return WindowOpenGLDisplayPlugin::eventFilter(receiver, event); -} - /* The swapbuffer call here is only required if we want to mirror the content to the screen. However, it should only be done if we can reliably disable v-sync on the mirror surface, @@ -419,36 +221,3 @@ void OculusDisplayPlugin::finishFrame() { } doneCurrent(); }; - - -#if 0 -/* -An alternative way to render the UI is to pass it specifically as a composition layer to -the Oculus SDK which should technically result in higher quality. However, the SDK doesn't -have a mechanism to present the image as a sphere section, which is our desired look. -*/ -ovrLayerQuad& uiLayer = getUiLayer(); -if (nullptr == uiLayer.ColorTexture || overlaySize != _uiFbo->size) { - _uiFbo->Resize(overlaySize); - uiLayer.ColorTexture = _uiFbo->color; - uiLayer.Viewport.Size.w = overlaySize.x; - uiLayer.Viewport.Size.h = overlaySize.y; - float overlayAspect = aspect(overlaySize); - uiLayer.QuadSize.x = 1.0f; - uiLayer.QuadSize.y = 1.0f / overlayAspect; -} - -_uiFbo->Bound([&] { - Q_ASSERT(0 == glGetError()); - using namespace oglplus; - Context::Viewport(_uiFbo->size.x, _uiFbo->size.y); - glClearColor(0, 0, 0, 0); - Context::Clear().ColorBuffer(); - - _program->Bind(); - glBindTexture(GL_TEXTURE_2D, overlayTexture); - _plane->Use(); - _plane->Draw(); - Q_ASSERT(0 == glGetError()); -}); -#endif diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h index d30356daa0..7db83884cd 100644 --- a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h @@ -7,43 +7,17 @@ // #pragma once -#include "../WindowOpenGLDisplayPlugin.h" +#include "OculusBaseDisplayPlugin.h" -#include - -#include - -class OffscreenGlCanvas; struct SwapFramebufferWrapper; -struct MirrorFramebufferWrapper; - using SwapFboPtr = QSharedPointer; -using MirrorFboPtr = QSharedPointer; -class OculusDisplayPlugin : public WindowOpenGLDisplayPlugin { +class OculusDisplayPlugin : public OculusBaseDisplayPlugin { public: - virtual bool isSupported() const override; + virtual void deactivate() override; virtual const QString & getName() const override; - virtual void init() override; - virtual void deinit() override; - - virtual void activate() override; - virtual void deactivate() override; - - virtual bool eventFilter(QObject* receiver, QEvent* event) override; - - // Stereo specific methods - virtual bool isHmd() const override { return true; } - virtual glm::mat4 getProjection(Eye eye, const glm::mat4& baseProjection) const override; - virtual glm::uvec2 getRecommendedRenderSize() const override; - virtual glm::uvec2 getRecommendedUiSize() const override { return uvec2(1920, 1080); } - virtual void resetSensors() override; - virtual glm::mat4 getEyePose(Eye eye) const override; - virtual glm::mat4 getHeadPose() const override; - protected: - virtual void preRender() override; virtual void display(GLuint finalTexture, const glm::uvec2& sceneSize) override; virtual void customizeContext() override; // Do not perform swap in finish @@ -51,30 +25,10 @@ protected: private: static const QString NAME; - - ovrPosef _eyePoses[2]; - - mat4 _eyeProjections[3]; - mat4 _compositeEyeProjections[2]; - uvec2 _desiredFramebufferSize; - ovrTrackingState _trackingState; bool _enableMirror{ false }; #if (OVR_MAJOR_VERSION >= 6) - ovrHmd _hmd; - float _ipd{ OVR_DEFAULT_IPD }; - unsigned int _frameIndex; - ovrEyeRenderDesc _eyeRenderDescs[2]; - ovrVector3f _eyeOffsets[2]; - ovrFovPort _eyeFovs[2]; - - ovrLayerEyeFov& getSceneLayer(); - ovrHmdDesc _hmdDesc; SwapFboPtr _sceneFbo; - ovrLayerEyeFov _sceneLayer; -#endif -#if (OVR_MAJOR_VERSION == 7) - ovrGraphicsLuid _luid; #endif }; From bef136d8110f513e5cb6a2926738ac27f58436d4 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 17 Sep 2015 11:21:14 -0700 Subject: [PATCH 058/192] AnimGraph: prefer QString over std::string --- libraries/animation/src/AnimBlendLinear.cpp | 2 +- libraries/animation/src/AnimBlendLinear.h | 6 +- libraries/animation/src/AnimClip.cpp | 8 +-- libraries/animation/src/AnimClip.h | 26 ++++---- .../animation/src/AnimInverseKinematics.cpp | 8 +-- .../animation/src/AnimInverseKinematics.h | 8 +-- libraries/animation/src/AnimManipulator.cpp | 7 +-- libraries/animation/src/AnimManipulator.h | 12 ++-- libraries/animation/src/AnimNode.h | 10 ++-- libraries/animation/src/AnimNodeLoader.cpp | 56 +++++++++-------- libraries/animation/src/AnimOverlay.cpp | 2 +- libraries/animation/src/AnimOverlay.h | 10 ++-- libraries/animation/src/AnimStateMachine.cpp | 8 +-- libraries/animation/src/AnimStateMachine.h | 24 ++++---- libraries/animation/src/AnimVariant.h | 60 +++++++++---------- 15 files changed, 122 insertions(+), 125 deletions(-) diff --git a/libraries/animation/src/AnimBlendLinear.cpp b/libraries/animation/src/AnimBlendLinear.cpp index 63c66a2b9d..bc95565f6f 100644 --- a/libraries/animation/src/AnimBlendLinear.cpp +++ b/libraries/animation/src/AnimBlendLinear.cpp @@ -13,7 +13,7 @@ #include "AnimationLogging.h" #include "AnimUtil.h" -AnimBlendLinear::AnimBlendLinear(const std::string& id, float alpha) : +AnimBlendLinear::AnimBlendLinear(const QString& id, float alpha) : AnimNode(AnimNode::Type::BlendLinear, id), _alpha(alpha) { diff --git a/libraries/animation/src/AnimBlendLinear.h b/libraries/animation/src/AnimBlendLinear.h index 3a09245575..56acd5c2f7 100644 --- a/libraries/animation/src/AnimBlendLinear.h +++ b/libraries/animation/src/AnimBlendLinear.h @@ -27,12 +27,12 @@ class AnimBlendLinear : public AnimNode { public: friend class AnimTests; - AnimBlendLinear(const std::string& id, float alpha); + AnimBlendLinear(const QString& id, float alpha); virtual ~AnimBlendLinear() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; - void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } protected: // for AnimDebugDraw rendering @@ -42,7 +42,7 @@ protected: float _alpha; - std::string _alphaVar; + QString _alphaVar; // no copies AnimBlendLinear(const AnimBlendLinear&) = delete; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index fdc5fc504a..23aa884933 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -13,7 +13,7 @@ #include "AnimationLogging.h" #include "AnimUtil.h" -AnimClip::AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag) : +AnimClip::AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag) : AnimNode(AnimNode::Type::Clip, id), _startFrame(startFrame), _endFrame(endFrame), @@ -68,9 +68,9 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, return _poses; } -void AnimClip::loadURL(const std::string& url) { +void AnimClip::loadURL(const QString& url) { auto animCache = DependencyManager::get(); - _networkAnim = animCache->getAnimation(QString::fromStdString(url)); + _networkAnim = animCache->getAnimation(url); _url = url; } @@ -127,7 +127,7 @@ void AnimClip::copyFromNetworkAnim() { for (int i = 0; i < animJointCount; i++) { int skeletonJoint = _skeleton->nameToJointIndex(animJoints.at(i).name); if (skeletonJoint == -1) { - qCWarning(animation) << "animation contains joint =" << animJoints.at(i).name << " which is not in the skeleton, url =" << _url.c_str(); + qCWarning(animation) << "animation contains joint =" << animJoints.at(i).name << " which is not in the skeleton, url =" << _url; } jointMap.push_back(skeletonJoint); } diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index 1b9649cc3e..3a76870c98 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -25,19 +25,19 @@ class AnimClip : public AnimNode { public: friend class AnimTests; - AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag); + AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag); virtual ~AnimClip() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; - void setStartFrameVar(const std::string& startFrameVar) { _startFrameVar = startFrameVar; } - void setEndFrameVar(const std::string& endFrameVar) { _endFrameVar = endFrameVar; } - void setTimeScaleVar(const std::string& timeScaleVar) { _timeScaleVar = timeScaleVar; } - void setLoopFlagVar(const std::string& loopFlagVar) { _loopFlagVar = loopFlagVar; } - void setFrameVar(const std::string& frameVar) { _frameVar = frameVar; } + void setStartFrameVar(const QString& startFrameVar) { _startFrameVar = startFrameVar; } + void setEndFrameVar(const QString& endFrameVar) { _endFrameVar = endFrameVar; } + void setTimeScaleVar(const QString& timeScaleVar) { _timeScaleVar = timeScaleVar; } + void setLoopFlagVar(const QString& loopFlagVar) { _loopFlagVar = loopFlagVar; } + void setFrameVar(const QString& frameVar) { _frameVar = frameVar; } protected: - void loadURL(const std::string& url); + void loadURL(const QString& url); virtual void setCurrentFrameInternal(float frame) override; @@ -53,18 +53,18 @@ protected: // _anim[frame][joint] std::vector _anim; - std::string _url; + QString _url; float _startFrame; float _endFrame; float _timeScale; bool _loopFlag; float _frame; - std::string _startFrameVar; - std::string _endFrameVar; - std::string _timeScaleVar; - std::string _loopFlagVar; - std::string _frameVar; + QString _startFrameVar; + QString _endFrameVar; + QString _timeScaleVar; + QString _loopFlagVar; + QString _frameVar; // no copies AnimClip(const AnimClip&) = delete; diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 863970ccaa..36b23c313e 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -17,7 +17,7 @@ #include "SwingTwistConstraint.h" #include "AnimationLogging.h" -AnimInverseKinematics::AnimInverseKinematics(const std::string& id) : AnimNode(AnimNode::Type::InverseKinematics, id) { +AnimInverseKinematics::AnimInverseKinematics(const QString& id) : AnimNode(AnimNode::Type::InverseKinematics, id) { } AnimInverseKinematics::~AnimInverseKinematics() { @@ -59,15 +59,15 @@ void AnimInverseKinematics::setTargetVars(const QString& jointName, const QStrin for (auto& targetVar: _targetVarVec) { if (targetVar.jointName == jointName) { // update existing targetVar - targetVar.positionVar = positionVar.toStdString(); - targetVar.rotationVar = rotationVar.toStdString(); + targetVar.positionVar = positionVar; + targetVar.rotationVar = rotationVar; found = true; break; } } if (!found) { // create a new entry - _targetVarVec.push_back(IKTargetVar(jointName, positionVar.toStdString(), rotationVar.toStdString())); + _targetVarVec.push_back(IKTargetVar(jointName, positionVar, rotationVar)); } } diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index eabc3448de..b59fb4d5fc 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -19,7 +19,7 @@ class RotationConstraint; class AnimInverseKinematics : public AnimNode { public: - AnimInverseKinematics(const std::string& id); + AnimInverseKinematics(const QString& id); virtual ~AnimInverseKinematics() override; void loadDefaultPoses(const AnimPoseVec& poses); @@ -45,15 +45,15 @@ protected: void initConstraints(); struct IKTargetVar { - IKTargetVar(const QString& jointNameIn, const std::string& positionVarIn, const std::string& rotationVarIn) : + IKTargetVar(const QString& jointNameIn, const QString& positionVarIn, const QString& rotationVarIn) : positionVar(positionVarIn), rotationVar(rotationVarIn), jointName(jointNameIn), jointIndex(-1), rootIndex(-1) {} - std::string positionVar; - std::string rotationVar; + QString positionVar; + QString rotationVar; QString jointName; int jointIndex; // cached joint index int rootIndex; // cached root index diff --git a/libraries/animation/src/AnimManipulator.cpp b/libraries/animation/src/AnimManipulator.cpp index c2951681e1..0640c418e3 100644 --- a/libraries/animation/src/AnimManipulator.cpp +++ b/libraries/animation/src/AnimManipulator.cpp @@ -12,7 +12,7 @@ #include "AnimUtil.h" #include "AnimationLogging.h" -AnimManipulator::AnimManipulator(const std::string& id, float alpha) : +AnimManipulator::AnimManipulator(const QString& id, float alpha) : AnimNode(AnimNode::Type::Manipulator, id), _alpha(alpha) { @@ -31,10 +31,9 @@ const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, floa for (auto& jointVar : _jointVars) { if (!jointVar.hasPerformedJointLookup) { - QString qJointName = QString::fromStdString(jointVar.jointName); - jointVar.jointIndex = _skeleton->nameToJointIndex(qJointName); + jointVar.jointIndex = _skeleton->nameToJointIndex(jointVar.jointName); if (jointVar.jointIndex < 0) { - qCWarning(animation) << "AnimManipulator could not find jointName" << qJointName << "in skeleton"; + qCWarning(animation) << "AnimManipulator could not find jointName" << jointVar.jointName << "in skeleton"; } jointVar.hasPerformedJointLookup = true; } diff --git a/libraries/animation/src/AnimManipulator.h b/libraries/animation/src/AnimManipulator.h index c04853b8e0..eca1a4aa71 100644 --- a/libraries/animation/src/AnimManipulator.h +++ b/libraries/animation/src/AnimManipulator.h @@ -19,20 +19,20 @@ class AnimManipulator : public AnimNode { public: friend class AnimTests; - AnimManipulator(const std::string& id, float alpha); + AnimManipulator(const QString& id, float alpha); virtual ~AnimManipulator() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; - void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; struct JointVar { - JointVar(const std::string& varIn, const std::string& jointNameIn) : var(varIn), jointName(jointNameIn), jointIndex(-1), hasPerformedJointLookup(false) {} - std::string var = ""; - std::string jointName = ""; + JointVar(const QString& varIn, const QString& jointNameIn) : var(varIn), jointName(jointNameIn), jointIndex(-1), hasPerformedJointLookup(false) {} + QString var = ""; + QString jointName = ""; int jointIndex = -1; bool hasPerformedJointLookup = false; }; @@ -45,7 +45,7 @@ protected: AnimPoseVec _poses; float _alpha; - std::string _alphaVar; + QString _alphaVar; std::vector _jointVars; diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h index b4992f95a3..9325ef3835 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -46,16 +46,16 @@ public: }; using Pointer = std::shared_ptr; using ConstPointer = std::shared_ptr; - using Triggers = std::vector; + using Triggers = std::vector; friend class AnimDebugDraw; - friend void buildChildMap(std::map& map, Pointer node); + friend void buildChildMap(std::map& map, Pointer node); friend class AnimStateMachine; - AnimNode(Type type, const std::string& id) : _type(type), _id(id) {} + AnimNode(Type type, const QString& id) : _type(type), _id(id) {} virtual ~AnimNode() {} - const std::string& getID() const { return _id; } + const QString& getID() const { return _id; } Type getType() const { return _type; } // hierarchy accessors @@ -105,7 +105,7 @@ protected: virtual const AnimPoseVec& getPosesInternal() const = 0; Type _type; - std::string _id; + QString _id; std::vector _children; AnimSkeleton::ConstPointer _skeleton; diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 23bea83e1c..b2afae4728 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -200,19 +200,19 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& READ_OPTIONAL_STRING(timeScaleVar, jsonObj); READ_OPTIONAL_STRING(loopFlagVar, jsonObj); - auto node = std::make_shared(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag); + auto node = std::make_shared(id, url, startFrame, endFrame, timeScale, loopFlag); if (!startFrameVar.isEmpty()) { - node->setStartFrameVar(startFrameVar.toStdString()); + node->setStartFrameVar(startFrameVar); } if (!endFrameVar.isEmpty()) { - node->setEndFrameVar(endFrameVar.toStdString()); + node->setEndFrameVar(endFrameVar); } if (!timeScaleVar.isEmpty()) { - node->setTimeScaleVar(timeScaleVar.toStdString()); + node->setTimeScaleVar(timeScaleVar); } if (!loopFlagVar.isEmpty()) { - node->setLoopFlagVar(loopFlagVar.toStdString()); + node->setLoopFlagVar(loopFlagVar); } return node; @@ -224,10 +224,10 @@ static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const Q READ_OPTIONAL_STRING(alphaVar, jsonObj); - auto node = std::make_shared(id.toStdString(), alpha); + auto node = std::make_shared(id, alpha); if (!alphaVar.isEmpty()) { - node->setAlphaVar(alphaVar.toStdString()); + node->setAlphaVar(alphaVar); } return node; @@ -271,31 +271,31 @@ static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QStri READ_OPTIONAL_STRING(boneSetVar, jsonObj); READ_OPTIONAL_STRING(alphaVar, jsonObj); - auto node = std::make_shared(id.toStdString(), boneSetEnum, alpha); + auto node = std::make_shared(id, boneSetEnum, alpha); if (!boneSetVar.isEmpty()) { - node->setBoneSetVar(boneSetVar.toStdString()); + node->setBoneSetVar(boneSetVar); } if (!alphaVar.isEmpty()) { - node->setAlphaVar(alphaVar.toStdString()); + node->setAlphaVar(alphaVar); } return node; } static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { - auto node = std::make_shared(id.toStdString()); + auto node = std::make_shared(id); return node; } static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr); - auto node = std::make_shared(id.toStdString(), alpha); + auto node = std::make_shared(id, alpha); READ_OPTIONAL_STRING(alphaVar, jsonObj); if (!alphaVar.isEmpty()) { - node->setAlphaVar(alphaVar.toStdString()); + node->setAlphaVar(alphaVar); } auto jointsValue = jsonObj.value("joints"); @@ -315,7 +315,7 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q READ_STRING(var, jointObj, id, jsonUrl, nullptr); READ_STRING(jointName, jointObj, id, jsonUrl, nullptr); - AnimManipulator::JointVar jointVar(var.toStdString(), jointName.toStdString()); + AnimManipulator::JointVar jointVar(var, jointName); node->addJointVar(jointVar); }; @@ -323,7 +323,7 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q } AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { - auto node = std::make_shared(id.toStdString()); + auto node = std::make_shared(id); auto targetsValue = jsonObj.value("targets"); if (!targetsValue.isArray()) { @@ -349,9 +349,9 @@ AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QS return node; } -void buildChildMap(std::map& map, AnimNode::Pointer node) { +void buildChildMap(std::map& map, AnimNode::Pointer node) { for ( auto child : node->_children ) { - map.insert(std::pair(child->_id, child)); + map.insert(std::pair(child->_id, child)); } } @@ -368,15 +368,15 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, } // build a map for all children by name. - std::map childMap; + std::map childMap; buildChildMap(childMap, node); // first pass parse all the states and build up the state and transition map. - using StringPair = std::pair; + using StringPair = std::pair; using TransitionMap = std::multimap; TransitionMap transitionMap; - using StateMap = std::map; + using StateMap = std::map; StateMap stateMap; auto statesArray = statesValue.toArray(); @@ -394,22 +394,20 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, READ_OPTIONAL_STRING(interpTargetVar, stateObj); READ_OPTIONAL_STRING(interpDurationVar, stateObj); - auto stdId = id.toStdString(); - - auto iter = childMap.find(stdId); + auto iter = childMap.find(id); if (iter == childMap.end()) { qCCritical(animation) << "AnimNodeLoader, could not find stateMachine child (state) with nodeId =" << nodeId << "stateId =" << id << "url =" << jsonUrl.toDisplayString(); return false; } - auto statePtr = std::make_shared(stdId, iter->second, interpTarget, interpDuration); + auto statePtr = std::make_shared(id, iter->second, interpTarget, interpDuration); assert(statePtr); if (!interpTargetVar.isEmpty()) { - statePtr->setInterpTargetVar(interpTargetVar.toStdString()); + statePtr->setInterpTargetVar(interpTargetVar); } if (!interpDurationVar.isEmpty()) { - statePtr->setInterpDurationVar(interpDurationVar.toStdString()); + statePtr->setInterpDurationVar(interpDurationVar); } smNode->addState(statePtr); @@ -432,7 +430,7 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, READ_STRING(var, transitionObj, nodeId, jsonUrl, false); READ_STRING(state, transitionObj, nodeId, jsonUrl, false); - transitionMap.insert(TransitionMap::value_type(statePtr, StringPair(var.toStdString(), state.toStdString()))); + transitionMap.insert(TransitionMap::value_type(statePtr, StringPair(var, state))); } } @@ -443,12 +441,12 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, if (iter != stateMap.end()) { srcState->addTransition(AnimStateMachine::State::Transition(transition.second.first, iter->second)); } else { - qCCritical(animation) << "AnimNodeLoader, bad state machine transtion from srcState =" << srcState->_id.c_str() << "dstState =" << transition.second.second.c_str() << "nodeId =" << nodeId << "url = " << jsonUrl.toDisplayString(); + qCCritical(animation) << "AnimNodeLoader, bad state machine transtion from srcState =" << srcState->_id << "dstState =" << transition.second.second << "nodeId =" << nodeId << "url = " << jsonUrl.toDisplayString(); return false; } } - auto iter = stateMap.find(currentState.toStdString()); + auto iter = stateMap.find(currentState); if (iter == stateMap.end()) { qCCritical(animation) << "AnimNodeLoader, bad currentState =" << currentState << "could not find child node" << "id =" << nodeId << "url = " << jsonUrl.toDisplayString(); } diff --git a/libraries/animation/src/AnimOverlay.cpp b/libraries/animation/src/AnimOverlay.cpp index 760a683aa6..08c4304b08 100644 --- a/libraries/animation/src/AnimOverlay.cpp +++ b/libraries/animation/src/AnimOverlay.cpp @@ -12,7 +12,7 @@ #include "AnimUtil.h" #include -AnimOverlay::AnimOverlay(const std::string& id, BoneSet boneSet, float alpha) : +AnimOverlay::AnimOverlay(const QString& id, BoneSet boneSet, float alpha) : AnimNode(AnimNode::Type::Overlay, id), _boneSet(boneSet), _alpha(alpha) { } diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 2a87c54997..eda8847d40 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -40,13 +40,13 @@ public: NumBoneSets }; - AnimOverlay(const std::string& id, BoneSet boneSet, float alpha); + AnimOverlay(const QString& id, BoneSet boneSet, float alpha); virtual ~AnimOverlay() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; - void setBoneSetVar(const std::string& boneSetVar) { _boneSetVar = boneSetVar; } - void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + void setBoneSetVar(const QString& boneSetVar) { _boneSetVar = boneSetVar; } + void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } protected: void buildBoneSet(BoneSet boneSet); @@ -60,8 +60,8 @@ public: float _alpha; std::vector _boneSetVec; - std::string _boneSetVar; - std::string _alphaVar; + QString _boneSetVar; + QString _alphaVar; void buildFullBodyBoneSet(); void buildUpperBodyBoneSet(); diff --git a/libraries/animation/src/AnimStateMachine.cpp b/libraries/animation/src/AnimStateMachine.cpp index 5de379dd33..8ce0fc95b0 100644 --- a/libraries/animation/src/AnimStateMachine.cpp +++ b/libraries/animation/src/AnimStateMachine.cpp @@ -12,7 +12,7 @@ #include "AnimUtil.h" #include "AnimationLogging.h" -AnimStateMachine::AnimStateMachine(const std::string& id) : +AnimStateMachine::AnimStateMachine(const QString& id) : AnimNode(AnimNode::Type::StateMachine, id) { } @@ -23,7 +23,7 @@ AnimStateMachine::~AnimStateMachine() { const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { - std::string desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID()); + QString desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID()); if (_currentState->getID() != desiredStateID) { // switch states bool foundState = false; @@ -35,7 +35,7 @@ const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, fl } } if (!foundState) { - qCCritical(animation) << "AnimStateMachine could not find state =" << desiredStateID.c_str() << ", referenced by _currentStateVar =" << _currentStateVar.c_str(); + qCCritical(animation) << "AnimStateMachine could not find state =" << desiredStateID << ", referenced by _currentStateVar =" << _currentStateVar; } } @@ -77,7 +77,7 @@ void AnimStateMachine::addState(State::Pointer state) { void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointer desiredState) { - qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID().c_str() << "->" << desiredState->getID().c_str(); + qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID() << "->" << desiredState->getID(); const float FRAMES_PER_SECOND = 30.0f; diff --git a/libraries/animation/src/AnimStateMachine.h b/libraries/animation/src/AnimStateMachine.h index f2d941a568..cb6c99f067 100644 --- a/libraries/animation/src/AnimStateMachine.h +++ b/libraries/animation/src/AnimStateMachine.h @@ -49,23 +49,23 @@ protected: class Transition { public: friend AnimStateMachine; - Transition(const std::string& var, State::Pointer state) : _var(var), _state(state) {} + Transition(const QString& var, State::Pointer state) : _var(var), _state(state) {} protected: - std::string _var; + QString _var; State::Pointer _state; }; - State(const std::string& id, AnimNode::Pointer node, float interpTarget, float interpDuration) : + State(const QString& id, AnimNode::Pointer node, float interpTarget, float interpDuration) : _id(id), _node(node), _interpTarget(interpTarget), _interpDuration(interpDuration) {} - void setInterpTargetVar(const std::string& interpTargetVar) { _interpTargetVar = interpTargetVar; } - void setInterpDurationVar(const std::string& interpDurationVar) { _interpDurationVar = interpDurationVar; } + void setInterpTargetVar(const QString& interpTargetVar) { _interpTargetVar = interpTargetVar; } + void setInterpDurationVar(const QString& interpDurationVar) { _interpDurationVar = interpDurationVar; } AnimNode::Pointer getNode() const { return _node; } - const std::string& getID() const { return _id; } + const QString& getID() const { return _id; } protected: @@ -74,13 +74,13 @@ protected: void addTransition(const Transition& transition) { _transitions.push_back(transition); } - std::string _id; + QString _id; AnimNode::Pointer _node; float _interpTarget; // frames float _interpDuration; // frames - std::string _interpTargetVar; - std::string _interpDurationVar; + QString _interpTargetVar; + QString _interpDurationVar; std::vector _transitions; @@ -92,12 +92,12 @@ protected: public: - AnimStateMachine(const std::string& id); + AnimStateMachine(const QString& id); virtual ~AnimStateMachine() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; - void setCurrentStateVar(std::string& currentStateVar) { _currentStateVar = currentStateVar; } + void setCurrentStateVar(QString& currentStateVar) { _currentStateVar = currentStateVar; } protected: @@ -123,7 +123,7 @@ protected: State::Pointer _currentState; std::vector _states; - std::string _currentStateVar; + QString _currentStateVar; private: // no copies diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index bcff4b2a9b..de224f936a 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -37,7 +37,7 @@ public: AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast(&_val) = value; } AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast(&_val) = value; } AnimVariant(const glm::mat4& value) : _type(Type::Mat4) { *reinterpret_cast(&_val) = value; } - AnimVariant(const std::string& value) : _type(Type::String) { _stringVal = value; } + AnimVariant(const QString& value) : _type(Type::String) { _stringVal = value; } bool isBool() const { return _type == Type::Bool; } bool isInt() const { return _type == Type::Int; } @@ -53,7 +53,7 @@ public: void setVec3(const glm::vec3& value) { assert(_type == Type::Vec3); *reinterpret_cast(&_val) = value; } void setQuat(const glm::quat& value) { assert(_type == Type::Quat); *reinterpret_cast(&_val) = value; } void setMat4(const glm::mat4& value) { assert(_type == Type::Mat4); *reinterpret_cast(&_val) = value; } - void setString(const std::string& value) { assert(_type == Type::String); _stringVal = value; } + void setString(const QString& value) { assert(_type == Type::String); _stringVal = value; } bool getBool() const { assert(_type == Type::Bool); return _val.boolVal; } int getInt() const { assert(_type == Type::Int); return _val.intVal; } @@ -61,11 +61,11 @@ public: const glm::vec3& getVec3() const { assert(_type == Type::Vec3); return *reinterpret_cast(&_val); } const glm::quat& getQuat() const { assert(_type == Type::Quat); return *reinterpret_cast(&_val); } const glm::mat4& getMat4() const { assert(_type == Type::Mat4); return *reinterpret_cast(&_val); } - const std::string& getString() const { assert(_type == Type::String); return _stringVal; } + const QString& getString() const { assert(_type == Type::String); return _stringVal; } protected: Type _type; - std::string _stringVal; + QString _stringVal; union { bool boolVal; int intVal; @@ -76,9 +76,9 @@ protected: class AnimVariantMap { public: - bool lookup(const std::string& key, bool defaultValue) const { + bool lookup(const QString& key, bool defaultValue) const { // check triggers first, then map - if (key.empty()) { + if (key.isEmpty()) { return defaultValue; } else if (_triggers.find(key) != _triggers.end()) { return true; @@ -88,8 +88,8 @@ public: } } - int lookup(const std::string& key, int defaultValue) const { - if (key.empty()) { + int lookup(const QString& key, int defaultValue) const { + if (key.isEmpty()) { return defaultValue; } else { auto iter = _map.find(key); @@ -97,8 +97,8 @@ public: } } - float lookup(const std::string& key, float defaultValue) const { - if (key.empty()) { + float lookup(const QString& key, float defaultValue) const { + if (key.isEmpty()) { return defaultValue; } else { auto iter = _map.find(key); @@ -106,8 +106,8 @@ public: } } - const glm::vec3& lookup(const std::string& key, const glm::vec3& defaultValue) const { - if (key.empty()) { + const glm::vec3& lookup(const QString& key, const glm::vec3& defaultValue) const { + if (key.isEmpty()) { return defaultValue; } else { auto iter = _map.find(key); @@ -115,8 +115,8 @@ public: } } - const glm::quat& lookup(const std::string& key, const glm::quat& defaultValue) const { - if (key.empty()) { + const glm::quat& lookup(const QString& key, const glm::quat& defaultValue) const { + if (key.isEmpty()) { return defaultValue; } else { auto iter = _map.find(key); @@ -124,8 +124,8 @@ public: } } - const glm::mat4& lookup(const std::string& key, const glm::mat4& defaultValue) const { - if (key.empty()) { + const glm::mat4& lookup(const QString& key, const glm::mat4& defaultValue) const { + if (key.isEmpty()) { return defaultValue; } else { auto iter = _map.find(key); @@ -133,8 +133,8 @@ public: } } - const std::string& lookup(const std::string& key, const std::string& defaultValue) const { - if (key.empty()) { + const QString& lookup(const QString& key, const QString& defaultValue) const { + if (key.isEmpty()) { return defaultValue; } else { auto iter = _map.find(key); @@ -142,23 +142,23 @@ public: } } - void set(const std::string& key, bool value) { _map[key] = AnimVariant(value); } - void set(const std::string& key, int value) { _map[key] = AnimVariant(value); } - void set(const std::string& key, float value) { _map[key] = AnimVariant(value); } - void set(const std::string& key, const glm::vec3& value) { _map[key] = AnimVariant(value); } - void set(const std::string& key, const glm::quat& value) { _map[key] = AnimVariant(value); } - void set(const std::string& key, const glm::mat4& value) { _map[key] = AnimVariant(value); } - void set(const std::string& key, const std::string& value) { _map[key] = AnimVariant(value); } - void unset(const std::string& key) { _map.erase(key); } + void set(const QString& key, bool value) { _map[key] = AnimVariant(value); } + void set(const QString& key, int value) { _map[key] = AnimVariant(value); } + void set(const QString& key, float value) { _map[key] = AnimVariant(value); } + void set(const QString& key, const glm::vec3& value) { _map[key] = AnimVariant(value); } + void set(const QString& key, const glm::quat& value) { _map[key] = AnimVariant(value); } + void set(const QString& key, const glm::mat4& value) { _map[key] = AnimVariant(value); } + void set(const QString& key, const QString& value) { _map[key] = AnimVariant(value); } + void unset(const QString& key) { _map.erase(key); } - void setTrigger(const std::string& key) { _triggers.insert(key); } + void setTrigger(const QString& key) { _triggers.insert(key); } void clearTriggers() { _triggers.clear(); } - bool hasKey(const std::string& key) const { return _map.find(key) != _map.end(); } + bool hasKey(const QString& key) const { return _map.find(key) != _map.end(); } protected: - std::map _map; - std::set _triggers; + std::map _map; + std::set _triggers; }; #endif // hifi_AnimVariant_h From b207d97f72c31c46c395021dcdefcdddcdfd0954 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 17 Sep 2015 12:28:17 -0700 Subject: [PATCH 059/192] Fix the lighting when the rear view mirror is showing up --- libraries/gpu/src/gpu/Resource.h | 25 ++++++++++++------- .../src/DeferredLightingEffect.cpp | 1 + 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 42897e9947..177c798e2c 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -160,15 +160,22 @@ typedef std::vector< BufferPointer > Buffers; class BufferView { +protected: + void initFromBuffer(const BufferPointer& buffer) { + _buffer = (buffer); + if (_buffer) { + _size = (buffer->getSize()); + } + } public: typedef Resource::Size Size; typedef int Index; BufferPointer _buffer; - Size _offset; - Size _size; + Size _offset{ 0 }; + Size _size{ 0 }; Element _element; - uint16 _stride; + uint16 _stride{ 1 }; BufferView() : _buffer(NULL), @@ -188,19 +195,19 @@ public: // create the BufferView and own the Buffer BufferView(Buffer* newBuffer, const Element& element = Element(gpu::SCALAR, gpu::UINT8, gpu::RAW)) : - _buffer(newBuffer), _offset(0), - _size(newBuffer->getSize()), _element(element), _stride(uint16(element.getSize())) - {}; + { + initFromBuffer(BufferPointer(newBuffer)); + }; BufferView(const BufferPointer& buffer, const Element& element = Element(gpu::SCALAR, gpu::UINT8, gpu::RAW)) : - _buffer(buffer), _offset(0), - _size(buffer->getSize()), _element(element), _stride(uint16(element.getSize())) - {}; + { + initFromBuffer(buffer); + }; BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = Element(gpu::SCALAR, gpu::UINT8, gpu::RAW)) : _buffer(buffer), _offset(offset), diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index b19e2f62b7..ce387e648b 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -582,6 +582,7 @@ void DeferredLightingEffect::render(RenderArgs* args) { batch.setResourceTexture(1, nullptr); batch.setResourceTexture(2, nullptr); batch.setResourceTexture(3, nullptr); + batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, nullptr); args->_context->render(batch); From 4758dd2a53975315d8a4f3212ffd92ef4b6d1440 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 17 Sep 2015 14:13:24 -0700 Subject: [PATCH 060/192] correct locking races in SendQueue/Connection cleanup --- libraries/networking/src/udt/Connection.cpp | 30 ++++++++++------ libraries/networking/src/udt/Connection.h | 5 +++ libraries/networking/src/udt/SendQueue.cpp | 35 +++++++++++++----- libraries/networking/src/udt/SendQueue.h | 8 ++++- libraries/networking/src/udt/Socket.cpp | 40 +++++++++++++++++---- 5 files changed, 90 insertions(+), 28 deletions(-) diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index 2fb28f81ee..1bda840a6c 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -56,14 +56,15 @@ void Connection::stopSendQueue() { // grab the send queue thread so we can wait on it QThread* sendQueueThread = _sendQueue->thread(); - // since we're stopping the send queue we should consider our handshake ACK not receieved - _hasReceivedHandshakeACK = false; - // tell the send queue to stop and be deleted + _sendQueue->stop(); _sendQueue->deleteLater(); _sendQueue.release(); + // since we're stopping the send queue we should consider our handshake ACK not receieved + _hasReceivedHandshakeACK = false; + // wait on the send queue thread so we know the send queue is gone sendQueueThread->quit(); sendQueueThread->wait(); @@ -111,7 +112,7 @@ void Connection::queueInactive() { qCDebug(networking) << "Connection SendQueue to" << _destination << "stopped and no data is being received - stopping connection."; #endif - emit connectionInactive(_destination); + deactivate(); } } @@ -170,7 +171,9 @@ void Connection::sync() { qCDebug(networking) << "Connection to" << _destination << "no longer receiving any data and there is currently no send queue - stopping connection."; #endif - emit connectionInactive(_destination); + deactivate(); + + return; } } @@ -207,7 +210,9 @@ void Connection::sync() { << CONNECTION_NOT_USED_EXPIRY_SECONDS << "seconds - stopping connection."; #endif - emit connectionInactive(_destination); + deactivate(); + + return; } } } @@ -728,11 +733,14 @@ void Connection::processHandshake(std::unique_ptr controlPacket) } void Connection::processHandshakeACK(std::unique_ptr controlPacket) { - // hand off this handshake ACK to the send queue so it knows it can start sending - getSendQueue().handshakeACK(); - - // indicate that handshake ACK was received - _hasReceivedHandshakeACK = true; + // if we've decided to clean up the send queue then this handshake ACK should be ignored, it's useless + if (_sendQueue) { + // hand off this handshake ACK to the send queue so it knows it can start sending + getSendQueue().handshakeACK(); + + // indicate that handshake ACK was received + _hasReceivedHandshakeACK = true; + } } void Connection::processTimeoutNAK(std::unique_ptr controlPacket) { diff --git a/libraries/networking/src/udt/Connection.h b/libraries/networking/src/udt/Connection.h index 2b1dec1ae9..13756c12f9 100644 --- a/libraries/networking/src/udt/Connection.h +++ b/libraries/networking/src/udt/Connection.h @@ -71,6 +71,8 @@ public: void queueReceivedMessagePacket(std::unique_ptr packet); ConnectionStats::Stats sampleStats() { return _stats.sample(); } + + bool isActive() const { return _isActive; } signals: void packetSent(); @@ -100,6 +102,8 @@ private: void resetReceiveState(); void resetRTT(); + void deactivate() { _isActive = false; emit connectionInactive(_destination); } + SendQueue& getSendQueue(); SequenceNumber nextACK() const; void updateRTT(int rtt); @@ -123,6 +127,7 @@ private: p_high_resolution_clock::time_point _lastReceiveTime; // holds the last time we received anything from sender bool _isReceivingData { false }; // flag used for expiry of receipt portion of connection + bool _isActive { true }; // flag used for inactivity of connection LossList _lossList; // List of all missing packets SequenceNumber _lastReceivedSequenceNumber; // The largest sequence number received from the peer diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index a09ea6ca9a..31c2f41259 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -65,6 +65,7 @@ std::unique_ptr SendQueue::create(Socket* socket, HifiSockAddr destin // Move queue to private thread and start it queue->moveToThread(thread); + thread->start(); return std::move(queue); @@ -89,7 +90,8 @@ void SendQueue::queuePacket(std::unique_ptr packet) { // call notify_one on the condition_variable_any in case the send thread is sleeping waiting for packets _emptyCondition.notify_one(); } - if (!this->thread()->isRunning()) { + + if (!this->thread()->isRunning() && _state == State::NotStarted) { this->thread()->start(); } } @@ -135,14 +137,15 @@ void SendQueue::queuePacketList(std::unique_ptr packetList) { // call notify_one on the condition_variable_any in case the send thread is sleeping waiting for packets _emptyCondition.notify_one(); } - + if (!this->thread()->isRunning()) { this->thread()->start(); } } void SendQueue::stop() { - _isRunning = false; + + _state = State::Stopped; // in case we're waiting to send another handshake, release the condition_variable now so we cleanup sooner _handshakeACKCondition.notify_one(); @@ -268,9 +271,23 @@ void SendQueue::sendNewPacketAndAddToSentList(std::unique_ptr newPacket, } void SendQueue::run() { - _isRunning = true; + if (_state == State::Stopped) { + // we've already been asked to stop before we even got a chance to start + // don't start now +#ifdef UDT_CONNECTION_DEBUG + qDebug() << "SendQueue asked to run after being told to stop. Will not run."; +#endif + return; + } else if (_state == State::Running) { +#ifdef UDT_CONNECTION_DEBUG + qDebug() << "SendQueue asked to run but is already running (according to state). Will not re-run."; +#endif + return; + } - while (_isRunning) { + _state = State::Running; + + while (_state == State::Running) { // Record how long the loop takes to execute auto loopStartTimestamp = p_high_resolution_clock::now(); @@ -314,11 +331,11 @@ void SendQueue::run() { } // since we're a while loop, give the thread a chance to process events - QCoreApplication::processEvents(); + QCoreApplication::sendPostedEvents(this, 0); // we just processed events so check now if we were just told to stop - if (!_isRunning) { - break; + if (_state != State::Running) { + return; } if (_hasReceivedHandshakeACK && !sentAPacket) { @@ -525,5 +542,5 @@ void SendQueue::deactivate() { // this queue is inactive - emit that signal and stop the while emit queueInactive(); - _isRunning = false; + _state = State::Stopped; } diff --git a/libraries/networking/src/udt/SendQueue.h b/libraries/networking/src/udt/SendQueue.h index 2e7ec90c45..88b6b045b0 100644 --- a/libraries/networking/src/udt/SendQueue.h +++ b/libraries/networking/src/udt/SendQueue.h @@ -45,6 +45,12 @@ class SendQueue : public QObject { Q_OBJECT public: + enum class State { + NotStarted, + Running, + Stopped + }; + static std::unique_ptr create(Socket* socket, HifiSockAddr destination); void queuePacket(std::unique_ptr packet); @@ -106,7 +112,7 @@ private: std::atomic _atomicCurrentSequenceNumber { 0 };// Atomic for last sequence number sent out std::atomic _packetSendPeriod { 0 }; // Interval between two packet send event in microseconds, set from CC - std::atomic _isRunning { false }; + std::atomic _state { State::NotStarted }; std::atomic _estimatedTimeout { 0 }; // Estimated timeout, set from CC std::atomic _timeoutExpiryCount { 0 }; // The number of times the timeout has expired without response from client diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 56a00c6808..c148374edb 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -183,8 +183,7 @@ Connection& Socket::findOrCreateConnection(const HifiSockAddr& sockAddr) { auto connection = std::unique_ptr(new Connection(this, sockAddr, _ccFactory->create())); // we queue the connection to cleanup connection in case it asks for it during its own rate control sync - QObject::connect(connection.get(), &Connection::connectionInactive, this, &Socket::cleanupConnection, - Qt::QueuedConnection); + QObject::connect(connection.get(), &Connection::connectionInactive, this, &Socket::cleanupConnection); #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "Creating new connection to" << sockAddr; @@ -208,11 +207,15 @@ void Socket::clearConnections() { } void Socket::cleanupConnection(HifiSockAddr sockAddr) { -#ifdef UDT_CONNECTION_DEBUG - qCDebug(networking) << "Socket::cleanupConnection called for UDT connection to" << sockAddr; -#endif + auto it = _connectionsHash.find(sockAddr); - _connectionsHash.erase(sockAddr); + if (it != _connectionsHash.end()) { +#ifdef UDT_CONNECTION_DEBUG + qCDebug(networking) << "Socket::cleanupConnection called for UDT connection to" << sockAddr; +#endif + + _connectionsHash.erase(sockAddr); + } } void Socket::messageReceived(std::unique_ptr packetList) { @@ -297,8 +300,31 @@ void Socket::connectToSendSignal(const HifiSockAddr& destinationAddr, QObject* r void Socket::rateControlSync() { // enumerate our list of connections and ask each of them to send off periodic ACK packet for rate control + + // this way we do this is a little funny looking - we need to avoid the case where we call sync and + // (because of our Qt direct connection to the Connection's signal that it has been deactivated) + // an iterator on _connectionsHash would be invalidated by our own call to cleanupConnection + + // collect the sockets for all connections in a vector + + std::vector sockAddrVector; + sockAddrVector.reserve(_connectionsHash.size()); + for (auto& connection : _connectionsHash) { - connection.second->sync(); + sockAddrVector.emplace_back(connection.first); + } + + // enumerate that vector of HifiSockAddr objects + for (auto& sockAddr : sockAddrVector) { + // pull out the respective connection via a quick find on the unordered_map + auto it = _connectionsHash.find(sockAddr); + + if (it != _connectionsHash.end()) { + // if the connection is erased while calling sync since we are not holding an iterator that was invalidated + // we're good to go + auto& connection = _connectionsHash[sockAddr]; + connection->sync(); + } } if (_synTimer->interval() != _synInterval) { From 36e2d4fc76f201c315df315f60fd50689b6a3baa Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 17 Sep 2015 14:16:03 -0700 Subject: [PATCH 061/192] add check for NotStarted state in PL queue --- libraries/networking/src/udt/SendQueue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index 31c2f41259..ac6c8238e8 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -138,7 +138,7 @@ void SendQueue::queuePacketList(std::unique_ptr packetList) { _emptyCondition.notify_one(); } - if (!this->thread()->isRunning()) { + if (!this->thread()->isRunning() && _state == State::NotStarted) { this->thread()->start(); } } From 1f9bb22b9efe4c86e7c6b778d23baf64805cd684 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 17 Sep 2015 14:17:03 -0700 Subject: [PATCH 062/192] fix some wording in Socket comment --- libraries/networking/src/udt/Socket.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index c148374edb..55e807301e 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -301,7 +301,7 @@ void Socket::rateControlSync() { // enumerate our list of connections and ask each of them to send off periodic ACK packet for rate control - // this way we do this is a little funny looking - we need to avoid the case where we call sync and + // the way we do this is a little funny looking - we need to avoid the case where we call sync and // (because of our Qt direct connection to the Connection's signal that it has been deactivated) // an iterator on _connectionsHash would be invalidated by our own call to cleanupConnection From a914ec82c315d87e6b994d6c579feb9330ffd91f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 17 Sep 2015 14:17:43 -0700 Subject: [PATCH 063/192] more comment correctness for Socket --- libraries/networking/src/udt/Socket.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 55e807301e..3327ea9dc7 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -320,7 +320,7 @@ void Socket::rateControlSync() { auto it = _connectionsHash.find(sockAddr); if (it != _connectionsHash.end()) { - // if the connection is erased while calling sync since we are not holding an iterator that was invalidated + // if the connection is erased while calling sync since we are re-using the iterator that was invalidated // we're good to go auto& connection = _connectionsHash[sockAddr]; connection->sync(); From 285a6cc7385f8c946685b3f39164df6e179e4288 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 17 Sep 2015 14:51:48 -0700 Subject: [PATCH 064/192] use erase directly for cleanupConnection --- libraries/networking/src/udt/Socket.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 3327ea9dc7..50f2e67007 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -207,14 +207,12 @@ void Socket::clearConnections() { } void Socket::cleanupConnection(HifiSockAddr sockAddr) { - auto it = _connectionsHash.find(sockAddr); + auto numErased = _connectionsHash.erase(sockAddr); - if (it != _connectionsHash.end()) { + if (numErased > 0) { #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "Socket::cleanupConnection called for UDT connection to" << sockAddr; #endif - - _connectionsHash.erase(sockAddr); } } From df0b9e7dbb12b7aa6309b3761eb91393d338ab91 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 17 Sep 2015 15:59:19 -0700 Subject: [PATCH 065/192] refactor bubble wand to prevent duplicate bubbles --- examples/toys/bubblewand/createWand.js | 17 +- examples/toys/bubblewand/wand.js | 403 ++++++++++++------------- 2 files changed, 204 insertions(+), 216 deletions(-) diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 7b902e2874..6548090b74 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -9,7 +9,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - +var IN_TOYBOX = false; Script.include("../../utilities.js"); Script.include("../../libraries/utils.js"); @@ -17,14 +17,25 @@ Script.include("../../libraries/utils.js"); var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; -var WAND_SCRIPT_URL = Script.resolvePath("wand.js?" + randInt(0, 4000)); +var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); //create the wand in front of the avatar var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +var tablePosition = { + x:546.48, + y:495.63, + z:506.25 +} var wand = Entities.addEntity({ + name:'Bubble Wand', type: "Model", modelURL: WAND_MODEL, - position: center, + position: IN_TOYBOX? tablePosition: center, + gravity: { + x: 0, + y: 0, + z: 0, + }, dimensions: { x: 0.025, y: 0.125, diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index f43fc0df03..2b57ccda78 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -13,229 +13,206 @@ (function() { - Script.include("../../utilities.js"); - Script.include("../../libraries/utils.js"); + Script.include("../../utilities.js"); + Script.include("../../libraries/utils.js"); - var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var BUBBLE_SCRIPT = Script.resolvePath('bubble.js?' + randInt(0, 5000)); + var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; + var BUBBLE_SCRIPT = Script.resolvePath('bubble.js'); - var HAND_SIZE = 0.25; - var TARGET_SIZE = 0.04; - - var BUBBLE_GRAVITY = { - x: 0, - y: -0.1, - z: 0 - }; - - var WAVE_MODE_GROWTH_FACTOR = 0.005; - var SHRINK_LOWER_LIMIT = 0.02; - var SHRINK_FACTOR = 0.001; - var BUBBLE_DIVISOR = 50; - var BUBBLE_LIFETIME_MIN = 3; - var BUBBLE_LIFETIME_MAX = 8; - var BUBBLE_SIZE_MIN = 1; - var BUBBLE_SIZE_MAX = 5; - var WAND_TIP_OFFSET = 0.05; - var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; - var MAX_VELOCITY_STRENGTH = 10; - var VELOCITY_THRESHOLD = 1; - var VELOCITY_STRENGTH_MULTIPLIER = 100; - - function getGustDetectorPosition() { - //put the zone in front of your avatar's face - var DISTANCE_IN_FRONT = 0.2; - var DISTANCE_UP = 0.5; - var DISTANCE_TO_SIDE = 0.0; - - var up = Quat.getUp(MyAvatar.orientation); - var front = Quat.getFront(MyAvatar.orientation); - var right = Quat.getRight(MyAvatar.orientation); - - var upOffset = Vec3.multiply(up, DISTANCE_UP); - var rightOffset = Vec3.multiply(right, DISTANCE_TO_SIDE); - var frontOffset = Vec3.multiply(front, DISTANCE_IN_FRONT); - - var offset = Vec3.sum(Vec3.sum(rightOffset, frontOffset), upOffset); - var position = Vec3.sum(MyAvatar.position, offset); - return position; - }; + var BUBBLE_GRAVITY = { + x: 0, + y: -0.1, + z: 0 + }; + var BUBBLE_DIVISOR = 50; + var BUBBLE_LIFETIME_MIN = 3; + var BUBBLE_LIFETIME_MAX = 8; + var BUBBLE_SIZE_MIN = 1; + var BUBBLE_SIZE_MAX = 5; + var GROWTH_FACTOR = 0.005; + var SHRINK_FACTOR = 0.001; + var SHRINK_LOWER_LIMIT = 0.02; + var WAND_TIP_OFFSET = 0.05; + var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; + var VELOCITY_STRENGH_MAX = 10; + var VELOCITY_STRENGTH_MULTIPLIER = 100; + var VELOCITY_THRESHOLD = 1; - var wandEntity = this; + var _this; - this.preload = function(entityID) { - this.entityID = entityID; - this.properties = Entities.getEntityProperties(this.entityID); - BubbleWand.init(); - } - - this.unload = function(entityID) { - Entities.editEntity(entityID, { - name: "" - }); - Script.update.disconnect(BubbleWand.update); - Entities.deleteEntity(BubbleWand.currentBubble); - while (BubbleWand.bubbles.length > 0) { - Entities.deleteEntity(BubbleWand.bubbles.pop()); + var BubbleWand = function() { + _this = this; + print('WAND CONSTRUCTOR!') } - }; - - - var BubbleWand = { - bubbles: [], - timeSinceMoved: 0, - currentBubble: null, - update: function(deltaTime) { - BubbleWand.internalUpdate(deltaTime); - }, - internalUpdate: function(deltaTime) { - var properties = Entities.getEntityProperties(wandEntity.entityID); - - var _t = this; - var GRAB_USER_DATA_KEY = "grabKey"; - var defaultGrabData = { - activated: false, - avatarId: null - }; - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, wandEntity.entityID, defaultGrabData); - if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { - // remember we're being grabbed so we can detect being released - _t.beingGrabbed = true; - - // print out that we're being grabbed - _t.handleGrabbedWand(properties); - - } else if (_t.beingGrabbed) { - - // if we are not being grabbed, and we previously were, then we were just released, remember that - // and print out a message - _t.beingGrabbed = false; - - return - } - var wandTipPosition = _t.getWandTipPosition(properties); - //update the bubble to stay with the wand tip - Entities.editEntity(_t.currentBubble, { - position: wandTipPosition, - - }); - }, - getWandTipPosition: function(properties) { - var upVector = Quat.getUp(properties.rotation); - var frontVector = Quat.getFront(properties.rotation); - var upOffset = Vec3.multiply(upVector, WAND_TIP_OFFSET); - var wandTipPosition = Vec3.sum(properties.position, upOffset); - return wandTipPosition - }, - handleGrabbedWand: function(properties) { - var _t = this; - - var wandPosition = properties.position; - - var wandTipPosition = _t.getWandTipPosition(properties) - _t.wandTipPosition = wandTipPosition; - - var velocity = Vec3.subtract(wandPosition, _t.lastPosition) - var velocityStrength = Vec3.length(velocity) * VELOCITY_STRENGTH_MULTIPLIER; - - if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { - velocityStrength = 0 - } - - //store the last position of the wand for velocity calculations - _t.lastPosition = wandPosition; - - if (velocityStrength > MAX_VELOCITY_STRENGTH) { - velocityStrength = MAX_VELOCITY_STRENGTH - } - - //actually grow the bubble - var dimensions = Entities.getEntityProperties(_t.currentBubble).dimensions; - - if (velocityStrength > VELOCITY_THRESHOLD) { - - //add some variation in bubble sizes - var bubbleSize = randInt(BUBBLE_SIZE_MIN, BUBBLE_SIZE_MAX); - bubbleSize = bubbleSize / BUBBLE_DIVISOR; - - //release the bubble if its dimensions are bigger than the bubble size - if (dimensions.x > bubbleSize) { - //bubbles pop after existing for a bit -- so set a random lifetime - var lifetime = randInt(BUBBLE_LIFETIME_MIN, BUBBLE_LIFETIME_MAX); - - Entities.editEntity(_t.currentBubble, { - velocity: velocity, - lifetime: lifetime - }); - - //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble - BubbleWand.spawnBubble(); - - return - } else { - dimensions.x += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - dimensions.y += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - dimensions.z += WAVE_MODE_GROWTH_FACTOR * velocityStrength; - - } - } else { - if (dimensions.x >= SHRINK_LOWER_LIMIT) { - dimensions.x -= SHRINK_FACTOR; - dimensions.y -= SHRINK_FACTOR; - dimensions.z -= SHRINK_FACTOR; - } - - } - - //update the bubble to stay with the wand tip - Entities.editEntity(_t.currentBubble, { - - dimensions: dimensions - }); - }, - spawnBubble: function() { - var _t = this; - - //create a new bubble at the tip of the wand - //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position - var properties = Entities.getEntityProperties(wandEntity.entityID); - var wandPosition = properties.position; - - wandTipPosition = _t.getWandTipPosition(properties); - _t.wandTipPosition = wandTipPosition; - - //store the position of the tip for use in velocity calculations - _t.lastPosition = wandPosition; - - //create a bubble at the wand tip - _t.currentBubble = Entities.addEntity({ - type: 'Model', - modelURL: BUBBLE_MODEL, - position: wandTipPosition, - dimensions: { - x: 0.01, - y: 0.01, - z: 0.01 + BubbleWand.prototype = { + bubbles: [], + currentBubble: null, + preload: function(entityID) { + this.entityID = entityID; + this.properties = Entities.getEntityProperties(this.entityID); + Script.update.connect(this.update); }, - collisionsWillMove: true, //true - ignoreForCollisions: false, //false - gravity: BUBBLE_GRAVITY, - shapeType: "sphere", - script: BUBBLE_SCRIPT, - }); - //add this bubble to an array of bubbles so we can keep track of them - _t.bubbles.push(_t.currentBubble) + unload: function(entityID) { + Script.update.disconnect(this.update); + }, + update: function(deltaTime) { + var GRAB_USER_DATA_KEY = "grabKey"; + var defaultGrabData = { + activated: false, + avatarId: null + }; + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, _this.entityID, defaultGrabData); + if (grabData.activated && grabData.avatarId === MyAvatar.sessionUUID) { + print('being grabbed') + + _this.beingGrabbed = true; + + + if (_this.currentBubble === null) { + _this.spawnBubble(); + } + var properties = Entities.getEntityProperties(_this.entityID); + + // remember we're being grabbed so we can detect being released + + + // print out that we're being grabbed + _this.growBubbleWithWandVelocity(properties); + + var wandTipPosition = _this.getWandTipPosition(properties); + //update the bubble to stay with the wand tip + Entities.editEntity(_this.currentBubble, { + position: wandTipPosition, + + }); + + } else if (_this.beingGrabbed) { + + // if we are not being grabbed, and we previously were, then we were just released, remember that + // and print out a message + _this.beingGrabbed = false; + Entities.deleteEntity(_this.currentBubble); + return + } + + + }, + getWandTipPosition: function(properties) { + var upVector = Quat.getUp(properties.rotation); + var frontVector = Quat.getFront(properties.rotation); + var upOffset = Vec3.multiply(upVector, WAND_TIP_OFFSET); + var wandTipPosition = Vec3.sum(properties.position, upOffset); + this.wandTipPosition = wandTipPosition; + return wandTipPosition + }, + growBubbleWithWandVelocity: function(properties) { + print('grow bubble') + var wandPosition = properties.position; + var wandTipPosition = this.getWandTipPosition(properties) + + + var velocity = Vec3.subtract(wandPosition, this.lastPosition) + var velocityStrength = Vec3.length(velocity) * VELOCITY_STRENGTH_MULTIPLIER; + + + + // if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { + // velocityStrength = 0 + // } + + // if (velocityStrength > VELOCITY_STRENGTH_MAX) { + // velocityStrength = VELOCITY_STRENGTH_MAX + // } + + print('VELOCITY STRENGTH:::'+velocityStrength) + print('V THRESH:'+ VELOCITY_THRESHOLD) + print('debug 1') + //store the last position of the wand for velocity calculations + this.lastPosition = wandPosition; + + //actually grow the bubble + var dimensions = Entities.getEntityProperties(this.currentBubble).dimensions; + print('dim x '+dimensions.x) + if (velocityStrength > VELOCITY_THRESHOLD) { + + //add some variation in bubble sizes + var bubbleSize = randInt(BUBBLE_SIZE_MIN, BUBBLE_SIZE_MAX); + bubbleSize = bubbleSize / BUBBLE_DIVISOR; + + print('bubbleSize '+ bubbleSize) + //release the bubble if its dimensions are bigger than the bubble size + if (dimensions.x > bubbleSize) { + //bubbles pop after existing for a bit -- so set a random lifetime + var lifetime = randInt(BUBBLE_LIFETIME_MIN, BUBBLE_LIFETIME_MAX); + + Entities.editEntity(this.currentBubble, { + velocity: velocity, + lifetime: lifetime + }); + + //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble + this.spawnBubble(); + + return + } else { + dimensions.x += GROWTH_FACTOR * velocityStrength; + dimensions.y += GROWTH_FACTOR * velocityStrength; + dimensions.z += GROWTH_FACTOR * velocityStrength; + + } + } else { + if (dimensions.x >= SHRINK_LOWER_LIMIT) { + dimensions.x -= SHRINK_FACTOR; + dimensions.y -= SHRINK_FACTOR; + dimensions.z -= SHRINK_FACTOR; + } + + } + + //make the bubble bigger + Entities.editEntity(this.currentBubble, { + dimensions: dimensions + }); + }, + spawnBubble: function() { + + //create a new bubble at the tip of the wand + //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position + var properties = Entities.getEntityProperties(this.entityID); + var wandPosition = properties.position; + + wandTipPosition = this.getWandTipPosition(properties); + this.wandTipPosition = wandTipPosition; + + //store the position of the tip for use in velocity calculations + this.lastPosition = wandPosition; + + //create a bubble at the wand tip + this.currentBubble = Entities.addEntity({ + name:'Bubble', + type: 'Model', + modelURL: BUBBLE_MODEL, + position: wandTipPosition, + dimensions: { + x: 0.01, + y: 0.01, + z: 0.01 + }, + collisionsWillMove: true, //true + ignoreForCollisions: false, //false + gravity: BUBBLE_GRAVITY, + shapeType: "sphere", + script: BUBBLE_SCRIPT, + }); + //add this bubble to an array of bubbles so we can keep track of them + this.bubbles.push(this.currentBubble) + + } - }, - init: function() { - var _t = this; - this.spawnBubble(); - Script.update.connect(BubbleWand.update); } - } - + return new BubbleWand(); }) \ No newline at end of file From 9746d31f5ef46b698fd5c7f6a0709b8f6605fb26 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Sep 2015 17:41:29 -0700 Subject: [PATCH 066/192] notes for moving hand updates --- libraries/animation/src/Rig.cpp | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 8b5bad5605..7180838f89 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -735,12 +735,8 @@ void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::q return; } - if (freeLineage.isEmpty()) { - return; - } - int numFree = freeLineage.size(); - if (_enableAnimGraph && _animSkeleton) { + // adebug: comment this stuff out to disable normal hand updates if (endIndex == _leftHandJointIndex) { _animVars.set("leftHandPosition", targetPosition); _animVars.set("leftHandRotation", targetRotation); @@ -751,6 +747,10 @@ void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::q return; } + if (freeLineage.isEmpty()) { + return; + } + // store and remember topmost parent transform glm::mat4 topParentTransform; { @@ -766,6 +766,7 @@ void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::q // relax toward default rotation // NOTE: ideally this should use dt and a relaxation timescale to compute how much to relax + int numFree = freeLineage.size(); for (int j = 0; j < numFree; j++) { int nextIndex = freeLineage.at(j); JointState& nextState = _jointStates[nextIndex]; @@ -987,6 +988,7 @@ void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, floa void Rig::updateNeckJoint(int index, const HeadParameters& params) { if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { if (_enableAnimGraph && _animSkeleton) { + // adebug comment this block out out to disable head target // the params.localHeadOrientation is composed incorrectly, so re-compose it correctly from pitch, yaw and roll. glm::quat realLocalHeadOrientation = (glm::angleAxis(glm::radians(-params.localHeadRoll), Z_AXIS) * glm::angleAxis(glm::radians(params.localHeadYaw), Y_AXIS) * @@ -1046,6 +1048,26 @@ void Rig::updateFromHandParameters(const HandParameters& params, float dt) { if (_enableAnimGraph && _animSkeleton) { + /* adebug add thsi stuff to update hands from another path + auto rootPose = _animSkeleton->getAbsoluteBindPose(_rootJointIndex); + // TODO: figure out how to get away without using this HACK + glm::quat yFlipHACK = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + if (params.isLeftEnabled) { + _animVars.set("leftHandPosition", yFlipHACK * (rootPose.trans + rootPose.rot * params.leftPosition)); + _animVars.set("leftHandRotation", yFlipHACK * rootPose.rot * params.leftOrientation); + } else { + _animVars.unset("leftHandPosition"); + _animVars.unset("leftHandRotation"); + } + if (params.isRightEnabled) { + _animVars.set("rightHandPosition", yFlipHACK * (rootPose.trans + rootPose.rot * params.rightPosition)); + _animVars.set("rightHandRotation", yFlipHACK * rootPose.rot * params.rightOrientation); + } else { + _animVars.unset("rightHandPosition"); + _animVars.unset("rightHandRotation"); + } + */ + // set leftHand grab vars _animVars.set("isLeftHandIdle", false); _animVars.set("isLeftHandPoint", false); From 4505d5999ca4ad72ec82baa8de880d166dc207c7 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 17 Sep 2015 15:54:36 -0700 Subject: [PATCH 067/192] route hand data differently for new Anim system --- libraries/animation/src/Rig.cpp | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 7180838f89..025cb5f3d1 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -736,14 +736,7 @@ void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::q } if (_enableAnimGraph && _animSkeleton) { - // adebug: comment this stuff out to disable normal hand updates - if (endIndex == _leftHandJointIndex) { - _animVars.set("leftHandPosition", targetPosition); - _animVars.set("leftHandRotation", targetRotation); - } else if (endIndex == _rightHandJointIndex) { - _animVars.set("rightHandPosition", targetPosition); - _animVars.set("rightHandRotation", targetRotation); - } + // the hand data goes through a different path: Rig::updateFromHandParameters() --> early-exit return; } @@ -988,7 +981,6 @@ void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, floa void Rig::updateNeckJoint(int index, const HeadParameters& params) { if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { if (_enableAnimGraph && _animSkeleton) { - // adebug comment this block out out to disable head target // the params.localHeadOrientation is composed incorrectly, so re-compose it correctly from pitch, yaw and roll. glm::quat realLocalHeadOrientation = (glm::angleAxis(glm::radians(-params.localHeadRoll), Z_AXIS) * glm::angleAxis(glm::radians(params.localHeadYaw), Y_AXIS) * @@ -1048,25 +1040,22 @@ void Rig::updateFromHandParameters(const HandParameters& params, float dt) { if (_enableAnimGraph && _animSkeleton) { - /* adebug add thsi stuff to update hands from another path - auto rootPose = _animSkeleton->getAbsoluteBindPose(_rootJointIndex); - // TODO: figure out how to get away without using this HACK + // TODO: figure out how to obtain the yFlip from where it is actually stored glm::quat yFlipHACK = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); if (params.isLeftEnabled) { - _animVars.set("leftHandPosition", yFlipHACK * (rootPose.trans + rootPose.rot * params.leftPosition)); - _animVars.set("leftHandRotation", yFlipHACK * rootPose.rot * params.leftOrientation); + _animVars.set("leftHandPosition", yFlipHACK * params.leftPosition); + _animVars.set("leftHandRotation", yFlipHACK * params.leftOrientation); } else { _animVars.unset("leftHandPosition"); _animVars.unset("leftHandRotation"); } if (params.isRightEnabled) { - _animVars.set("rightHandPosition", yFlipHACK * (rootPose.trans + rootPose.rot * params.rightPosition)); - _animVars.set("rightHandRotation", yFlipHACK * rootPose.rot * params.rightOrientation); + _animVars.set("rightHandPosition", yFlipHACK * params.rightPosition); + _animVars.set("rightHandRotation", yFlipHACK * params.rightOrientation); } else { _animVars.unset("rightHandPosition"); _animVars.unset("rightHandRotation"); } - */ // set leftHand grab vars _animVars.set("isLeftHandIdle", false); From 4459708b0e928e708bd1498973d6f223c7ab1879 Mon Sep 17 00:00:00 2001 From: Shared Vive Room Date: Thu, 17 Sep 2015 17:16:12 -0700 Subject: [PATCH 068/192] Fix for getEyePose in OpenVRDisplayPlugin --- .../src/display-plugins/openvr/OpenVrDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/openvr/OpenVrDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/openvr/OpenVrDisplayPlugin.cpp index 95e0ef741d..fab9cc5dd4 100644 --- a/libraries/display-plugins/src/display-plugins/openvr/OpenVrDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/openvr/OpenVrDisplayPlugin.cpp @@ -161,7 +161,7 @@ void OpenVrDisplayPlugin::resetSensors() { } glm::mat4 OpenVrDisplayPlugin::getEyePose(Eye eye) const { - return _eyesData[eye]._eyeOffset * getHeadPose(); + return getHeadPose() * _eyesData[eye]._eyeOffset; } glm::mat4 OpenVrDisplayPlugin::getHeadPose() const { From 4e2cb00ec342301e4789e6367d1adf503f15120f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 17:33:53 -0700 Subject: [PATCH 069/192] first cut at only returning type specific properties in getEntityProperties --- .../src/RenderableModelEntityItem.cpp | 2 +- .../src/RenderableModelEntityItem.h | 2 +- libraries/entities/src/BoxEntityItem.cpp | 2 +- libraries/entities/src/BoxEntityItem.h | 2 +- libraries/entities/src/EntityItem.cpp | 6 +- libraries/entities/src/EntityItem.h | 2 +- .../entities/src/EntityItemProperties.cpp | 205 ++++++++++-------- libraries/entities/src/EntityItemProperties.h | 12 +- .../entities/src/EntityItemPropertiesMacros.h | 16 +- libraries/entities/src/EntityPropertyFlags.h | 25 --- .../entities/src/EntityScriptingInterface.cpp | 6 +- .../entities/src/EntityScriptingInterface.h | 2 +- libraries/entities/src/EntityTree.cpp | 8 +- libraries/entities/src/LightEntityItem.cpp | 2 +- libraries/entities/src/LightEntityItem.h | 2 +- libraries/entities/src/LineEntityItem.cpp | 2 +- libraries/entities/src/LineEntityItem.h | 2 +- libraries/entities/src/ModelEntityItem.cpp | 2 +- libraries/entities/src/ModelEntityItem.h | 2 +- .../entities/src/ParticleEffectEntityItem.cpp | 2 +- .../entities/src/ParticleEffectEntityItem.h | 2 +- libraries/entities/src/PolyLineEntityItem.cpp | 2 +- libraries/entities/src/PolyLineEntityItem.h | 2 +- libraries/entities/src/PolyVoxEntityItem.cpp | 2 +- libraries/entities/src/PolyVoxEntityItem.h | 2 +- libraries/entities/src/SphereEntityItem.cpp | 2 +- libraries/entities/src/SphereEntityItem.h | 2 +- libraries/entities/src/TextEntityItem.cpp | 2 +- libraries/entities/src/TextEntityItem.h | 2 +- libraries/entities/src/WebEntityItem.cpp | 2 +- libraries/entities/src/WebEntityItem.h | 2 +- libraries/entities/src/ZoneEntityItem.cpp | 2 +- libraries/entities/src/ZoneEntityItem.h | 2 +- libraries/shared/src/PropertyFlags.h | 1 + 34 files changed, 171 insertions(+), 160 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 9f0ce93721..74317b7eff 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -358,7 +358,7 @@ bool RenderableModelEntityItem::needsToCallUpdate() const { return _needsInitialSimulation || ModelEntityItem::needsToCallUpdate(); } -EntityItemProperties RenderableModelEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties RenderableModelEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = ModelEntityItem::getProperties(desiredProperties); // get the properties from our base class if (_originalTexturesRead) { properties.setTextureNames(_originalTextures); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index a893cf98f9..5b61046816 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -35,7 +35,7 @@ public: virtual ~RenderableModelEntityItem(); - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, diff --git a/libraries/entities/src/BoxEntityItem.cpp b/libraries/entities/src/BoxEntityItem.cpp index 88b4fec9e6..4f30060207 100644 --- a/libraries/entities/src/BoxEntityItem.cpp +++ b/libraries/entities/src/BoxEntityItem.cpp @@ -32,7 +32,7 @@ BoxEntityItem::BoxEntityItem(const EntityItemID& entityItemID, const EntityItemP setProperties(properties); } -EntityItemProperties BoxEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties BoxEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class properties._color = getXColor(); diff --git a/libraries/entities/src/BoxEntityItem.h b/libraries/entities/src/BoxEntityItem.h index e1bb284980..21e36c7031 100644 --- a/libraries/entities/src/BoxEntityItem.h +++ b/libraries/entities/src/BoxEntityItem.h @@ -23,7 +23,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 3bebbed527..ce719ee976 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1019,8 +1019,10 @@ quint64 EntityItem::getExpiry() const { return _created + (quint64)(_lifetime * (float)USECS_PER_SECOND); } -EntityItemProperties EntityItem::getProperties(QScriptValue desiredProperties) const { - EntityItemProperties properties; +EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProperties) const { + EncodeBitstreamParams params; // unknown + EntityPropertyFlags propertyFlags = desiredProperties.isEmpty() ? getEntityProperties(params) : desiredProperties; + EntityItemProperties properties(propertyFlags); properties._id = getID(); properties._idSet = true; properties._created = _created; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index bb736a40a2..a50e32aaee 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -131,7 +131,7 @@ public: EntityItemID getEntityItemID() const { return EntityItemID(_id); } // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; /// returns true if something changed virtual bool setProperties(const EntityItemProperties& properties); diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index dbcad8a328..5acc6158aa 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -36,7 +36,7 @@ StagePropertyGroup EntityItemProperties::_staticStage; EntityPropertyList PROP_LAST_ITEM = (EntityPropertyList)(PROP_AFTER_LAST_ITEM - 1); -EntityItemProperties::EntityItemProperties() : +EntityItemProperties::EntityItemProperties(EntityPropertyFlags desiredProperties) : CONSTRUCT_PROPERTY(visible, ENTITY_ITEM_DEFAULT_VISIBLE), CONSTRUCT_PROPERTY(position, 0.0f), @@ -140,7 +140,8 @@ _localRenderAlphaChanged(false), _defaultSettings(true), _naturalDimensions(1.0f, 1.0f, 1.0f), -_naturalPosition(0.0f, 0.0f, 0.0f) +_naturalPosition(0.0f, 0.0f, 0.0f), +_desiredProperties(desiredProperties) { } @@ -423,25 +424,25 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool EntityItemProperties defaultEntityProperties; if (_idSet) { - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(id, _id.toString()); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(id, _id.toString()); } - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(type, EntityTypes::getEntityTypeName(_type)); - COPY_PROPERTY_TO_QSCRIPTVALUE(position); - COPY_PROPERTY_TO_QSCRIPTVALUE(dimensions); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(type, EntityTypes::getEntityTypeName(_type)); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_POSITION, position); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DIMENSIONS, dimensions); if (!skipDefaults) { - COPY_PROPERTY_TO_QSCRIPTVALUE(naturalDimensions); // gettable, but not settable - COPY_PROPERTY_TO_QSCRIPTVALUE(naturalPosition); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DIMENSIONS, naturalDimensions); // gettable, but not settable + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_POSITION, naturalPosition); } - COPY_PROPERTY_TO_QSCRIPTVALUE(rotation); - COPY_PROPERTY_TO_QSCRIPTVALUE(velocity); - COPY_PROPERTY_TO_QSCRIPTVALUE(gravity); - COPY_PROPERTY_TO_QSCRIPTVALUE(acceleration); - COPY_PROPERTY_TO_QSCRIPTVALUE(damping); - COPY_PROPERTY_TO_QSCRIPTVALUE(restitution); - COPY_PROPERTY_TO_QSCRIPTVALUE(friction); - COPY_PROPERTY_TO_QSCRIPTVALUE(density); - COPY_PROPERTY_TO_QSCRIPTVALUE(lifetime); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ROTATION, rotation); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VELOCITY, velocity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GRAVITY, gravity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACCELERATION, acceleration); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DAMPING, damping); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RESTITUTION, restitution); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FRICTION, friction); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DENSITY, density); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIFETIME, lifetime); if (!skipDefaults || _lifetime != defaultEntityProperties._lifetime) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(age, getAge()); // gettable, but not settable @@ -450,78 +451,76 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool auto created = QDateTime::fromMSecsSinceEpoch(getCreated() / 1000.0f, Qt::UTC); // usec per msec created.setTimeSpec(Qt::OffsetFromUTC); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(created, created.toString(Qt::ISODate)); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(created, created.toString(Qt::ISODate)); - COPY_PROPERTY_TO_QSCRIPTVALUE(script); - COPY_PROPERTY_TO_QSCRIPTVALUE(scriptTimestamp); - COPY_PROPERTY_TO_QSCRIPTVALUE(registrationPoint); - COPY_PROPERTY_TO_QSCRIPTVALUE(angularVelocity); - COPY_PROPERTY_TO_QSCRIPTVALUE(angularDamping); - COPY_PROPERTY_TO_QSCRIPTVALUE(visible); - COPY_PROPERTY_TO_QSCRIPTVALUE(color); - COPY_PROPERTY_TO_QSCRIPTVALUE(colorSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(colorStart); - COPY_PROPERTY_TO_QSCRIPTVALUE(colorFinish); - COPY_PROPERTY_TO_QSCRIPTVALUE(alpha); - COPY_PROPERTY_TO_QSCRIPTVALUE(alphaSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(alphaStart); - COPY_PROPERTY_TO_QSCRIPTVALUE(alphaFinish); - COPY_PROPERTY_TO_QSCRIPTVALUE(modelURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(compoundShapeURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(animationURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(animationIsPlaying); - COPY_PROPERTY_TO_QSCRIPTVALUE(animationFPS); - COPY_PROPERTY_TO_QSCRIPTVALUE(animationFrameIndex); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(animationSettings, getAnimationSettings()); - COPY_PROPERTY_TO_QSCRIPTVALUE(glowLevel); - COPY_PROPERTY_TO_QSCRIPTVALUE(localRenderAlpha); - COPY_PROPERTY_TO_QSCRIPTVALUE(ignoreForCollisions); - COPY_PROPERTY_TO_QSCRIPTVALUE(collisionsWillMove); - COPY_PROPERTY_TO_QSCRIPTVALUE(isSpotlight); - COPY_PROPERTY_TO_QSCRIPTVALUE(intensity); - COPY_PROPERTY_TO_QSCRIPTVALUE(exponent); - COPY_PROPERTY_TO_QSCRIPTVALUE(cutoff); - COPY_PROPERTY_TO_QSCRIPTVALUE(locked); - COPY_PROPERTY_TO_QSCRIPTVALUE(textures); - COPY_PROPERTY_TO_QSCRIPTVALUE(userData); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SCRIPT, script); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SCRIPT_TIMESTAMP, scriptTimestamp); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_REGISTRATION_POINT, registrationPoint); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANGULAR_VELOCITY, angularVelocity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANGULAR_DAMPING, angularDamping); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VISIBLE, visible); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR, color); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_SPREAD, colorSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_START, colorStart); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_FINISH, colorFinish); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_SPREAD, alphaSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_START, alphaStart); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_FINISH, alphaFinish); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_URL, modelURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_URL, animationURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_PLAYING, animationIsPlaying); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FPS, animationFPS); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FRAME_INDEX, animationFrameIndex); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_SETTINGS, animationSettings, getAnimationSettings()); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IGNORE_FOR_COLLISIONS, ignoreForCollisions); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISIONS_WILL_MOVE, collisionsWillMove); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_SPOTLIGHT, isSpotlight); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_INTENSITY, intensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EXPONENT, exponent); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CUTOFF, cutoff); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); //COPY_PROPERTY_TO_QSCRIPTVALUE(simulationOwner); // TODO: expose this for JSON saves? - COPY_PROPERTY_TO_QSCRIPTVALUE(text); - COPY_PROPERTY_TO_QSCRIPTVALUE(lineHeight); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(textColor, getTextColor()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(backgroundColor, getBackgroundColor()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(shapeType, getShapeTypeAsString()); - COPY_PROPERTY_TO_QSCRIPTVALUE(maxParticles); - COPY_PROPERTY_TO_QSCRIPTVALUE(lifespan); - COPY_PROPERTY_TO_QSCRIPTVALUE(emitRate); - COPY_PROPERTY_TO_QSCRIPTVALUE(emitVelocity); - COPY_PROPERTY_TO_QSCRIPTVALUE(velocitySpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(emitAcceleration); - COPY_PROPERTY_TO_QSCRIPTVALUE(accelerationSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(particleRadius); - COPY_PROPERTY_TO_QSCRIPTVALUE(radiusSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(radiusStart); - COPY_PROPERTY_TO_QSCRIPTVALUE(radiusFinish); - COPY_PROPERTY_TO_QSCRIPTVALUE(marketplaceID); - COPY_PROPERTY_TO_QSCRIPTVALUE(name); - COPY_PROPERTY_TO_QSCRIPTVALUE(collisionSoundURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXT, text); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_HEIGHT, lineHeight); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_TEXT_COLOR, textColor, getTextColor()); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BACKGROUND_COLOR, backgroundColor, getBackgroundColor()); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MAX_PARTICLES, maxParticles); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIFESPAN, lifespan); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_RATE, emitRate); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_VELOCITY, emitVelocity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VELOCITY_SPREAD, velocitySpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_ACCELERATION, emitAcceleration); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACCELERATION_SPREAD, accelerationSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARTICLE_RADIUS, particleRadius); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_SPREAD, radiusSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_START, radiusStart); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_FINISH, radiusFinish); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(keyLightColor); - COPY_PROPERTY_TO_QSCRIPTVALUE(keyLightIntensity); - COPY_PROPERTY_TO_QSCRIPTVALUE(keyLightAmbientIntensity); - COPY_PROPERTY_TO_QSCRIPTVALUE(keyLightDirection); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(backgroundMode, getBackgroundModeAsString()); - COPY_PROPERTY_TO_QSCRIPTVALUE(sourceUrl); - COPY_PROPERTY_TO_QSCRIPTVALUE(voxelVolumeSize); - COPY_PROPERTY_TO_QSCRIPTVALUE(voxelData); - COPY_PROPERTY_TO_QSCRIPTVALUE(voxelSurfaceStyle); - COPY_PROPERTY_TO_QSCRIPTVALUE(lineWidth); - COPY_PROPERTY_TO_QSCRIPTVALUE(linePoints); - COPY_PROPERTY_TO_QSCRIPTVALUE(href); - COPY_PROPERTY_TO_QSCRIPTVALUE(description); - COPY_PROPERTY_TO_QSCRIPTVALUE(faceCamera); - COPY_PROPERTY_TO_QSCRIPTVALUE(actionData); - COPY_PROPERTY_TO_QSCRIPTVALUE(normals); - COPY_PROPERTY_TO_QSCRIPTVALUE(strokeWidths); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_COLOR, keyLightColor); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_INTENSITY, keyLightIntensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_INTENSITY, keyLightAmbientIntensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_DIRECTION, keyLightDirection); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BACKGROUND_MODE, backgroundMode, getBackgroundModeAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOURCE_URL, sourceUrl); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_VOLUME_SIZE, voxelVolumeSize); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_DATA, voxelData); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_SURFACE_STYLE, voxelSurfaceStyle); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_WIDTH, lineWidth); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_POINTS, linePoints); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_HREF, href); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DESCRIPTION, description); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FACE_CAMERA, faceCamera); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACTION_DATA, actionData); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NORMALS, normals); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_STROKE_WIDTHS, strokeWidths); // Sitting properties support if (!skipDefaults) { @@ -534,7 +533,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool sittingPoints.setProperty(i, sittingPoint); } sittingPoints.setProperty("length", _sittingPoints.size()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(sittingPoints, sittingPoints); // gettable, but not settable + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(sittingPoints, sittingPoints); // gettable, but not settable } if (!skipDefaults) { @@ -560,17 +559,21 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool _atmosphere.copyToScriptValue(properties, engine, skipDefaults, defaultEntityProperties); _skybox.copyToScriptValue(properties, engine, skipDefaults, defaultEntityProperties); - COPY_PROPERTY_TO_QSCRIPTVALUE(xTextureURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(yTextureURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(zTextureURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_TEXTURE_URL, xTextureURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_TEXTURE_URL, yTextureURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_TEXTURE_URL, zTextureURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(xNNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(yNNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(zNNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_N_NEIGHBOR_ID, xNNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_N_NEIGHBOR_ID, yNNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_N_NEIGHBOR_ID, zNNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(xPNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(yPNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(zPNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_P_NEIGHBOR_ID, xPNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_P_NEIGHBOR_ID, yPNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_P_NEIGHBOR_ID, zPNeighborID); + + // FIXME - I don't think these properties are supported any more + //COPY_PROPERTY_TO_QSCRIPTVALUE(glowLevel); + //COPY_PROPERTY_TO_QSCRIPTVALUE(localRenderAlpha); return properties; } @@ -709,6 +712,16 @@ void EntityItemPropertiesFromScriptValueHonorReadOnly(const QScriptValue &object } +QScriptValue EntityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags) { + QScriptValue result = engine->newObject(); + return result; +} + +void EntityPropertyFlagsFromScriptValue(const QScriptValue& object, EntityPropertyFlags& flags) { + +} + + // TODO: Implement support for edit packets that can span an MTU sized buffer. We need to implement a mechanism for the // encodeEntityEditPacket() method to communicate the the caller which properties couldn't fit in the buffer. Similar // to how we handle this in the Octree streaming case. diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index cec6b456a7..9e7eb80b49 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -58,7 +58,7 @@ class EntityItemProperties { friend class PolyVoxEntityItem; // TODO: consider removing this friend relationship and use public methods friend class PolyLineEntityItem; // TODO: consider removing this friend relationship and use public methods public: - EntityItemProperties(); + EntityItemProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()); virtual ~EntityItemProperties(); EntityTypes::EntityType getType() const { return _type; } @@ -259,13 +259,19 @@ private: QStringList _textureNames; glm::vec3 _naturalDimensions; glm::vec3 _naturalPosition; + + EntityPropertyFlags _desiredProperties; // if set will narrow scopes of copy/to/from to just these properties }; Q_DECLARE_METATYPE(EntityItemProperties); QScriptValue EntityItemPropertiesToScriptValue(QScriptEngine* engine, const EntityItemProperties& properties); QScriptValue EntityItemNonDefaultPropertiesToScriptValue(QScriptEngine* engine, const EntityItemProperties& properties); -void EntityItemPropertiesFromScriptValueIgnoreReadOnly(const QScriptValue &object, EntityItemProperties& properties); -void EntityItemPropertiesFromScriptValueHonorReadOnly(const QScriptValue &object, EntityItemProperties& properties); +void EntityItemPropertiesFromScriptValueIgnoreReadOnly(const QScriptValue& object, EntityItemProperties& properties); +void EntityItemPropertiesFromScriptValueHonorReadOnly(const QScriptValue& object, EntityItemProperties& properties); + +Q_DECLARE_METATYPE(EntityPropertyFlags); +QScriptValue EntityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags); +void EntityPropertyFlagsFromScriptValue(const QScriptValue& object, EntityPropertyFlags& flags); // define these inline here so the macros work diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index a3e31024d1..22b07a9abc 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -122,8 +122,9 @@ inline QScriptValue convertScriptValue(QScriptEngine* e, const EntityItemID& v) properties.setProperty(#g, groupProperties); \ } -#define COPY_PROPERTY_TO_QSCRIPTVALUE(P) \ - if (!skipDefaults || defaultEntityProperties._##P != _##P) { \ +#define COPY_PROPERTY_TO_QSCRIPTVALUE(p,P) \ + if ((_desiredProperties.isEmpty() || _desiredProperties.getHasProperty(p)) && \ + (!skipDefaults || defaultEntityProperties._##P != _##P)) { \ QScriptValue V = convertScriptValue(engine, _##P); \ properties.setProperty(#P, V); \ } @@ -131,12 +132,19 @@ inline QScriptValue convertScriptValue(QScriptEngine* e, const EntityItemID& v) #define COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(P, G) \ properties.setProperty(#P, G); -#define COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(P, G) \ - if (!skipDefaults || defaultEntityProperties._##P != _##P) { \ +#define COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(p, P, G) \ + if ((_desiredProperties.isEmpty() || _desiredProperties.getHasProperty(p)) && \ + (!skipDefaults || defaultEntityProperties._##P != _##P)) { \ QScriptValue V = convertScriptValue(engine, G); \ properties.setProperty(#P, V); \ } +#define COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(P, G) \ + if (!skipDefaults || defaultEntityProperties._##P != _##P) { \ + QScriptValue V = convertScriptValue(engine, G); \ + properties.setProperty(#P, V); \ + } + typedef glm::vec3 glmVec3; typedef glm::quat glmQuat; typedef QVector qVectorVec3; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index d4f880ed8f..df0aec01bf 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -12,33 +12,8 @@ #ifndef hifi_EntityPropertyFlags_h #define hifi_EntityPropertyFlags_h -/* -#include - -#include -#include - -#include -#include -#include -#include - -#include -#include // for SittingPoint -*/ - #include -/* -#include -#include - -#include "AtmospherePropertyGroup.h" -#include "EntityItemID.h" -#include "EntityItemPropertiesMacros.h" -#include "EntityTypes.h" -*/ - enum EntityPropertyList { PROP_PAGED_PROPERTY, PROP_CUSTOM_PROPERTIES_INCLUDED, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 5d4934ca7e..936e57eec4 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -100,11 +100,11 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) { - QScriptValue allProperties; - return getEntityProperties(identity, allProperties); + EntityPropertyFlags noSpecificProperties; + return getEntityProperties(identity, noSpecificProperties); } -EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity, QScriptValue desiredProperties) { +EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity, EntityPropertyFlags desiredProperties) { EntityItemProperties results; if (_entityTree) { _entityTree->withReadLock([&] { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index fe91535e04..e344154fb4 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -78,7 +78,7 @@ public slots: /// gets the current model properties for a specific model /// this function will not find return results in script engine contexts which don't have access to models Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid entityID); - Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid identity, QScriptValue desiredProperties); + Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid identity, EntityPropertyFlags desiredProperties); /// edits a model updating only the included properties, will return the identified EntityItemID in case of /// successful edit, if the input entityID is for an unknown model this function will have no effect diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index c4c02d364f..85ae6233e1 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -119,6 +119,8 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityTreeElementPointer containingElement, const SharedNodePointer& senderNode) { EntityItemProperties properties = origProperties; + qDebug() << "EntityTree::updateEntityWithElement() entity:" << entity->getEntityItemID(); + bool allowLockChange; QUuid senderID; if (senderNode.isNull()) { @@ -224,7 +226,10 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI QString entityScriptAfter = entity->getScript(); quint64 entityScriptTimestampAfter = entity->getScriptTimestamp(); bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; - if (entityScriptBefore != entityScriptAfter || reload) { + qDebug() << "EntityTree::updateEntityWithElement() entityScriptTimestampBefore:" << entityScriptTimestampBefore; + qDebug() << "EntityTree::updateEntityWithElement() entityScriptTimestampAfter:" << entityScriptTimestampAfter; + qDebug() << "EntityTree::updateEntityWithElement() reload:" << reload; + if (entityScriptBefore != entityScriptAfter || reload) { emitEntityScriptChanging(entity->getEntityItemID(), reload); // the entity script has changed } maybeNotifyNewCollisionSoundURL(collisionSoundURLBefore, entity->getCollisionSoundURL()); @@ -285,6 +290,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti } void EntityTree::emitEntityScriptChanging(const EntityItemID& entityItemID, const bool reload) { + qDebug() << "EntityTree::emitEntityScriptChanging(entityItemID:" << entityItemID << ", reload:" << reload<<")"; emit entityScriptChanging(entityItemID, reload); } diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index cb4130ead3..1d2e358799 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -56,7 +56,7 @@ void LightEntityItem::setDimensions(const glm::vec3& value) { } -EntityItemProperties LightEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties LightEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(isSpotlight, getIsSpotlight); diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h index fd94e6ef5c..0590955700 100644 --- a/libraries/entities/src/LightEntityItem.h +++ b/libraries/entities/src/LightEntityItem.h @@ -26,7 +26,7 @@ public: virtual void setDimensions(const glm::vec3& value); // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/LineEntityItem.cpp b/libraries/entities/src/LineEntityItem.cpp index 0fb2ea9e01..856d443a44 100644 --- a/libraries/entities/src/LineEntityItem.cpp +++ b/libraries/entities/src/LineEntityItem.cpp @@ -43,7 +43,7 @@ LineEntityItem::LineEntityItem(const EntityItemID& entityItemID, const EntityIte } -EntityItemProperties LineEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties LineEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index d06e2393b3..faa14d788e 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -23,7 +23,7 @@ class LineEntityItem : public EntityItem { ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index deb7db279b..70747937d8 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -43,7 +43,7 @@ ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID, const EntityI _color[0] = _color[1] = _color[2] = 0; } -EntityItemProperties ModelEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties ModelEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 281487f810..bf6d7a9785 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -25,7 +25,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index f032a988a2..b493cbfa48 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -147,7 +147,7 @@ void ParticleEffectEntityItem::computeAndUpdateDimensions() { } -EntityItemProperties ParticleEffectEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties ParticleEffectEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index dea8e0e8c8..053301206a 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -25,7 +25,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of this entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/PolyLineEntityItem.cpp b/libraries/entities/src/PolyLineEntityItem.cpp index 0b8593fdf2..29a44547ff 100644 --- a/libraries/entities/src/PolyLineEntityItem.cpp +++ b/libraries/entities/src/PolyLineEntityItem.cpp @@ -45,7 +45,7 @@ _strokeWidths(QVector(0.0f)) setProperties(properties); } -EntityItemProperties PolyLineEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties PolyLineEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { QWriteLocker lock(&_quadReadWriteLock); EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index efbadd73b4..27a116e1b1 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -23,7 +23,7 @@ class PolyLineEntityItem : public EntityItem { ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index a19430b245..70ae25c65c 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -104,7 +104,7 @@ const glm::vec3& PolyVoxEntityItem::getVoxelVolumeSize() const { } -EntityItemProperties PolyVoxEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties PolyVoxEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(voxelVolumeSize, getVoxelVolumeSize); COPY_ENTITY_PROPERTY_TO_PROPERTIES(voxelData, getVoxelData); diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index e34a99c1e1..4cacf22457 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -23,7 +23,7 @@ class PolyVoxEntityItem : public EntityItem { ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/SphereEntityItem.cpp b/libraries/entities/src/SphereEntityItem.cpp index 03d0f223f0..b00544e979 100644 --- a/libraries/entities/src/SphereEntityItem.cpp +++ b/libraries/entities/src/SphereEntityItem.cpp @@ -37,7 +37,7 @@ SphereEntityItem::SphereEntityItem(const EntityItemID& entityItemID, const Entit _volumeMultiplier *= PI / 6.0f; } -EntityItemProperties SphereEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties SphereEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class properties.setColor(getXColor()); return properties; diff --git a/libraries/entities/src/SphereEntityItem.h b/libraries/entities/src/SphereEntityItem.h index af1c87c57e..46798d6b10 100644 --- a/libraries/entities/src/SphereEntityItem.h +++ b/libraries/entities/src/SphereEntityItem.h @@ -23,7 +23,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index f67cc8375b..a4ce27f8b2 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -47,7 +47,7 @@ void TextEntityItem::setDimensions(const glm::vec3& value) { EntityItem::setDimensions(glm::vec3(value.x, value.y, TEXT_ENTITY_ITEM_FIXED_DEPTH)); } -EntityItemProperties TextEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties TextEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(text, getText); diff --git a/libraries/entities/src/TextEntityItem.h b/libraries/entities/src/TextEntityItem.h index 1254330dc6..101cef50b5 100644 --- a/libraries/entities/src/TextEntityItem.h +++ b/libraries/entities/src/TextEntityItem.h @@ -27,7 +27,7 @@ public: virtual ShapeType getShapeType() const { return SHAPE_TYPE_BOX; } // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 10d0101771..a4f60d5150 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -40,7 +40,7 @@ void WebEntityItem::setDimensions(const glm::vec3& value) { EntityItem::setDimensions(glm::vec3(value.x, value.y, WEB_ENTITY_ITEM_FIXED_DEPTH)); } -EntityItemProperties WebEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties WebEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(sourceUrl, getSourceUrl); return properties; diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index 58464e8f25..a2ca955916 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -26,7 +26,7 @@ public: virtual ShapeType getShapeType() const { return SHAPE_TYPE_BOX; } // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index c7c387d77e..8d843bd0b9 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -73,7 +73,7 @@ EnvironmentData ZoneEntityItem::getEnvironmentData() const { return result; } -EntityItemProperties ZoneEntityItem::getProperties(QScriptValue desiredProperties) const { +EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(keyLightColor, getKeyLightColor); diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 91a194e2e3..dbc3cede63 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -27,7 +27,7 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(QScriptValue desiredProperties = QScriptValue()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time diff --git a/libraries/shared/src/PropertyFlags.h b/libraries/shared/src/PropertyFlags.h index 8e74fd728b..2d512ece61 100644 --- a/libraries/shared/src/PropertyFlags.h +++ b/libraries/shared/src/PropertyFlags.h @@ -45,6 +45,7 @@ public: _maxFlag(INT_MIN), _minFlag(INT_MAX), _trailingFlipped(false), _encodedLength(0) { decode(fromEncoded); } void clear() { _flags.clear(); _maxFlag = INT_MIN; _minFlag = INT_MAX; _trailingFlipped = false; _encodedLength = 0; } + bool isEmpty() const { return _maxFlag == INT_MIN && _minFlag == INT_MAX && _trailingFlipped == false && _encodedLength == 0; } Enum firstFlag() const { return (Enum)_minFlag; } Enum lastFlag() const { return (Enum)_maxFlag; } From 81255c8379b0cabd4ad26fd2cd901eac21539c77 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 17:40:41 -0700 Subject: [PATCH 070/192] whitespace repair --- libraries/entities/src/EntityItem.h | 2 +- libraries/entities/src/EntityScriptingInterface.cpp | 4 ++-- libraries/entities/src/EntityTree.cpp | 8 +------- libraries/script-engine/src/ScriptCache.cpp | 4 +--- libraries/script-engine/src/ScriptEngine.cpp | 4 +--- libraries/script-engine/src/ScriptEngine.h | 4 ++-- 6 files changed, 8 insertions(+), 18 deletions(-) diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a50e32aaee..deebfc400d 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -131,7 +131,7 @@ public: EntityItemID getEntityItemID() const { return EntityItemID(_id); } // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; /// returns true if something changed virtual bool setProperties(const EntityItemProperties& properties); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 936e57eec4..0b54572c7f 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -105,12 +105,12 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit } EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity, EntityPropertyFlags desiredProperties) { - EntityItemProperties results; + EntityItemProperties results; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(identity)); if (entity) { - results = entity->getProperties(desiredProperties); + results = entity->getProperties(desiredProperties); // TODO: improve sitting points and naturalDimensions in the future, // for now we've included the old sitting points model behavior for entity types that are models diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 85ae6233e1..c4c02d364f 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -119,8 +119,6 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityTreeElementPointer containingElement, const SharedNodePointer& senderNode) { EntityItemProperties properties = origProperties; - qDebug() << "EntityTree::updateEntityWithElement() entity:" << entity->getEntityItemID(); - bool allowLockChange; QUuid senderID; if (senderNode.isNull()) { @@ -226,10 +224,7 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI QString entityScriptAfter = entity->getScript(); quint64 entityScriptTimestampAfter = entity->getScriptTimestamp(); bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; - qDebug() << "EntityTree::updateEntityWithElement() entityScriptTimestampBefore:" << entityScriptTimestampBefore; - qDebug() << "EntityTree::updateEntityWithElement() entityScriptTimestampAfter:" << entityScriptTimestampAfter; - qDebug() << "EntityTree::updateEntityWithElement() reload:" << reload; - if (entityScriptBefore != entityScriptAfter || reload) { + if (entityScriptBefore != entityScriptAfter || reload) { emitEntityScriptChanging(entity->getEntityItemID(), reload); // the entity script has changed } maybeNotifyNewCollisionSoundURL(collisionSoundURLBefore, entity->getCollisionSoundURL()); @@ -290,7 +285,6 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti } void EntityTree::emitEntityScriptChanging(const EntityItemID& entityItemID, const bool reload) { - qDebug() << "EntityTree::emitEntityScriptChanging(entityItemID:" << entityItemID << ", reload:" << reload<<")"; emit entityScriptChanging(entityItemID, reload); } diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 70076dd28f..96e624c187 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -28,7 +28,7 @@ ScriptCache::ScriptCache(QObject* parent) { } void ScriptCache::clearCache() { - _scriptCache.clear(); + _scriptCache.clear(); } QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool reload) { @@ -99,8 +99,6 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable return; } - qCDebug(scriptengine) << "ScriptCache::getScriptContents() scriptOrURL:" << scriptOrURL << " forceDownload:" << forceDownload << " on thread[" << QThread::currentThread() << "] expected thread[" << thread() << "]"; - if (_scriptCache.contains(url) && !forceDownload) { qCDebug(scriptengine) << "Found script in cache:" << url.toString(); #if 1 // def THREAD_DEBUGGING diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index d8392f1598..771906aa95 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -910,9 +910,7 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif - qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() scriptOrURL:" << entityScript << "forceDownload:" << forceRedownload << "on thread[" << QThread::currentThread() << "] expected thread[" << thread() << "]"; - - DependencyManager::get()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { + DependencyManager::get()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 001f54221b..83e65823a5 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -109,8 +109,8 @@ public: // Entity Script Related methods Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload = false); // will call the preload method once loaded Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method - Q_INVOKABLE void unloadAllEntityScripts(); - Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName); + Q_INVOKABLE void unloadAllEntityScripts(); + Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision); From b16dfb8a9e624adbe702d90a64327246dd0675dc Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 17:41:54 -0700 Subject: [PATCH 071/192] whitespace repair --- libraries/script-engine/src/ScriptEngine.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 771906aa95..692a320b4e 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -909,7 +909,6 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif - DependencyManager::get()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { #ifdef THREAD_DEBUGGING qDebug() << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; From 08babecb75305cc4d44d53a0c728620d527703c3 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 19:51:21 -0700 Subject: [PATCH 072/192] add support for mapping EntityPropertyFlags to QScriptValues and wire in desiredProps --- .../entities/src/EntityItemProperties.cpp | 127 +++++++++++++++++- libraries/entities/src/EntityItemProperties.h | 3 + .../entities/src/EntityItemPropertiesMacros.h | 5 + .../entities/src/EntityScriptingInterface.cpp | 4 +- .../entities/src/EntityScriptingInterface.h | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 1 + 6 files changed, 137 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5acc6158aa..61329e61a9 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -713,15 +713,138 @@ void EntityItemPropertiesFromScriptValueHonorReadOnly(const QScriptValue &object QScriptValue EntityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags) { - QScriptValue result = engine->newObject(); + return EntityItemProperties::entityPropertyFlagsToScriptValue(engine, flags); + QScriptValue result = engine->newObject(); return result; } void EntityPropertyFlagsFromScriptValue(const QScriptValue& object, EntityPropertyFlags& flags) { - + EntityItemProperties::entityPropertyFlagsFromScriptValue(object, flags); } +QScriptValue EntityItemProperties::entityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags) { + QScriptValue result = engine->newObject(); + return result; +} + +static QHash _propertyStringsToEnums; + +void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue& object, EntityPropertyFlags& flags) { + static std::once_flag initMap; + + std::call_once(initMap, [](){ + ADD_PROPERTY_TO_MAP(PROP_VISIBLE, Visible, visible, bool); + ADD_PROPERTY_TO_MAP(PROP_POSITION, Position, position, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_DIMENSIONS, Dimensions, dimensions, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_ROTATION, Rotation, rotation, glm::quat); + ADD_PROPERTY_TO_MAP(PROP_DENSITY, Density, density, float); + ADD_PROPERTY_TO_MAP(PROP_VELOCITY, Velocity, velocity, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_GRAVITY, Gravity, gravity, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_ACCELERATION, Acceleration, acceleration, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_DAMPING, Damping, damping, float); + ADD_PROPERTY_TO_MAP(PROP_RESTITUTION, Restitution, restitution, float); + ADD_PROPERTY_TO_MAP(PROP_FRICTION, Friction, friction, float); + ADD_PROPERTY_TO_MAP(PROP_LIFETIME, Lifetime, lifetime, float); + ADD_PROPERTY_TO_MAP(PROP_SCRIPT, Script, script, QString); + ADD_PROPERTY_TO_MAP(PROP_SCRIPT_TIMESTAMP, ScriptTimestamp, scriptTimestamp, quint64); + ADD_PROPERTY_TO_MAP(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString); + ADD_PROPERTY_TO_MAP(PROP_COLOR, Color, color, xColor); + ADD_PROPERTY_TO_MAP(PROP_COLOR_SPREAD, ColorSpread, colorSpread, xColor); + ADD_PROPERTY_TO_MAP(PROP_COLOR_START, ColorStart, colorStart, xColor); + ADD_PROPERTY_TO_MAP(PROP_COLOR_FINISH, ColorFinish, colorFinish, xColor); + ADD_PROPERTY_TO_MAP(PROP_ALPHA, Alpha, alpha, float); + ADD_PROPERTY_TO_MAP(PROP_ALPHA_SPREAD, AlphaSpread, alphaSpread, float); + ADD_PROPERTY_TO_MAP(PROP_ALPHA_START, AlphaStart, alphaStart, float); + ADD_PROPERTY_TO_MAP(PROP_ALPHA_FINISH, AlphaFinish, alphaFinish, float); + ADD_PROPERTY_TO_MAP(PROP_MODEL_URL, ModelURL, modelURL, QString); + ADD_PROPERTY_TO_MAP(PROP_COMPOUND_SHAPE_URL, CompoundShapeURL, compoundShapeURL, QString); + ADD_PROPERTY_TO_MAP(PROP_ANIMATION_URL, AnimationURL, animationURL, QString); + ADD_PROPERTY_TO_MAP(PROP_ANIMATION_FPS, AnimationFPS, animationFPS, float); + ADD_PROPERTY_TO_MAP(PROP_ANIMATION_FRAME_INDEX, AnimationFrameIndex, animationFrameIndex, float); + ADD_PROPERTY_TO_MAP(PROP_ANIMATION_PLAYING, AnimationIsPlaying, animationIsPlaying, bool); + ADD_PROPERTY_TO_MAP(PROP_REGISTRATION_POINT, RegistrationPoint, registrationPoint, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_ANGULAR_VELOCITY, AngularVelocity, angularVelocity, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_ANGULAR_DAMPING, AngularDamping, angularDamping, float); + ADD_PROPERTY_TO_MAP(PROP_IGNORE_FOR_COLLISIONS, IgnoreForCollisions, ignoreForCollisions, bool); + ADD_PROPERTY_TO_MAP(PROP_COLLISIONS_WILL_MOVE, CollisionsWillMove, collisionsWillMove, bool); + ADD_PROPERTY_TO_MAP(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool); + ADD_PROPERTY_TO_MAP(PROP_INTENSITY, Intensity, intensity, float); + ADD_PROPERTY_TO_MAP(PROP_EXPONENT, Exponent, exponent, float); + ADD_PROPERTY_TO_MAP(PROP_CUTOFF, Cutoff, cutoff, float); + ADD_PROPERTY_TO_MAP(PROP_LOCKED, Locked, locked, bool); + ADD_PROPERTY_TO_MAP(PROP_TEXTURES, Textures, textures, QString); + ADD_PROPERTY_TO_MAP(PROP_ANIMATION_SETTINGS, AnimationSettings, animationSettings, QString); + ADD_PROPERTY_TO_MAP(PROP_USER_DATA, UserData, userData, QString); + ADD_PROPERTY_TO_MAP(PROP_SIMULATION_OWNER, SimulationOwner, simulationOwner, SimulationOwner); + ADD_PROPERTY_TO_MAP(PROP_TEXT, Text, text, QString); + ADD_PROPERTY_TO_MAP(PROP_LINE_HEIGHT, LineHeight, lineHeight, float); + ADD_PROPERTY_TO_MAP(PROP_TEXT_COLOR, TextColor, textColor, xColor); + ADD_PROPERTY_TO_MAP(PROP_BACKGROUND_COLOR, BackgroundColor, backgroundColor, xColor); + ADD_PROPERTY_TO_MAP(PROP_SHAPE_TYPE, ShapeType, shapeType, ShapeType); + ADD_PROPERTY_TO_MAP(PROP_MAX_PARTICLES, MaxParticles, maxParticles, quint32); + ADD_PROPERTY_TO_MAP(PROP_LIFESPAN, Lifespan, lifespan, float); + ADD_PROPERTY_TO_MAP(PROP_EMIT_RATE, EmitRate, emitRate, float); + ADD_PROPERTY_TO_MAP(PROP_EMIT_VELOCITY, EmitVelocity, emitVelocity, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_VELOCITY_SPREAD, VelocitySpread, velocitySpread, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_EMIT_ACCELERATION, EmitAcceleration, emitAcceleration, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_ACCELERATION_SPREAD, AccelerationSpread, accelerationSpread, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_PARTICLE_RADIUS, ParticleRadius, particleRadius, float); + ADD_PROPERTY_TO_MAP(PROP_RADIUS_SPREAD, RadiusSpread, radiusSpread, float); + ADD_PROPERTY_TO_MAP(PROP_RADIUS_START, RadiusStart, radiusStart, float); + ADD_PROPERTY_TO_MAP(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float); + ADD_PROPERTY_TO_MAP(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString); + ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); + ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLightIntensity, keyLightIntensity, float); + ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_AMBIENT_INTENSITY, KeyLightAmbientIntensity, keyLightAmbientIntensity, float); + ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_DIRECTION, KeyLightDirection, keyLightDirection, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_VOXEL_DATA, VoxelData, voxelData, QByteArray); + ADD_PROPERTY_TO_MAP(PROP_VOXEL_SURFACE_STYLE, VoxelSurfaceStyle, voxelSurfaceStyle, uint16_t); + ADD_PROPERTY_TO_MAP(PROP_NAME, Name, name, QString); + ADD_PROPERTY_TO_MAP(PROP_BACKGROUND_MODE, BackgroundMode, backgroundMode, BackgroundMode); + ADD_PROPERTY_TO_MAP(PROP_SOURCE_URL, SourceUrl, sourceUrl, QString); + ADD_PROPERTY_TO_MAP(PROP_LINE_WIDTH, LineWidth, lineWidth, float); + ADD_PROPERTY_TO_MAP(PROP_LINE_POINTS, LinePoints, linePoints, QVector); + ADD_PROPERTY_TO_MAP(PROP_HREF, Href, href, QString); + ADD_PROPERTY_TO_MAP(PROP_DESCRIPTION, Description, description, QString); + ADD_PROPERTY_TO_MAP(PROP_FACE_CAMERA, FaceCamera, faceCamera, bool); + ADD_PROPERTY_TO_MAP(PROP_ACTION_DATA, ActionData, actionData, QByteArray); + ADD_PROPERTY_TO_MAP(PROP_NORMALS, Normals, normals, QVector); + ADD_PROPERTY_TO_MAP(PROP_STROKE_WIDTHS, StrokeWidths, strokeWidths, QVector); + ADD_PROPERTY_TO_MAP(PROP_X_TEXTURE_URL, XTextureURL, xTextureURL, QString); + ADD_PROPERTY_TO_MAP(PROP_Y_TEXTURE_URL, YTextureURL, yTextureURL, QString); + ADD_PROPERTY_TO_MAP(PROP_Z_TEXTURE_URL, ZTextureURL, zTextureURL, QString); + ADD_PROPERTY_TO_MAP(PROP_X_N_NEIGHBOR_ID, XNNeighborID, xNNeighborID, EntityItemID); + ADD_PROPERTY_TO_MAP(PROP_Y_N_NEIGHBOR_ID, YNNeighborID, yNNeighborID, EntityItemID); + ADD_PROPERTY_TO_MAP(PROP_Z_N_NEIGHBOR_ID, ZNNeighborID, zNNeighborID, EntityItemID); + ADD_PROPERTY_TO_MAP(PROP_X_P_NEIGHBOR_ID, XPNeighborID, xPNeighborID, EntityItemID); + ADD_PROPERTY_TO_MAP(PROP_Y_P_NEIGHBOR_ID, YPNeighborID, yPNeighborID, EntityItemID); + ADD_PROPERTY_TO_MAP(PROP_Z_P_NEIGHBOR_ID, ZPNeighborID, zPNeighborID, EntityItemID); + + // FIXME - these are not yet handled + //ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64); + //DEFINE_PROPERTY_GROUP(Stage, stage, StagePropertyGroup); + //DEFINE_PROPERTY_GROUP(Atmosphere, atmosphere, AtmospherePropertyGroup); + //DEFINE_PROPERTY_GROUP(Skybox, skybox, SkyboxPropertyGroup); + + }); + + if (object.isString()) { + if (_propertyStringsToEnums.contains(object.toString())) { + flags << _propertyStringsToEnums[object.toString()]; + } + } else if (object.isArray()) { + quint32 length = object.property("length").toInt32(); + for (quint32 i = 0; i < length; i++) { + QString propertyName = object.property(i).toString(); + if (_propertyStringsToEnums.contains(propertyName)) { + flags << _propertyStringsToEnums[propertyName]; + } + } + } +} + // TODO: Implement support for edit packets that can span an MTU sized buffer. We need to implement a mechanism for the // encodeEntityEditPacket() method to communicate the the caller which properties couldn't fit in the buffer. Similar // to how we handle this in the Octree streaming case. diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 9e7eb80b49..9856911207 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -67,6 +67,9 @@ public: virtual QScriptValue copyToScriptValue(QScriptEngine* engine, bool skipDefaults) const; virtual void copyFromScriptValue(const QScriptValue& object, bool honorReadOnly); + static QScriptValue entityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags); + static void entityPropertyFlagsFromScriptValue(const QScriptValue& object, EntityPropertyFlags& flags); + // editing related features supported by all entities quint64 getLastEdited() const { return _lastEdited; } float getEditedAgo() const /// Elapsed seconds since this entity was last edited diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 22b07a9abc..358606ef6a 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -304,6 +304,11 @@ inline xColor xColor_convertFromScriptValue(const QScriptValue& v, bool& isValid T _##n; \ static T _static##N; +//(PROP_VISIBLE, Visible, visible, bool); + +#define ADD_PROPERTY_TO_MAP(P, N, n, T) \ + _propertyStringsToEnums[#n] = P; + #define DEFINE_PROPERTY(P, N, n, T) \ public: \ T get##N() const { return _##n; } \ diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 0b54572c7f..5d00e51fd1 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -100,8 +100,8 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) { - EntityPropertyFlags noSpecificProperties; - return getEntityProperties(identity, noSpecificProperties); + EntityPropertyFlags noSpecificProperties; + return getEntityProperties(identity, noSpecificProperties); } EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity, EntityPropertyFlags desiredProperties) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e344154fb4..485df5cb03 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -78,7 +78,7 @@ public slots: /// gets the current model properties for a specific model /// this function will not find return results in script engine contexts which don't have access to models Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid entityID); - Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid identity, EntityPropertyFlags desiredProperties); + Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid identity, EntityPropertyFlags desiredProperties); /// edits a model updating only the included properties, will return the identified EntityItemID in case of /// successful edit, if the input entityID is for an unknown model this function will have no effect diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 692a320b4e..b8a97f0902 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -280,6 +280,7 @@ void ScriptEngine::init() { _controllerScriptingInterface->registerControllerTypes(this); } + qScriptRegisterMetaType(this, EntityPropertyFlagsToScriptValue, EntityPropertyFlagsFromScriptValue); qScriptRegisterMetaType(this, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly); qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue); qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue); From 6de424237ff107e6bc7d66ec3e2ac8053b56343d Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 20:24:15 -0700 Subject: [PATCH 073/192] add support for property groups in the desired properties logic --- .../entities/src/AtmospherePropertyGroup.cpp | 16 +++++----- .../entities/src/AtmospherePropertyGroup.h | 2 +- .../entities/src/EntityItemProperties.cpp | 30 ++++++++++++++----- .../entities/src/EntityItemPropertiesMacros.h | 10 ++++--- libraries/entities/src/EntityPropertyFlags.h | 2 +- libraries/entities/src/PropertyGroup.h | 2 +- .../entities/src/SkyboxPropertyGroup.cpp | 6 ++-- libraries/entities/src/SkyboxPropertyGroup.h | 2 +- libraries/entities/src/StagePropertyGroup.cpp | 16 +++++----- libraries/entities/src/StagePropertyGroup.h | 2 +- 10 files changed, 53 insertions(+), 35 deletions(-) diff --git a/libraries/entities/src/AtmospherePropertyGroup.cpp b/libraries/entities/src/AtmospherePropertyGroup.cpp index 364612d4db..f8117dbb62 100644 --- a/libraries/entities/src/AtmospherePropertyGroup.cpp +++ b/libraries/entities/src/AtmospherePropertyGroup.cpp @@ -32,14 +32,14 @@ AtmospherePropertyGroup::AtmospherePropertyGroup() { _hasStars = true; } -void AtmospherePropertyGroup::copyToScriptValue(QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Atmosphere, atmosphere, Center, center); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Atmosphere, atmosphere, InnerRadius, innerRadius); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Atmosphere, atmosphere, OuterRadius, outerRadius); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Atmosphere, atmosphere, MieScattering, mieScattering); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Atmosphere, atmosphere, RayleighScattering, rayleighScattering); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Atmosphere, atmosphere, ScatteringWavelengths, scatteringWavelengths); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Atmosphere, atmosphere, HasStars, hasStars); +void AtmospherePropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ATMOSPHERE_CENTER, Atmosphere, atmosphere, Center, center); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ATMOSPHERE_INNER_RADIUS, Atmosphere, atmosphere, InnerRadius, innerRadius); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ATMOSPHERE_OUTER_RADIUS, Atmosphere, atmosphere, OuterRadius, outerRadius); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ATMOSPHERE_MIE_SCATTERING, Atmosphere, atmosphere, MieScattering, mieScattering); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ATMOSPHERE_RAYLEIGH_SCATTERING, Atmosphere, atmosphere, RayleighScattering, rayleighScattering); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ATMOSPHERE_SCATTERING_WAVELENGTHS, Atmosphere, atmosphere, ScatteringWavelengths, scatteringWavelengths); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ATMOSPHERE_HAS_STARS, Atmosphere, atmosphere, HasStars, hasStars); } void AtmospherePropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) { diff --git a/libraries/entities/src/AtmospherePropertyGroup.h b/libraries/entities/src/AtmospherePropertyGroup.h index e081033cd6..c4b50822fa 100644 --- a/libraries/entities/src/AtmospherePropertyGroup.h +++ b/libraries/entities/src/AtmospherePropertyGroup.h @@ -53,7 +53,7 @@ public: virtual ~AtmospherePropertyGroup() {} // EntityItemProperty related helpers - virtual void copyToScriptValue(QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const; + virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings); virtual void debugDump() const; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 61329e61a9..102611dc2f 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -555,10 +555,6 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(originalTextures, textureNamesList); // gettable, but not settable } - _stage.copyToScriptValue(properties, engine, skipDefaults, defaultEntityProperties); - _atmosphere.copyToScriptValue(properties, engine, skipDefaults, defaultEntityProperties); - _skybox.copyToScriptValue(properties, engine, skipDefaults, defaultEntityProperties); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_TEXTURE_URL, xTextureURL); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_TEXTURE_URL, yTextureURL); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_TEXTURE_URL, zTextureURL); @@ -571,6 +567,10 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_P_NEIGHBOR_ID, yPNeighborID); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_P_NEIGHBOR_ID, zPNeighborID); + _stage.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + _atmosphere.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + _skybox.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + // FIXME - I don't think these properties are supported any more //COPY_PROPERTY_TO_QSCRIPTVALUE(glowLevel); //COPY_PROPERTY_TO_QSCRIPTVALUE(localRenderAlpha); @@ -822,11 +822,27 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_Y_P_NEIGHBOR_ID, YPNeighborID, yPNeighborID, EntityItemID); ADD_PROPERTY_TO_MAP(PROP_Z_P_NEIGHBOR_ID, ZPNeighborID, zPNeighborID, EntityItemID); + ADD_GROUP_PROPERTY_TO_MAP(PROP_SKYBOX_COLOR, Skybox, skybox, Color, color); + ADD_GROUP_PROPERTY_TO_MAP(PROP_SKYBOX_URL, Skybox, skybox, URL, url); + + ADD_GROUP_PROPERTY_TO_MAP(PROP_ATMOSPHERE_CENTER, Atmosphere, atmosphere, Center, center); + ADD_GROUP_PROPERTY_TO_MAP(PROP_ATMOSPHERE_INNER_RADIUS, Atmosphere, atmosphere, InnerRadius, innerRadius); + ADD_GROUP_PROPERTY_TO_MAP(PROP_ATMOSPHERE_OUTER_RADIUS, Atmosphere, atmosphere, OuterRadius, outerRadius); + ADD_GROUP_PROPERTY_TO_MAP(PROP_ATMOSPHERE_MIE_SCATTERING, Atmosphere, atmosphere, MieScattering, mieScattering); + ADD_GROUP_PROPERTY_TO_MAP(PROP_ATMOSPHERE_RAYLEIGH_SCATTERING, Atmosphere, atmosphere, RayleighScattering, rayleighScattering); + ADD_GROUP_PROPERTY_TO_MAP(PROP_ATMOSPHERE_SCATTERING_WAVELENGTHS, Atmosphere, atmosphere, ScatteringWavelengths, scatteringWavelengths); + ADD_GROUP_PROPERTY_TO_MAP(PROP_ATMOSPHERE_HAS_STARS, Atmosphere, atmosphere, HasStars, hasStars); + + ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_SUN_MODEL_ENABLED, Stage, stage, SunModelEnabled, sunModelEnabled); + ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_LATITUDE, Stage, stage, Latitude, latitude); + ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_LONGITUDE, Stage, stage, Longitude, longitude); + ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_ALTITUDE, Stage, stage, Altitude, altitude); + ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_DAY, Stage, stage, Day, day); + ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_HOUR, Stage, stage, Hour, hour); + ADD_GROUP_PROPERTY_TO_MAP(PROP_STAGE_AUTOMATIC_HOURDAY, Stage, stage, AutomaticHourDay, automaticHourDay); + // FIXME - these are not yet handled //ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64); - //DEFINE_PROPERTY_GROUP(Stage, stage, StagePropertyGroup); - //DEFINE_PROPERTY_GROUP(Atmosphere, atmosphere, AtmospherePropertyGroup); - //DEFINE_PROPERTY_GROUP(Skybox, skybox, SkyboxPropertyGroup); }); diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 358606ef6a..964529afca 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -111,8 +111,9 @@ inline QScriptValue convertScriptValue(QScriptEngine* e, const QByteArray& v) { inline QScriptValue convertScriptValue(QScriptEngine* e, const EntityItemID& v) { return QScriptValue(QUuid(v).toString()); } -#define COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(G,g,P,p) \ - if (!skipDefaults || defaultEntityProperties.get##G().get##P() != get##P()) { \ +#define COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(X,G,g,P,p) \ + if ((desiredProperties.isEmpty() || desiredProperties.getHasProperty(X)) && \ + (!skipDefaults || defaultEntityProperties.get##G().get##P() != get##P())) { \ QScriptValue groupProperties = properties.property(#g); \ if (!groupProperties.isValid()) { \ groupProperties = engine->newObject(); \ @@ -304,11 +305,12 @@ inline xColor xColor_convertFromScriptValue(const QScriptValue& v, bool& isValid T _##n; \ static T _static##N; -//(PROP_VISIBLE, Visible, visible, bool); - #define ADD_PROPERTY_TO_MAP(P, N, n, T) \ _propertyStringsToEnums[#n] = P; +#define ADD_GROUP_PROPERTY_TO_MAP(P, G, g, N, n) \ + _propertyStringsToEnums[#g "." #n] = P; + #define DEFINE_PROPERTY(P, N, n, T) \ public: \ T get##N() const { return _##n; } \ diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index df0aec01bf..68c318a579 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -165,6 +165,7 @@ enum EntityPropertyList { PROP_STAGE_ALTITUDE = PROP_SPECULAR_COLOR_UNUSED, PROP_STAGE_DAY = PROP_LINEAR_ATTENUATION_UNUSED, PROP_STAGE_HOUR = PROP_QUADRATIC_ATTENUATION_UNUSED, + PROP_STAGE_AUTOMATIC_HOURDAY = PROP_ANIMATION_FRAME_INDEX, PROP_ATMOSPHERE_CENTER = PROP_MAX_PARTICLES, PROP_ATMOSPHERE_INNER_RADIUS = PROP_LIFESPAN, PROP_ATMOSPHERE_OUTER_RADIUS = PROP_EMIT_RATE, @@ -175,7 +176,6 @@ enum EntityPropertyList { PROP_BACKGROUND_MODE = PROP_MODEL_URL, PROP_SKYBOX_COLOR = PROP_ANIMATION_URL, PROP_SKYBOX_URL = PROP_ANIMATION_FPS, - PROP_STAGE_AUTOMATIC_HOURDAY = PROP_ANIMATION_FRAME_INDEX, // Aliases/Piggyback properties for Web. These properties intentionally reuse the enum values for // other properties which will never overlap with each other. diff --git a/libraries/entities/src/PropertyGroup.h b/libraries/entities/src/PropertyGroup.h index 7ac4b54a8e..f780907896 100644 --- a/libraries/entities/src/PropertyGroup.h +++ b/libraries/entities/src/PropertyGroup.h @@ -55,7 +55,7 @@ public: virtual ~PropertyGroup() {} // EntityItemProperty related helpers - virtual void copyToScriptValue(QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const = 0; + virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const = 0; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) = 0; virtual void debugDump() const { } diff --git a/libraries/entities/src/SkyboxPropertyGroup.cpp b/libraries/entities/src/SkyboxPropertyGroup.cpp index 5be7c1eb49..624a382d55 100644 --- a/libraries/entities/src/SkyboxPropertyGroup.cpp +++ b/libraries/entities/src/SkyboxPropertyGroup.cpp @@ -20,9 +20,9 @@ SkyboxPropertyGroup::SkyboxPropertyGroup() { _url = QString(); } -void SkyboxPropertyGroup::copyToScriptValue(QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Skybox, skybox, Color, color); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Skybox, skybox, URL, url); +void SkyboxPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_SKYBOX_COLOR, Skybox, skybox, Color, color); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_SKYBOX_URL, Skybox, skybox, URL, url); } void SkyboxPropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) { diff --git a/libraries/entities/src/SkyboxPropertyGroup.h b/libraries/entities/src/SkyboxPropertyGroup.h index a92ec5abbb..25d982cbe7 100644 --- a/libraries/entities/src/SkyboxPropertyGroup.h +++ b/libraries/entities/src/SkyboxPropertyGroup.h @@ -33,7 +33,7 @@ public: virtual ~SkyboxPropertyGroup() {} // EntityItemProperty related helpers - virtual void copyToScriptValue(QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const; + virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings); virtual void debugDump() const; diff --git a/libraries/entities/src/StagePropertyGroup.cpp b/libraries/entities/src/StagePropertyGroup.cpp index 937dc2412e..fb27f9a464 100644 --- a/libraries/entities/src/StagePropertyGroup.cpp +++ b/libraries/entities/src/StagePropertyGroup.cpp @@ -36,14 +36,14 @@ StagePropertyGroup::StagePropertyGroup() { _automaticHourDay = false; } -void StagePropertyGroup::copyToScriptValue(QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Stage, stage, SunModelEnabled, sunModelEnabled); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Stage, stage, Latitude, latitude); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Stage, stage, Longitude, longitude); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Stage, stage, Altitude, altitude); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Stage, stage, Day, day); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Stage, stage, Hour, hour); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(Stage, stage, AutomaticHourDay, automaticHourDay); +void StagePropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_STAGE_SUN_MODEL_ENABLED, Stage, stage, SunModelEnabled, sunModelEnabled); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_STAGE_LATITUDE, Stage, stage, Latitude, latitude); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_STAGE_LONGITUDE, Stage, stage, Longitude, longitude); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_STAGE_ALTITUDE, Stage, stage, Altitude, altitude); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_STAGE_DAY, Stage, stage, Day, day); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_STAGE_HOUR, Stage, stage, Hour, hour); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_STAGE_AUTOMATIC_HOURDAY, Stage, stage, AutomaticHourDay, automaticHourDay); } void StagePropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings) { diff --git a/libraries/entities/src/StagePropertyGroup.h b/libraries/entities/src/StagePropertyGroup.h index c5df4fe0bd..32eb3462a3 100644 --- a/libraries/entities/src/StagePropertyGroup.h +++ b/libraries/entities/src/StagePropertyGroup.h @@ -33,7 +33,7 @@ public: virtual ~StagePropertyGroup() {} // EntityItemProperty related helpers - virtual void copyToScriptValue(QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const; + virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const; virtual void copyFromScriptValue(const QScriptValue& object, bool& _defaultSettings); virtual void debugDump() const; From 7f9a6d7b8ef1747119a996840a9ec5a11b4bc551 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 20:26:50 -0700 Subject: [PATCH 074/192] whitespace fixes --- libraries/entities/src/BoxEntityItem.h | 4 ++-- libraries/entities/src/LineEntityItem.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/BoxEntityItem.h b/libraries/entities/src/BoxEntityItem.h index 21e36c7031..fdc66dd3e5 100644 --- a/libraries/entities/src/BoxEntityItem.h +++ b/libraries/entities/src/BoxEntityItem.h @@ -23,8 +23,8 @@ public: ALLOW_INSTANTIATION // This class can be instantiated // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; - virtual bool setProperties(const EntityItemProperties& properties); + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; + virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index faa14d788e..7e0f49c984 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -22,8 +22,8 @@ class LineEntityItem : public EntityItem { ALLOW_INSTANTIATION // This class can be instantiated - // methods for getting/setting all properties of an entity - virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; + // methods for getting/setting all properties of an entity + virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const; virtual bool setProperties(const EntityItemProperties& properties); // TODO: eventually only include properties changed since the params.lastViewFrustumSent time From 45c34e161736f42f047d8edc2ae0d15249a0bf9a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 20:55:19 -0700 Subject: [PATCH 075/192] only copy properties to the script values for properties that our type actually has --- .../entities/src/EntityItemProperties.cpp | 204 +++++++++++------- 1 file changed, 122 insertions(+), 82 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 102611dc2f..65b41b5734 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -428,6 +428,15 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool } COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(type, EntityTypes::getEntityTypeName(_type)); + auto created = QDateTime::fromMSecsSinceEpoch(getCreated() / 1000.0f, Qt::UTC); // usec per msec + created.setTimeSpec(Qt::OffsetFromUTC); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(created, created.toString(Qt::ISODate)); + + if (!skipDefaults || _lifetime != defaultEntityProperties._lifetime) { + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(age, getAge()); // gettable, but not settable + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(ageAsText, formatSecondsElapsed(getAge())); // gettable, but not settable + } + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_POSITION, position); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DIMENSIONS, dimensions); if (!skipDefaults) { @@ -443,84 +452,131 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FRICTION, friction); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DENSITY, density); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIFETIME, lifetime); - - if (!skipDefaults || _lifetime != defaultEntityProperties._lifetime) { - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(age, getAge()); // gettable, but not settable - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(ageAsText, formatSecondsElapsed(getAge())); // gettable, but not settable - } - - auto created = QDateTime::fromMSecsSinceEpoch(getCreated() / 1000.0f, Qt::UTC); // usec per msec - created.setTimeSpec(Qt::OffsetFromUTC); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(created, created.toString(Qt::ISODate)); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SCRIPT, script); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SCRIPT_TIMESTAMP, scriptTimestamp); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_REGISTRATION_POINT, registrationPoint); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANGULAR_VELOCITY, angularVelocity); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANGULAR_DAMPING, angularDamping); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VISIBLE, visible); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR, color); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_SPREAD, colorSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_START, colorStart); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_FINISH, colorFinish); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_SPREAD, alphaSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_START, alphaStart); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_FINISH, alphaFinish); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_URL, modelURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_URL, animationURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_PLAYING, animationIsPlaying); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FPS, animationFPS); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FRAME_INDEX, animationFrameIndex); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_SETTINGS, animationSettings, getAnimationSettings()); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IGNORE_FOR_COLLISIONS, ignoreForCollisions); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISIONS_WILL_MOVE, collisionsWillMove); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_SPOTLIGHT, isSpotlight); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_INTENSITY, intensity); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EXPONENT, exponent); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CUTOFF, cutoff); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); - //COPY_PROPERTY_TO_QSCRIPTVALUE(simulationOwner); // TODO: expose this for JSON saves? - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXT, text); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_HEIGHT, lineHeight); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_TEXT_COLOR, textColor, getTextColor()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BACKGROUND_COLOR, backgroundColor, getBackgroundColor()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString()); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MAX_PARTICLES, maxParticles); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIFESPAN, lifespan); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_RATE, emitRate); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_VELOCITY, emitVelocity); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VELOCITY_SPREAD, velocitySpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_ACCELERATION, emitAcceleration); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACCELERATION_SPREAD, accelerationSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARTICLE_RADIUS, particleRadius); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_SPREAD, radiusSpread); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_START, radiusStart); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_FINISH, radiusFinish); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); - - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_COLOR, keyLightColor); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_INTENSITY, keyLightIntensity); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_INTENSITY, keyLightAmbientIntensity); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_DIRECTION, keyLightDirection); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BACKGROUND_MODE, backgroundMode, getBackgroundModeAsString()); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOURCE_URL, sourceUrl); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_VOLUME_SIZE, voxelVolumeSize); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_DATA, voxelData); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_SURFACE_STYLE, voxelSurfaceStyle); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_WIDTH, lineWidth); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_POINTS, linePoints); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_HREF, href); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DESCRIPTION, description); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FACE_CAMERA, faceCamera); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACTION_DATA, actionData); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NORMALS, normals); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_STROKE_WIDTHS, strokeWidths); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); + + // Boxes, Spheres, Light, Line, Model(??), Particle, PolyLine + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR, color); + + // Particles only + if (_type == EntityTypes::ParticleEffect) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MAX_PARTICLES, maxParticles); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIFESPAN, lifespan); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_RATE, emitRate); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_VELOCITY, emitVelocity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VELOCITY_SPREAD, velocitySpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMIT_ACCELERATION, emitAcceleration); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACCELERATION_SPREAD, accelerationSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARTICLE_RADIUS, particleRadius); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_SPREAD, radiusSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_START, radiusStart); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RADIUS_FINISH, radiusFinish); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_SPREAD, colorSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_START, colorStart); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLOR_FINISH, colorFinish); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_SPREAD, alphaSpread); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_START, alphaStart); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA_FINISH, alphaFinish); + } + + // Models only + if (_type == EntityTypes::Model) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_URL, modelURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_URL, animationURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); + } + + + if (_type == EntityTypes::Model || _type == EntityTypes::Zone || _type == EntityTypes::ParticleEffect) { + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString()); + } + + // Models & Particles + if (_type == EntityTypes::Model || _type == EntityTypes::ParticleEffect) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_PLAYING, animationIsPlaying); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FPS, animationFPS); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FRAME_INDEX, animationFrameIndex); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ANIMATION_SETTINGS, animationSettings, getAnimationSettings()); + } + + // Lights only + if (_type == EntityTypes::Light) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_SPOTLIGHT, isSpotlight); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_INTENSITY, intensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EXPONENT, exponent); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CUTOFF, cutoff); + } + + // Text only + if (_type == EntityTypes::Text) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXT, text); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_HEIGHT, lineHeight); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_TEXT_COLOR, textColor, getTextColor()); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BACKGROUND_COLOR, backgroundColor, getBackgroundColor()); + } + + // Zones only + if (_type == EntityTypes::Zone) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_COLOR, keyLightColor); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_INTENSITY, keyLightIntensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_INTENSITY, keyLightAmbientIntensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_DIRECTION, keyLightDirection); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BACKGROUND_MODE, backgroundMode, getBackgroundModeAsString()); + + _stage.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + _atmosphere.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + _skybox.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); + } + + // Web only + if (_type == EntityTypes::Web) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOURCE_URL, sourceUrl); + } + + // PolyVoxel only + if (_type == EntityTypes::PolyVox) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_VOLUME_SIZE, voxelVolumeSize); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_DATA, voxelData); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_SURFACE_STYLE, voxelSurfaceStyle); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_TEXTURE_URL, xTextureURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_TEXTURE_URL, yTextureURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_TEXTURE_URL, zTextureURL); + + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_N_NEIGHBOR_ID, xNNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_N_NEIGHBOR_ID, yNNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_N_NEIGHBOR_ID, zNNeighborID); + + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_P_NEIGHBOR_ID, xPNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_P_NEIGHBOR_ID, yPNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_P_NEIGHBOR_ID, zPNeighborID); + } + + // Lines & PolyLines + if (_type == EntityTypes::Line || _type == EntityTypes::PolyLine) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_WIDTH, lineWidth); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_POINTS, linePoints); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NORMALS, normals); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_STROKE_WIDTHS, strokeWidths); + } + + //COPY_PROPERTY_TO_QSCRIPTVALUE(simulationOwner); // TODO: expose this for JSON saves? // Sitting properties support if (!skipDefaults) { @@ -555,22 +611,6 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(originalTextures, textureNamesList); // gettable, but not settable } - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_TEXTURE_URL, xTextureURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_TEXTURE_URL, yTextureURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_TEXTURE_URL, zTextureURL); - - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_N_NEIGHBOR_ID, xNNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_N_NEIGHBOR_ID, yNNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_N_NEIGHBOR_ID, zNNeighborID); - - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_P_NEIGHBOR_ID, xPNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_P_NEIGHBOR_ID, yPNeighborID); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_P_NEIGHBOR_ID, zPNeighborID); - - _stage.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); - _atmosphere.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); - _skybox.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); - // FIXME - I don't think these properties are supported any more //COPY_PROPERTY_TO_QSCRIPTVALUE(glowLevel); //COPY_PROPERTY_TO_QSCRIPTVALUE(localRenderAlpha); From 5ac084ad4f8abe2e0c49579f0384844b1566501b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 17 Sep 2015 20:57:07 -0700 Subject: [PATCH 076/192] Randomize bubble gravity, code cleanup --- examples/toys/bubblewand/bubble.js | 38 ++++++------- examples/toys/bubblewand/createWand.js | 3 +- examples/toys/bubblewand/wand.js | 78 ++++++++++++++------------ 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 2629210c96..2904ca938f 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -14,7 +14,7 @@ Script.include("../../utilities.js"); Script.include("../../libraries/utils.js"); - var BUBBLE_PARTICLE_TEXTURE = "https://raw.githubusercontent.com/ericrius1/SantasLair/santa/assets/smokeparticle.png" + var BUBBLE_PARTICLE_TEXTURE = "http://hifi-public.s3.amazonaws.com/james/bubblewand/textures/bubble_particle.png" BUBBLE_PARTICLE_COLOR = { @@ -30,10 +30,10 @@ this.preload = function(entityID) { // print('bubble preload') _this.entityID = entityID; - Script.update.connect(_this.internalUpdate); + Script.update.connect(_this.update); }; - this.internalUpdate = function() { + this.update = function() { // we want the position at unload but for some reason it keeps getting set to 0,0,0 -- so i just exclude that location. sorry origin bubbles. var tmpProperties = Entities.getEntityProperties(_this.entityID); if (tmpProperties.position.x !== 0 && tmpProperties.position.y !== 0 && tmpProperties.position.z !== 0) { @@ -43,20 +43,18 @@ }; this.unload = function(entityID) { - Script.update.disconnect(this.internalUpdate); - var position = properties.position; - _this.endOfBubble(position); + Script.update.disconnect(this.update); + + //TODO: Unload doesn't seem like the right place to do this. We really want to know that our lifetime is over. + _this.createBurstParticles(); }; - this.endOfBubble = function(position) { - this.createBurstParticles(position); - } - this.createBurstParticles = function(position) { - var _t = this; - //get the current position of the bubble + this.createBurstParticles = function() { + //get the current position and dimensions of the bubble var position = properties.position; - //var orientation = properties.orientation; + var dimensions = properties.dimensions; + var animationSettings = JSON.stringify({ fps: 30, @@ -73,15 +71,11 @@ animationIsPlaying: true, position: position, lifetime: 0.2, - dimensions: { - x: 1, - y: 1, - z: 1 - }, + dimensions: dimensions, emitVelocity: { - x: 0, - y: 0, - z: 0 + x: 0.25, + y: 0.25, + z: 0.25 }, velocitySpread: { x: 0.45, @@ -94,7 +88,7 @@ z: 0 }, alphaStart: 1.0, - alpha: 1, + alpha: 0.5, alphaFinish: 0.0, textures: BUBBLE_PARTICLE_TEXTURE, color: BUBBLE_PARTICLE_COLOR, diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 6548090b74..51ac9738f1 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -20,6 +20,7 @@ var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); //create the wand in front of the avatar var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); + var tablePosition = { x:546.48, y:495.63, @@ -33,7 +34,7 @@ var wand = Entities.addEntity({ position: IN_TOYBOX? tablePosition: center, gravity: { x: 0, - y: 0, + y: -9.8, z: 0, }, dimensions: { diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 2b57ccda78..38fbf161b8 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -19,16 +19,17 @@ var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var BUBBLE_SCRIPT = Script.resolvePath('bubble.js'); - var BUBBLE_GRAVITY = { - x: 0, - y: -0.1, - z: 0 - }; - var BUBBLE_DIVISOR = 50; + var BUBBLE_INITIAL_DIMENSIONS = { + x: 0.01, + y: 0.01, + z: 0.01 + } + var BUBBLE_LIFETIME_MIN = 3; var BUBBLE_LIFETIME_MAX = 8; var BUBBLE_SIZE_MIN = 1; var BUBBLE_SIZE_MAX = 5; + var BUBBLE_DIVISOR = 50; var GROWTH_FACTOR = 0.005; var SHRINK_FACTOR = 0.001; var SHRINK_LOWER_LIMIT = 0.02; @@ -43,7 +44,6 @@ var BubbleWand = function() { _this = this; - print('WAND CONSTRUCTOR!') } BubbleWand.prototype = { @@ -65,9 +65,9 @@ }; var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, _this.entityID, defaultGrabData); if (grabData.activated && grabData.avatarId === MyAvatar.sessionUUID) { - print('being grabbed') - _this.beingGrabbed = true; + // remember we're being grabbed so we can detect being released + _this.beingGrabbed = true; if (_this.currentBubble === null) { @@ -75,13 +75,10 @@ } var properties = Entities.getEntityProperties(_this.entityID); - // remember we're being grabbed so we can detect being released - - - // print out that we're being grabbed _this.growBubbleWithWandVelocity(properties); var wandTipPosition = _this.getWandTipPosition(properties); + //update the bubble to stay with the wand tip Entities.editEntity(_this.currentBubble, { position: wandTipPosition, @@ -91,15 +88,19 @@ } else if (_this.beingGrabbed) { // if we are not being grabbed, and we previously were, then we were just released, remember that - // and print out a message + _this.beingGrabbed = false; + + //remove the current bubble when the wand is released Entities.deleteEntity(_this.currentBubble); return } - }, getWandTipPosition: function(properties) { + + //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position + var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); var upOffset = Vec3.multiply(upVector, WAND_TIP_OFFSET); @@ -107,16 +108,26 @@ this.wandTipPosition = wandTipPosition; return wandTipPosition }, + randomizeBubbleGravity: function() { + + var randomNumber = randInt(0, 3) + var gravity: { + x: 0, + y: -randomNumber / 10, + z: 0 + } + return gravity + }, growBubbleWithWandVelocity: function(properties) { - print('grow bubble') + var wandPosition = properties.position; var wandTipPosition = this.getWandTipPosition(properties) - + var velocity = Vec3.subtract(wandPosition, this.lastPosition) var velocityStrength = Vec3.length(velocity) * VELOCITY_STRENGTH_MULTIPLIER; - + // if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { // velocityStrength = 0 @@ -126,22 +137,19 @@ // velocityStrength = VELOCITY_STRENGTH_MAX // } - print('VELOCITY STRENGTH:::'+velocityStrength) - print('V THRESH:'+ VELOCITY_THRESHOLD) - print('debug 1') + //store the last position of the wand for velocity calculations this.lastPosition = wandPosition; //actually grow the bubble var dimensions = Entities.getEntityProperties(this.currentBubble).dimensions; - print('dim x '+dimensions.x) + if (velocityStrength > VELOCITY_THRESHOLD) { //add some variation in bubble sizes var bubbleSize = randInt(BUBBLE_SIZE_MIN, BUBBLE_SIZE_MAX); bubbleSize = bubbleSize / BUBBLE_DIVISOR; - - print('bubbleSize '+ bubbleSize) + //release the bubble if its dimensions are bigger than the bubble size if (dimensions.x > bubbleSize) { //bubbles pop after existing for a bit -- so set a random lifetime @@ -149,7 +157,8 @@ Entities.editEntity(this.currentBubble, { velocity: velocity, - lifetime: lifetime + lifetime: lifetime, + gravity: this.randomizeBubbleGravity() }); //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble @@ -157,12 +166,15 @@ return } else { + + // if the bubble is not yet full size, make the current bubble bigger dimensions.x += GROWTH_FACTOR * velocityStrength; dimensions.y += GROWTH_FACTOR * velocityStrength; dimensions.z += GROWTH_FACTOR * velocityStrength; } } else { + // if the wand is not moving, make the current bubble smaller if (dimensions.x >= SHRINK_LOWER_LIMIT) { dimensions.x -= SHRINK_FACTOR; dimensions.y -= SHRINK_FACTOR; @@ -171,7 +183,7 @@ } - //make the bubble bigger + //adjust the bubble dimensions Entities.editEntity(this.currentBubble, { dimensions: dimensions }); @@ -179,7 +191,6 @@ spawnBubble: function() { //create a new bubble at the tip of the wand - //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position var properties = Entities.getEntityProperties(this.entityID); var wandPosition = properties.position; @@ -191,18 +202,13 @@ //create a bubble at the wand tip this.currentBubble = Entities.addEntity({ - name:'Bubble', + name: 'Bubble', type: 'Model', modelURL: BUBBLE_MODEL, position: wandTipPosition, - dimensions: { - x: 0.01, - y: 0.01, - z: 0.01 - }, - collisionsWillMove: true, //true - ignoreForCollisions: false, //false - gravity: BUBBLE_GRAVITY, + dimensions: BUBBLE_INITIAL_DIMENSIONS, + collisionsWillMove: true, + ignoreForCollisions: false, shapeType: "sphere", script: BUBBLE_SCRIPT, }); From d0a46224276fbb7e32c34941d06d07b96a76d461 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 21:01:56 -0700 Subject: [PATCH 077/192] removed some debug code --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 3e4569e70f..c55eaaeff9 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -778,7 +778,6 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const bool reload) { - qDebug() << "entitySciptChanging() entityID:" << entityID << "reload:" << reload; if (_tree && !_shuttingDown) { _entitiesScriptEngine->unloadEntityScript(entityID); checkAndCallPreload(entityID, reload); From 0811da14b88c21556b6a386db8b2ace12b6d795f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 21:14:07 -0700 Subject: [PATCH 078/192] use new feature for getEntityUserData --- examples/libraries/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/libraries/utils.js b/examples/libraries/utils.js index 1275975fd8..ea0bae745e 100644 --- a/examples/libraries/utils.js +++ b/examples/libraries/utils.js @@ -60,7 +60,7 @@ setEntityUserData = function(id, data) { // FIXME do non-destructive modification of the existing user data getEntityUserData = function(id) { var results = null; - var properties = Entities.getEntityProperties(id); + var properties = Entities.getEntityProperties(id, "userData"); if (properties.userData) { try { results = JSON.parse(properties.userData); From 8a7798fcd759eef45cebf18e1a44138145f4fbc9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 17 Sep 2015 22:00:44 -0700 Subject: [PATCH 079/192] disable bubble particle burst until i know the right for an entity to watch and see if it is deleted. --- examples/toys/bubblewand/bubble.js | 25 ++++++++++++++----------- examples/toys/bubblewand/createWand.js | 5 +++-- examples/toys/bubblewand/wand.js | 19 +++++++++---------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 2904ca938f..69764c0c97 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -16,7 +16,6 @@ var BUBBLE_PARTICLE_TEXTURE = "http://hifi-public.s3.amazonaws.com/james/bubblewand/textures/bubble_particle.png" - BUBBLE_PARTICLE_COLOR = { red: 0, green: 40, @@ -46,7 +45,7 @@ Script.update.disconnect(this.update); //TODO: Unload doesn't seem like the right place to do this. We really want to know that our lifetime is over. - _this.createBurstParticles(); + // _this.createBurstParticles(); }; @@ -70,12 +69,16 @@ animationSettings: animationSettings, animationIsPlaying: true, position: position, - lifetime: 0.2, - dimensions: dimensions, + lifetime: 0.1, + dimensions: { + x: 10, + y: 10, + z: 10 + }, emitVelocity: { - x: 0.25, - y: 0.25, - z: 0.25 + x: 0.35, + y: 0.35, + z: 0.35 }, velocitySpread: { x: 0.45, @@ -87,12 +90,12 @@ y: -0.1, z: 0 }, - alphaStart: 1.0, + alphaStart: 0.5, alpha: 0.5, - alphaFinish: 0.0, + alphaFinish: 0, textures: BUBBLE_PARTICLE_TEXTURE, - color: BUBBLE_PARTICLE_COLOR, - lifespan: 0.2, + // color: BUBBLE_PARTICLE_COLOR, + lifespan: 0.1, visible: true, locked: false }); diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 51ac9738f1..a54e438e5d 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -19,7 +19,7 @@ var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wa var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); //create the wand in front of the avatar -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +var center = Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0, y: 0.5, z: 0}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); var tablePosition = { x:546.48, @@ -34,7 +34,8 @@ var wand = Entities.addEntity({ position: IN_TOYBOX? tablePosition: center, gravity: { x: 0, - y: -9.8, + y:0, + // y: -9.8, z: 0, }, dimensions: { diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 38fbf161b8..e0495f5f40 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -7,7 +7,6 @@ // // Makes bubbles when you wave the object around. // -// // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -17,7 +16,7 @@ Script.include("../../libraries/utils.js"); var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var BUBBLE_SCRIPT = Script.resolvePath('bubble.js'); + var BUBBLE_SCRIPT = Script.resolvePath('bubble.js?'+randInt(0,10000)); var BUBBLE_INITIAL_DIMENSIONS = { x: 0.01, @@ -39,7 +38,6 @@ var VELOCITY_STRENGTH_MULTIPLIER = 100; var VELOCITY_THRESHOLD = 1; - var _this; var BubbleWand = function() { @@ -69,10 +67,11 @@ // remember we're being grabbed so we can detect being released _this.beingGrabbed = true; - + //the first time we want to make a bubble if (_this.currentBubble === null) { - _this.spawnBubble(); + _this.createBubbleAtTipOfWand(); } + var properties = Entities.getEntityProperties(_this.entityID); _this.growBubbleWithWandVelocity(properties); @@ -93,6 +92,7 @@ //remove the current bubble when the wand is released Entities.deleteEntity(_this.currentBubble); + _this.currentBubble=null return } @@ -110,8 +110,8 @@ }, randomizeBubbleGravity: function() { - var randomNumber = randInt(0, 3) - var gravity: { + var randomNumber = randInt(0, 3); + var gravity= { x: 0, y: -randomNumber / 10, z: 0 @@ -137,7 +137,6 @@ // velocityStrength = VELOCITY_STRENGTH_MAX // } - //store the last position of the wand for velocity calculations this.lastPosition = wandPosition; @@ -162,7 +161,7 @@ }); //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble - this.spawnBubble(); + this.createBubbleAtTipOfWand(); return } else { @@ -188,7 +187,7 @@ dimensions: dimensions }); }, - spawnBubble: function() { + createBubbleAtTipOfWand: function() { //create a new bubble at the tip of the wand var properties = Entities.getEntityProperties(this.entityID); From 4a34b142d1d2924127dc4f7efa03b66b9cdae7ef Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Sep 2015 22:05:14 -0700 Subject: [PATCH 080/192] remove render element proxies --- .../src/EntityTreeRenderer.cpp | 81 ------------------- .../src/EntityTreeRenderer.h | 5 -- 2 files changed, 86 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c55eaaeff9..ebdf0f0339 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -52,9 +52,7 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf _lastMouseEventValid(false), _viewState(viewState), _scriptingServices(scriptingServices), - _displayElementChildProxies(false), _displayModelBounds(false), - _displayModelElementProxy(false), _dontDoPrecisionPicking(false) { REGISTER_ENTITY_TYPE_WITH_FACTORY(Model, RenderableModelEntityItem::factory) @@ -372,92 +370,13 @@ const FBXGeometry* EntityTreeRenderer::getCollisionGeometryForEntity(EntityItemP return result; } -void EntityTreeRenderer::renderElementProxy(EntityTreeElementPointer entityTreeElement, RenderArgs* args) { - auto deferredLighting = DependencyManager::get(); - Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - Transform transform; - - glm::vec3 elementCenter = entityTreeElement->getAACube().calcCenter(); - float elementSize = entityTreeElement->getScale(); - - auto drawWireCube = [&](glm::vec3 offset, float size, glm::vec4 color) { - transform.setTranslation(elementCenter + offset); - batch.setModelTransform(transform); - deferredLighting->renderWireCube(batch, size, color); - }; - - drawWireCube(glm::vec3(), elementSize, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f)); - - if (_displayElementChildProxies) { - // draw the children - float halfSize = elementSize / 2.0f; - float quarterSize = elementSize / 4.0f; - - drawWireCube(glm::vec3(-quarterSize, -quarterSize, -quarterSize), halfSize, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f)); - drawWireCube(glm::vec3(quarterSize, -quarterSize, -quarterSize), halfSize, glm::vec4(1.0f, 0.0f, 1.0f, 1.0f)); - drawWireCube(glm::vec3(-quarterSize, quarterSize, -quarterSize), halfSize, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); - drawWireCube(glm::vec3(-quarterSize, -quarterSize, quarterSize), halfSize, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)); - drawWireCube(glm::vec3(quarterSize, quarterSize, quarterSize), halfSize, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); - drawWireCube(glm::vec3(-quarterSize, quarterSize, quarterSize), halfSize, glm::vec4(0.0f, 0.5f, 0.5f, 1.0f)); - drawWireCube(glm::vec3(quarterSize, -quarterSize, quarterSize), halfSize, glm::vec4(0.5f, 0.0f, 0.0f, 1.0f)); - drawWireCube(glm::vec3(quarterSize, quarterSize, -quarterSize), halfSize, glm::vec4(0.0f, 0.5f, 0.0f, 1.0f)); - } -} - -void EntityTreeRenderer::renderProxies(EntityItemPointer entity, RenderArgs* args) { - bool isShadowMode = args->_renderMode == RenderArgs::SHADOW_RENDER_MODE; - if (!isShadowMode && _displayModelBounds) { - PerformanceTimer perfTimer("renderProxies"); - - AACube maxCube = entity->getMaximumAACube(); - AACube minCube = entity->getMinimumAACube(); - AABox entityBox = entity->getAABox(); - - glm::vec3 maxCenter = maxCube.calcCenter(); - glm::vec3 minCenter = minCube.calcCenter(); - glm::vec3 entityBoxCenter = entityBox.calcCenter(); - glm::vec3 entityBoxScale = entityBox.getScale(); - - auto deferredLighting = DependencyManager::get(); - Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - Transform transform; - - // draw the max bounding cube - transform.setTranslation(maxCenter); - batch.setModelTransform(transform); - deferredLighting->renderWireCube(batch, maxCube.getScale(), glm::vec4(1.0f, 1.0f, 0.0f, 1.0f)); - - // draw the min bounding cube - transform.setTranslation(minCenter); - batch.setModelTransform(transform); - deferredLighting->renderWireCube(batch, minCube.getScale(), glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); - - // draw the entityBox bounding box - transform.setTranslation(entityBoxCenter); - transform.setScale(entityBoxScale); - batch.setModelTransform(transform); - deferredLighting->renderWireCube(batch, 1.0f, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)); - - // Rotated bounding box - batch.setModelTransform(entity->getTransformToCenter()); - deferredLighting->renderWireCube(batch, 1.0f, glm::vec4(1.0f, 0.0f, 1.0f, 1.0f)); - } -} - void EntityTreeRenderer::renderElement(OctreeElementPointer element, RenderArgs* args) { // actually render it here... // we need to iterate the actual entityItems of the element EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); - bool isShadowMode = args->_renderMode == RenderArgs::SHADOW_RENDER_MODE; - if (!isShadowMode && _displayModelElementProxy && entityTreeElement->size() > 0) { - renderElementProxy(entityTreeElement, args); - } - entityTreeElement->forEachEntity([&](EntityItemPointer entityItem) { if (entityItem->isVisible()) { // NOTE: Zone Entities are a special case we handle here... diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 18874957fd..2691c3c66f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -114,9 +114,7 @@ public slots: void updateEntityRenderStatus(bool shouldRenderEntities); // optional slots that can be wired to menu items - void setDisplayElementChildProxies(bool value) { _displayElementChildProxies = value; } void setDisplayModelBounds(bool value) { _displayModelBounds = value; } - void setDisplayModelElementProxy(bool value) { _displayModelElementProxy = value; } void setDontDoPrecisionPicking(bool value) { _dontDoPrecisionPicking = value; } protected: @@ -134,7 +132,6 @@ private: void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false); QList _releasedModels; - void renderProxies(EntityItemPointer entity, RenderArgs* args); RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType, bool precisionPicking); @@ -157,9 +154,7 @@ private: MouseEvent _lastMouseEvent; AbstractViewStateInterface* _viewState; AbstractScriptingServicesInterface* _scriptingServices; - bool _displayElementChildProxies; bool _displayModelBounds; - bool _displayModelElementProxy; bool _dontDoPrecisionPicking; bool _shuttingDown = false; From 420acde72004d7299dd9a2c6739dbe54871b237a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 17 Sep 2015 22:07:52 -0700 Subject: [PATCH 081/192] blend IK effects between distinct end effectors --- .../animation/src/AnimInverseKinematics.cpp | 78 ++++++++++++------- .../animation/src/AnimInverseKinematics.h | 33 +++++++- 2 files changed, 81 insertions(+), 30 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 36b23c313e..409c243612 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -153,6 +153,11 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar ++constraintItr; } } else { + // clear the accumulators before we start the IK solver + for (auto& accumulatorPair: _accumulators) { + accumulatorPair.second.clear(); + } + // compute absolute poses that correspond to relative target poses AnimPoseVec absolutePoses; computeAbsolutePoses(absolutePoses); @@ -165,8 +170,8 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar quint64 expiry = usecTimestampNow() + MAX_IK_TIME; do { largestError = 0.0f; + int lowestMovedIndex = _relativePoses.size(); for (auto& target: targets) { - int lowestMovedIndex = _relativePoses.size() - 1; int tipIndex = target.index; AnimPose targetPose = target.pose; int rootIndex = target.rootIndex; @@ -226,7 +231,8 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar glm::inverse(absolutePoses[pivotIndex].rot); } } - _relativePoses[pivotIndex].rot = newRot; + // store the rotation change in the accumulator + _accumulators[pivotIndex].add(newRot); } // this joint has been changed so we check to see if it has the lowest index if (pivotIndex < lowestMovedIndex) { @@ -243,36 +249,49 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar if (largestError < error) { largestError = error; } - - if (lowestMovedIndex <= _maxTargetIndex && lowestMovedIndex < tipIndex) { - // only update the absolutePoses that matter: those between lowestMovedIndex and _maxTargetIndex - for (int i = lowestMovedIndex; i <= _maxTargetIndex; ++i) { - int parentIndex = _skeleton->getParentIndex(i); - if (parentIndex != -1) { - absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; - } - } - } - - // finally set the relative rotation of the tip to agree with absolute target rotation - int parentIndex = _skeleton->getParentIndex(tipIndex); - if (parentIndex != -1) { - // compute tip's new parent-relative rotation - // Q = Qp * q --> q' = Qp^ * Q - glm::quat newRelativeRotation = glm::inverse(absolutePoses[parentIndex].rot) * targetPose.rot; - RotationConstraint* constraint = getConstraint(tipIndex); - if (constraint) { - constraint->apply(newRelativeRotation); - // TODO: ATM the final rotation target just fails but we need to provide - // feedback to the IK system so that it can adjust the bones up the skeleton - // to help this rotation target get met. - } - _relativePoses[tipIndex].rot = newRelativeRotation; - absolutePoses[tipIndex].rot = targetPose.rot; - } } ++numLoops; + + // harvest accumulated rotations and apply the average + for (auto& accumulatorPair: _accumulators) { + RotationAccumulator& accumulator = accumulatorPair.second; + if (accumulator.size() > 0) { + _relativePoses[accumulatorPair.first].rot = accumulator.getAverage(); + accumulator.clear(); + } + } + + // only update the absolutePoses that need it: those between lowestMovedIndex and _maxTargetIndex + if (lowestMovedIndex < _maxTargetIndex) { + for (int i = lowestMovedIndex; i <= _maxTargetIndex; ++i) { + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex != -1) { + absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; + } + } + } } while (largestError > ACCEPTABLE_RELATIVE_ERROR && numLoops < MAX_IK_LOOPS && usecTimestampNow() < expiry); + + // finally set the relative rotation of each tip to agree with absolute target rotation + for (auto& target: targets) { + int tipIndex = target.index; + int parentIndex = _skeleton->getParentIndex(tipIndex); + if (parentIndex != -1) { + AnimPose targetPose = target.pose; + // compute tip's new parent-relative rotation + // Q = Qp * q --> q' = Qp^ * Q + glm::quat newRelativeRotation = glm::inverse(absolutePoses[parentIndex].rot) * targetPose.rot; + RotationConstraint* constraint = getConstraint(tipIndex); + if (constraint) { + constraint->apply(newRelativeRotation); + // TODO: ATM the final rotation target just fails but we need to provide + // feedback to the IK system so that it can adjust the bones up the skeleton + // to help this rotation target get met. + } + _relativePoses[tipIndex].rot = newRelativeRotation; + absolutePoses[tipIndex].rot = targetPose.rot; + } + } } return _relativePoses; } @@ -628,6 +647,7 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele _maxTargetIndex = 0; + _accumulators.clear(); if (skeleton) { initConstraints(); } else { diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index b59fb4d5fc..ae8ab34bdc 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -16,6 +16,37 @@ class RotationConstraint; +class RotationAccumulator { +public: + RotationAccumulator() {} + + uint32_t size() const { return _rotations.size(); } + + void add(const glm::quat& rotation) { _rotations.push_back(rotation); } + + glm::quat getAverage() { + glm::quat average; + uint32_t numRotations = _rotations.size(); + if (numRotations > 0) { + average = _rotations[0]; + for (uint32_t i = 1; i < numRotations; ++i) { + glm::quat rotation = _rotations[i]; + if (glm::dot(average, rotation) < 0.0f) { + rotation = -rotation; + } + average += rotation; + } + average = glm::normalize(average); + } + return average; + } + + void clear() { _rotations.clear(); } + +private: + std::vector _rotations; +}; + class AnimInverseKinematics : public AnimNode { public: @@ -24,7 +55,6 @@ public: void loadDefaultPoses(const AnimPoseVec& poses); void loadPoses(const AnimPoseVec& poses); - const AnimPoseVec& getRelativePoses() const { return _relativePoses; } void computeAbsolutePoses(AnimPoseVec& absolutePoses) const; void setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar); @@ -60,6 +90,7 @@ protected: }; std::map _constraints; + std::map _accumulators; std::vector _targetVarVec; AnimPoseVec _defaultRelativePoses; // poses of the relaxed state AnimPoseVec _relativePoses; // current relative poses From b6a153d92690a9a2b19f1adfd23ffb61d85b83c1 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 17 Sep 2015 22:30:44 -0700 Subject: [PATCH 082/192] split RotationAccumulator into its own files --- .../animation/src/AnimInverseKinematics.h | 36 +++---------------- .../animation/src/RotationAccumulator.cpp | 29 +++++++++++++++ libraries/animation/src/RotationAccumulator.h | 34 ++++++++++++++++++ 3 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 libraries/animation/src/RotationAccumulator.cpp create mode 100644 libraries/animation/src/RotationAccumulator.h diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index ae8ab34bdc..c4bda1be89 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -12,41 +12,15 @@ #include +#include +#include + #include "AnimNode.h" +#include "RotationAccumulator.h" + class RotationConstraint; -class RotationAccumulator { -public: - RotationAccumulator() {} - - uint32_t size() const { return _rotations.size(); } - - void add(const glm::quat& rotation) { _rotations.push_back(rotation); } - - glm::quat getAverage() { - glm::quat average; - uint32_t numRotations = _rotations.size(); - if (numRotations > 0) { - average = _rotations[0]; - for (uint32_t i = 1; i < numRotations; ++i) { - glm::quat rotation = _rotations[i]; - if (glm::dot(average, rotation) < 0.0f) { - rotation = -rotation; - } - average += rotation; - } - average = glm::normalize(average); - } - return average; - } - - void clear() { _rotations.clear(); } - -private: - std::vector _rotations; -}; - class AnimInverseKinematics : public AnimNode { public: diff --git a/libraries/animation/src/RotationAccumulator.cpp b/libraries/animation/src/RotationAccumulator.cpp new file mode 100644 index 0000000000..22b84afc77 --- /dev/null +++ b/libraries/animation/src/RotationAccumulator.cpp @@ -0,0 +1,29 @@ +// +// RotationAccumulator.h +// +// Copyright 2015 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 "RotationAccumulator.h" + +#include + +glm::quat RotationAccumulator::getAverage() { + glm::quat average; + uint32_t numRotations = _rotations.size(); + if (numRotations > 0) { + average = _rotations[0]; + for (uint32_t i = 1; i < numRotations; ++i) { + glm::quat rotation = _rotations[i]; + if (glm::dot(average, rotation) < 0.0f) { + rotation = -rotation; + } + average += rotation; + } + average = glm::normalize(average); + } + return average; +} diff --git a/libraries/animation/src/RotationAccumulator.h b/libraries/animation/src/RotationAccumulator.h new file mode 100644 index 0000000000..d6854d9b01 --- /dev/null +++ b/libraries/animation/src/RotationAccumulator.h @@ -0,0 +1,34 @@ +// +// RotationAccumulator.h +// +// Copyright 2015 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_RotationAccumulator_h +#define hifi_RotationAccumulator_h + +#include + +#include +#include + +class RotationAccumulator { +public: + RotationAccumulator() {} + + uint32_t size() const { return _rotations.size(); } + + void add(const glm::quat& rotation) { _rotations.push_back(rotation); } + + glm::quat getAverage(); + + void clear() { _rotations.clear(); } + +private: + std::vector _rotations; +}; + +#endif // hifi_RotationAccumulator_h From 6d7b129b835ca0ee73da659a7d5369bdf36284a7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 17 Sep 2015 23:15:18 -0700 Subject: [PATCH 083/192] rework handControllerGrab.js --- examples/controllers/handControllerGrab.js | 519 +++++++++------------ 1 file changed, 231 insertions(+), 288 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index f38d4a1008..7490a811c5 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -10,31 +10,22 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - Script.include("../libraries/utils.js"); +var RIGHT_HAND = 1; +var LEFT_HAND = 0; + +var GRAB_RADIUS = 0.3; var RADIUS_FACTOR = 4; - -var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK"); -var rightTriggerAction = RIGHT_HAND_CLICK; - -var GRAB_USER_DATA_KEY = "grabKey"; - -var LEFT_HAND_CLICK = Controller.findAction("LEFT_HAND_CLICK"); -var leftTriggerAction = LEFT_HAND_CLICK; - -var LIFETIME = 10; -var EXTRA_TIME = 5; -var POINTER_CHECK_TIME = 5000; - -var ZERO_VEC = { - x: 0, - y: 0, - z: 0 -} +var ZERO_VEC = {x: 0, y: 0, z: 0}; +var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}"; var LINE_LENGTH = 500; -var THICK_LINE_WIDTH = 7; -var THIN_LINE_WIDTH = 2; + +var rightController = new controller(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK")); +var leftController = new controller(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK")); + +var startTime = Date.now(); +var LIFETIME = 10; var NO_INTERSECT_COLOR = { red: 10, @@ -47,35 +38,16 @@ var INTERSECT_COLOR = { blue: 10 }; -var GRAB_RADIUS = 0.3; -var GRAB_COLOR = { - red: 250, - green: 10, - blue: 250 -}; -var SHOW_LINE_THRESHOLD = 0.2; -var DISTANCE_HOLD_THRESHOLD = 0.8; +var STATE_SEARCHING = 0; +var STATE_DISTANCE_HOLDING = 1; +var STATE_CLOSE_GRABBING = 2; +var STATE_CONTINUE_CLOSE_GRABBING = 3; +var STATE_RELEASE = 4; -var right4Action = 18; -var left4Action = 17; - -var RIGHT = 1; -var LEFT = 0; -var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right"); -var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left"); -var startTime = Date.now(); - - -//Need to wait before calling these methods for some reason... -Script.setTimeout(function() { - rightController.checkPointer(); - leftController.checkPointer(); -}, 100) - -function controller(side, triggerAction, pullAction, hand) { +function controller(hand, triggerAction) { this.hand = hand; - if (hand === "right") { + if (this.hand === RIGHT_HAND) { this.getHandPosition = MyAvatar.getRightPalmPosition; this.getHandRotation = MyAvatar.getRightPalmRotation; } else { @@ -83,309 +55,280 @@ function controller(side, triggerAction, pullAction, hand) { this.getHandRotation = MyAvatar.getLeftPalmRotation; } this.triggerAction = triggerAction; - this.pullAction = pullAction; - this.actionID = null; - this.distanceHolding = false; - this.closeGrabbing = false; - this.triggerValue = 0; - this.prevTriggerValue = 0; - this.palm = 2 * side; - this.tip = 2 * side + 1; - this.pointer = null; + this.palm = 2 * hand; + this.tip = 2 * hand + 1; + + this.actionID = null; // action this script created... + this.grabbedEntity = null; // on this entity. + this.grabbedVelocity = ZERO_VEC; + this.state = 0; // 0 = searching, 1 = distanceHolding, 2 = closeGrabbing + this.pointer = null; // entity-id of line object + this.triggerValue = 0; // rolling average of trigger value } -controller.prototype.updateLine = function() { - if (this.pointer != null) { - if (Entities.getEntityProperties(this.pointer).id != this.pointer) { - this.pointer = null; - } +controller.prototype.update = function() { + switch(this.state) { + case STATE_SEARCHING: + search(this); + break; + case STATE_DISTANCE_HOLDING: + distanceHolding(this); + break; + case STATE_CLOSE_GRABBING: + closeGrabbing(this); + break; + case STATE_CONTINUE_CLOSE_GRABBING: + continueCloseGrabbing(this); + break; + case STATE_RELEASE: + release(this); + break; } +} - if (this.pointer == null) { - this.lineCreationTime = Date.now(); - this.pointer = Entities.addEntity({ + +function lineOn(self, closePoint, farPoint, color) { + // draw a line + if (self.pointer == null) { + self.pointer = Entities.addEntity({ type: "Line", name: "pointer", - color: NO_INTERSECT_COLOR, - dimensions: { - x: 1000, - y: 1000, - z: 1000 - }, + dimensions: {x: 1000, y: 1000, z: 1000}, visible: true, + position: closePoint, + linePoints: [ ZERO_VEC, farPoint ], + color: color, lifetime: LIFETIME }); - } - - var handPosition = this.getHandPosition(); - var direction = Quat.getUp(this.getHandRotation()); - - //only check if we havent already grabbed an object - if (this.distanceHolding) { - Entities.editEntity(this.pointer, { - position: handPosition, - linePoints: [ ZERO_VEC, Vec3.subtract(Entities.getEntityProperties(this.grabbedEntity).position, handPosition) ], + } else { + Entities.editEntity(self.pointer, { + position: closePoint, + linePoints: [ ZERO_VEC, farPoint ], + color: color, lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME }); + } +} + +function lineOff(self) { + if (self.pointer != null) { + Entities.deleteEntity(self.pointer); + } + self.pointer = null; +} + +function triggerSmoothedSqueezed(self) { + var triggerValue = Controller.getActionValue(self.triggerAction); + self.triggerValue = (self.triggerValue * 0.7) + (triggerValue * 0.3); // smooth out trigger value + return self.triggerValue > 0.2; +} + +function triggerSqueezed(self) { + var triggerValue = Controller.getActionValue(self.triggerAction); + return triggerValue > 0.2; +} + + +function search(self) { + if (!triggerSmoothedSqueezed(self)) { + self.state = STATE_RELEASE; return; } - Entities.editEntity(this.pointer, { - position: handPosition, - linePoints: [ ZERO_VEC, Vec3.multiply(direction, LINE_LENGTH) ], - lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME - }); - - if (this.checkForIntersections(handPosition, direction)) { - Entities.editEntity(this.pointer, { - color: INTERSECT_COLOR, - }); - } else { - Entities.editEntity(this.pointer, { - color: NO_INTERSECT_COLOR, - }); - } -} - - -controller.prototype.checkPointer = function() { - var self = this; - Script.setTimeout(function() { - var props = Entities.getEntityProperties(self.pointer); - Entities.editEntity(self.pointer, { - lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME - }); - self.checkPointer(); - }, POINTER_CHECK_TIME); -} - -controller.prototype.checkForIntersections = function(origin, direction) { - var pickRay = { - origin: origin, - direction: direction - }; - + // the trigger is being pressed, do a ray test + var handPosition = self.getHandPosition(); + var pickRay = {origin: handPosition, direction: Quat.getUp(self.getHandRotation())}; var intersection = Entities.findRayIntersection(pickRay, true); - if (intersection.intersects && intersection.properties.collisionsWillMove === 1) { - var handPosition = Controller.getSpatialControlPosition(this.palm); - this.distanceToEntity = Vec3.distance(handPosition, intersection.properties.position); - var intersectionDistance = Vec3.distance(handPosition, intersection.intersection); - + if (intersection.intersects && + intersection.properties.collisionsWillMove === 1 && + intersection.properties.locked === 0) { + // the ray is intersecting something we can move. + var handControllerPosition = Controller.getSpatialControlPosition(self.palm); + var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection); + self.grabbedEntity = intersection.entityID; if (intersectionDistance < 0.6) { - //We are grabbing an entity, so let it know we've grabbed it - this.grabbedEntity = intersection.entityID; - this.activateEntity(this.grabbedEntity); - this.hidePointer(); - this.shouldDisplayLine = false; - this.grabEntity(); - return true; + // the hand is very close to the intersected object. go into close-grabbing mode. + self.state = STATE_CLOSE_GRABBING; } else { - Entities.editEntity(this.pointer, { - linePoints: [ - ZERO_VEC, - Vec3.multiply(direction, this.distanceToEntity) - ] - }); - this.grabbedEntity = intersection.entityID; - return true; + // the hand is far from the intersected object. go into distance-holding mode + self.state = STATE_DISTANCE_HOLDING; + lineOn(self, pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } - } - return false; -} - - -controller.prototype.attemptMove = function() { - if (this.grabbedEntity || this.distanceHolding) { - var handPosition = Controller.getSpatialControlPosition(this.palm); - var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); - - this.distanceHolding = true; - if (this.actionID === null) { - this.currentObjectPosition = Entities.getEntityProperties(this.grabbedEntity).position; - this.currentObjectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation; - - this.handPreviousPosition = handPosition; - this.handPreviousRotation = handRotation; - - this.actionID = Entities.addAction("spring", this.grabbedEntity, { - targetPosition: this.currentObjectPosition, - linearTimeScale: .1, - targetRotation: this.currentObjectRotation, - angularTimeScale: .1 - }); + } else { + // forward ray test failed, try sphere test. + var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var minDistance = GRAB_RADIUS; + var grabbedEntity = null; + for (var i = 0; i < nearbyEntities.length; i++) { + var props = Entities.getEntityProperties(nearbyEntities[i]); + var distance = Vec3.distance(props.position, handPosition); + if (distance < minDistance && props.name !== "pointer" && + props.collisionsWillMove === 1 && + props.locked === 0) { + self.grabbedEntity = nearbyEntities[i]; + minDistance = distance; + } + } + if (self.grabbedEntity === null) { + lineOn(self, pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } else { - var radius = Math.max(Vec3.distance(this.currentObjectPosition, handPosition) * RADIUS_FACTOR, 1.0); - - var handMoved = Vec3.subtract(handPosition, this.handPreviousPosition); - this.handPreviousPosition = handPosition; - var superHandMoved = Vec3.multiply(handMoved, radius); - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); - - // ---------------- this tracks hand rotation - // var handChange = Quat.multiply(handRotation, Quat.inverse(this.handPreviousRotation)); - // this.handPreviousRotation = handRotation; - // this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); - // ---------------- - - // ---------------- this doubles hand rotation - var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, 2.0), - Quat.inverse(this.handPreviousRotation)); - this.handPreviousRotation = handRotation; - this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); - // ---------------- - - - Entities.updateAction(this.grabbedEntity, this.actionID, { - targetPosition: this.currentObjectPosition, linearTimeScale: .1, - targetRotation: this.currentObjectRotation, angularTimeScale: .1 - }); + self.state = STATE_CLOSE_GRABBING; } } } -controller.prototype.showPointer = function() { - Entities.editEntity(this.pointer, { - visible: true - }); -} - -controller.prototype.hidePointer = function() { - Entities.editEntity(this.pointer, { - visible: false - }); -} - - -controller.prototype.letGo = function() { - if (this.grabbedEntity && this.actionID) { - this.deactivateEntity(this.grabbedEntity); - Entities.deleteAction(this.grabbedEntity, this.actionID); +function distanceHolding(self) { + if (!triggerSmoothedSqueezed(self)) { + self.state = STATE_RELEASE; + return; } - this.grabbedEntity = null; - this.actionID = null; - this.distanceHolding = false; - this.closeGrabbing = false; -} -controller.prototype.update = function() { - this.triggerValue = Controller.getActionValue(this.triggerAction); - if (this.triggerValue > SHOW_LINE_THRESHOLD && this.prevTriggerValue < SHOW_LINE_THRESHOLD) { - //First check if an object is within close range and then run the close grabbing logic - if (this.checkForInRangeObject()) { - this.grabEntity(); - } else { - this.showPointer(); - this.shouldDisplayLine = true; + var handPosition = self.getHandPosition(); + var handControllerPosition = Controller.getSpatialControlPosition(self.palm); + var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(self.palm)); + var grabbedProperties = Entities.getEntityProperties(self.grabbedEntity); + + lineOn(self, handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); + + if (self.actionID === null) { + // first time here since trigger pulled -- add the action and initialize some variables + self.currentObjectPosition = grabbedProperties.position; + self.currentObjectRotation = grabbedProperties.rotation; + self.handPreviousPosition = handControllerPosition; + self.handPreviousRotation = handRotation; + + self.actionID = Entities.addAction("spring", self.grabbedEntity, { + targetPosition: self.currentObjectPosition, + linearTimeScale: .1, + targetRotation: self.currentObjectRotation, + angularTimeScale: .1 + }); + if (self.actionID == NULL_ACTION_ID) { + self.actionID = null; } - } else if (this.triggerValue < SHOW_LINE_THRESHOLD && this.prevTriggerValue > SHOW_LINE_THRESHOLD) { - this.hidePointer(); - this.letGo(); - this.shouldDisplayLine = false; - } + } else { + // the action was set up on a previous call. update the targets. + var radius = Math.max(Vec3.distance(self.currentObjectPosition, handControllerPosition) * RADIUS_FACTOR, RADIUS_FACTOR); - if (this.shouldDisplayLine) { - this.updateLine(); - } - if (this.triggerValue > DISTANCE_HOLD_THRESHOLD && !this.closeGrabbing) { - this.attemptMove(); - } + var handMoved = Vec3.subtract(handControllerPosition, self.handPreviousPosition); + self.handPreviousPosition = handControllerPosition; + var superHandMoved = Vec3.multiply(handMoved, radius); + self.currentObjectPosition = Vec3.sum(self.currentObjectPosition, superHandMoved); - this.prevTriggerValue = this.triggerValue; + // this doubles hand rotation + var handChange = Quat.multiply(Quat.slerp(self.handPreviousRotation, handRotation, 2.0), + Quat.inverse(self.handPreviousRotation)); + self.handPreviousRotation = handRotation; + self.currentObjectRotation = Quat.multiply(handChange, self.currentObjectRotation); + + Entities.updateAction(self.grabbedEntity, self.actionID, { + targetPosition: self.currentObjectPosition, linearTimeScale: .1, + targetRotation: self.currentObjectRotation, angularTimeScale: .1 + }); + } } -controller.prototype.grabEntity = function() { - var handRotation = this.getHandRotation(); - var handPosition = this.getHandPosition(); - this.closeGrabbing = true; - //check if our entity has instructions on how to be grabbed, otherwise, just use default relative position and rotation - var userData = getEntityUserData(this.grabbedEntity); - var objectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation; +function closeGrabbing(self) { + if (!triggerSmoothedSqueezed(self)) { + self.state = STATE_RELEASE; + return; + } + + lineOff(self); + + var grabbedProperties = Entities.getEntityProperties(self.grabbedEntity); + + var handRotation = self.getHandRotation(); + var handPosition = self.getHandPosition(); + + var objectRotation = grabbedProperties.rotation; var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); - var objectPosition = Entities.getEntityProperties(this.grabbedEntity).position; - var offset = Vec3.subtract(objectPosition, handPosition); + self.currentObjectPosition = grabbedProperties.position; + self.currentObjectTime = Date.now(); + var offset = Vec3.subtract(self.currentObjectPosition, handPosition); var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); - var relativePosition = offsetPosition; - var relativeRotation = offsetRotation; - if (userData.grabFrame) { - if (userData.grabFrame.relativePosition) { - relativePosition = userData.grabFrame.relativePosition; - } - if (userData.grabFrame.relativeRotation) { - relativeRotation = userData.grabFrame.relativeRotation; - } - } - this.actionID = Entities.addAction("hold", this.grabbedEntity, { - hand: this.hand, + self.actionID = Entities.addAction("hold", self.grabbedEntity, { + hand: self.hand == RIGHT_HAND ? "right" : "left", timeScale: 0.05, - relativePosition: relativePosition, - relativeRotation: relativeRotation + relativePosition: offsetPosition, + relativeRotation: offsetRotation }); + if (self.actionID == NULL_ACTION_ID) { + self.actionID = null; + } else { + self.state = STATE_CONTINUE_CLOSE_GRABBING; + } } -controller.prototype.checkForInRangeObject = function() { - var handPosition = Controller.getSpatialControlPosition(this.palm); - var entities = Entities.findEntities(handPosition, GRAB_RADIUS); - var minDistance = GRAB_RADIUS; - var grabbedEntity = null; - //Get nearby entities and assign nearest - for (var i = 0; i < entities.length; i++) { - var props = Entities.getEntityProperties(entities[i]); - var distance = Vec3.distance(props.position, handPosition); - if (distance < minDistance && props.name !== "pointer" && props.collisionsWillMove === 1) { - grabbedEntity = entities[i]; - minDistance = distance; +function continueCloseGrabbing(self) { + if (!triggerSmoothedSqueezed(self)) { + self.state = STATE_RELEASE; + return; + } + + // keep track of the measured velocity of the held object + var grabbedProperties = Entities.getEntityProperties(self.grabbedEntity); + var now = Date.now(); + + var deltaPosition = Vec3.subtract(grabbedProperties.position, self.currentObjectPosition); + var deltaTime = (now - self.currentObjectTime) / 1000.0; // convert to seconds + + if (deltaTime > 0.0) { + var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); + // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger + // value would otherwise give the held object time to slow down. + if (triggerSqueezed(self)) { + self.grabbedVelocity = Vec3.sum(Vec3.multiply(self.grabbedVelocity, 0.1), + Vec3.multiply(grabbedVelocity, 0.9)); } } - if (grabbedEntity === null) { - return false; - } else { - //We are grabbing an entity, so let it know we've grabbed it - this.grabbedEntity = grabbedEntity; - this.activateEntity(this.grabbedEntity); - return true; + self.currentObjectPosition = grabbedProperties.position; + self.currentObjectTime = now; +} + + +function release(self) { + lineOff(self); + + if (self.grabbedEntity != null && self.actionID != null) { + Entities.deleteAction(self.grabbedEntity, self.actionID); } + + // the action will tend to quickly bring an object's velocity to zero. now that + // the action is gone, set the objects velocity to something the holder might expect. + Entities.editEntity(self.grabbedEntity, {velocity: self.grabbedVelocity}); + + self.grabbedVelocity = ZERO_VEC; + self.grabbedEntity = null; + self.actionID = null; + self.state = STATE_SEARCHING; } -controller.prototype.activateEntity = function(entity) { - var data = { - activated: true, - avatarId: MyAvatar.sessionUUID - }; - setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); -} - -controller.prototype.deactivateEntity = function(entity) { - var data = { - activated: false, - avatarId: null - }; - setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); -} controller.prototype.cleanup = function() { - Entities.deleteEntity(this.pointer); - if (this.grabbedEntity) { - Entities.deleteAction(this.grabbedEntity, this.actionID); - } + release(this); } + function update() { rightController.update(); leftController.update(); } + function cleanup() { rightController.cleanup(); leftController.cleanup(); } + Script.scriptEnding.connect(cleanup); Script.update.connect(update) From 9e94e7f1d0f6f96952eb1c98afde7db9dc6e9223 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Sep 2015 07:11:36 -0700 Subject: [PATCH 084/192] less complicated RotationAccumulator --- .../animation/src/RotationAccumulator.cpp | 25 +++++++++---------- libraries/animation/src/RotationAccumulator.h | 14 ++++------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/libraries/animation/src/RotationAccumulator.cpp b/libraries/animation/src/RotationAccumulator.cpp index 22b84afc77..fccb63fa35 100644 --- a/libraries/animation/src/RotationAccumulator.cpp +++ b/libraries/animation/src/RotationAccumulator.cpp @@ -11,19 +11,18 @@ #include -glm::quat RotationAccumulator::getAverage() { - glm::quat average; - uint32_t numRotations = _rotations.size(); - if (numRotations > 0) { - average = _rotations[0]; - for (uint32_t i = 1; i < numRotations; ++i) { - glm::quat rotation = _rotations[i]; - if (glm::dot(average, rotation) < 0.0f) { - rotation = -rotation; - } - average += rotation; +void RotationAccumulator::add(glm::quat rotation) { + if (_numRotations == 0) { + _rotationSum = rotation; + } else { + if (glm::dot(_rotationSum, rotation) < 0.0f) { + rotation = -rotation; } - average = glm::normalize(average); + _rotationSum += rotation; } - return average; + ++_numRotations; +} + +glm::quat RotationAccumulator::getAverage() { + return (_numRotations > 0) ? glm::normalize(_rotationSum) : glm::quat(); } diff --git a/libraries/animation/src/RotationAccumulator.h b/libraries/animation/src/RotationAccumulator.h index d6854d9b01..500f554271 100644 --- a/libraries/animation/src/RotationAccumulator.h +++ b/libraries/animation/src/RotationAccumulator.h @@ -10,25 +10,21 @@ #ifndef hifi_RotationAccumulator_h #define hifi_RotationAccumulator_h -#include - #include -#include class RotationAccumulator { public: - RotationAccumulator() {} + int size() const { return _numRotations; } - uint32_t size() const { return _rotations.size(); } - - void add(const glm::quat& rotation) { _rotations.push_back(rotation); } + void add(glm::quat rotation); glm::quat getAverage(); - void clear() { _rotations.clear(); } + void clear() { _numRotations = 0; } private: - std::vector _rotations; + glm::quat _rotationSum; + int _numRotations = 0; }; #endif // hifi_RotationAccumulator_h From b8c8ea2b53e3787e7e566326bcd061c4b7530a63 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 07:20:35 -0700 Subject: [PATCH 085/192] move magic numbers to constant variables, add some comments --- examples/controllers/handControllerGrab.js | 100 ++++++++++++++------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 7490a811c5..c80084eec2 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -12,39 +12,67 @@ Script.include("../libraries/utils.js"); + +///////////////////////////////////////////////////////////////// +// +// these tune time-averaging and "on" value for analog trigger +// + +var TRIGGER_SMOOTH_RATIO = 0.7; +var TRIGGER_ON_VALUE = 0.2; + +///////////////////////////////////////////////////////////////// +// +// distant manipulation +// + +var DISTANCE_HOLDING_RADIUS_FACTOR = 4; // multiplied by distance between hand and object +var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position +var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did +var NO_INTERSECT_COLOR = {red: 10, green: 10, blue: 255}; // line color when pick misses +var INTERSECT_COLOR = {red: 250, green: 10, blue: 10}; // line color when pick hits +var LINE_ENTITY_DIMENSIONS = {x: 1000, y: 1000, z: 1000}; +var LINE_LENGTH = 500; + + +///////////////////////////////////////////////////////////////// +// +// close grabbing +// + +var GRAB_RADIUS = 0.3; // if the ray misses but an object is this close, it will still be selected +var CLOSE_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position +var CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO = 0.9; // adjust time-averaging of held object's velocity +var CLOSE_PICK_MAX_DISTANCE = 0.6; // max length of pick-ray for close grabbing to be selected + + +///////////////////////////////////////////////////////////////// +// +// other constants +// + var RIGHT_HAND = 1; var LEFT_HAND = 0; -var GRAB_RADIUS = 0.3; -var RADIUS_FACTOR = 4; var ZERO_VEC = {x: 0, y: 0, z: 0}; var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}"; -var LINE_LENGTH = 500; +var MSEC_PER_SEC = 1000.0; var rightController = new controller(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK")); var leftController = new controller(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK")); +// these control how long an abandoned pointer line will hang around var startTime = Date.now(); var LIFETIME = 10; -var NO_INTERSECT_COLOR = { - red: 10, - green: 10, - blue: 255 -}; -var INTERSECT_COLOR = { - red: 250, - green: 10, - blue: 10 -}; - - +// states for the state machine var STATE_SEARCHING = 0; var STATE_DISTANCE_HOLDING = 1; var STATE_CLOSE_GRABBING = 2; var STATE_CONTINUE_CLOSE_GRABBING = 3; var STATE_RELEASE = 4; + function controller(hand, triggerAction) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -56,11 +84,11 @@ function controller(hand, triggerAction) { } this.triggerAction = triggerAction; this.palm = 2 * hand; - this.tip = 2 * hand + 1; + // this.tip = 2 * hand + 1; // unused, but I'm leaving this here for fear it will be needed this.actionID = null; // action this script created... this.grabbedEntity = null; // on this entity. - this.grabbedVelocity = ZERO_VEC; + this.grabbedVelocity = ZERO_VEC; // rolling average of held object's velocity this.state = 0; // 0 = searching, 1 = distanceHolding, 2 = closeGrabbing this.pointer = null; // entity-id of line object this.triggerValue = 0; // rolling average of trigger value @@ -94,7 +122,7 @@ function lineOn(self, closePoint, farPoint, color) { self.pointer = Entities.addEntity({ type: "Line", name: "pointer", - dimensions: {x: 1000, y: 1000, z: 1000}, + dimensions: LINE_ENTITY_DIMENSIONS, visible: true, position: closePoint, linePoints: [ ZERO_VEC, farPoint ], @@ -106,7 +134,7 @@ function lineOn(self, closePoint, farPoint, color) { position: closePoint, linePoints: [ ZERO_VEC, farPoint ], color: color, - lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME + lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME }); } } @@ -121,13 +149,14 @@ function lineOff(self) { function triggerSmoothedSqueezed(self) { var triggerValue = Controller.getActionValue(self.triggerAction); - self.triggerValue = (self.triggerValue * 0.7) + (triggerValue * 0.3); // smooth out trigger value - return self.triggerValue > 0.2; + // smooth out trigger value + self.triggerValue = (self.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + return self.triggerValue > TRIGGER_ON_VALUE; } function triggerSqueezed(self) { var triggerValue = Controller.getActionValue(self.triggerAction); - return triggerValue > 0.2; + return triggerValue > TRIGGER_ON_VALUE; } @@ -148,7 +177,7 @@ function search(self) { var handControllerPosition = Controller.getSpatialControlPosition(self.palm); var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection); self.grabbedEntity = intersection.entityID; - if (intersectionDistance < 0.6) { + if (intersectionDistance < CLOSE_PICK_MAX_DISTANCE) { // the hand is very close to the intersected object. go into close-grabbing mode. self.state = STATE_CLOSE_GRABBING; } else { @@ -202,16 +231,18 @@ function distanceHolding(self) { self.actionID = Entities.addAction("spring", self.grabbedEntity, { targetPosition: self.currentObjectPosition, - linearTimeScale: .1, + linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, targetRotation: self.currentObjectRotation, - angularTimeScale: .1 + angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME }); if (self.actionID == NULL_ACTION_ID) { self.actionID = null; } } else { // the action was set up on a previous call. update the targets. - var radius = Math.max(Vec3.distance(self.currentObjectPosition, handControllerPosition) * RADIUS_FACTOR, RADIUS_FACTOR); + var radius = Math.max(Vec3.distance(self.currentObjectPosition, + handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, + DISTANCE_HOLDING_RADIUS_FACTOR); var handMoved = Vec3.subtract(handControllerPosition, self.handPreviousPosition); self.handPreviousPosition = handControllerPosition; @@ -219,14 +250,15 @@ function distanceHolding(self) { self.currentObjectPosition = Vec3.sum(self.currentObjectPosition, superHandMoved); // this doubles hand rotation - var handChange = Quat.multiply(Quat.slerp(self.handPreviousRotation, handRotation, 2.0), + var handChange = Quat.multiply(Quat.slerp(self.handPreviousRotation, handRotation, + DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), Quat.inverse(self.handPreviousRotation)); self.handPreviousRotation = handRotation; self.currentObjectRotation = Quat.multiply(handChange, self.currentObjectRotation); Entities.updateAction(self.grabbedEntity, self.actionID, { - targetPosition: self.currentObjectPosition, linearTimeScale: .1, - targetRotation: self.currentObjectRotation, angularTimeScale: .1 + targetPosition: self.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: self.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME }); } } @@ -255,7 +287,7 @@ function closeGrabbing(self) { self.actionID = Entities.addAction("hold", self.grabbedEntity, { hand: self.hand == RIGHT_HAND ? "right" : "left", - timeScale: 0.05, + timeScale: CLOSE_GRABBING_ACTION_TIMEFRAME, relativePosition: offsetPosition, relativeRotation: offsetRotation }); @@ -277,16 +309,16 @@ function continueCloseGrabbing(self) { var grabbedProperties = Entities.getEntityProperties(self.grabbedEntity); var now = Date.now(); - var deltaPosition = Vec3.subtract(grabbedProperties.position, self.currentObjectPosition); - var deltaTime = (now - self.currentObjectTime) / 1000.0; // convert to seconds + var deltaPosition = Vec3.subtract(grabbedProperties.position, self.currentObjectPosition); // meters + var deltaTime = (now - self.currentObjectTime) / MSEC_PER_SEC; // convert to seconds if (deltaTime > 0.0) { var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger // value would otherwise give the held object time to slow down. if (triggerSqueezed(self)) { - self.grabbedVelocity = Vec3.sum(Vec3.multiply(self.grabbedVelocity, 0.1), - Vec3.multiply(grabbedVelocity, 0.9)); + self.grabbedVelocity = Vec3.sum(Vec3.multiply(self.grabbedVelocity, (1.0 - CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)), + Vec3.multiply(grabbedVelocity, CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)); } } From 0ba1e8c945144caded23fefb504ed0d3e777015d Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Sep 2015 09:49:57 -0700 Subject: [PATCH 086/192] remove renderingInWorldInterface signals --- interface/src/Application.cpp | 8 -------- interface/src/Application.h | 3 --- .../src/scripting/GlobalServicesScriptingInterface.cpp | 6 ++++-- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a313308023..09abfbe01e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3648,14 +3648,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se sceneInterface->setEngineDrawnOverlay3DItems(engineRC->_numDrawnOverlay3DItems); } - if (!selfAvatarOnly) { - // give external parties a change to hook in - { - PerformanceTimer perfTimer("inWorldInterface"); - emit renderingInWorldInterface(); - } - } - activeRenderingThread = nullptr; } diff --git a/interface/src/Application.h b/interface/src/Application.h index 6213dae4fa..0a591bf500 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -347,9 +347,6 @@ signals: /// Fired when we're simulating; allows external parties to hook in. void simulating(float deltaTime); - /// Fired when we're rendering in-world interface elements; allows external parties to hook in. - void renderingInWorldInterface(); - /// Fired when the import window is closed void importDone(); diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.cpp b/interface/src/scripting/GlobalServicesScriptingInterface.cpp index 26bee34d75..668bd92664 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.cpp +++ b/interface/src/scripting/GlobalServicesScriptingInterface.cpp @@ -22,8 +22,10 @@ GlobalServicesScriptingInterface::GlobalServicesScriptingInterface() { connect(&accountManager, &AccountManager::logoutComplete, this, &GlobalServicesScriptingInterface::loggedOut); _downloading = false; - connect(Application::getInstance(), &Application::renderingInWorldInterface, - this, &GlobalServicesScriptingInterface::checkDownloadInfo); + QTimer* checkDownloadTimer = new QTimer(this); + connect(checkDownloadTimer, &QTimer::timeout, this, &GlobalServicesScriptingInterface::checkDownloadInfo); + const int CHECK_DOWNLOAD_INTERVAL = MSECS_PER_SECOND / 2; + checkDownloadTimer->start(CHECK_DOWNLOAD_INTERVAL); auto discoverabilityManager = DependencyManager::get(); connect(discoverabilityManager.data(), &DiscoverabilityManager::discoverabilityModeChanged, From b08f5679998aedd4cdee3ec6989e33999f228f3d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 09:56:45 -0700 Subject: [PATCH 087/192] put controller specific functions inside the controller object --- examples/controllers/handControllerGrab.js | 471 +++++++++++---------- 1 file changed, 238 insertions(+), 233 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index c80084eec2..ed69d1844d 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -58,9 +58,6 @@ var ZERO_VEC = {x: 0, y: 0, z: 0}; var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}"; var MSEC_PER_SEC = 1000.0; -var rightController = new controller(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK")); -var leftController = new controller(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK")); - // these control how long an abandoned pointer line will hang around var startTime = Date.now(); var LIFETIME = 10; @@ -92,262 +89,270 @@ function controller(hand, triggerAction) { this.state = 0; // 0 = searching, 1 = distanceHolding, 2 = closeGrabbing this.pointer = null; // entity-id of line object this.triggerValue = 0; // rolling average of trigger value -} - -controller.prototype.update = function() { - switch(this.state) { - case STATE_SEARCHING: - search(this); - break; - case STATE_DISTANCE_HOLDING: - distanceHolding(this); - break; - case STATE_CLOSE_GRABBING: - closeGrabbing(this); - break; - case STATE_CONTINUE_CLOSE_GRABBING: - continueCloseGrabbing(this); - break; - case STATE_RELEASE: - release(this); - break; - } -} - - -function lineOn(self, closePoint, farPoint, color) { - // draw a line - if (self.pointer == null) { - self.pointer = Entities.addEntity({ - type: "Line", - name: "pointer", - dimensions: LINE_ENTITY_DIMENSIONS, - visible: true, - position: closePoint, - linePoints: [ ZERO_VEC, farPoint ], - color: color, - lifetime: LIFETIME - }); - } else { - Entities.editEntity(self.pointer, { - position: closePoint, - linePoints: [ ZERO_VEC, farPoint ], - color: color, - lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME - }); - } -} - - -function lineOff(self) { - if (self.pointer != null) { - Entities.deleteEntity(self.pointer); - } - self.pointer = null; -} - -function triggerSmoothedSqueezed(self) { - var triggerValue = Controller.getActionValue(self.triggerAction); - // smooth out trigger value - self.triggerValue = (self.triggerValue * TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); - return self.triggerValue > TRIGGER_ON_VALUE; -} - -function triggerSqueezed(self) { - var triggerValue = Controller.getActionValue(self.triggerAction); - return triggerValue > TRIGGER_ON_VALUE; -} - - -function search(self) { - if (!triggerSmoothedSqueezed(self)) { - self.state = STATE_RELEASE; - return; - } - - // the trigger is being pressed, do a ray test - var handPosition = self.getHandPosition(); - var pickRay = {origin: handPosition, direction: Quat.getUp(self.getHandRotation())}; - var intersection = Entities.findRayIntersection(pickRay, true); - if (intersection.intersects && - intersection.properties.collisionsWillMove === 1 && - intersection.properties.locked === 0) { - // the ray is intersecting something we can move. - var handControllerPosition = Controller.getSpatialControlPosition(self.palm); - var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection); - self.grabbedEntity = intersection.entityID; - if (intersectionDistance < CLOSE_PICK_MAX_DISTANCE) { - // the hand is very close to the intersected object. go into close-grabbing mode. - self.state = STATE_CLOSE_GRABBING; - } else { - // the hand is far from the intersected object. go into distance-holding mode - self.state = STATE_DISTANCE_HOLDING; - lineOn(self, pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + this.update = function() { + switch(this.state) { + case STATE_SEARCHING: + this.search(); + break; + case STATE_DISTANCE_HOLDING: + this.distanceHolding(); + break; + case STATE_CLOSE_GRABBING: + this.closeGrabbing(); + break; + case STATE_CONTINUE_CLOSE_GRABBING: + this.continueCloseGrabbing(); + break; + case STATE_RELEASE: + this.release(); + break; } - } else { - // forward ray test failed, try sphere test. - var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS); - var minDistance = GRAB_RADIUS; - var grabbedEntity = null; - for (var i = 0; i < nearbyEntities.length; i++) { - var props = Entities.getEntityProperties(nearbyEntities[i]); - var distance = Vec3.distance(props.position, handPosition); - if (distance < minDistance && props.name !== "pointer" && - props.collisionsWillMove === 1 && - props.locked === 0) { - self.grabbedEntity = nearbyEntities[i]; - minDistance = distance; + } + + + this.lineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.pointer == null) { + this.pointer = Entities.addEntity({ + type: "Line", + name: "pointer", + dimensions: LINE_ENTITY_DIMENSIONS, + visible: true, + position: closePoint, + linePoints: [ ZERO_VEC, farPoint ], + color: color, + lifetime: LIFETIME + }); + } else { + Entities.editEntity(this.pointer, { + position: closePoint, + linePoints: [ ZERO_VEC, farPoint ], + color: color, + lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME + }); + } + } + + + this.lineOff = function() { + if (this.pointer != null) { + Entities.deleteEntity(this.pointer); + } + this.pointer = null; + } + + + this.triggerSmoothedSqueezed = function() { + var triggerValue = Controller.getActionValue(this.triggerAction); + // smooth out trigger value + this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + return this.triggerValue > TRIGGER_ON_VALUE; + } + + + this.triggerSqueezed = function() { + var triggerValue = Controller.getActionValue(this.triggerAction); + return triggerValue > TRIGGER_ON_VALUE; + } + + + this.search = function() { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + + // the trigger is being pressed, do a ray test + var handPosition = this.getHandPosition(); + var pickRay = {origin: handPosition, direction: Quat.getUp(this.getHandRotation())}; + var intersection = Entities.findRayIntersection(pickRay, true); + if (intersection.intersects && + intersection.properties.collisionsWillMove === 1 && + intersection.properties.locked === 0) { + // the ray is intersecting something we can move. + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); + var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection); + this.grabbedEntity = intersection.entityID; + if (intersectionDistance < CLOSE_PICK_MAX_DISTANCE) { + // the hand is very close to the intersected object. go into close-grabbing mode. + this.state = STATE_CLOSE_GRABBING; + } else { + // the hand is far from the intersected object. go into distance-holding mode + this.state = STATE_DISTANCE_HOLDING; + this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + } + } else { + // forward ray test failed, try sphere test. + var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var minDistance = GRAB_RADIUS; + var grabbedEntity = null; + for (var i = 0; i < nearbyEntities.length; i++) { + var props = Entities.getEntityProperties(nearbyEntities[i]); + var distance = Vec3.distance(props.position, handPosition); + if (distance < minDistance && props.name !== "pointer" && + props.collisionsWillMove === 1 && + props.locked === 0) { + this.grabbedEntity = nearbyEntities[i]; + minDistance = distance; + } + } + if (this.grabbedEntity === null) { + this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + } else { + this.state = STATE_CLOSE_GRABBING; } } - if (self.grabbedEntity === null) { - lineOn(self, pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + } + + + this.distanceHolding = function() { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + + var handPosition = this.getHandPosition(); + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); + var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity); + + this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); + + if (this.actionID === null) { + // first time here since trigger pulled -- add the action and initialize some variables + this.currentObjectPosition = grabbedProperties.position; + this.currentObjectRotation = grabbedProperties.rotation; + this.handPreviousPosition = handControllerPosition; + this.handPreviousRotation = handRotation; + + this.actionID = Entities.addAction("spring", this.grabbedEntity, { + targetPosition: this.currentObjectPosition, + linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: this.currentObjectRotation, + angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + }); + if (this.actionID == NULL_ACTION_ID) { + this.actionID = null; + } } else { - self.state = STATE_CLOSE_GRABBING; + // the action was set up on a previous call. update the targets. + var radius = Math.max(Vec3.distance(this.currentObjectPosition, + handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, + DISTANCE_HOLDING_RADIUS_FACTOR); + + var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition); + this.handPreviousPosition = handControllerPosition; + var superHandMoved = Vec3.multiply(handMoved, radius); + this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); + + // this doubles hand rotation + var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, + DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + Quat.inverse(this.handPreviousRotation)); + this.handPreviousRotation = handRotation; + this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); + + Entities.updateAction(this.grabbedEntity, this.actionID, { + targetPosition: this.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: this.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + }); } } -} -function distanceHolding(self) { - if (!triggerSmoothedSqueezed(self)) { - self.state = STATE_RELEASE; - return; - } + this.closeGrabbing = function() { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } - var handPosition = self.getHandPosition(); - var handControllerPosition = Controller.getSpatialControlPosition(self.palm); - var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(self.palm)); - var grabbedProperties = Entities.getEntityProperties(self.grabbedEntity); + this.lineOff(); - lineOn(self, handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity); - if (self.actionID === null) { - // first time here since trigger pulled -- add the action and initialize some variables - self.currentObjectPosition = grabbedProperties.position; - self.currentObjectRotation = grabbedProperties.rotation; - self.handPreviousPosition = handControllerPosition; - self.handPreviousRotation = handRotation; + var handRotation = this.getHandRotation(); + var handPosition = this.getHandPosition(); - self.actionID = Entities.addAction("spring", self.grabbedEntity, { - targetPosition: self.currentObjectPosition, - linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, - targetRotation: self.currentObjectRotation, - angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + var objectRotation = grabbedProperties.rotation; + var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); + + this.currentObjectPosition = grabbedProperties.position; + this.currentObjectTime = Date.now(); + var offset = Vec3.subtract(this.currentObjectPosition, handPosition); + var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); + + this.actionID = Entities.addAction("hold", this.grabbedEntity, { + hand: this.hand == RIGHT_HAND ? "right" : "left", + timeScale: CLOSE_GRABBING_ACTION_TIMEFRAME, + relativePosition: offsetPosition, + relativeRotation: offsetRotation }); - if (self.actionID == NULL_ACTION_ID) { - self.actionID = null; - } - } else { - // the action was set up on a previous call. update the targets. - var radius = Math.max(Vec3.distance(self.currentObjectPosition, - handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, - DISTANCE_HOLDING_RADIUS_FACTOR); - - var handMoved = Vec3.subtract(handControllerPosition, self.handPreviousPosition); - self.handPreviousPosition = handControllerPosition; - var superHandMoved = Vec3.multiply(handMoved, radius); - self.currentObjectPosition = Vec3.sum(self.currentObjectPosition, superHandMoved); - - // this doubles hand rotation - var handChange = Quat.multiply(Quat.slerp(self.handPreviousRotation, handRotation, - DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - Quat.inverse(self.handPreviousRotation)); - self.handPreviousRotation = handRotation; - self.currentObjectRotation = Quat.multiply(handChange, self.currentObjectRotation); - - Entities.updateAction(self.grabbedEntity, self.actionID, { - targetPosition: self.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, - targetRotation: self.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME - }); - } -} - - -function closeGrabbing(self) { - if (!triggerSmoothedSqueezed(self)) { - self.state = STATE_RELEASE; - return; - } - - lineOff(self); - - var grabbedProperties = Entities.getEntityProperties(self.grabbedEntity); - - var handRotation = self.getHandRotation(); - var handPosition = self.getHandPosition(); - - var objectRotation = grabbedProperties.rotation; - var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); - - self.currentObjectPosition = grabbedProperties.position; - self.currentObjectTime = Date.now(); - var offset = Vec3.subtract(self.currentObjectPosition, handPosition); - var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); - - self.actionID = Entities.addAction("hold", self.grabbedEntity, { - hand: self.hand == RIGHT_HAND ? "right" : "left", - timeScale: CLOSE_GRABBING_ACTION_TIMEFRAME, - relativePosition: offsetPosition, - relativeRotation: offsetRotation - }); - if (self.actionID == NULL_ACTION_ID) { - self.actionID = null; - } else { - self.state = STATE_CONTINUE_CLOSE_GRABBING; - } -} - - -function continueCloseGrabbing(self) { - if (!triggerSmoothedSqueezed(self)) { - self.state = STATE_RELEASE; - return; - } - - // keep track of the measured velocity of the held object - var grabbedProperties = Entities.getEntityProperties(self.grabbedEntity); - var now = Date.now(); - - var deltaPosition = Vec3.subtract(grabbedProperties.position, self.currentObjectPosition); // meters - var deltaTime = (now - self.currentObjectTime) / MSEC_PER_SEC; // convert to seconds - - if (deltaTime > 0.0) { - var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); - // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger - // value would otherwise give the held object time to slow down. - if (triggerSqueezed(self)) { - self.grabbedVelocity = Vec3.sum(Vec3.multiply(self.grabbedVelocity, (1.0 - CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)), - Vec3.multiply(grabbedVelocity, CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)); + if (this.actionID == NULL_ACTION_ID) { + this.actionID = null; + } else { + this.state = STATE_CONTINUE_CLOSE_GRABBING; } } - self.currentObjectPosition = grabbedProperties.position; - self.currentObjectTime = now; -} + this.continueCloseGrabbing = function() { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } -function release(self) { - lineOff(self); + // keep track of the measured velocity of the held object + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity); + var now = Date.now(); - if (self.grabbedEntity != null && self.actionID != null) { - Entities.deleteAction(self.grabbedEntity, self.actionID); + var deltaPosition = Vec3.subtract(grabbedProperties.position, this.currentObjectPosition); // meters + var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds + + if (deltaTime > 0.0) { + var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); + // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger + // value would otherwise give the held object time to slow down. + if (this.triggerSqueezed()) { + this.grabbedVelocity = + Vec3.sum(Vec3.multiply(this.grabbedVelocity, + (1.0 - CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)), + Vec3.multiply(grabbedVelocity, CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)); + } + } + + this.currentObjectPosition = grabbedProperties.position; + this.currentObjectTime = now; } - // the action will tend to quickly bring an object's velocity to zero. now that - // the action is gone, set the objects velocity to something the holder might expect. - Entities.editEntity(self.grabbedEntity, {velocity: self.grabbedVelocity}); - self.grabbedVelocity = ZERO_VEC; - self.grabbedEntity = null; - self.actionID = null; - self.state = STATE_SEARCHING; + this.release = function() { + this.lineOff(); + + if (this.grabbedEntity != null && this.actionID != null) { + Entities.deleteAction(this.grabbedEntity, this.actionID); + } + + // the action will tend to quickly bring an object's velocity to zero. now that + // the action is gone, set the objects velocity to something the holder might expect. + Entities.editEntity(this.grabbedEntity, {velocity: this.grabbedVelocity}); + + this.grabbedVelocity = ZERO_VEC; + this.grabbedEntity = null; + this.actionID = null; + this.state = STATE_SEARCHING; + } + + + this.cleanup = function() { + release(); + } } -controller.prototype.cleanup = function() { - release(this); -} +var rightController = new controller(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK")); +var leftController = new controller(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK")); function update() { From 9a3a87eedf94726882e1896383bc604eb1d4bb01 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 18 Sep 2015 10:03:21 -0700 Subject: [PATCH 088/192] improve velocity calculations for wand --- examples/toys/bubblewand/bubble.js | 7 +++-- examples/toys/bubblewand/createWand.js | 4 +-- examples/toys/bubblewand/wand.js | 39 +++++++++++++++++--------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index 69764c0c97..f69116ef92 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -71,9 +71,9 @@ position: position, lifetime: 0.1, dimensions: { - x: 10, - y: 10, - z: 10 + x: 1, + y: 1, + z: 1 }, emitVelocity: { x: 0.35, @@ -90,6 +90,7 @@ y: -0.1, z: 0 }, + particleRadius:0.1, alphaStart: 0.5, alpha: 0.5, alphaFinish: 0, diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index a54e438e5d..37041fdbf6 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -18,7 +18,7 @@ Script.include("../../libraries/utils.js"); var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); -//create the wand in front of the avatar +//create the wand in front of the avatar blahy var center = Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0, y: 0.5, z: 0}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); var tablePosition = { @@ -26,7 +26,7 @@ var tablePosition = { y:495.63, z:506.25 } - +print('test refresh') var wand = Entities.addEntity({ name:'Bubble Wand', type: "Model", diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index e0495f5f40..9da4901d56 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -16,7 +16,7 @@ Script.include("../../libraries/utils.js"); var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var BUBBLE_SCRIPT = Script.resolvePath('bubble.js?'+randInt(0,10000)); + var BUBBLE_SCRIPT = Script.resolvePath('bubble.js?' + randInt(0, 10000)); var BUBBLE_INITIAL_DIMENSIONS = { x: 0.01, @@ -29,14 +29,14 @@ var BUBBLE_SIZE_MIN = 1; var BUBBLE_SIZE_MAX = 5; var BUBBLE_DIVISOR = 50; + var BUBBLE_LINEAR_DAMPING = 0.4; var GROWTH_FACTOR = 0.005; var SHRINK_FACTOR = 0.001; var SHRINK_LOWER_LIMIT = 0.02; var WAND_TIP_OFFSET = 0.05; var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; var VELOCITY_STRENGH_MAX = 10; - var VELOCITY_STRENGTH_MULTIPLIER = 100; - var VELOCITY_THRESHOLD = 1; + var VELOCITY_THRESHOLD = 0.5; var _this; @@ -73,8 +73,8 @@ } var properties = Entities.getEntityProperties(_this.entityID); - - _this.growBubbleWithWandVelocity(properties); + var dt = deltaTime; + _this.growBubbleWithWandVelocity(properties, dt); var wandTipPosition = _this.getWandTipPosition(properties); @@ -92,7 +92,7 @@ //remove the current bubble when the wand is released Entities.deleteEntity(_this.currentBubble); - _this.currentBubble=null + _this.currentBubble = null return } @@ -108,27 +108,34 @@ this.wandTipPosition = wandTipPosition; return wandTipPosition }, + addCollisionsToBubbleAfterCreation: function(bubble) { + Entities.editEntity(bubble, { + collisionsWillMove: true + }) + + }, randomizeBubbleGravity: function() { var randomNumber = randInt(0, 3); - var gravity= { + var gravity = { x: 0, y: -randomNumber / 10, z: 0 } return gravity }, - growBubbleWithWandVelocity: function(properties) { - + growBubbleWithWandVelocity: function(properties, deltaTime) { var wandPosition = properties.position; var wandTipPosition = this.getWandTipPosition(properties) - var velocity = Vec3.subtract(wandPosition, this.lastPosition) - var velocityStrength = Vec3.length(velocity) * VELOCITY_STRENGTH_MULTIPLIER; - - - + var distance = Vec3.subtract(wandPosition, this.lastPosition); + var velocity = Vec3.multiply(distance,1/deltaTime); + + + var velocityStrength = Vec3.length(velocity); + print('velocityStrength' +velocityStrength) + // if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { // velocityStrength = 0 // } @@ -187,6 +194,9 @@ dimensions: dimensions }); }, + setBubbleOwner:function(bubble){ + setEntityCustomData(BUBBLE_USER_DATA_KEY, bubble, { avatarID: MyAvatar.sessionUUID }); + }, createBubbleAtTipOfWand: function() { //create a new bubble at the tip of the wand @@ -208,6 +218,7 @@ dimensions: BUBBLE_INITIAL_DIMENSIONS, collisionsWillMove: true, ignoreForCollisions: false, + linearDamping: BUBBLE_LINEAR_DAMPING, shapeType: "sphere", script: BUBBLE_SCRIPT, }); From 6ed0a57d9f9e314e55d51d832105feb8b7e52c4e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Sep 2015 10:09:23 -0700 Subject: [PATCH 089/192] avoid unecessary computation of last absolutePose --- libraries/animation/src/AnimInverseKinematics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 409c243612..084139747a 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -263,7 +263,7 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar // only update the absolutePoses that need it: those between lowestMovedIndex and _maxTargetIndex if (lowestMovedIndex < _maxTargetIndex) { - for (int i = lowestMovedIndex; i <= _maxTargetIndex; ++i) { + for (int i = lowestMovedIndex; i < _maxTargetIndex; ++i) { int parentIndex = _skeleton->getParentIndex(i); if (parentIndex != -1) { absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; From a85afb5280f0747786674bc9515d4f7fa50fe6ee Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Sep 2015 10:13:21 -0700 Subject: [PATCH 090/192] simplify logic of RotationAccumulator::add() --- libraries/animation/src/RotationAccumulator.cpp | 12 +++++------- libraries/animation/src/RotationAccumulator.h | 4 +++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/animation/src/RotationAccumulator.cpp b/libraries/animation/src/RotationAccumulator.cpp index fccb63fa35..b3ec790d20 100644 --- a/libraries/animation/src/RotationAccumulator.cpp +++ b/libraries/animation/src/RotationAccumulator.cpp @@ -12,14 +12,12 @@ #include void RotationAccumulator::add(glm::quat rotation) { - if (_numRotations == 0) { - _rotationSum = rotation; - } else { - if (glm::dot(_rotationSum, rotation) < 0.0f) { - rotation = -rotation; - } - _rotationSum += rotation; + // make sure both quaternions are on the same hyper-hemisphere before we add them + if (glm::dot(_rotationSum, rotation) < 0.0f) { + rotation = -rotation; } + // sum the rotation linearly (lerp) + _rotationSum += rotation; ++_numRotations; } diff --git a/libraries/animation/src/RotationAccumulator.h b/libraries/animation/src/RotationAccumulator.h index 500f554271..bb30c14363 100644 --- a/libraries/animation/src/RotationAccumulator.h +++ b/libraries/animation/src/RotationAccumulator.h @@ -14,6 +14,8 @@ class RotationAccumulator { public: + RotationAccumulator() : _rotationSum(0.0f, 0.0f, 0.0f, 0.0f), _numRotations(0) { } + int size() const { return _numRotations; } void add(glm::quat rotation); @@ -24,7 +26,7 @@ public: private: glm::quat _rotationSum; - int _numRotations = 0; + int _numRotations; }; #endif // hifi_RotationAccumulator_h From 4cb2249cda69af25bd8121e33a8043776ddeebc6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Sep 2015 10:16:19 -0700 Subject: [PATCH 091/192] premature optimization: remove another branch --- libraries/animation/src/RotationAccumulator.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/libraries/animation/src/RotationAccumulator.cpp b/libraries/animation/src/RotationAccumulator.cpp index b3ec790d20..e3de402ef8 100644 --- a/libraries/animation/src/RotationAccumulator.cpp +++ b/libraries/animation/src/RotationAccumulator.cpp @@ -12,12 +12,8 @@ #include void RotationAccumulator::add(glm::quat rotation) { - // make sure both quaternions are on the same hyper-hemisphere before we add them - if (glm::dot(_rotationSum, rotation) < 0.0f) { - rotation = -rotation; - } - // sum the rotation linearly (lerp) - _rotationSum += rotation; + // make sure both quaternions are on the same hyper-hemisphere before we add them linearly (lerp) + _rotationSum += copysignf(1.0f, glm::dot(_rotationSum, rotation)) * rotation; ++_numRotations; } From 15ea9219d6c9110a26ceb8f615eef11c5e61c438 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 18 Sep 2015 10:41:44 -0700 Subject: [PATCH 092/192] collision timeouts --- examples/toys/bubblewand/bubble.js | 16 +++++++++++++--- examples/toys/bubblewand/wand.js | 25 +++++++++++++++++-------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index f69116ef92..ebb5a36d2d 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -16,6 +16,8 @@ var BUBBLE_PARTICLE_TEXTURE = "http://hifi-public.s3.amazonaws.com/james/bubblewand/textures/bubble_particle.png" + var BUBBLE_USER_DATA_KEY = "BubbleKey"; + BUBBLE_PARTICLE_COLOR = { red: 0, green: 40, @@ -44,8 +46,16 @@ this.unload = function(entityID) { Script.update.disconnect(this.update); - //TODO: Unload doesn't seem like the right place to do this. We really want to know that our lifetime is over. - // _this.createBurstParticles(); + var defaultGrabData = { + avatarId: null + }; + + var bubbleCreator = getEntityCustomData(BUBBLE_USER_DATA_KEY, entityID, defaultGrabData); + + if (bubbleCreator === MyAvatar.sessionUUID) { + this.createBurstParticles(); + } + }; @@ -90,7 +100,7 @@ y: -0.1, z: 0 }, - particleRadius:0.1, + particleRadius: 0.1, alphaStart: 0.5, alpha: 0.5, alphaFinish: 0, diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 9da4901d56..0e960f1f64 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -10,6 +10,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + (function() { Script.include("../../utilities.js"); @@ -18,6 +19,7 @@ var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var BUBBLE_SCRIPT = Script.resolvePath('bubble.js?' + randInt(0, 10000)); + var BUBBLE_USER_DATA_KEY = "BubbleKey"; var BUBBLE_INITIAL_DIMENSIONS = { x: 0.01, y: 0.01, @@ -109,6 +111,8 @@ return wandTipPosition }, addCollisionsToBubbleAfterCreation: function(bubble) { + + print('adding collisions to bubble' + bubble); Entities.editEntity(bubble, { collisionsWillMove: true }) @@ -130,12 +134,12 @@ var distance = Vec3.subtract(wandPosition, this.lastPosition); - var velocity = Vec3.multiply(distance,1/deltaTime); - - + var velocity = Vec3.multiply(distance, 1 / deltaTime); + + var velocityStrength = Vec3.length(velocity); - print('velocityStrength' +velocityStrength) - + print('velocityStrength' + velocityStrength) + // if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { // velocityStrength = 0 // } @@ -167,6 +171,9 @@ gravity: this.randomizeBubbleGravity() }); + //wait to make the bubbles collidable, so that they dont hit each other and the wand + Script.setTimeout(this.addCollisionsToBubbleAfterCreation(this.currentBubble), lifetime / 2); + //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble this.createBubbleAtTipOfWand(); @@ -194,8 +201,10 @@ dimensions: dimensions }); }, - setBubbleOwner:function(bubble){ - setEntityCustomData(BUBBLE_USER_DATA_KEY, bubble, { avatarID: MyAvatar.sessionUUID }); + setBubbleOwner: function(bubble) { + setEntityCustomData(BUBBLE_USER_DATA_KEY, bubble, { + avatarID: MyAvatar.sessionUUID + }); }, createBubbleAtTipOfWand: function() { @@ -216,7 +225,7 @@ modelURL: BUBBLE_MODEL, position: wandTipPosition, dimensions: BUBBLE_INITIAL_DIMENSIONS, - collisionsWillMove: true, + collisionsWillMove: false, ignoreForCollisions: false, linearDamping: BUBBLE_LINEAR_DAMPING, shapeType: "sphere", From e723a5c0fd62c862a280cc6bd6a67a0234e4d1fc Mon Sep 17 00:00:00 2001 From: Seiji Emery Date: Fri, 18 Sep 2015 11:18:33 -0700 Subject: [PATCH 093/192] whitespace + "waiting for entity server" No functionality changes, but now prints "waiting for entity server" once if waiting for more than X seconds in startup(). --- examples/example/entities/platform.js | 2163 +++++++++++++------------ 1 file changed, 1087 insertions(+), 1076 deletions(-) diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js index d4fe7d0f68..1748198cce 100644 --- a/examples/example/entities/platform.js +++ b/examples/example/entities/platform.js @@ -15,29 +15,29 @@ // UI and debug console implemented using uiwidgets / 2d overlays Script.include("../../libraries/uiwidgets.js"); -if (typeof(UI) === 'undefined') { // backup link in case the user downloaded this somewhere - print("Missing library script -- loading from public.highfidelity.io"); - Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); - if (typeof(UI) === 'undefined') { - print("Cannot load UIWidgets library -- check your internet connection", COLORS.RED); - throw new Error("Could not load uiwidgets.js"); - } +if (typeof(UI) === 'undefined') { // backup link in case the user downloaded this somewhere + print("Missing library script -- loading from public.highfidelity.io"); + Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js'); + if (typeof(UI) === 'undefined') { + print("Cannot load UIWidgets library -- check your internet connection", COLORS.RED); + throw new Error("Could not load uiwidgets.js"); + } } // Platform script (function () { var SCRIPT_NAME = "platform.js"; -var USE_DEBUG_LOG = true; // Turns on the 2dOverlay-based debug log. If false, just redirects to print. +var USE_DEBUG_LOG = true; // Turns on the 2dOverlay-based debug log. If false, just redirects to print. var NUM_DEBUG_LOG_LINES = 10; -var LOG_ENTITY_CREATION_MESSAGES = false; // detailed debugging (init) -var LOG_UPDATE_STATUS_MESSAGES = false; // detailed debugging (startup) +var LOG_ENTITY_CREATION_MESSAGES = false; // detailed debugging (init) +var LOG_UPDATE_STATUS_MESSAGES = false; // detailed debugging (startup) -var MAX_UPDATE_INTERVAL = 0.2; // restrict to 5 updates / sec +var MAX_UPDATE_INTERVAL = 0.2; // restrict to 5 updates / sec var AVATAR_HEIGHT_OFFSET = 1.5; // offset to make the platform spawn under your feet. Might need to be adjusted for unusually proportioned avatars. var USE_ENTITY_TIMEOUTS = true; -var ENTITY_TIMEOUT_DURATION = 30.0; // kill entities in 30 secs if they don't get any updates -var ENTITY_REFRESH_INTERVAL = 10.0; // poke the entities every 10s so they don't die until we're done with them +var ENTITY_TIMEOUT_DURATION = 30.0; // kill entities in 30 secs if they don't get any updates +var ENTITY_REFRESH_INTERVAL = 10.0; // poke the entities every 10s so they don't die until we're done with them // Initial state var NUM_PLATFORM_ENTITIES = 400; @@ -46,1039 +46,1039 @@ var RADIUS = 5.0; // Defines min/max for onscreen platform radius, density, and entity width/height/depth sliders. // Color limits are hardcoded at [0, 255]. var PLATFORM_RADIUS_RANGE = [ 1.0, 15.0 ]; -var PLATFORM_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density. +var PLATFORM_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density. var PLATFORM_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits // Utils (function () { - if (typeof(Math.randRange) === 'undefined') { - Math.randRange = function (min, max) { - return Math.random() * (max - min) + min; - } - } - if (typeof(Math.randInt) === 'undefined') { - Math.randInt = function (n) { - return Math.floor(Math.random() * n) | 0; - } - } - function fromComponents (r, g, b, a) { - this.red = r; - this.green = g; - this.blue = b; - this.alpha = a || 1.0; - } - function fromHex (c) { - this.red = parseInt(c[1] + c[2], 16); - this.green = parseInt(c[3] + c[4], 16); - this.blue = parseInt(c[5] + c[6], 16); - } - var Color = this.Color = function () { - if (arguments.length >= 3) { - fromComponents.apply(this, arguments); - } else if (arguments.length == 1 && arguments[0].length == 7 && arguments[0][0] == '#') { - fromHex.apply(this, arguments); - } else { - throw new Error("Invalid arguments to new Color(): " + JSON.stringify(arguments)); - } - } - Color.prototype.toString = function () { - return "[Color: " + JSON.stringify(this) + "]"; - } + if (typeof(Math.randRange) === 'undefined') { + Math.randRange = function (min, max) { + return Math.random() * (max - min) + min; + } + } + if (typeof(Math.randInt) === 'undefined') { + Math.randInt = function (n) { + return Math.floor(Math.random() * n) | 0; + } + } + function fromComponents (r, g, b, a) { + this.red = r; + this.green = g; + this.blue = b; + this.alpha = a || 1.0; + } + function fromHex (c) { + this.red = parseInt(c[1] + c[2], 16); + this.green = parseInt(c[3] + c[4], 16); + this.blue = parseInt(c[5] + c[6], 16); + } + var Color = this.Color = function () { + if (arguments.length >= 3) { + fromComponents.apply(this, arguments); + } else if (arguments.length == 1 && arguments[0].length == 7 && arguments[0][0] == '#') { + fromHex.apply(this, arguments); + } else { + throw new Error("Invalid arguments to new Color(): " + JSON.stringify(arguments)); + } + } + Color.prototype.toString = function () { + return "[Color: " + JSON.stringify(this) + "]"; + } })(); // RNG models (function () { - /// Encapsulates a simple color model that generates colors using a linear, pseudo-random color distribution. - var RandomColorModel = this.RandomColorModel = function () { - this.shadeRange = 0; // = 200; - this.minColor = 55; // = 100; - this.redRange = 255; // = 200; - this.greenRange = 0; // = 10; - this.blueRange = 0; // = 0; - }; - /// Generates 4 numbers in [0, 1] corresponding to each color attribute (uniform shade and additive red, green, blue). - /// This is done in a separate step from actually generating the colors, since it allows us to either A) completely - /// rebuild / re-randomize the color values, or B) reuse the RNG values but with different color parameters, which - /// enables us to do realtime color editing on the same visuals (awesome!). - RandomColorModel.prototype.generateSeed = function () { - return [ Math.random(), Math.random(), Math.random(), Math.random() ]; - }; - /// Takes a random 'seed' (4 floats from this.generateSeed()) and calculates a pseudo-random - /// color by combining that with the color model's current parameters. - RandomColorModel.prototype.getRandom = function (r) { - // logMessage("color seed values " + JSON.stringify(r)); - var shade = Math.min(255, this.minColor + r[0] * this.shadeRange); + /// Encapsulates a simple color model that generates colors using a linear, pseudo-random color distribution. + var RandomColorModel = this.RandomColorModel = function () { + this.shadeRange = 0; // = 200; + this.minColor = 55; // = 100; + this.redRange = 255; // = 200; + this.greenRange = 0; // = 10; + this.blueRange = 0; // = 0; + }; + /// Generates 4 numbers in [0, 1] corresponding to each color attribute (uniform shade and additive red, green, blue). + /// This is done in a separate step from actually generating the colors, since it allows us to either A) completely + /// rebuild / re-randomize the color values, or B) reuse the RNG values but with different color parameters, which + /// enables us to do realtime color editing on the same visuals (awesome!). + RandomColorModel.prototype.generateSeed = function () { + return [ Math.random(), Math.random(), Math.random(), Math.random() ]; + }; + /// Takes a random 'seed' (4 floats from this.generateSeed()) and calculates a pseudo-random + /// color by combining that with the color model's current parameters. + RandomColorModel.prototype.getRandom = function (r) { + // logMessage("color seed values " + JSON.stringify(r)); + var shade = Math.min(255, this.minColor + r[0] * this.shadeRange); - // No clamping on the color components, so they may overflow. - // However, this creates some pretty interesting visuals, so we're not "fixing" this. - var color = { - red: shade + r[1] * this.redRange, - green: shade + r[2] * this.greenRange, - blue: shade + r[3] * this.blueRange - }; - // logMessage("this: " + JSON.stringify(this)); - // logMessage("color: " + JSON.stringify(color), COLORS.RED); - return color; - }; - /// Custom property iterator used to setup UI (sliders, etc) - RandomColorModel.prototype.setupUI = function (callback) { - var _this = this; - [ - ['shadeRange', 'shade range'], - ['minColor', 'shade min'], - ['redRange', 'red (additive)'], - ['greenRange', 'green (additive)'], - ['blueRange', 'blue (additive)'] - ].forEach(function (v) { - // name, value, min, max, onValueChanged - callback(v[1], _this[v[0]], 0, 255, function (value) { _this[v[0]] = value }); - }); - } + // No clamping on the color components, so they may overflow. + // However, this creates some pretty interesting visuals, so we're not "fixing" this. + var color = { + red: shade + r[1] * this.redRange, + green: shade + r[2] * this.greenRange, + blue: shade + r[3] * this.blueRange + }; + // logMessage("this: " + JSON.stringify(this)); + // logMessage("color: " + JSON.stringify(color), COLORS.RED); + return color; + }; + /// Custom property iterator used to setup UI (sliders, etc) + RandomColorModel.prototype.setupUI = function (callback) { + var _this = this; + [ + ['shadeRange', 'shade range'], + ['minColor', 'shade min'], + ['redRange', 'red (additive)'], + ['greenRange', 'green (additive)'], + ['blueRange', 'blue (additive)'] + ].forEach(function (v) { + // name, value, min, max, onValueChanged + callback(v[1], _this[v[0]], 0, 255, function (value) { _this[v[0]] = value }); + }); + } - /// Generates pseudo-random dimensions for our cubes / shapes. - var RandomShapeModel = this.RandomShapeModel = function () { - this.widthRange = [ 0.3, 0.7 ]; - this.depthRange = [ 0.5, 0.8 ]; - this.heightRange = [ 0.01, 0.08 ]; - }; - /// Generates 3 seed numbers in [0, 1] - RandomShapeModel.prototype.generateSeed = function () { - return [ Math.random(), Math.random(), Math.random() ]; - } - /// Combines seed values with width/height/depth ranges to produce vec3 dimensions for a cube / sphere. - RandomShapeModel.prototype.getRandom = function (r) { - return { - x: r[0] * (this.widthRange[1] - this.widthRange[0]) + this.widthRange[0], - y: r[1] * (this.heightRange[1] - this.heightRange[0]) + this.heightRange[0], - z: r[2] * (this.depthRange[1] - this.depthRange[0]) + this.depthRange[0] - }; - } - /// Custom property iterator used to setup UI (sliders, etc) - RandomShapeModel.prototype.setupUI = function (callback) { - var _this = this; - var dimensionsMin = PLATFORM_SHAPE_DIMENSIONS_RANGE[0]; - var dimensionsMax = PLATFORM_SHAPE_DIMENSIONS_RANGE[1]; - [ - ['widthRange', 'width'], - ['depthRange', 'depth'], - ['heightRange', 'height'] - ].forEach(function (v) { - // name, value, min, max, onValueChanged - callback(v[1], _this[v[0]], dimensionsMin, dimensionsMax, function (value) { _this[v[0]] = value }); - }); - } + /// Generates pseudo-random dimensions for our cubes / shapes. + var RandomShapeModel = this.RandomShapeModel = function () { + this.widthRange = [ 0.3, 0.7 ]; + this.depthRange = [ 0.5, 0.8 ]; + this.heightRange = [ 0.01, 0.08 ]; + }; + /// Generates 3 seed numbers in [0, 1] + RandomShapeModel.prototype.generateSeed = function () { + return [ Math.random(), Math.random(), Math.random() ]; + } + /// Combines seed values with width/height/depth ranges to produce vec3 dimensions for a cube / sphere. + RandomShapeModel.prototype.getRandom = function (r) { + return { + x: r[0] * (this.widthRange[1] - this.widthRange[0]) + this.widthRange[0], + y: r[1] * (this.heightRange[1] - this.heightRange[0]) + this.heightRange[0], + z: r[2] * (this.depthRange[1] - this.depthRange[0]) + this.depthRange[0] + }; + } + /// Custom property iterator used to setup UI (sliders, etc) + RandomShapeModel.prototype.setupUI = function (callback) { + var _this = this; + var dimensionsMin = PLATFORM_SHAPE_DIMENSIONS_RANGE[0]; + var dimensionsMax = PLATFORM_SHAPE_DIMENSIONS_RANGE[1]; + [ + ['widthRange', 'width'], + ['depthRange', 'depth'], + ['heightRange', 'height'] + ].forEach(function (v) { + // name, value, min, max, onValueChanged + callback(v[1], _this[v[0]], dimensionsMin, dimensionsMax, function (value) { _this[v[0]] = value }); + }); + } - /// Combines color + shape PRNG models and hides their implementation details. - var RandomAttribModel = this.RandomAttribModel = function () { - this.colorModel = new RandomColorModel(); - this.shapeModel = new RandomShapeModel(); - } - /// Completely re-randomizes obj's `color` and `dimensions` parameters based on the current model params. - RandomAttribModel.prototype.randomizeShapeAndColor = function (obj) { - // logMessage("randomizing " + JSON.stringify(obj)); - obj._colorSeed = this.colorModel.generateSeed(); - obj._shapeSeed = this.shapeModel.generateSeed(); - this.updateShapeAndColor(obj); - // logMessage("color seed: " + JSON.stringify(obj._colorSeed), COLORS.RED); - // logMessage("randomized color: " + JSON.stringify(obj.color), COLORS.RED); - // logMessage("randomized: " + JSON.stringify(obj)); - return obj; - } - /// Updates obj's `color` and `dimensions` params to use the current model params. - /// Reuses hidden seed attribs; _must_ have called randomizeShapeAndColor(obj) at some point before - /// calling this. - RandomAttribModel.prototype.updateShapeAndColor = function (obj) { - try { - // logMessage("update shape and color: " + this.colorModel); - obj.color = this.colorModel.getRandom(obj._colorSeed); - obj.dimensions = this.shapeModel.getRandom(obj._shapeSeed); - } catch (e) { - logMessage("update shape / color failed", COLORS.RED); - logMessage('' + e, COLORS.RED); - logMessage("obj._colorSeed = " + JSON.stringify(obj._colorSeed)); - logMessage("obj._shapeSeed = " + JSON.stringify(obj._shapeSeed)); - // logMessage("obj = " + JSON.stringify(obj)); - throw e; - } - return obj; - } + /// Combines color + shape PRNG models and hides their implementation details. + var RandomAttribModel = this.RandomAttribModel = function () { + this.colorModel = new RandomColorModel(); + this.shapeModel = new RandomShapeModel(); + } + /// Completely re-randomizes obj's `color` and `dimensions` parameters based on the current model params. + RandomAttribModel.prototype.randomizeShapeAndColor = function (obj) { + // logMessage("randomizing " + JSON.stringify(obj)); + obj._colorSeed = this.colorModel.generateSeed(); + obj._shapeSeed = this.shapeModel.generateSeed(); + this.updateShapeAndColor(obj); + // logMessage("color seed: " + JSON.stringify(obj._colorSeed), COLORS.RED); + // logMessage("randomized color: " + JSON.stringify(obj.color), COLORS.RED); + // logMessage("randomized: " + JSON.stringify(obj)); + return obj; + } + /// Updates obj's `color` and `dimensions` params to use the current model params. + /// Reuses hidden seed attribs; _must_ have called randomizeShapeAndColor(obj) at some point before + /// calling this. + RandomAttribModel.prototype.updateShapeAndColor = function (obj) { + try { + // logMessage("update shape and color: " + this.colorModel); + obj.color = this.colorModel.getRandom(obj._colorSeed); + obj.dimensions = this.shapeModel.getRandom(obj._shapeSeed); + } catch (e) { + logMessage("update shape / color failed", COLORS.RED); + logMessage('' + e, COLORS.RED); + logMessage("obj._colorSeed = " + JSON.stringify(obj._colorSeed)); + logMessage("obj._shapeSeed = " + JSON.stringify(obj._shapeSeed)); + // logMessage("obj = " + JSON.stringify(obj)); + throw e; + } + return obj; + } })(); // Status / logging UI (ignore this) (function () { - var COLORS = this.COLORS = { - 'GREEN': new Color("#2D870C"), - 'RED': new Color("#AF1E07"), - 'LIGHT_GRAY': new Color("#CCCCCC"), - 'DARK_GRAY': new Color("#4E4E4E") - }; - function buildDebugLog () { - var LINE_WIDTH = 400; - var LINE_HEIGHT = 20; - - var lines = []; - var lineIndex = 0; - for (var i = 0; i < NUM_DEBUG_LOG_LINES; ++i) { - lines.push(new UI.Label({ - text: " ", visible: false, - width: LINE_WIDTH, height: LINE_HEIGHT, - })); - } - var title = new UI.Label({ - text: SCRIPT_NAME, visible: true, - width: LINE_WIDTH, height: LINE_HEIGHT, - }); - - var overlay = new UI.Box({ - visible: true, - width: LINE_WIDTH, height: 0, - backgroundColor: COLORS.DARK_GRAY, - backgroundAlpha: 0.3 - }); - overlay.setPosition(280, 10); - relayoutFrom(0); - UI.updateLayout(); - - function relayoutFrom (n) { - var layoutPos = { - x: overlay.position.x, - y: overlay.position.y - }; - - title.setPosition(layoutPos.x, layoutPos.y); - layoutPos.y += LINE_HEIGHT; - - // for (var i = n; i >= 0; --i) { - for (var i = n + 1; i < lines.length; ++i) { - if (lines[i].visible) { - lines[i].setPosition(layoutPos.x, layoutPos.y); - layoutPos.y += LINE_HEIGHT; - } - } - // for (var i = lines.length - 1; i > n; --i) { - for (var i = 0; i <= n; ++i) { - if (lines[i].visible) { - lines[i].setPosition(layoutPos.x, layoutPos.y); - layoutPos.y += LINE_HEIGHT; - } - } - overlay.height = (layoutPos.y - overlay.position.y + 10); - overlay.getOverlay().update({ - height: overlay.height - }); - } - this.logMessage = function (text, color, alpha) { - lines[lineIndex].setVisible(true); - relayoutFrom(lineIndex); - - lines[lineIndex].getOverlay().update({ - text: text, - visible: true, - color: color || COLORS.LIGHT_GRAY, - alpha: alpha !== undefined ? alpha : 1.0, - x: lines[lineIndex].position.x, - y: lines[lineIndex].position.y - }); - lineIndex = (lineIndex + 1) % lines.length; - UI.updateLayout(); - } - } - if (USE_DEBUG_LOG) { - buildDebugLog(); - } else { - this.logMessage = function (msg) { - print(SCRIPT_NAME + ": " + msg); - } - } + var COLORS = this.COLORS = { + 'GREEN': new Color("#2D870C"), + 'RED': new Color("#AF1E07"), + 'LIGHT_GRAY': new Color("#CCCCCC"), + 'DARK_GRAY': new Color("#4E4E4E") + }; + function buildDebugLog () { + var LINE_WIDTH = 400; + var LINE_HEIGHT = 20; + + var lines = []; + var lineIndex = 0; + for (var i = 0; i < NUM_DEBUG_LOG_LINES; ++i) { + lines.push(new UI.Label({ + text: " ", visible: false, + width: LINE_WIDTH, height: LINE_HEIGHT, + })); + } + var title = new UI.Label({ + text: SCRIPT_NAME, visible: true, + width: LINE_WIDTH, height: LINE_HEIGHT, + }); + + var overlay = new UI.Box({ + visible: true, + width: LINE_WIDTH, height: 0, + backgroundColor: COLORS.DARK_GRAY, + backgroundAlpha: 0.3 + }); + overlay.setPosition(280, 10); + relayoutFrom(0); + UI.updateLayout(); + + function relayoutFrom (n) { + var layoutPos = { + x: overlay.position.x, + y: overlay.position.y + }; + + title.setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + + // for (var i = n; i >= 0; --i) { + for (var i = n + 1; i < lines.length; ++i) { + if (lines[i].visible) { + lines[i].setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + } + } + // for (var i = lines.length - 1; i > n; --i) { + for (var i = 0; i <= n; ++i) { + if (lines[i].visible) { + lines[i].setPosition(layoutPos.x, layoutPos.y); + layoutPos.y += LINE_HEIGHT; + } + } + overlay.height = (layoutPos.y - overlay.position.y + 10); + overlay.getOverlay().update({ + height: overlay.height + }); + } + this.logMessage = function (text, color, alpha) { + lines[lineIndex].setVisible(true); + relayoutFrom(lineIndex); + + lines[lineIndex].getOverlay().update({ + text: text, + visible: true, + color: color || COLORS.LIGHT_GRAY, + alpha: alpha !== undefined ? alpha : 1.0, + x: lines[lineIndex].position.x, + y: lines[lineIndex].position.y + }); + lineIndex = (lineIndex + 1) % lines.length; + UI.updateLayout(); + } + } + if (USE_DEBUG_LOG) { + buildDebugLog(); + } else { + this.logMessage = function (msg) { + print(SCRIPT_NAME + ": " + msg); + } + } })(); // Utils (ignore) (function () { - // Utility function - var withDefaults = this.withDefaults = function (properties, defaults) { - // logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults)); - properties = properties || {}; - if (defaults) { - for (var k in defaults) { - properties[k] = defaults[k]; - } - } - return properties; - } - var withReadonlyProp = this.withReadonlyProp = function (propname, value, obj) { - Object.defineProperty(obj, propname, { - value: value, - writable: false - }); - return obj; - } + // Utility function + var withDefaults = this.withDefaults = function (properties, defaults) { + // logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults)); + properties = properties || {}; + if (defaults) { + for (var k in defaults) { + properties[k] = defaults[k]; + } + } + return properties; + } + var withReadonlyProp = this.withReadonlyProp = function (propname, value, obj) { + Object.defineProperty(obj, propname, { + value: value, + writable: false + }); + return obj; + } - // Math utils - if (typeof(Math.randRange) === 'undefined') { - Math.randRange = function (min, max) { - return Math.random() * (max - min) + min; - } - } - if (typeof(Math.randInt) === 'undefined') { - Math.randInt = function (n) { - return Math.floor(Math.random() * n) | 0; - } - } + // Math utils + if (typeof(Math.randRange) === 'undefined') { + Math.randRange = function (min, max) { + return Math.random() * (max - min) + min; + } + } + if (typeof(Math.randInt) === 'undefined') { + Math.randInt = function (n) { + return Math.floor(Math.random() * n) | 0; + } + } - /// Random distrib: Get a random point within a circle on the xz plane with radius r, center p. - this.randomCirclePoint = function (r, pos) { - var a = Math.random(), b = Math.random(); - if (b < a) { - var tmp = b; - b = a; - a = tmp; - } - var point = { - x: pos.x + b * r * Math.cos(2 * Math.PI * a / b), - y: pos.y, - z: pos.z + b * r * Math.sin(2 * Math.PI * a / b) - }; - if (LOG_ENTITY_CREATION_MESSAGES) { - // logMessage("input params: " + JSON.stringify({ radius: r, position: pos }), COLORS.GREEN); - // logMessage("a = " + a + ", b = " + b); - logMessage("generated point: " + JSON.stringify(point), COLORS.RED); - } - return point; - } + /// Random distrib: Get a random point within a circle on the xz plane with radius r, center p. + this.randomCirclePoint = function (r, pos) { + var a = Math.random(), b = Math.random(); + if (b < a) { + var tmp = b; + b = a; + a = tmp; + } + var point = { + x: pos.x + b * r * Math.cos(2 * Math.PI * a / b), + y: pos.y, + z: pos.z + b * r * Math.sin(2 * Math.PI * a / b) + }; + if (LOG_ENTITY_CREATION_MESSAGES) { + // logMessage("input params: " + JSON.stringify({ radius: r, position: pos }), COLORS.GREEN); + // logMessage("a = " + a + ", b = " + b); + logMessage("generated point: " + JSON.stringify(point), COLORS.RED); + } + return point; + } - // Entity utils. NOT using overlayManager for... reasons >.> - var makeEntity = this.makeEntity = function (properties) { - if (LOG_ENTITY_CREATION_MESSAGES) { - logMessage("Creating entity: " + JSON.stringify(properties)); - } - var entity = Entities.addEntity(properties); - return withReadonlyProp("type", properties.type, { - update: function (properties) { - Entities.editEntity(entity, properties); - }, - destroy: function () { - Entities.deleteEntity(entity) - }, - getId: function () { - return entity; - } - }); - } - // this.makeLight = function (properties) { - // return makeEntity(withDefaults(properties, { - // type: "Light", - // isSpotlight: false, - // diffuseColor: { red: 255, green: 100, blue: 100 }, - // ambientColor: { red: 200, green: 80, blue: 80 } - // })); - // } - this.makeBox = function (properties) { - // logMessage("Creating box: " + JSON.stringify(properties)); - return makeEntity(withDefaults(properties, { - type: "Box" - })); - } + // Entity utils. NOT using overlayManager for... reasons >.> + var makeEntity = this.makeEntity = function (properties) { + if (LOG_ENTITY_CREATION_MESSAGES) { + logMessage("Creating entity: " + JSON.stringify(properties)); + } + var entity = Entities.addEntity(properties); + return withReadonlyProp("type", properties.type, { + update: function (properties) { + Entities.editEntity(entity, properties); + }, + destroy: function () { + Entities.deleteEntity(entity) + }, + getId: function () { + return entity; + } + }); + } + // this.makeLight = function (properties) { + // return makeEntity(withDefaults(properties, { + // type: "Light", + // isSpotlight: false, + // diffuseColor: { red: 255, green: 100, blue: 100 }, + // ambientColor: { red: 200, green: 80, blue: 80 } + // })); + // } + this.makeBox = function (properties) { + // logMessage("Creating box: " + JSON.stringify(properties)); + return makeEntity(withDefaults(properties, { + type: "Box" + })); + } })(); // Platform (function () { - /// Encapsulates a platform 'piece'. Owns an entity (`box`), and handles destruction and some other state. - var PlatformComponent = this.PlatformComponent = function (properties) { - // logMessage("Platform component initialized with " + Object.keys(properties), COLORS.GREEN); - this.position = properties.position || null; - this.color = properties.color || null; - this.dimensions = properties.dimensions || null; - this.entityType = properties.type || "Box"; + /// Encapsulates a platform 'piece'. Owns an entity (`box`), and handles destruction and some other state. + var PlatformComponent = this.PlatformComponent = function (properties) { + // logMessage("Platform component initialized with " + Object.keys(properties), COLORS.GREEN); + this.position = properties.position || null; + this.color = properties.color || null; + this.dimensions = properties.dimensions || null; + this.entityType = properties.type || "Box"; - // logMessage("Spawning with type: '" + this.entityType + "' (properties.type = '" + properties.type + "')", COLORS.GREEN); + // logMessage("Spawning with type: '" + this.entityType + "' (properties.type = '" + properties.type + "')", COLORS.GREEN); - if (properties._colorSeed) - this._colorSeed = properties._colorSeed; - if (properties._shapeSeed) - this._shapeSeed = properties._shapeSeed; + if (properties._colorSeed) + this._colorSeed = properties._colorSeed; + if (properties._shapeSeed) + this._shapeSeed = properties._shapeSeed; - // logMessage("dimensions: " + JSON.stringify(this.dimensions)); - // logMessage("color: " + JSON.stringify(this.color)); + // logMessage("dimensions: " + JSON.stringify(this.dimensions)); + // logMessage("color: " + JSON.stringify(this.color)); - this.cachedEntity = null; - this.activeEntity = this.spawnEntity(this.entityType); - }; - PlatformComponent.prototype.spawnEntity = function (type) { - return makeEntity({ - type: type, - position: this.position, - dimensions: this.dimensions, - color: this.color, - lifetime: USE_ENTITY_TIMEOUTS ? ENTITY_TIMEOUT_DURATION : -1.0, - alpha: 0.5 - }); - } - if (USE_ENTITY_TIMEOUTS) { - PlatformComponent.prototype.pokeEntity = function () { - // Kinda inefficient, but there's no way to get around this :/ - var age = Entities.getEntityProperties(this.activeEntity.getId()).age; - this.activeEntity.update({ lifetime: ENTITY_TIMEOUT_DURATION + age }); - } - } else { - PlatformComponent.prototype.pokeEntity = function () {} - } - /// Updates platform to be at position p, and calls .update() with the current - /// position, color, and dimensions parameters. - PlatformComponent.prototype.update = function (position) { - if (position) - this.position = position; - // logMessage("updating with " + JSON.stringify(this)); - this.activeEntity.update(this); - } - function swap (a, b) { - var tmp = a; - a = b; - b = tmp; - } - PlatformComponent.prototype.swapEntityType = function (newType) { - if (this.entityType !== newType) { - this.entityType = newType; - // logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')"); - if (this.activeEntity) { - this.activeEntity.destroy(); - } - this.activeEntity = this.spawnEntity(newType); - // if (this.cachedEntity && this.cachedEntity.type == newType) { - // this.cachedEntity.update({ visible: true }); - // this.activeEntity.update({ visible: false }); - // swap(this.cachedEntity, this.activeEntity); - // this.update(this.position); - // } else { - // this.activeEntity.update({ visible: false }); - // this.cachedEntity = this.activeEntity; - // this.activeEntity = spawnEntity(newType); - // } - } - } - /// Swap state with another component - PlatformComponent.prototype.swap = function (other) { - swap(this.position, other.position); - swap(this.dimensions, other.dimensions); - swap(this.color, other.color); - swap(this.entityType, other.entityType); - swap(this.activeEntity, other.activeEntity); - swap(this._colorSeed, other._colorSeed); - swap(this._shapeSeed, other._shapeSeed); - } - PlatformComponent.prototype.destroy = function () { - if (this.activeEntity) { - this.activeEntity.destroy(); - this.activeEntity = null; - } - if (this.cachedEntity) { - this.cachedEntity.destroy(); - this.cachedEntity = null; - } - } + this.cachedEntity = null; + this.activeEntity = this.spawnEntity(this.entityType); + }; + PlatformComponent.prototype.spawnEntity = function (type) { + return makeEntity({ + type: type, + position: this.position, + dimensions: this.dimensions, + color: this.color, + lifetime: USE_ENTITY_TIMEOUTS ? ENTITY_TIMEOUT_DURATION : -1.0, + alpha: 0.5 + }); + } + if (USE_ENTITY_TIMEOUTS) { + PlatformComponent.prototype.pokeEntity = function () { + // Kinda inefficient, but there's no way to get around this :/ + var age = Entities.getEntityProperties(this.activeEntity.getId()).age; + this.activeEntity.update({ lifetime: ENTITY_TIMEOUT_DURATION + age }); + } + } else { + PlatformComponent.prototype.pokeEntity = function () {} + } + /// Updates platform to be at position p, and calls .update() with the current + /// position, color, and dimensions parameters. + PlatformComponent.prototype.update = function (position) { + if (position) + this.position = position; + // logMessage("updating with " + JSON.stringify(this)); + this.activeEntity.update(this); + } + function swap (a, b) { + var tmp = a; + a = b; + b = tmp; + } + PlatformComponent.prototype.swapEntityType = function (newType) { + if (this.entityType !== newType) { + this.entityType = newType; + // logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')"); + if (this.activeEntity) { + this.activeEntity.destroy(); + } + this.activeEntity = this.spawnEntity(newType); + // if (this.cachedEntity && this.cachedEntity.type == newType) { + // this.cachedEntity.update({ visible: true }); + // this.activeEntity.update({ visible: false }); + // swap(this.cachedEntity, this.activeEntity); + // this.update(this.position); + // } else { + // this.activeEntity.update({ visible: false }); + // this.cachedEntity = this.activeEntity; + // this.activeEntity = spawnEntity(newType); + // } + } + } + /// Swap state with another component + PlatformComponent.prototype.swap = function (other) { + swap(this.position, other.position); + swap(this.dimensions, other.dimensions); + swap(this.color, other.color); + swap(this.entityType, other.entityType); + swap(this.activeEntity, other.activeEntity); + swap(this._colorSeed, other._colorSeed); + swap(this._shapeSeed, other._shapeSeed); + } + PlatformComponent.prototype.destroy = function () { + if (this.activeEntity) { + this.activeEntity.destroy(); + this.activeEntity = null; + } + if (this.cachedEntity) { + this.cachedEntity.destroy(); + this.cachedEntity = null; + } + } - // util - function inRange (p1, p2, radius) { - return Vec3.distance(p1, p2) < Math.abs(radius); - } + // util + function inRange (p1, p2, radius) { + return Vec3.distance(p1, p2) < Math.abs(radius); + } - /// Encapsulates a moving platform that follows the avatar around (mostly). - var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) { - this.position = position; - this.radius = radius; - this.randomizer = new RandomAttribModel(); - this.boxType = "Box"; - this.boxTypes = [ "Box", "Sphere" ]; + /// Encapsulates a moving platform that follows the avatar around (mostly). + var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) { + this.position = position; + this.radius = radius; + this.randomizer = new RandomAttribModel(); + this.boxType = "Box"; + this.boxTypes = [ "Box", "Sphere" ]; - logMessage("Spawning " + n + " entities", COLORS.GREEN); - var boxes = this.boxes = []; - while (n > 0) { - boxes.push(this.spawnEntity()); - --n; - } - this.targetDensity = this.getEntityDensity(); - this.pendingUpdates = {}; - this.updateTimer = 0.0; + logMessage("Spawning " + n + " entities", COLORS.GREEN); + var boxes = this.boxes = []; + while (n > 0) { + boxes.push(this.spawnEntity()); + --n; + } + this.targetDensity = this.getEntityDensity(); + this.pendingUpdates = {}; + this.updateTimer = 0.0; - this.platformHeight = position.y; - this.oldPos = { x: position.x, y: position.y, z: position.z }; - this.oldRadius = radius; + this.platformHeight = position.y; + this.oldPos = { x: position.x, y: position.y, z: position.z }; + this.oldRadius = radius; - // this.sendPokes(); - } - DynamicPlatform.prototype.toString = function () { - return "[DynamicPlatform (" + this.boxes.length + " entities)]"; - } - DynamicPlatform.prototype.spawnEntity = function () { - // logMessage("Called spawn entity. this.boxType = '" + this.boxType + "'") - var properties = { position: this.randomPoint(), type: this.boxType }; - this.randomizer.randomizeShapeAndColor(properties); - return new PlatformComponent(properties); - } - DynamicPlatform.prototype.updateEntityAttribs = function () { - var _this = this; - this.setPendingUpdate('updateEntityAttribs', function () { - // logMessage("updating model", COLORS.GREEN); - _this.boxes.forEach(function (box) { - this.randomizer.updateShapeAndColor(box); - box.update(); - }, _this); - }); - } - DynamicPlatform.prototype.toggleBoxType = function () { - var _this = this; - this.setPendingUpdate('toggleBoxType', function () { - // Swap / cycle through types: find index of current type and set next type to idx+1 - for (var idx = 0; idx < _this.boxTypes.length; ++idx) { - if (_this.boxTypes[idx] === _this.boxType) { - var nextIndex = (idx + 1) % _this.boxTypes.length; - logMessage("swapping box type from '" + _this.boxType + "' to '" + _this.boxTypes[nextIndex] + "'", COLORS.GREEN); - _this.boxType = _this.boxTypes[nextIndex]; - break; - } - } - _this.boxes.forEach(function (box) { - box.swapEntityType(_this.boxType); - }, _this); - }); - } - DynamicPlatform.prototype.getBoxType = function () { - return this.boxType; - } + // this.sendPokes(); + } + DynamicPlatform.prototype.toString = function () { + return "[DynamicPlatform (" + this.boxes.length + " entities)]"; + } + DynamicPlatform.prototype.spawnEntity = function () { + // logMessage("Called spawn entity. this.boxType = '" + this.boxType + "'") + var properties = { position: this.randomPoint(), type: this.boxType }; + this.randomizer.randomizeShapeAndColor(properties); + return new PlatformComponent(properties); + } + DynamicPlatform.prototype.updateEntityAttribs = function () { + var _this = this; + this.setPendingUpdate('updateEntityAttribs', function () { + // logMessage("updating model", COLORS.GREEN); + _this.boxes.forEach(function (box) { + this.randomizer.updateShapeAndColor(box); + box.update(); + }, _this); + }); + } + DynamicPlatform.prototype.toggleBoxType = function () { + var _this = this; + this.setPendingUpdate('toggleBoxType', function () { + // Swap / cycle through types: find index of current type and set next type to idx+1 + for (var idx = 0; idx < _this.boxTypes.length; ++idx) { + if (_this.boxTypes[idx] === _this.boxType) { + var nextIndex = (idx + 1) % _this.boxTypes.length; + logMessage("swapping box type from '" + _this.boxType + "' to '" + _this.boxTypes[nextIndex] + "'", COLORS.GREEN); + _this.boxType = _this.boxTypes[nextIndex]; + break; + } + } + _this.boxes.forEach(function (box) { + box.swapEntityType(_this.boxType); + }, _this); + }); + } + DynamicPlatform.prototype.getBoxType = function () { + return this.boxType; + } - // if (USE_ENTITY_TIMEOUTS) { - // DynamicPlatform.prototype.sendPokes = function () { - // var _this = this; - // function poke () { - // logMessage("Poking entities so they don't die", COLORS.GREEN); - // _this.boxes.forEach(function (box) { - // box.pokeEntity(); - // }, _this); + // if (USE_ENTITY_TIMEOUTS) { + // DynamicPlatform.prototype.sendPokes = function () { + // var _this = this; + // function poke () { + // logMessage("Poking entities so they don't die", COLORS.GREEN); + // _this.boxes.forEach(function (box) { + // box.pokeEntity(); + // }, _this); - // if (_this.pendingUpdates['keepalive']) { - // logMessage("previous timer: " + _this.pendingUpdates['keepalive'].timer + "; new timer: " + ENTITY_REFRESH_INTERVAL) - // } - // _this.pendingUpdates['keepalive'] = { - // callback: poke, - // timer: ENTITY_REFRESH_INTERVAL, - // skippedUpdates: 0 - // }; - // // _this.setPendingUpdate('keepalive', poke); - // // _this.pendingUpdates['keepalive'].timer = ENTITY_REFRESH_INTERVAL; - // } - // poke(); - // } - // } else { - // DynamicPlatform.prototype.sendPokes = function () {}; - // } + // if (_this.pendingUpdates['keepalive']) { + // logMessage("previous timer: " + _this.pendingUpdates['keepalive'].timer + "; new timer: " + ENTITY_REFRESH_INTERVAL) + // } + // _this.pendingUpdates['keepalive'] = { + // callback: poke, + // timer: ENTITY_REFRESH_INTERVAL, + // skippedUpdates: 0 + // }; + // // _this.setPendingUpdate('keepalive', poke); + // // _this.pendingUpdates['keepalive'].timer = ENTITY_REFRESH_INTERVAL; + // } + // poke(); + // } + // } else { + // DynamicPlatform.prototype.sendPokes = function () {}; + // } - /// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms). - /// This is to prevent UI code from running full entity updates every 10 ms (or whatever). - DynamicPlatform.prototype.setPendingUpdate = function (name, callback) { - if (!this.pendingUpdates[name]) { - // logMessage("Queued update for " + name, COLORS.GREEN); - this.pendingUpdates[name] = { - callback: callback, - timer: 0.0, - skippedUpdates: 0 - } - } else { - // logMessage("Deferred update for " + name, COLORS.GREEN); - this.pendingUpdates[name].callback = callback; - this.pendingUpdates[name].skippedUpdates++; - // logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds"); - } - } - /// Runs all queued updates as soon as they can execute (each one has a cooldown timer). - DynamicPlatform.prototype.processPendingUpdates = function (dt) { - for (var k in this.pendingUpdates) { - if (this.pendingUpdates[k].timer >= 0.0) - this.pendingUpdates[k].timer -= dt; + /// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms). + /// This is to prevent UI code from running full entity updates every 10 ms (or whatever). + DynamicPlatform.prototype.setPendingUpdate = function (name, callback) { + if (!this.pendingUpdates[name]) { + // logMessage("Queued update for " + name, COLORS.GREEN); + this.pendingUpdates[name] = { + callback: callback, + timer: 0.0, + skippedUpdates: 0 + } + } else { + // logMessage("Deferred update for " + name, COLORS.GREEN); + this.pendingUpdates[name].callback = callback; + this.pendingUpdates[name].skippedUpdates++; + // logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds"); + } + } + /// Runs all queued updates as soon as they can execute (each one has a cooldown timer). + DynamicPlatform.prototype.processPendingUpdates = function (dt) { + for (var k in this.pendingUpdates) { + if (this.pendingUpdates[k].timer >= 0.0) + this.pendingUpdates[k].timer -= dt; - if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) { - // logMessage("Dispatching update for " + k); - try { - this.pendingUpdates[k].callback(); - } catch (e) { - logMessage("update for \"" + k + "\" failed: " + e, COLORS.RED); - } - this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL; - this.pendingUpdates[k].skippedUpdates = 0; - this.pendingUpdates[k].callback = null; - } else { - // logMessage("Deferred update for " + k + " for " + this.pendingUpdates[k].timer + " seconds"); - } - } - } + if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) { + // logMessage("Dispatching update for " + k); + try { + this.pendingUpdates[k].callback(); + } catch (e) { + logMessage("update for \"" + k + "\" failed: " + e, COLORS.RED); + } + this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL; + this.pendingUpdates[k].skippedUpdates = 0; + this.pendingUpdates[k].callback = null; + } else { + // logMessage("Deferred update for " + k + " for " + this.pendingUpdates[k].timer + " seconds"); + } + } + } - /// Updates the platform based on the avatar's current position (spawning / despawning entities as needed), - /// and calls processPendingUpdates() once this is done. - /// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full - /// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a - /// normal walking / flying speed. - DynamicPlatform.prototype.updatePosition = function (dt, position) { - // logMessage("updating " + this); - position.y = this.platformHeight; - this.position = position; + /// Updates the platform based on the avatar's current position (spawning / despawning entities as needed), + /// and calls processPendingUpdates() once this is done. + /// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full + /// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a + /// normal walking / flying speed. + DynamicPlatform.prototype.updatePosition = function (dt, position) { + // logMessage("updating " + this); + position.y = this.platformHeight; + this.position = position; - var toUpdate = []; - this.boxes.forEach(function (box, i) { - // if (Math.abs(box.position.y - position.y) > HEIGHT_TOLERANCE || !inRange(box, position, radius)) { - if (!inRange(box.position, this.position, this.radius)) { - toUpdate.push(i); - } - }, this); + var toUpdate = []; + this.boxes.forEach(function (box, i) { + // if (Math.abs(box.position.y - position.y) > HEIGHT_TOLERANCE || !inRange(box, position, radius)) { + if (!inRange(box.position, this.position, this.radius)) { + toUpdate.push(i); + } + }, this); - var MAX_TRIES = toUpdate.length * 8; - var tries = MAX_TRIES; - var moved = 0; - var recalcs = 0; - toUpdate.forEach(function (index) { - if ((index % 2 == 0) || tries > 0) { - do { - var randomPoint = this.randomPoint(this.position, this.radius); - ++recalcs - } while (--tries > 0 && inRange(randomPoint, this.oldPos, this.oldRadiuss)); - - if (LOG_UPDATE_STATUS_MESSAGES && tries <= 0) { - logMessage("updatePlatform() gave up after " + MAX_TRIES + " iterations (" + moved + " / " + toUpdate.length + " successful updates)", COLORS.RED); - logMessage("old pos: " + JSON.stringify(this.oldPos) + ", old radius: " + this.oldRadius); - logMessage("new pos: " + JSON.stringify(this.position) + ", new radius: " + this.radius); - } - } else { - var randomPoint = this.randomPoint(position, this.radius); - } + var MAX_TRIES = toUpdate.length * 8; + var tries = MAX_TRIES; + var moved = 0; + var recalcs = 0; + toUpdate.forEach(function (index) { + if ((index % 2 == 0) || tries > 0) { + do { + var randomPoint = this.randomPoint(this.position, this.radius); + ++recalcs + } while (--tries > 0 && inRange(randomPoint, this.oldPos, this.oldRadiuss)); + + if (LOG_UPDATE_STATUS_MESSAGES && tries <= 0) { + logMessage("updatePlatform() gave up after " + MAX_TRIES + " iterations (" + moved + " / " + toUpdate.length + " successful updates)", COLORS.RED); + logMessage("old pos: " + JSON.stringify(this.oldPos) + ", old radius: " + this.oldRadius); + logMessage("new pos: " + JSON.stringify(this.position) + ", new radius: " + this.radius); + } + } else { + var randomPoint = this.randomPoint(position, this.radius); + } - this.randomizer.randomizeShapeAndColor(this.boxes[index]); - this.boxes[index].update(randomPoint); - // this.boxes[index].setValues({ - // position: randomPoint, - // // dimensions: this.randomDimensions(), - // // color: this.randomColor() - // }); - ++moved; - }, this); - recalcs = recalcs - toUpdate.length; + this.randomizer.randomizeShapeAndColor(this.boxes[index]); + this.boxes[index].update(randomPoint); + // this.boxes[index].setValues({ + // position: randomPoint, + // // dimensions: this.randomDimensions(), + // // color: this.randomColor() + // }); + ++moved; + }, this); + recalcs = recalcs - toUpdate.length; - this.oldPos = position; - this.oldRadius = this.radius; - if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) { - logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs"); - } - } + this.oldPos = position; + this.oldRadius = this.radius; + if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) { + logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs"); + } + } - DynamicPlatform.prototype.update = function (dt, position) { - this.updatePosition(dt, position); - this.processPendingUpdates(dt); - this.sendPokes(dt); - } + DynamicPlatform.prototype.update = function (dt, position) { + this.updatePosition(dt, position); + this.processPendingUpdates(dt); + this.sendPokes(dt); + } - if (USE_ENTITY_TIMEOUTS) { - DynamicPlatform.prototype.sendPokes = function (dt) { - logMessage("starting keepalive", COLORS.GREEN); - // logMessage("dt = " + dt, COLORS.RED); - // var original = this.sendPokes; - var pokeTimer = 0.0; - this.sendPokes = function (dt) { - // logMessage("dt = " + dt); - if ((pokeTimer -= dt) < 0.0) { - // logMessage("Poking entities so they don't die", COLORS.GREEN); - this.boxes.forEach(function (box) { - box.pokeEntity(); - }, this); - pokeTimer = ENTITY_REFRESH_INTERVAL; - } else { - // logMessage("Poking entities in " + pokeTimer + " seconds"); - } - } - // logMessage("this.sendPokes === past this.sendPokes? " + (this.sendPokes === original), COLORS.GREEN); - this.sendPokes(dt); - } - } else { - DynamicPlatform.prototype.sendPokes = function () {}; - } - DynamicPlatform.prototype.getEntityCount = function () { - return this.boxes.length; - } - DynamicPlatform.prototype.getEntityCountWithRadius = function (radius) { - var est = Math.floor((radius * radius) / (this.radius * this.radius) * this.getEntityCount()); - var actual = Math.floor(Math.PI * radius * radius * this.getEntityDensity()); + if (USE_ENTITY_TIMEOUTS) { + DynamicPlatform.prototype.sendPokes = function (dt) { + logMessage("starting keepalive", COLORS.GREEN); + // logMessage("dt = " + dt, COLORS.RED); + // var original = this.sendPokes; + var pokeTimer = 0.0; + this.sendPokes = function (dt) { + // logMessage("dt = " + dt); + if ((pokeTimer -= dt) < 0.0) { + // logMessage("Poking entities so they don't die", COLORS.GREEN); + this.boxes.forEach(function (box) { + box.pokeEntity(); + }, this); + pokeTimer = ENTITY_REFRESH_INTERVAL; + } else { + // logMessage("Poking entities in " + pokeTimer + " seconds"); + } + } + // logMessage("this.sendPokes === past this.sendPokes? " + (this.sendPokes === original), COLORS.GREEN); + this.sendPokes(dt); + } + } else { + DynamicPlatform.prototype.sendPokes = function () {}; + } + DynamicPlatform.prototype.getEntityCount = function () { + return this.boxes.length; + } + DynamicPlatform.prototype.getEntityCountWithRadius = function (radius) { + var est = Math.floor((radius * radius) / (this.radius * this.radius) * this.getEntityCount()); + var actual = Math.floor(Math.PI * radius * radius * this.getEntityDensity()); - if (est != actual) { - logMessage("assert failed: getEntityCountWithRadius() -- est " + est + " != actual " + actual); - } - return est; - } - DynamicPlatform.prototype.getEntityCountWithDensity = function (density) { - return Math.floor(Math.PI * this.radius * this.radius * density); - } + if (est != actual) { + logMessage("assert failed: getEntityCountWithRadius() -- est " + est + " != actual " + actual); + } + return est; + } + DynamicPlatform.prototype.getEntityCountWithDensity = function (density) { + return Math.floor(Math.PI * this.radius * this.radius * density); + } - /// Sets the entity count to n. Don't call this directly -- use setRadius / density instead. - DynamicPlatform.prototype.setEntityCount = function (n) { - if (n > this.boxes.length) { - // logMessage("Setting entity count to " + n + " (adding " + (n - this.boxes.length) + " entities)", COLORS.GREEN); + /// Sets the entity count to n. Don't call this directly -- use setRadius / density instead. + DynamicPlatform.prototype.setEntityCount = function (n) { + if (n > this.boxes.length) { + // logMessage("Setting entity count to " + n + " (adding " + (n - this.boxes.length) + " entities)", COLORS.GREEN); - // Spawn new boxes - n = n - this.boxes.length; - for (; n > 0; --n) { - // var properties = { position: this.randomPoint() }; - // this.randomizer.randomizeShapeAndColor(properties); - // this.boxes.push(new PlatformComponent(properties)); - this.boxes.push(this.spawnEntity()); - } - } else if (n < this.boxes.length) { - // logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN); + // Spawn new boxes + n = n - this.boxes.length; + for (; n > 0; --n) { + // var properties = { position: this.randomPoint() }; + // this.randomizer.randomizeShapeAndColor(properties); + // this.boxes.push(new PlatformComponent(properties)); + this.boxes.push(this.spawnEntity()); + } + } else if (n < this.boxes.length) { + // logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN); - // Destroy random boxes (technically, the most recent ones, but it should be sorta random) - n = this.boxes.length - n; - for (; n > 0; --n) { - this.boxes.pop().destroy(); - } - } - } - /// Calculate the entity density based on radial surface area. - DynamicPlatform.prototype.getEntityDensity = function () { - return (this.boxes.length * 1.0) / (Math.PI * this.radius * this.radius); - } - /// Queues a setDensity update. This is expensive, so we don't call it directly from UI. - DynamicPlatform.prototype.setDensityOnNextUpdate = function (density) { - var _this = this; - this.targetDensity = density; - this.setPendingUpdate('density', function () { - _this.updateEntityDensity(density); - }); - } - DynamicPlatform.prototype.updateEntityDensity = function (density) { - this.setEntityCount(Math.floor(density * Math.PI * this.radius * this.radius)); - } - DynamicPlatform.prototype.getRadius = function () { - return this.radius; - } - /// Queues a setRadius update. This is expensive, so we don't call it directly from UI. - DynamicPlatform.prototype.setRadiusOnNextUpdate = function (radius) { - var _this = this; - this.setPendingUpdate('radius', function () { - _this.setRadius(radius); - }); - } - var DEBUG_RADIUS_RECALC = false; - DynamicPlatform.prototype.setRadius = function (radius) { - if (radius < this.radius) { // Reduce case - // logMessage("Setting radius to " + radius + " (shrink by " + (this.radius - radius) + ")", COLORS.GREEN ); - this.radius = radius; + // Destroy random boxes (technically, the most recent ones, but it should be sorta random) + n = this.boxes.length - n; + for (; n > 0; --n) { + this.boxes.pop().destroy(); + } + } + } + /// Calculate the entity density based on radial surface area. + DynamicPlatform.prototype.getEntityDensity = function () { + return (this.boxes.length * 1.0) / (Math.PI * this.radius * this.radius); + } + /// Queues a setDensity update. This is expensive, so we don't call it directly from UI. + DynamicPlatform.prototype.setDensityOnNextUpdate = function (density) { + var _this = this; + this.targetDensity = density; + this.setPendingUpdate('density', function () { + _this.updateEntityDensity(density); + }); + } + DynamicPlatform.prototype.updateEntityDensity = function (density) { + this.setEntityCount(Math.floor(density * Math.PI * this.radius * this.radius)); + } + DynamicPlatform.prototype.getRadius = function () { + return this.radius; + } + /// Queues a setRadius update. This is expensive, so we don't call it directly from UI. + DynamicPlatform.prototype.setRadiusOnNextUpdate = function (radius) { + var _this = this; + this.setPendingUpdate('radius', function () { + _this.setRadius(radius); + }); + } + var DEBUG_RADIUS_RECALC = false; + DynamicPlatform.prototype.setRadius = function (radius) { + if (radius < this.radius) { // Reduce case + // logMessage("Setting radius to " + radius + " (shrink by " + (this.radius - radius) + ")", COLORS.GREEN ); + this.radius = radius; - // Remove all entities outside of current bounds. Requires swapping, since we want to maintain a contiguous array. - // Algorithm: two pointers at front and back. We traverse fwd and back, swapping elems so that all entities in bounds - // are at the front of the array, and all entities out of bounds are at the back. We then pop + destroy all entities - // at the back to reduce the entity count. - var count = this.boxes.length; - var toDelete = 0; - var swapList = []; - if (DEBUG_RADIUS_RECALC) { - logMessage("starting at i = 0, j = " + (count - 1)); - } - for (var i = 0, j = count - 1; i < j; ) { - // Find first elem outside of bounds that we can move to the back - while (inRange(this.boxes[i].position, this.position, this.radius) && i < j) { - ++i; - } - // Find first elem in bounds that we can move to the front - while (!inRange(this.boxes[j].position, this.position, this.radius) && i < j) { - --j; ++toDelete; - } - if (i < j) { - // swapList.push([i, j]); - if (DEBUG_RADIUS_RECALC) { - logMessage("swapping " + i + ", " + j); - } - this.boxes[i].swap(this.boxes[j]); - ++i, --j; ++toDelete; - } else { - if (DEBUG_RADIUS_RECALC) { - logMessage("terminated at i = " + i + ", j = " + j, COLORS.RED); - } - } - } - if (DEBUG_RADIUS_RECALC) { - logMessage("toDelete = " + toDelete, COLORS.RED); - } - // Sanity check - if (toDelete > this.boxes.length) { - logMessage("Error: toDelete " + toDelete + " > entity count " + this.boxes.length + " (setRadius algorithm)", COLORS.RED); - toDelete = this.boxes.length; - } - if (toDelete > 0) { - // logMessage("Deleting " + toDelete + " entities as part of radius resize", COLORS.GREEN); - } - // Delete cleared boxes - for (; toDelete > 0; --toDelete) { - this.boxes.pop().destroy(); - } - // fix entity density (just in case -- we may have uneven entity distribution) - this.updateEntityDensity(this.targetDensity); - } else if (radius > this.radius) { - // Grow case (much simpler) - // logMessage("Setting radius to " + radius + " (grow by " + (radius - this.radius) + ")", COLORS.GREEN); + // Remove all entities outside of current bounds. Requires swapping, since we want to maintain a contiguous array. + // Algorithm: two pointers at front and back. We traverse fwd and back, swapping elems so that all entities in bounds + // are at the front of the array, and all entities out of bounds are at the back. We then pop + destroy all entities + // at the back to reduce the entity count. + var count = this.boxes.length; + var toDelete = 0; + var swapList = []; + if (DEBUG_RADIUS_RECALC) { + logMessage("starting at i = 0, j = " + (count - 1)); + } + for (var i = 0, j = count - 1; i < j; ) { + // Find first elem outside of bounds that we can move to the back + while (inRange(this.boxes[i].position, this.position, this.radius) && i < j) { + ++i; + } + // Find first elem in bounds that we can move to the front + while (!inRange(this.boxes[j].position, this.position, this.radius) && i < j) { + --j; ++toDelete; + } + if (i < j) { + // swapList.push([i, j]); + if (DEBUG_RADIUS_RECALC) { + logMessage("swapping " + i + ", " + j); + } + this.boxes[i].swap(this.boxes[j]); + ++i, --j; ++toDelete; + } else { + if (DEBUG_RADIUS_RECALC) { + logMessage("terminated at i = " + i + ", j = " + j, COLORS.RED); + } + } + } + if (DEBUG_RADIUS_RECALC) { + logMessage("toDelete = " + toDelete, COLORS.RED); + } + // Sanity check + if (toDelete > this.boxes.length) { + logMessage("Error: toDelete " + toDelete + " > entity count " + this.boxes.length + " (setRadius algorithm)", COLORS.RED); + toDelete = this.boxes.length; + } + if (toDelete > 0) { + // logMessage("Deleting " + toDelete + " entities as part of radius resize", COLORS.GREEN); + } + // Delete cleared boxes + for (; toDelete > 0; --toDelete) { + this.boxes.pop().destroy(); + } + // fix entity density (just in case -- we may have uneven entity distribution) + this.updateEntityDensity(this.targetDensity); + } else if (radius > this.radius) { + // Grow case (much simpler) + // logMessage("Setting radius to " + radius + " (grow by " + (radius - this.radius) + ")", COLORS.GREEN); - // Add entities based on entity density - // var density = this.getEntityDensity(); - var density = this.targetDensity; - var oldArea = Math.PI * this.radius * this.radius; - var n = Math.floor(density * Math.PI * (radius * radius - this.radius * this.radius)); + // Add entities based on entity density + // var density = this.getEntityDensity(); + var density = this.targetDensity; + var oldArea = Math.PI * this.radius * this.radius; + var n = Math.floor(density * Math.PI * (radius * radius - this.radius * this.radius)); - if (n > 0) { - // logMessage("Adding " + n + " entities", COLORS.GREEN); + if (n > 0) { + // logMessage("Adding " + n + " entities", COLORS.GREEN); - // Add entities (we use a slightly different algorithm to place them in the area between two concentric circles. - // This is *slightly* less uniform (the reason we're not using this everywhere is entities would be tightly clustered - // at the platform center and become spread out as the radius increases), but the use-case here is just incremental - // radius resizes and the user's not likely to notice the difference). - for (; n > 0; --n) { - var theta = Math.randRange(0.0, Math.PI * 2.0); - var r = Math.randRange(this.radius, radius); - // logMessage("theta = " + theta + ", r = " + r); - var pos = { - x: Math.cos(theta) * r + this.position.x, - y: this.position.y, - z: Math.sin(theta) * r + this.position.y - }; + // Add entities (we use a slightly different algorithm to place them in the area between two concentric circles. + // This is *slightly* less uniform (the reason we're not using this everywhere is entities would be tightly clustered + // at the platform center and become spread out as the radius increases), but the use-case here is just incremental + // radius resizes and the user's not likely to notice the difference). + for (; n > 0; --n) { + var theta = Math.randRange(0.0, Math.PI * 2.0); + var r = Math.randRange(this.radius, radius); + // logMessage("theta = " + theta + ", r = " + r); + var pos = { + x: Math.cos(theta) * r + this.position.x, + y: this.position.y, + z: Math.sin(theta) * r + this.position.y + }; - // var properties = { position: pos }; - // this.randomizer.randomizeShapeAndColor(properties); - // this.boxes.push(new PlatformComponent(properties)); - this.boxes.push(this.spawnEntity()); - } - } - this.radius = radius; - } - } - DynamicPlatform.prototype.updateHeight = function (height) { - logMessage("Setting platform height to " + height); - this.platformHeight = height; + // var properties = { position: pos }; + // this.randomizer.randomizeShapeAndColor(properties); + // this.boxes.push(new PlatformComponent(properties)); + this.boxes.push(this.spawnEntity()); + } + } + this.radius = radius; + } + } + DynamicPlatform.prototype.updateHeight = function (height) { + logMessage("Setting platform height to " + height); + this.platformHeight = height; - // Invalidate current boxes to trigger a rebuild - this.boxes.forEach(function (box) { - box.position.x += this.oldRadius * 100; - }); - // this.update(dt, position, radius); - } - /// Gets a random point within the platform bounds. - /// Should maybe get moved to the RandomAttribModel (would be much cleaner), but this works for now. - DynamicPlatform.prototype.randomPoint = function (position, radius) { - position = position || this.position; - radius = radius !== undefined ? radius : this.radius; - return randomCirclePoint(radius, position); - } - /// Old. The RandomAttribModel replaces this and enables realtime editing of the *****_RANGE params. - // DynamicPlatform.prototype.randomDimensions = function () { - // return { - // x: Math.randRange(WIDTH_RANGE[0], WIDTH_RANGE[1]), - // y: Math.randRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]), - // z: Math.randRange(DEPTH_RANGE[0], DEPTH_RANGE[1]) - // }; - // } - // DynamicPlatform.prototype.randomColor = function () { - // var shade = Math.randRange(SHADE_RANGE[0], SHADE_RANGE[1]); - // // var h = HUE_RANGE; - // return { - // red: shade + Math.randRange(RED_RANGE[0], RED_RANGE[1]) | 0, - // green: shade + Math.randRange(GREEN_RANGE[0], GREEN_RANGE[1]) | 0, - // blue: shade + Math.randRange(BLUE_RANGE[0], BLUE_RANGE[1]) | 0 - // } - // // return COLORS[Math.randInt(COLORS.length)] - // } + // Invalidate current boxes to trigger a rebuild + this.boxes.forEach(function (box) { + box.position.x += this.oldRadius * 100; + }); + // this.update(dt, position, radius); + } + /// Gets a random point within the platform bounds. + /// Should maybe get moved to the RandomAttribModel (would be much cleaner), but this works for now. + DynamicPlatform.prototype.randomPoint = function (position, radius) { + position = position || this.position; + radius = radius !== undefined ? radius : this.radius; + return randomCirclePoint(radius, position); + } + /// Old. The RandomAttribModel replaces this and enables realtime editing of the *****_RANGE params. + // DynamicPlatform.prototype.randomDimensions = function () { + // return { + // x: Math.randRange(WIDTH_RANGE[0], WIDTH_RANGE[1]), + // y: Math.randRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]), + // z: Math.randRange(DEPTH_RANGE[0], DEPTH_RANGE[1]) + // }; + // } + // DynamicPlatform.prototype.randomColor = function () { + // var shade = Math.randRange(SHADE_RANGE[0], SHADE_RANGE[1]); + // // var h = HUE_RANGE; + // return { + // red: shade + Math.randRange(RED_RANGE[0], RED_RANGE[1]) | 0, + // green: shade + Math.randRange(GREEN_RANGE[0], GREEN_RANGE[1]) | 0, + // blue: shade + Math.randRange(BLUE_RANGE[0], BLUE_RANGE[1]) | 0 + // } + // // return COLORS[Math.randInt(COLORS.length)] + // } - /// Cleanup. - DynamicPlatform.prototype.destroy = function () { - this.boxes.forEach(function (box) { - box.destroy(); - }); - this.boxes = []; - } + /// Cleanup. + DynamicPlatform.prototype.destroy = function () { + this.boxes.forEach(function (box) { + box.destroy(); + }); + this.boxes = []; + } })(); // UI (function () { - var CATCH_SETUP_ERRORS = true; + var CATCH_SETUP_ERRORS = true; - // Util functions for setting up widgets (the widget library is intended to be used like this) - function makePanel (dir, properties) { - return new UI.WidgetStack(withDefaults(properties, { - dir: dir - })); - } - function addSpacing (parent, width, height) { - parent.add(new UI.Box({ - backgroundAlpha: 0.0, - width: width, height: height - })); - } - function addLabel (parent, text) { - return parent.add(new UI.Label({ - text: text, - width: 200, - height: 20 - })); - } - function addSlider (parent, label, min, max, getValue, onValueChanged) { - try { - var layout = parent.add(new UI.WidgetStack({ dir: "+x" })); - var textLabel = layout.add(new UI.Label({ - text: label, - width: 130, - height: 20 - })); - var valueLabel = layout.add(new UI.Label({ - text: "" + (+getValue().toFixed(1)), - width: 60, - height: 20 - })); - var slider = layout.add(new UI.Slider({ - value: getValue(), minValue: min, maxValue: max, - width: 300, height: 20, - slider: { - width: 30, - height: 18 - }, - onValueChanged: function (value) { - valueLabel.setText("" + (+value.toFixed(1))); - onValueChanged(value, slider); - UI.updateLayout(); - } - })); - return slider; - } catch (e) { - logMessage("" + e, COLORS.RED); - logMessage("parent: " + parent, COLORS.RED); - logMessage("label: " + label, COLORS.RED); - logMessage("min: " + min, COLORS.RED); - logMessage("max: " + max, COLORS.RED); - logMessage("getValue: " + getValue, COLORS.RED); - logMessage("onValueChanged: " + onValueChanged, COLORS.RED); - throw e; - } - } - function addButton (parent, label, onClicked) { - var button = parent.add(new UI.Box({ - text: label, - width: 160, - height: 26, - leftMargin: 8, - topMargin: 3 - })); - button.addAction('onClick', onClicked); - return button; - } - function moveToBottomLeftScreenCorner (widget) { - var border = 5; - var pos = { - x: border, - y: Controller.getViewportDimensions().y - widget.getHeight() - border - }; - if (widget.position.x != pos.x || widget.position.y != pos.y) { - widget.setPosition(pos.x, pos.y); - UI.updateLayout(); - } - } - var _export = this; + // Util functions for setting up widgets (the widget library is intended to be used like this) + function makePanel (dir, properties) { + return new UI.WidgetStack(withDefaults(properties, { + dir: dir + })); + } + function addSpacing (parent, width, height) { + parent.add(new UI.Box({ + backgroundAlpha: 0.0, + width: width, height: height + })); + } + function addLabel (parent, text) { + return parent.add(new UI.Label({ + text: text, + width: 200, + height: 20 + })); + } + function addSlider (parent, label, min, max, getValue, onValueChanged) { + try { + var layout = parent.add(new UI.WidgetStack({ dir: "+x" })); + var textLabel = layout.add(new UI.Label({ + text: label, + width: 130, + height: 20 + })); + var valueLabel = layout.add(new UI.Label({ + text: "" + (+getValue().toFixed(1)), + width: 60, + height: 20 + })); + var slider = layout.add(new UI.Slider({ + value: getValue(), minValue: min, maxValue: max, + width: 300, height: 20, + slider: { + width: 30, + height: 18 + }, + onValueChanged: function (value) { + valueLabel.setText("" + (+value.toFixed(1))); + onValueChanged(value, slider); + UI.updateLayout(); + } + })); + return slider; + } catch (e) { + logMessage("" + e, COLORS.RED); + logMessage("parent: " + parent, COLORS.RED); + logMessage("label: " + label, COLORS.RED); + logMessage("min: " + min, COLORS.RED); + logMessage("max: " + max, COLORS.RED); + logMessage("getValue: " + getValue, COLORS.RED); + logMessage("onValueChanged: " + onValueChanged, COLORS.RED); + throw e; + } + } + function addButton (parent, label, onClicked) { + var button = parent.add(new UI.Box({ + text: label, + width: 160, + height: 26, + leftMargin: 8, + topMargin: 3 + })); + button.addAction('onClick', onClicked); + return button; + } + function moveToBottomLeftScreenCorner (widget) { + var border = 5; + var pos = { + x: border, + y: Controller.getViewportDimensions().y - widget.getHeight() - border + }; + if (widget.position.x != pos.x || widget.position.y != pos.y) { + widget.setPosition(pos.x, pos.y); + UI.updateLayout(); + } + } + var _export = this; - /// Setup the UI. Creates a bunch of sliders for setting the platform radius, density, and entity color / shape properties. - /// The entityCount slider is readonly. - function _setupUI (platform) { - var layoutContainer = makePanel("+y", { visible: true }); - // layoutContainer.setPosition(10, 280); - // makeDraggable(layoutContainer); - _export.onScreenResize = function () { - moveToBottomLeftScreenCorner(layoutContainer); - } - var topSection = layoutContainer.add(makePanel("+x")); addSpacing(layoutContainer, 1, 5); - var btmSection = layoutContainer.add(makePanel("+x")); + /// Setup the UI. Creates a bunch of sliders for setting the platform radius, density, and entity color / shape properties. + /// The entityCount slider is readonly. + function _setupUI (platform) { + var layoutContainer = makePanel("+y", { visible: false }); + // layoutContainer.setPosition(10, 280); + // makeDraggable(layoutContainer); + _export.onScreenResize = function () { + moveToBottomLeftScreenCorner(layoutContainer); + } + var topSection = layoutContainer.add(makePanel("+x")); addSpacing(layoutContainer, 1, 5); + var btmSection = layoutContainer.add(makePanel("+x")); - var controls = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); - var buttons = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); + var controls = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); + var buttons = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1); - var colorControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); - var shapeControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); + var colorControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); + var shapeControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1); - // Top controls - addLabel(controls, "Platform (platform.js)"); - controls.radiusSlider = addSlider(controls, "radius", PLATFORM_RADIUS_RANGE[0], PLATFORM_RADIUS_RANGE[1], function () { return platform.getRadius() }, - function (value) { - platform.setRadiusOnNextUpdate(value); - controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value)); - }); - addSpacing(controls, 1, 2); - controls.densitySlider = addSlider(controls, "entity density", PLATFORM_DENSITY_RANGE[0], PLATFORM_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, - function (value) { - platform.setDensityOnNextUpdate(value); - controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value)); - }); - addSpacing(controls, 1, 2); + // Top controls + addLabel(controls, "Platform (platform.js)"); + controls.radiusSlider = addSlider(controls, "radius", PLATFORM_RADIUS_RANGE[0], PLATFORM_RADIUS_RANGE[1], function () { return platform.getRadius() }, + function (value) { + platform.setRadiusOnNextUpdate(value); + controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value)); + }); + addSpacing(controls, 1, 2); + controls.densitySlider = addSlider(controls, "entity density", PLATFORM_DENSITY_RANGE[0], PLATFORM_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, + function (value) { + platform.setDensityOnNextUpdate(value); + controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value)); + }); + addSpacing(controls, 1, 2); - var minEntities = Math.PI * PLATFORM_RADIUS_RANGE[0] * PLATFORM_RADIUS_RANGE[0] * PLATFORM_DENSITY_RANGE[0]; - var maxEntities = Math.PI * PLATFORM_RADIUS_RANGE[1] * PLATFORM_RADIUS_RANGE[1] * PLATFORM_DENSITY_RANGE[1]; - controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() }, - function (value) {}); - controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions) - controls.entityCountSlider.slider.actions = {}; + var minEntities = Math.PI * PLATFORM_RADIUS_RANGE[0] * PLATFORM_RADIUS_RANGE[0] * PLATFORM_DENSITY_RANGE[0]; + var maxEntities = Math.PI * PLATFORM_RADIUS_RANGE[1] * PLATFORM_RADIUS_RANGE[1] * PLATFORM_DENSITY_RANGE[1]; + controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() }, + function (value) {}); + controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions) + controls.entityCountSlider.slider.actions = {}; - // Buttons - addSpacing(buttons, 1, 22); - addButton(buttons, 'rebuild', function () { - platform.updateHeight(MyAvatar.position.y - AVATAR_HEIGHT_OFFSET); - }); - addSpacing(buttons, 1, 2); - addButton(buttons, 'toggle entity type', function () { - platform.toggleBoxType(); - }); - - // Bottom controls + // Buttons + addSpacing(buttons, 1, 22); + addButton(buttons, 'rebuild', function () { + platform.updateHeight(MyAvatar.position.y - AVATAR_HEIGHT_OFFSET); + }); + addSpacing(buttons, 1, 2); + addButton(buttons, 'toggle entity type', function () { + platform.toggleBoxType(); + }); + + // Bottom controls - // Iterate over controls (making sliders) for the RNG shape / dimensions model - platform.randomizer.shapeModel.setupUI(function (name, value, min, max, setValue) { - // logMessage("platform.randomizer.shapeModel." + name + " = " + value); - var internal = { - avg: (value[0] + value[1]) * 0.5, - range: Math.abs(value[0] - value[1]) - }; - // logMessage(JSON.stringify(internal), COLORS.GREEN); - addSlider(shapeControls, name + ' avg', min, max, function () { return internal.avg; }, function (value) { - internal.avg = value; - setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); - platform.updateEntityAttribs(); - }); - addSpacing(shapeControls, 1, 2); - addSlider(shapeControls, name + ' range', min, max, function () { return internal.range }, function (value) { - internal.range = value; - setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); - platform.updateEntityAttribs(); - }); - addSpacing(shapeControls, 1, 2); - }); - // Do the same for the color model - platform.randomizer.colorModel.setupUI(function (name, value, min, max, setValue) { - // logMessage("platform.randomizer.colorModel." + name + " = " + value); - addSlider(colorControls, name, min, max, function () { return value; }, function (value) { - setValue(value); - platform.updateEntityAttribs(); - }); - addSpacing(colorControls, 1, 2); - }); - - moveToBottomLeftScreenCorner(layoutContainer); - layoutContainer.setVisible(true); - } - this.setupUI = function (platform) { - if (CATCH_SETUP_ERRORS) { - try { - _setupUI(platform); - } catch (e) { - logMessage("Error setting up ui: " + e, COLORS.RED); - } - } else { - _setupUI(platform); - } - } + // Iterate over controls (making sliders) for the RNG shape / dimensions model + platform.randomizer.shapeModel.setupUI(function (name, value, min, max, setValue) { + // logMessage("platform.randomizer.shapeModel." + name + " = " + value); + var internal = { + avg: (value[0] + value[1]) * 0.5, + range: Math.abs(value[0] - value[1]) + }; + // logMessage(JSON.stringify(internal), COLORS.GREEN); + addSlider(shapeControls, name + ' avg', min, max, function () { return internal.avg; }, function (value) { + internal.avg = value; + setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); + platform.updateEntityAttribs(); + }); + addSpacing(shapeControls, 1, 2); + addSlider(shapeControls, name + ' range', min, max, function () { return internal.range }, function (value) { + internal.range = value; + setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]); + platform.updateEntityAttribs(); + }); + addSpacing(shapeControls, 1, 2); + }); + // Do the same for the color model + platform.randomizer.colorModel.setupUI(function (name, value, min, max, setValue) { + // logMessage("platform.randomizer.colorModel." + name + " = " + value); + addSlider(colorControls, name, min, max, function () { return value; }, function (value) { + setValue(value); + platform.updateEntityAttribs(); + }); + addSpacing(colorControls, 1, 2); + }); + + moveToBottomLeftScreenCorner(layoutContainer); + layoutContainer.setVisible(true); + } + this.setupUI = function (platform) { + if (CATCH_SETUP_ERRORS) { + try { + _setupUI(platform); + } catch (e) { + logMessage("Error setting up ui: " + e, COLORS.RED); + } + } else { + _setupUI(platform); + } + } })(); // Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog @@ -1092,119 +1092,130 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false; // Setup everything (function () { - var doLater = null; - if (CATCH_ERRORS_FROM_EVENT_UPDATES) { - // Decorates a function w/ explicit error catching + printing to the debug log. - function catchErrors (fcn) { - return function () { - try { - fcn.apply(this, arguments); - } catch (e) { - logMessage('' + e, COLORS.RED); - logMessage("while calling " + fcn); - logMessage("Called by: " + arguments.callee.caller); - } - } - } - // We need to do this after the functions are registered... - doLater = function () { - // Intercept errors from functions called by Script.update and Script.ScriptEnding. - [ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) { - this[fcn] = catchErrors(this[fcn]); - }); - }; - // These need to be wrapped first though: + var doLater = null; + if (CATCH_ERRORS_FROM_EVENT_UPDATES) { + // Decorates a function w/ explicit error catching + printing to the debug log. + function catchErrors (fcn) { + return function () { + try { + fcn.apply(this, arguments); + } catch (e) { + logMessage('' + e, COLORS.RED); + logMessage("while calling " + fcn); + logMessage("Called by: " + arguments.callee.caller); + } + } + } + // We need to do this after the functions are registered... + doLater = function () { + // Intercept errors from functions called by Script.update and Script.ScriptEnding. + [ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) { + this[fcn] = catchErrors(this[fcn]); + }); + }; + // These need to be wrapped first though: - // Intercept errors from UI functions called by Controller.****Event. - [ 'handleMousePress', 'handleMouseMove', 'handleMouseRelease' ].forEach(function (fcn) { - UI[fcn] = catchErrors(UI[fcn]); - }); - } + // Intercept errors from UI functions called by Controller.****Event. + [ 'handleMousePress', 'handleMouseMove', 'handleMouseRelease' ].forEach(function (fcn) { + UI[fcn] = catchErrors(UI[fcn]); + }); + } - function getTargetPlatformPosition () { - var pos = MyAvatar.position; - pos.y -= AVATAR_HEIGHT_OFFSET; - return pos; - } + function getTargetPlatformPosition () { + var pos = MyAvatar.position; + pos.y -= AVATAR_HEIGHT_OFFSET; + return pos; + } - // Program state - var platform = this.platform = null; - var lastHeight = null; + // Program state + var platform = this.platform = null; + var lastHeight = null; - // Init - this.initPlatform = function () { - platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS); - lastHeight = getTargetPlatformPosition().y; - } + // Init + this.initPlatform = function () { + platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS); + lastHeight = getTargetPlatformPosition().y; + } - // Handle relative screen positioning (UI) - var lastDimensions = Controller.getViewportDimensions(); - function checkScreenDimensions () { - var dimensions = Controller.getViewportDimensions(); - if (dimensions.x != lastDimensions.x || dimensions.y != lastDimensions.y) { - onScreenResize(dimensions.x, dimensions.y); - } - lastDimensions = dimensions; - } + // Handle relative screen positioning (UI) + var lastDimensions = Controller.getViewportDimensions(); + function checkScreenDimensions () { + var dimensions = Controller.getViewportDimensions(); + if (dimensions.x != lastDimensions.x || dimensions.y != lastDimensions.y) { + onScreenResize(dimensions.x, dimensions.y); + } + lastDimensions = dimensions; + } - // Update - this.update = function (dt) { - checkScreenDimensions(); - var pos = getTargetPlatformPosition(); - platform.update(dt, getTargetPlatformPosition(), platform.getRadius()); - } + // Update + this.update = function (dt) { + checkScreenDimensions(); + var pos = getTargetPlatformPosition(); + platform.update(dt, getTargetPlatformPosition(), platform.getRadius()); + } - // Teardown - this.teardown = function () { - try { - platform.destroy(); - UI.teardown(); + // Teardown + this.teardown = function () { + try { + platform.destroy(); + UI.teardown(); - Controller.mousePressEvent.disconnect(UI.handleMousePress); - Controller.mouseMoveEvent.disconnect(UI.handleMouseMove); - Controller.mouseReleaseEvent.disconnect(UI.handleMouseRelease); - } catch (e) { - logMessage("" + e, COLORS.RED); - } - } + Controller.mousePressEvent.disconnect(UI.handleMousePress); + Controller.mouseMoveEvent.disconnect(UI.handleMouseMove); + Controller.mouseReleaseEvent.disconnect(UI.handleMouseRelease); + } catch (e) { + logMessage("" + e, COLORS.RED); + } + } - if (doLater) { - doLater(); - } + if (doLater) { + doLater(); + } - // Delays startup until / if entities can be spawned. - this.startup = function () { - if (Entities.canAdjustLocks() && Entities.canRez()) { - Script.update.disconnect(this.startup); + // Delays startup until / if entities can be spawned. + this.startup = function (dt) { + if (Entities.canAdjustLocks() && Entities.canRez()) { + Script.update.disconnect(this.startup); - function init () { - logMessage("initializing..."); - - this.initPlatform(); + function init () { + logMessage("initializing..."); + + this.initPlatform(); - Script.update.connect(this.update); - Script.scriptEnding.connect(this.teardown); - - this.setupUI(platform); - - logMessage("finished initializing.", COLORS.GREEN); - } - if (CATCH_INIT_ERRORS) { - try { - init(); - } catch (error) { - logMessage("" + error, COLORS.RED); - } - } else { - init(); - } + Script.update.connect(this.update); + Script.scriptEnding.connect(this.teardown); + + this.setupUI(platform); + + logMessage("finished initializing.", COLORS.GREEN); + } + if (CATCH_INIT_ERRORS) { + try { + init(); + } catch (error) { + logMessage("" + error, COLORS.RED); + } + } else { + init(); + } - Controller.mousePressEvent.connect(UI.handleMousePress); - Controller.mouseMoveEvent.connect(UI.handleMouseMove); - Controller.mouseReleaseEvent.connect(UI.handleMouseRelease); - } - } - Script.update.connect(this.startup); + Controller.mousePressEvent.connect(UI.handleMousePress); + Controller.mouseMoveEvent.connect(UI.handleMouseMove); + Controller.mouseReleaseEvent.connect(UI.handleMouseRelease); + } else { + if (!startup.printedWarnMsg) { + startup.timer = startup.timer || startup.ENTITY_SERVER_WAIT_TIME; + if ((startup.timer -= dt) < 0.0) { + logMessage("Waiting for entity server"); + startup.printedWarnMsg = true; + } + + } + } + } + startup.ENTITY_SERVER_WAIT_TIME = 0.2; // print "waiting for entity server" if more than this time has elapsed in startup() + + Script.update.connect(this.startup); })(); })(); From 6edc817bf26c66347db23940d50704098202299b Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Sep 2015 11:35:50 -0700 Subject: [PATCH 094/192] move the best zone logic out of EntityTreeRenderer::render() --- .../src/EntityTreeRenderer.cpp | 85 +++++++------------ .../src/EntityTreeRenderer.h | 2 +- 2 files changed, 33 insertions(+), 54 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index ebdf0f0339..4d42b79092 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -165,12 +165,41 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { _tree->withReadLock([&] { std::static_pointer_cast(_tree)->findEntities(avatarPosition, radius, foundEntities); + // Whenever you're in an intersection between zones, we will always choose the smallest zone. + _bestZone = NULL; // NOTE: Is this what we want? + _bestZoneVolume = std::numeric_limits::max(); + // create a list of entities that actually contain the avatar's position foreach(EntityItemPointer entity, foundEntities) { if (entity->contains(avatarPosition)) { entitiesContainingAvatar << entity->getEntityItemID(); + + // if this entity is a zone, use this time to determine the bestZone + if (entity->getType() == EntityTypes::Zone) { + float entityVolumeEstimate = entity->getVolumeEstimate(); + if (entityVolumeEstimate < _bestZoneVolume) { + _bestZoneVolume = entityVolumeEstimate; + _bestZone = std::dynamic_pointer_cast(entity); + } else if (entityVolumeEstimate == _bestZoneVolume) { + if (!_bestZone) { + _bestZoneVolume = entityVolumeEstimate; + _bestZone = std::dynamic_pointer_cast(entity); + } else { + // in the case of the volume being equal, we will use the + // EntityItemID to deterministically pick one entity over the other + if (entity->getEntityItemID() < _bestZone->getEntityItemID()) { + _bestZoneVolume = entityVolumeEstimate; + _bestZone = std::dynamic_pointer_cast(entity); + } + } + } + + } } } + + applyZonePropertiesToScene(_bestZone); + }); // Note: at this point we don't need to worry about the tree being locked, because we only deal with @@ -306,23 +335,9 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr_renderer = this; - _tree->withReadLock([&] { - // Whenever you're in an intersection between zones, we will always choose the smallest zone. - _bestZone = NULL; // NOTE: Is this what we want? - _bestZoneVolume = std::numeric_limits::max(); - - // FIX ME: right now the renderOperation does the following: - // 1) determining the best zone (not really rendering) - // 2) render the debug cell details - // we should clean this up - _tree->recurseTreeWithOperation(renderOperation, renderArgs); - - applyZonePropertiesToScene(_bestZone); - }); - } + // FIXME - currently the EntityItem rendering code still depends on knowing about the EntityTreeRenderer + // because it uses it as a model loading service. We don't actually do anything in rendering other than this. + renderArgs->_renderer = this; deleteReleasedModels(); // seems like as good as any other place to do some memory cleanup } @@ -370,42 +385,6 @@ const FBXGeometry* EntityTreeRenderer::getCollisionGeometryForEntity(EntityItemP return result; } -void EntityTreeRenderer::renderElement(OctreeElementPointer element, RenderArgs* args) { - // actually render it here... - // we need to iterate the actual entityItems of the element - EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); - - bool isShadowMode = args->_renderMode == RenderArgs::SHADOW_RENDER_MODE; - - entityTreeElement->forEachEntity([&](EntityItemPointer entityItem) { - if (entityItem->isVisible()) { - // NOTE: Zone Entities are a special case we handle here... - if (entityItem->getType() == EntityTypes::Zone) { - if (entityItem->contains(_viewState->getAvatarPosition())) { - float entityVolumeEstimate = entityItem->getVolumeEstimate(); - if (entityVolumeEstimate < _bestZoneVolume) { - _bestZoneVolume = entityVolumeEstimate; - _bestZone = std::dynamic_pointer_cast(entityItem); - } else if (entityVolumeEstimate == _bestZoneVolume) { - if (!_bestZone) { - _bestZoneVolume = entityVolumeEstimate; - _bestZone = std::dynamic_pointer_cast(entityItem); - } else { - // in the case of the volume being equal, we will use the - // EntityItemID to deterministically pick one entity over the other - if (entityItem->getEntityItemID() < _bestZone->getEntityItemID()) { - _bestZoneVolume = entityVolumeEstimate; - _bestZone = std::dynamic_pointer_cast(entityItem); - } - } - } - } - } - } - }); - -} - float EntityTreeRenderer::getSizeScale() const { return _viewState->getSizeScale(); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 2691c3c66f..1664920a1d 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -40,7 +40,7 @@ public: virtual char getMyNodeType() const { return NodeType::EntityServer; } virtual PacketType getMyQueryMessageType() const { return PacketType::EntityQuery; } virtual PacketType getExpectedPacketType() const { return PacketType::EntityData; } - virtual void renderElement(OctreeElementPointer element, RenderArgs* args); + virtual void renderElement(OctreeElementPointer element, RenderArgs* args) { } virtual float getSizeScale() const; virtual int getBoundaryLevelAdjust() const; virtual void setTree(OctreePointer newTree); From 07f3abfc911a83a5f2562f8019ab5690835194f9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Sep 2015 12:01:23 -0700 Subject: [PATCH 095/192] fix bugs --- libraries/animation/src/AnimInverseKinematics.cpp | 10 ++++------ libraries/animation/src/RotationAccumulator.cpp | 5 +++++ libraries/animation/src/RotationAccumulator.h | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 084139747a..6af58e89a1 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -262,12 +262,10 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar } // only update the absolutePoses that need it: those between lowestMovedIndex and _maxTargetIndex - if (lowestMovedIndex < _maxTargetIndex) { - for (int i = lowestMovedIndex; i < _maxTargetIndex; ++i) { - int parentIndex = _skeleton->getParentIndex(i); - if (parentIndex != -1) { - absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; - } + for (int i = lowestMovedIndex; i <= _maxTargetIndex; ++i) { + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex != -1) { + absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; } } } while (largestError > ACCEPTABLE_RELATIVE_ERROR && numLoops < MAX_IK_LOOPS && usecTimestampNow() < expiry); diff --git a/libraries/animation/src/RotationAccumulator.cpp b/libraries/animation/src/RotationAccumulator.cpp index e3de402ef8..58ce4b9f36 100644 --- a/libraries/animation/src/RotationAccumulator.cpp +++ b/libraries/animation/src/RotationAccumulator.cpp @@ -20,3 +20,8 @@ void RotationAccumulator::add(glm::quat rotation) { glm::quat RotationAccumulator::getAverage() { return (_numRotations > 0) ? glm::normalize(_rotationSum) : glm::quat(); } + +void RotationAccumulator::clear() { + _rotationSum *= 0.0f; + _numRotations = 0; +} diff --git a/libraries/animation/src/RotationAccumulator.h b/libraries/animation/src/RotationAccumulator.h index bb30c14363..634a3d0eac 100644 --- a/libraries/animation/src/RotationAccumulator.h +++ b/libraries/animation/src/RotationAccumulator.h @@ -22,7 +22,7 @@ public: glm::quat getAverage(); - void clear() { _numRotations = 0; } + void clear(); private: glm::quat _rotationSum; From 3c64db5c8629e7f3f691acc2bce42abdca8eeb24 Mon Sep 17 00:00:00 2001 From: Shared Vive Room Date: Fri, 18 Sep 2015 12:02:05 -0700 Subject: [PATCH 096/192] Adjust controller offset from 6 inches to 3 inches When using the vive controller the position of your wrist should match your actual wrist a bit better, unless your name is Shaquille O'Neal. --- .../input-plugins/src/input-plugins/ViveControllerManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp b/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp index 5410db11a4..4af3dd97d0 100644 --- a/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp +++ b/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp @@ -42,7 +42,7 @@ const unsigned int GRIP_BUTTON = 1U << 2; const unsigned int TRACKPAD_BUTTON = 1U << 3; const unsigned int TRIGGER_BUTTON = 1U << 4; -const float CONTROLLER_LENGTH_OFFSET = 0.175f; +const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches const QString CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b"; const QString ViveControllerManager::NAME = "OpenVR"; From 416acb1d4ad171bf2edb84d7f08fce09795b9ef2 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Sep 2015 12:03:58 -0700 Subject: [PATCH 097/192] remove call to _entities.render() in displaySide() --- interface/src/Application.cpp | 3 +-- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 8 +------- libraries/entities-renderer/src/EntityTreeRenderer.h | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 09abfbe01e..1b6be53e83 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1063,7 +1063,7 @@ void Application::paintGL() { auto lodManager = DependencyManager::get(); - RenderArgs renderArgs(_gpuContext, nullptr, getViewFrustum(), lodManager->getOctreeSizeScale(), + RenderArgs renderArgs(_gpuContext, getEntities(), getViewFrustum(), lodManager->getOctreeSizeScale(), lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); @@ -3562,7 +3562,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se (RenderArgs::DebugFlags) (renderDebugFlags | (int)RenderArgs::RENDER_DEBUG_SIMULATION_OWNERSHIP); } renderArgs->_debugFlags = renderDebugFlags; - _entities.render(renderArgs); //ViveControllerManager::getInstance().updateRendering(renderArgs, _main3DScene, pendingChanges); } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 4d42b79092..c6c08471e1 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -149,6 +149,7 @@ void EntityTreeRenderer::update() { } } + deleteReleasedModels(); } void EntityTreeRenderer::checkEnterLeaveEntities() { @@ -334,13 +335,6 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr_renderer = this; - deleteReleasedModels(); // seems like as good as any other place to do some memory cleanup -} - const FBXGeometry* EntityTreeRenderer::getGeometryForEntity(EntityItemPointer entityItem) { const FBXGeometry* result = NULL; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 1664920a1d..59919de27d 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -53,7 +53,7 @@ public: void processEraseMessage(NLPacket& packet, const SharedNodePointer& sourceNode); virtual void init(); - virtual void render(RenderArgs* renderArgs) override; + virtual void render(RenderArgs* renderArgs) override { } virtual const FBXGeometry* getGeometryForEntity(EntityItemPointer entityItem); virtual const Model* getModelForEntityItem(EntityItemPointer entityItem); From 0d375110710a70f85e5b0f9bc28993c028df51a5 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Sep 2015 14:06:38 -0700 Subject: [PATCH 098/192] add support for scripts to call methods on entity scripts --- examples/controllers/handControllerGrab.js | 38 ++++++++++++-- examples/entityScripts/detectGrabExample.js | 52 ++++--------------- .../src/EntityTreeRenderer.cpp | 1 + .../src/EntitiesScriptEngineProvider.h | 25 +++++++++ .../entities/src/EntityScriptingInterface.cpp | 9 ++++ .../entities/src/EntityScriptingInterface.h | 10 +++- libraries/script-engine/src/ScriptEngine.h | 3 +- 7 files changed, 92 insertions(+), 46 deletions(-) create mode 100644 libraries/entities/src/EntitiesScriptEngineProvider.h diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index ed69d1844d..109a4ab57c 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -69,6 +69,7 @@ var STATE_CLOSE_GRABBING = 2; var STATE_CONTINUE_CLOSE_GRABBING = 3; var STATE_RELEASE = 4; +var GRAB_USER_DATA_KEY = "grabKey"; function controller(hand, triggerAction) { this.hand = hand; @@ -89,6 +90,7 @@ function controller(hand, triggerAction) { this.state = 0; // 0 = searching, 1 = distanceHolding, 2 = closeGrabbing this.pointer = null; // entity-id of line object this.triggerValue = 0; // rolling average of trigger value + this.alreadyDistanceHolding = false; // FIXME - I'll leave it to Seth to potentially make this another state this.update = function() { switch(this.state) { @@ -210,13 +212,20 @@ function controller(hand, triggerAction) { this.distanceHolding = function() { if (!this.triggerSmoothedSqueezed()) { this.state = STATE_RELEASE; + this.alreadyDistanceHolding = false; return; } + if (!this.alreadyDistanceHolding) { + this.activateEntity(this.grabbedEntity); + this.alreadyDistanceHolding = true; + } + var handPosition = this.getHandPosition(); var handControllerPosition = Controller.getSpatialControlPosition(this.palm); var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position","rotation"]); + Entities.callEntityMethod(this.grabbedEntity, "distanceHolding"); this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); @@ -270,7 +279,9 @@ function controller(hand, triggerAction) { this.lineOff(); - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity); + this.activateEntity(this.grabbedEntity); + + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, "position"); var handRotation = this.getHandRotation(); var handPosition = this.getHandPosition(); @@ -294,6 +305,7 @@ function controller(hand, triggerAction) { } else { this.state = STATE_CONTINUE_CLOSE_GRABBING; } + Entities.callEntityMethod(this.grabbedEntity, "closeGrabbing"); } @@ -324,6 +336,7 @@ function controller(hand, triggerAction) { this.currentObjectPosition = grabbedProperties.position; this.currentObjectTime = now; + Entities.callEntityMethod(this.grabbedEntity, "continueCloseGrabbing"); } @@ -336,7 +349,10 @@ function controller(hand, triggerAction) { // the action will tend to quickly bring an object's velocity to zero. now that // the action is gone, set the objects velocity to something the holder might expect. - Entities.editEntity(this.grabbedEntity, {velocity: this.grabbedVelocity}); + Entities.editEntity(this.grabbedEntity, { velocity: this.grabbedVelocity }); + + Entities.callEntityMethod(this.grabbedEntity, "release"); + this.deactivateEntity(this.grabbedEntity); this.grabbedVelocity = ZERO_VEC; this.grabbedEntity = null; @@ -348,6 +364,22 @@ function controller(hand, triggerAction) { this.cleanup = function() { release(); } + + this.activateEntity = function(entity) { + var data = { + activated: true, + avatarId: MyAvatar.sessionUUID + }; + setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); + } + + this.deactivateEntity = function(entity) { + var data = { + activated: false, + avatarId: null + }; + setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); + } } diff --git a/examples/entityScripts/detectGrabExample.js b/examples/entityScripts/detectGrabExample.js index cdc79e119d..c84d3250cc 100644 --- a/examples/entityScripts/detectGrabExample.js +++ b/examples/entityScripts/detectGrabExample.js @@ -12,7 +12,6 @@ // (function() { - Script.include("../libraries/utils.js"); var _this; @@ -24,39 +23,18 @@ DetectGrabbed.prototype = { - // update() will be called regulary, because we've hooked the update signal in our preload() function - // we will check out userData for the grabData. In the case of the hydraGrab script, it will tell us - // if we're currently being grabbed and if the person grabbing us is the current interfaces avatar. - // we will watch this for state changes and print out if we're being grabbed or released when it changes. - update: function() { - var GRAB_USER_DATA_KEY = "grabKey"; + distanceHolding: function () { + print("I am being distance held... entity:" + this.entityID); + }, - // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID - var entityID = _this.entityID; - - // we want to assume that if there is no grab data, then we are not being grabbed - var defaultGrabData = { activated: false, avatarId: null }; - - // this handy function getEntityCustomData() is available in utils.js and it will return just the specific section - // of user data we asked for. If it's not available it returns our default data. - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, defaultGrabData); - - // if the grabData says we're being grabbed, and the owner ID is our session, then we are being grabbed by this interface - if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { - - // remember we're being grabbed so we can detect being released - _this.beingGrabbed = true; - - // print out that we're being grabbed - print("I'm being grabbed..."); - - } else if (_this.beingGrabbed) { - - // if we are not being grabbed, and we previously were, then we were just released, remember that - // and print out a message - _this.beingGrabbed = false; - print("I'm was released..."); - } + closeGrabbing: function () { + print("I was just grabbed... entity:" + this.entityID); + }, + continueCloseGrabbing: function () { + print("I am still being grabbed... entity:" + this.entityID); + }, + release: function () { + print("I was released... entity:" + this.entityID); }, // preload() will be called when the entity has become visible (or known) to the interface @@ -65,14 +43,6 @@ // * connecting to the update signal so we can check our grabbed state preload: function(entityID) { this.entityID = entityID; - Script.update.connect(this.update); - }, - - // unload() will be called when our entity is no longer available. It may be because we were deleted, - // or because we've left the domain or quit the application. In all cases we want to unhook our connection - // to the update signal - unload: function(entityID) { - Script.update.disconnect(this.update); }, }; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c55eaaeff9..056aaab1a5 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -112,6 +112,7 @@ void EntityTreeRenderer::init() { _scriptingServices->getControllerScriptingInterface()); _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine); _entitiesScriptEngine->runInThread(); + DependencyManager::get()->setEntitiesScriptEngine(_entitiesScriptEngine); } // make sure our "last avatar position" is something other than our current position, so that on our diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h new file mode 100644 index 0000000000..d112a6c0f9 --- /dev/null +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -0,0 +1,25 @@ +// +// EntitiesScriptEngineProvider.h +// libraries/entities/src +// +// Created by Brad Hefta-Gaub on Sept. 18, 2015 +// Copyright 2015 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 +// +// TODO: How will we handle collision callbacks with Entities +// + +#ifndef hifi_EntitiesScriptEngineProvider_h +#define hifi_EntitiesScriptEngineProvider_h + +#include +#include "EntityItemID.h" + +class EntitiesScriptEngineProvider { +public: + virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName) = 0; +}; + +#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 4b8b4e2903..1d403e37cd 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -11,6 +11,7 @@ #include "EntityScriptingInterface.h" +#include "EntityItemID.h" #include #include "EntitiesLogging.h" @@ -211,6 +212,14 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { } } +void EntityScriptingInterface::callEntityMethod(QUuid id, const QString& method) { + if (_entitiesScriptEngine) { + EntityItemID entityID{ id }; + _entitiesScriptEngine->callEntityScriptMethod(entityID, method); + } +} + + QUuid EntityScriptingInterface::findClosestEntity(const glm::vec3& center, float radius) const { EntityItemID result; if (_entityTree) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 8d2b0b6892..8484b7189a 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -20,18 +20,19 @@ #include #include #include + #include "PolyVoxEntityItem.h" #include "LineEntityItem.h" #include "PolyLineEntityItem.h" #include "EntityTree.h" #include "EntityEditPacketSender.h" +#include "EntitiesScriptEngineProvider.h" class EntityTree; class MouseEvent; - class RayToEntityIntersectionResult { public: RayToEntityIntersectionResult(); @@ -63,6 +64,7 @@ public: void setEntityTree(EntityTreePointer modelTree); EntityTreePointer getEntityTree() { return _entityTree; } + void setEntitiesScriptEngine(EntitiesScriptEngineProvider* engine) { _entitiesScriptEngine = engine; } public slots: @@ -86,6 +88,11 @@ public slots: /// deletes a model Q_INVOKABLE void deleteEntity(QUuid entityID); + /// Allows a script to call a method on an entity's script. The method will execute in the entity script + /// engine. If the entity does not have an entity script or the method does not exist, this call will have + /// no effect. + Q_INVOKABLE void callEntityMethod(QUuid entityID, const QString& method); + /// finds the closest model to the center point, within the radius /// will return a EntityItemID.isKnownID = false if no models are in the radius /// this function will not find any models in script engine contexts which don't have access to models @@ -180,6 +187,7 @@ private: bool precisionPicking); EntityTreePointer _entityTree; + EntitiesScriptEngineProvider* _entitiesScriptEngine = nullptr; }; #endif // hifi_EntityScriptingInterface_h diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 83e65823a5..3cfeb3447e 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -25,6 +25,7 @@ #include #include #include +#include #include "AbstractControllerScriptingInterface.h" #include "ArrayBufferClass.h" @@ -46,7 +47,7 @@ public: QScriptValue scriptObject; }; -class ScriptEngine : public QScriptEngine, public ScriptUser { +class ScriptEngine : public QScriptEngine, public ScriptUser, public EntitiesScriptEngineProvider { Q_OBJECT public: ScriptEngine(const QString& scriptContents = NO_SCRIPT, From 7a8cee4cc3001650e02fc41a9254257551aa1a99 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 14:13:08 -0700 Subject: [PATCH 099/192] fix release velocity so throwing things works reliably --- examples/controllers/handControllerGrab.js | 26 +++++++++++++--------- examples/libraries/utils.js | 7 ++++-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index ed69d1844d..df90de1b67 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -18,7 +18,7 @@ Script.include("../libraries/utils.js"); // these tune time-averaging and "on" value for analog trigger // -var TRIGGER_SMOOTH_RATIO = 0.7; +var TRIGGER_SMOOTH_RATIO = 0.0; // 0.0 disables smoothing of trigger value var TRIGGER_ON_VALUE = 0.2; ///////////////////////////////////////////////////////////////// @@ -42,9 +42,9 @@ var LINE_LENGTH = 500; var GRAB_RADIUS = 0.3; // if the ray misses but an object is this close, it will still be selected var CLOSE_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position -var CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO = 0.9; // adjust time-averaging of held object's velocity +var CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable. var CLOSE_PICK_MAX_DISTANCE = 0.6; // max length of pick-ray for close grabbing to be selected - +var RELEASE_VELOCITY_MULTIPLIER = 2.0; // affects throwing things ///////////////////////////////////////////////////////////////// // @@ -278,9 +278,8 @@ function controller(hand, triggerAction) { var objectRotation = grabbedProperties.rotation; var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectTime = Date.now(); - var offset = Vec3.subtract(this.currentObjectPosition, handPosition); + currentObjectPosition = grabbedProperties.position; + var offset = Vec3.subtract(currentObjectPosition, handPosition); var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); this.actionID = Entities.addAction("hold", this.grabbedEntity, { @@ -294,6 +293,9 @@ function controller(hand, triggerAction) { } else { this.state = STATE_CONTINUE_CLOSE_GRABBING; } + + this.currentHandControllerPosition = Controller.getSpatialControlPosition(this.palm); + this.currentObjectTime = Date.now(); } @@ -304,13 +306,13 @@ function controller(hand, triggerAction) { } // keep track of the measured velocity of the held object - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity); + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); var now = Date.now(); - var deltaPosition = Vec3.subtract(grabbedProperties.position, this.currentObjectPosition); // meters + var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerPosition); // meters var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds - if (deltaTime > 0.0) { + if (deltaTime > 0.0 && !vec3equal(this.currentHandControllerPosition, handControllerPosition)) { var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger // value would otherwise give the held object time to slow down. @@ -322,7 +324,7 @@ function controller(hand, triggerAction) { } } - this.currentObjectPosition = grabbedProperties.position; + this.currentHandControllerPosition = handControllerPosition; this.currentObjectTime = now; } @@ -336,7 +338,9 @@ function controller(hand, triggerAction) { // the action will tend to quickly bring an object's velocity to zero. now that // the action is gone, set the objects velocity to something the holder might expect. - Entities.editEntity(this.grabbedEntity, {velocity: this.grabbedVelocity}); + Entities.editEntity(this.grabbedEntity, + {velocity: Vec3.multiply(this.grabbedVelocity, RELEASE_VELOCITY_MULTIPLIER)} + ); this.grabbedVelocity = ZERO_VEC; this.grabbedEntity = null; diff --git a/examples/libraries/utils.js b/examples/libraries/utils.js index 1275975fd8..865a5a55b1 100644 --- a/examples/libraries/utils.js +++ b/examples/libraries/utils.js @@ -6,11 +6,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -vec3toStr = function (v, digits) { +vec3toStr = function(v, digits) { if (!digits) { digits = 3; } return "{ " + v.x.toFixed(digits) + ", " + v.y.toFixed(digits) + ", " + v.z.toFixed(digits)+ " }"; } +vec3equal = function(v0, v1) { + return (v0.x == v1.x) && (v0.y == v1.y) && (v0.z == v1.z); +} colorMix = function(colorA, colorB, mix) { var result = {}; @@ -175,4 +178,4 @@ pointInExtents = function(point, minPoint, maxPoint) { return (point.x >= minPoint.x && point.x <= maxPoint.x) && (point.y >= minPoint.y && point.y <= maxPoint.y) && (point.z >= minPoint.z && point.z <= maxPoint.z); -} \ No newline at end of file +} From 020fb25ace3dfb179fbdddd8aced64d5521a15eb Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Sep 2015 14:24:01 -0700 Subject: [PATCH 100/192] CR feedback --- libraries/entities-renderer/src/EntityTreeRenderer.h | 3 --- libraries/octree/src/OctreeRenderer.h | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 59919de27d..d100597969 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -40,7 +40,6 @@ public: virtual char getMyNodeType() const { return NodeType::EntityServer; } virtual PacketType getMyQueryMessageType() const { return PacketType::EntityQuery; } virtual PacketType getExpectedPacketType() const { return PacketType::EntityData; } - virtual void renderElement(OctreeElementPointer element, RenderArgs* args) { } virtual float getSizeScale() const; virtual int getBoundaryLevelAdjust() const; virtual void setTree(OctreePointer newTree); @@ -53,7 +52,6 @@ public: void processEraseMessage(NLPacket& packet, const SharedNodePointer& sourceNode); virtual void init(); - virtual void render(RenderArgs* renderArgs) override { } virtual const FBXGeometry* getGeometryForEntity(EntityItemPointer entityItem); virtual const Model* getModelForEntityItem(EntityItemPointer entityItem); @@ -128,7 +126,6 @@ private: void addEntityToScene(EntityItemPointer entity); void applyZonePropertiesToScene(std::shared_ptr zone); - void renderElementProxy(EntityTreeElementPointer entityTreeElement, RenderArgs* args); void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false); QList _releasedModels; diff --git a/libraries/octree/src/OctreeRenderer.h b/libraries/octree/src/OctreeRenderer.h index 5fda4c9f4a..e2d97f0484 100644 --- a/libraries/octree/src/OctreeRenderer.h +++ b/libraries/octree/src/OctreeRenderer.h @@ -38,7 +38,7 @@ public: virtual char getMyNodeType() const = 0; virtual PacketType getMyQueryMessageType() const = 0; virtual PacketType getExpectedPacketType() const = 0; - virtual void renderElement(OctreeElementPointer element, RenderArgs* args) = 0; + virtual void renderElement(OctreeElementPointer element, RenderArgs* args) { } virtual float getSizeScale() const { return DEFAULT_OCTREE_SIZE_SCALE; } virtual int getBoundaryLevelAdjust() const { return 0; } From 031761fa57104875bb36fd2446cd6e850c0f80e6 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 18 Sep 2015 14:27:57 -0700 Subject: [PATCH 101/192] added some logging while things were broken --- examples/toys/bubblewand/bubble.js | 56 ++++++++++++++------------ examples/toys/bubblewand/createWand.js | 14 +++---- examples/toys/bubblewand/wand.js | 27 ++++++++----- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index ebb5a36d2d..b52f0d326a 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -18,12 +18,6 @@ var BUBBLE_USER_DATA_KEY = "BubbleKey"; - BUBBLE_PARTICLE_COLOR = { - red: 0, - green: 40, - blue: 255, - }; - var _this = this; var properties; @@ -41,18 +35,28 @@ properties = tmpProperties; } + // var defaultBubbleData={ + // avatarID:'noAvatar' + // } + + var entityData = getEntityCustomData(BUBBLE_USER_DATA_KEY, _this.entityID); + + if (entityData && entityData.avatarID && entityData.avatarID === MyAvatar.sessionUUID) { + _this.bubbleCreator = true + + } + + + }; this.unload = function(entityID) { Script.update.disconnect(this.update); - var defaultGrabData = { - avatarId: null - }; + print('bubble unload') - var bubbleCreator = getEntityCustomData(BUBBLE_USER_DATA_KEY, entityID, defaultGrabData); - - if (bubbleCreator === MyAvatar.sessionUUID) { + if (this.bubbleCreator) { + print('PLAYING BURST') this.createBurstParticles(); } @@ -77,36 +81,36 @@ var particleBurst = Entities.addEntity({ type: "ParticleEffect", animationSettings: animationSettings, + emitRate: 100, animationIsPlaying: true, position: position, - lifetime: 0.1, + lifespan: 1, dimensions: { x: 1, y: 1, z: 1 }, emitVelocity: { - x: 0.35, - y: 0.35, - z: 0.35 + x: 1, + y: 1, + z: 1 }, velocitySpread: { - x: 0.45, - y: 0.45, - z: 0.45 + x: 1, + y: 1, + z: 1 }, emitAcceleration: { - x: 0, - y: -0.1, - z: 0 + x: 0.25, + y: 0.25, + z: 0.25 }, - particleRadius: 0.1, - alphaStart: 0.5, + radiusSpread: 0.01, + particleRadius: 0.02, + alphaStart: 1.0, alpha: 0.5, alphaFinish: 0, textures: BUBBLE_PARTICLE_TEXTURE, - // color: BUBBLE_PARTICLE_COLOR, - lifespan: 0.1, visible: true, locked: false }); diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 37041fdbf6..a50b7185f6 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -4,20 +4,20 @@ // Script Type: Entity Spawner // Created by James B. Pollack @imgntn -- 09/03/2015 // Copyright 2015 High Fidelity, Inc. -// +// // Loads a wand model and attaches the bubble wand behavior. // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html var IN_TOYBOX = false; -Script.include("../../utilities.js"); +Script.include("../../utilities.js"); Script.include("../../libraries/utils.js"); var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; -var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); +var WAND_SCRIPT_URL = Script.resolvePath("wand.js?"+randInt(0,500)); //create the wand in front of the avatar blahy var center = Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0, y: 0.5, z: 0}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); @@ -26,7 +26,7 @@ var tablePosition = { y:495.63, z:506.25 } -print('test refresh') + var wand = Entities.addEntity({ name:'Bubble Wand', type: "Model", @@ -39,9 +39,9 @@ var wand = Entities.addEntity({ z: 0, }, dimensions: { - x: 0.025, - y: 0.125, - z: 0.025 + x: 0.05, + y: 0.25, + z: 0.05 }, //must be enabled to be grabbable in the physics engine collisionsWillMove: true, diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 0e960f1f64..3d574d89ca 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -17,8 +17,8 @@ Script.include("../../libraries/utils.js"); var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var BUBBLE_SCRIPT = Script.resolvePath('bubble.js?' + randInt(0, 10000)); - + var BUBBLE_SCRIPT = Script.resolvePath('bubble.js'); +//test var BUBBLE_USER_DATA_KEY = "BubbleKey"; var BUBBLE_INITIAL_DIMENSIONS = { x: 0.01, @@ -35,7 +35,7 @@ var GROWTH_FACTOR = 0.005; var SHRINK_FACTOR = 0.001; var SHRINK_LOWER_LIMIT = 0.02; - var WAND_TIP_OFFSET = 0.05; + var WAND_TIP_OFFSET = 0.095; var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; var VELOCITY_STRENGH_MAX = 10; var VELOCITY_THRESHOLD = 0.5; @@ -58,17 +58,19 @@ Script.update.disconnect(this.update); }, update: function(deltaTime) { + print('BW update') var GRAB_USER_DATA_KEY = "grabKey"; var defaultGrabData = { activated: false, avatarId: null }; var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, _this.entityID, defaultGrabData); + print('grabData'+JSON.stringify(grabData)) if (grabData.activated && grabData.avatarId === MyAvatar.sessionUUID) { // remember we're being grabbed so we can detect being released _this.beingGrabbed = true; - + print('being grabbed') //the first time we want to make a bubble if (_this.currentBubble === null) { _this.createBubbleAtTipOfWand(); @@ -87,9 +89,8 @@ }); } else if (_this.beingGrabbed) { - // if we are not being grabbed, and we previously were, then we were just released, remember that - + print('let go') _this.beingGrabbed = false; //remove the current bubble when the wand is released @@ -97,6 +98,7 @@ _this.currentBubble = null return } + print('not grabbed') }, getWandTipPosition: function(properties) { @@ -129,6 +131,7 @@ return gravity }, growBubbleWithWandVelocity: function(properties, deltaTime) { + print('grow bubble') var wandPosition = properties.position; var wandTipPosition = this.getWandTipPosition(properties) @@ -138,8 +141,7 @@ var velocityStrength = Vec3.length(velocity); - print('velocityStrength' + velocityStrength) - + velocityStrength = velocityStrength; // if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { // velocityStrength = 0 // } @@ -155,13 +157,15 @@ var dimensions = Entities.getEntityProperties(this.currentBubble).dimensions; if (velocityStrength > VELOCITY_THRESHOLD) { - + print('velocity over threshold') //add some variation in bubble sizes var bubbleSize = randInt(BUBBLE_SIZE_MIN, BUBBLE_SIZE_MAX); bubbleSize = bubbleSize / BUBBLE_DIVISOR; //release the bubble if its dimensions are bigger than the bubble size if (dimensions.x > bubbleSize) { + + print('release the bubble') //bubbles pop after existing for a bit -- so set a random lifetime var lifetime = randInt(BUBBLE_LIFETIME_MIN, BUBBLE_LIFETIME_MAX); @@ -174,9 +178,9 @@ //wait to make the bubbles collidable, so that they dont hit each other and the wand Script.setTimeout(this.addCollisionsToBubbleAfterCreation(this.currentBubble), lifetime / 2); + this.setBubbleOwner(this.currentBubble); //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble this.createBubbleAtTipOfWand(); - return } else { @@ -202,8 +206,9 @@ }); }, setBubbleOwner: function(bubble) { + print('SET BUBBLE OWNER', bubble) setEntityCustomData(BUBBLE_USER_DATA_KEY, bubble, { - avatarID: MyAvatar.sessionUUID + avatarID: MyAvatar.sessionUUID, }); }, createBubbleAtTipOfWand: function() { From a897c17eb254080f445ef08a82eea839d73eece6 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 18 Sep 2015 14:31:40 -0700 Subject: [PATCH 102/192] Remove grab-frame and reset position code --- examples/toys/flashlight/flashlight.js | 28 ++------------------------ 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/examples/toys/flashlight/flashlight.js b/examples/toys/flashlight/flashlight.js index 165f693e8e..d5bdb4a630 100644 --- a/examples/toys/flashlight/flashlight.js +++ b/examples/toys/flashlight/flashlight.js @@ -31,15 +31,6 @@ _this._spotlight = null; }; - - GRAB_FRAME_USER_DATA_KEY = "grabFrame"; - - // These constants define the Flashlight model Grab Frame - var MODEL_GRAB_FRAME = { - relativePosition: {x: 0, y: -0.1, z: 0}, - relativeRotation: Quat.angleAxis(180, {x: 1, y: 0, z: 0}) - }; - // These constants define the Spotlight position and orientation relative to the model var MODEL_LIGHT_POSITION = {x: 0, y: 0, z: 0}; var MODEL_LIGHT_ROTATION = Quat.angleAxis (-90, {x: 1, y: 0, z: 0}); @@ -98,8 +89,6 @@ }); _this._hasSpotlight = true; - _this._startModelPosition = modelProperties.position; - _this._startModelRotation = modelProperties.rotation; debugPrint("Flashlight:: creating a spotlight"); } else { @@ -120,10 +109,6 @@ _this._hasSpotlight = false; _this._spotlight = null; - // Reset model to initial position - Entities.editEntity(_this.entityID, {position: _this._startModelPosition, rotation: _this._startModelRotation, - velocity: {x: 0, y: 0, z: 0}, angularVelocity: {x: 0, y: 0, z: 0}}); - // if we are not being grabbed, and we previously were, then we were just released, remember that // and print out a message _this.beingGrabbed = false; @@ -139,17 +124,8 @@ _this.entityID = entityID; var modelProperties = Entities.getEntityProperties(entityID); - _this._startModelPosition = modelProperties.position; - _this._startModelRotation = modelProperties.rotation; - - // Make sure the Flashlight entity has a correct grab frame setup - var userData = getEntityUserData(entityID); - debugPrint(JSON.stringify(userData)); - if (!userData.grabFrame) { - setEntityCustomData(GRAB_FRAME_USER_DATA_KEY, entityID, MODEL_GRAB_FRAME); - debugPrint(JSON.stringify(MODEL_GRAB_FRAME)); - debugPrint("Assigned the grab frmae for the Flashlight entity"); - } + + Script.update.connect(this.update); }, From d1a7aca7f091c38266b1924df4f05286e323b11c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 14:45:49 -0700 Subject: [PATCH 103/192] add continue-distance-holding state. don't call callEntityMethod unless action creation works. increase distance-holding multiplier. --- examples/controllers/handControllerGrab.js | 105 +++++++++++---------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 554d5117ea..fc82e0c065 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -26,7 +26,7 @@ var TRIGGER_ON_VALUE = 0.2; // distant manipulation // -var DISTANCE_HOLDING_RADIUS_FACTOR = 4; // multiplied by distance between hand and object +var DISTANCE_HOLDING_RADIUS_FACTOR = 5; // multiplied by distance between hand and object var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did var NO_INTERSECT_COLOR = {red: 10, green: 10, blue: 255}; // line color when pick misses @@ -65,9 +65,10 @@ var LIFETIME = 10; // states for the state machine var STATE_SEARCHING = 0; var STATE_DISTANCE_HOLDING = 1; -var STATE_CLOSE_GRABBING = 2; -var STATE_CONTINUE_CLOSE_GRABBING = 3; -var STATE_RELEASE = 4; +var STATE_CONTINUE_DISTANCE_HOLDING = 2; +var STATE_CLOSE_GRABBING = 3; +var STATE_CONTINUE_CLOSE_GRABBING = 4; +var STATE_RELEASE = 5; var GRAB_USER_DATA_KEY = "grabKey"; @@ -90,7 +91,6 @@ function controller(hand, triggerAction) { this.state = 0; // 0 = searching, 1 = distanceHolding, 2 = closeGrabbing this.pointer = null; // entity-id of line object this.triggerValue = 0; // rolling average of trigger value - this.alreadyDistanceHolding = false; // FIXME - I'll leave it to Seth to potentially make this another state this.update = function() { switch(this.state) { @@ -100,6 +100,9 @@ function controller(hand, triggerAction) { case STATE_DISTANCE_HOLDING: this.distanceHolding(); break; + case STATE_CONTINUE_DISTANCE_HOLDING: + this.continueDistanceHolding(); + break; case STATE_CLOSE_GRABBING: this.closeGrabbing(); break; @@ -210,64 +213,68 @@ function controller(hand, triggerAction) { this.distanceHolding = function() { - if (!this.triggerSmoothedSqueezed()) { - this.state = STATE_RELEASE; - this.alreadyDistanceHolding = false; - return; + var handControllerPosition = Controller.getSpatialControlPosition(this.palm); + var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position","rotation"]); + + // add the action and initialize some variables + this.currentObjectPosition = grabbedProperties.position; + this.currentObjectRotation = grabbedProperties.rotation; + this.handPreviousPosition = handControllerPosition; + this.handPreviousRotation = handRotation; + + this.actionID = Entities.addAction("spring", this.grabbedEntity, { + targetPosition: this.currentObjectPosition, + linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: this.currentObjectRotation, + angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + }); + if (this.actionID == NULL_ACTION_ID) { + this.actionID = null; } - if (!this.alreadyDistanceHolding) { + if (this.actionID != null) { + this.state = STATE_CONTINUE_DISTANCE_HOLDING; this.activateEntity(this.grabbedEntity); - this.alreadyDistanceHolding = true; + Entities.callEntityMethod(this.grabbedEntity, "distanceHolding"); + } + } + + + this.continueDistanceHolding = function() { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; } var handPosition = this.getHandPosition(); var handControllerPosition = Controller.getSpatialControlPosition(this.palm); var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position","rotation"]); - Entities.callEntityMethod(this.grabbedEntity, "distanceHolding"); this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); - if (this.actionID === null) { - // first time here since trigger pulled -- add the action and initialize some variables - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectRotation = grabbedProperties.rotation; - this.handPreviousPosition = handControllerPosition; - this.handPreviousRotation = handRotation; + // the action was set up on a previous call. update the targets. + var radius = Math.max(Vec3.distance(this.currentObjectPosition, + handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, + DISTANCE_HOLDING_RADIUS_FACTOR); - this.actionID = Entities.addAction("spring", this.grabbedEntity, { - targetPosition: this.currentObjectPosition, - linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, - targetRotation: this.currentObjectRotation, - angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME - }); - if (this.actionID == NULL_ACTION_ID) { - this.actionID = null; - } - } else { - // the action was set up on a previous call. update the targets. - var radius = Math.max(Vec3.distance(this.currentObjectPosition, - handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, - DISTANCE_HOLDING_RADIUS_FACTOR); + var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition); + this.handPreviousPosition = handControllerPosition; + var superHandMoved = Vec3.multiply(handMoved, radius); + this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); - var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition); - this.handPreviousPosition = handControllerPosition; - var superHandMoved = Vec3.multiply(handMoved, radius); - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); + // this doubles hand rotation + var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, + DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + Quat.inverse(this.handPreviousRotation)); + this.handPreviousRotation = handRotation; + this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); - // this doubles hand rotation - var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, - DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - Quat.inverse(this.handPreviousRotation)); - this.handPreviousRotation = handRotation; - this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); - - Entities.updateAction(this.grabbedEntity, this.actionID, { - targetPosition: this.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, - targetRotation: this.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME - }); - } + Entities.updateAction(this.grabbedEntity, this.actionID, { + targetPosition: this.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: this.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + }); } @@ -303,11 +310,11 @@ function controller(hand, triggerAction) { this.actionID = null; } else { this.state = STATE_CONTINUE_CLOSE_GRABBING; + Entities.callEntityMethod(this.grabbedEntity, "closeGrabbing"); } this.currentHandControllerPosition = Controller.getSpatialControlPosition(this.palm); this.currentObjectTime = Date.now(); - Entities.callEntityMethod(this.grabbedEntity, "closeGrabbing"); } From 612e906a443a7fc0f317252792210f78b28fcadb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 15:09:05 -0700 Subject: [PATCH 104/192] change the names of entityMethods which the grab script will call. adjust the release velocity multiplier --- examples/controllers/handControllerGrab.js | 63 +++++++++++++--------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index fc82e0c065..20b0003cc7 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -37,14 +37,14 @@ var LINE_LENGTH = 500; ///////////////////////////////////////////////////////////////// // -// close grabbing +// near grabbing // var GRAB_RADIUS = 0.3; // if the ray misses but an object is this close, it will still be selected -var CLOSE_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position -var CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable. -var CLOSE_PICK_MAX_DISTANCE = 0.6; // max length of pick-ray for close grabbing to be selected -var RELEASE_VELOCITY_MULTIPLIER = 2.0; // affects throwing things +var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position +var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable. +var NEAR_PICK_MAX_DISTANCE = 0.6; // max length of pick-ray for close grabbing to be selected +var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things ///////////////////////////////////////////////////////////////// // @@ -66,8 +66,8 @@ var LIFETIME = 10; var STATE_SEARCHING = 0; var STATE_DISTANCE_HOLDING = 1; var STATE_CONTINUE_DISTANCE_HOLDING = 2; -var STATE_CLOSE_GRABBING = 3; -var STATE_CONTINUE_CLOSE_GRABBING = 4; +var STATE_NEAR_GRABBING = 3; +var STATE_CONTINUE_NEAR_GRABBING = 4; var STATE_RELEASE = 5; var GRAB_USER_DATA_KEY = "grabKey"; @@ -88,7 +88,7 @@ function controller(hand, triggerAction) { this.actionID = null; // action this script created... this.grabbedEntity = null; // on this entity. this.grabbedVelocity = ZERO_VEC; // rolling average of held object's velocity - this.state = 0; // 0 = searching, 1 = distanceHolding, 2 = closeGrabbing + this.state = 0; this.pointer = null; // entity-id of line object this.triggerValue = 0; // rolling average of trigger value @@ -103,11 +103,11 @@ function controller(hand, triggerAction) { case STATE_CONTINUE_DISTANCE_HOLDING: this.continueDistanceHolding(); break; - case STATE_CLOSE_GRABBING: - this.closeGrabbing(); + case STATE_NEAR_GRABBING: + this.nearGrabbing(); break; - case STATE_CONTINUE_CLOSE_GRABBING: - this.continueCloseGrabbing(); + case STATE_CONTINUE_NEAR_GRABBING: + this.continueNearGrabbing(); break; case STATE_RELEASE: this.release(); @@ -180,9 +180,9 @@ function controller(hand, triggerAction) { var handControllerPosition = Controller.getSpatialControlPosition(this.palm); var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection); this.grabbedEntity = intersection.entityID; - if (intersectionDistance < CLOSE_PICK_MAX_DISTANCE) { + if (intersectionDistance < NEAR_PICK_MAX_DISTANCE) { // the hand is very close to the intersected object. go into close-grabbing mode. - this.state = STATE_CLOSE_GRABBING; + this.state = STATE_NEAR_GRABBING; } else { // the hand is far from the intersected object. go into distance-holding mode this.state = STATE_DISTANCE_HOLDING; @@ -206,7 +206,7 @@ function controller(hand, triggerAction) { if (this.grabbedEntity === null) { this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } else { - this.state = STATE_CLOSE_GRABBING; + this.state = STATE_NEAR_GRABBING; } } } @@ -236,7 +236,13 @@ function controller(hand, triggerAction) { if (this.actionID != null) { this.state = STATE_CONTINUE_DISTANCE_HOLDING; this.activateEntity(this.grabbedEntity); - Entities.callEntityMethod(this.grabbedEntity, "distanceHolding"); + Entities.callEntityMethod(this.grabbedEntity, "startDistantGrab"); + + if (this.hand === RIGHT_HAND) { + Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); + } else { + Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); + } } } @@ -271,6 +277,8 @@ function controller(hand, triggerAction) { this.handPreviousRotation = handRotation; this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); + Entities.callEntityMethod(this.grabbedEntity, "continueDistantGrab"); + Entities.updateAction(this.grabbedEntity, this.actionID, { targetPosition: this.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, targetRotation: this.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME @@ -278,7 +286,7 @@ function controller(hand, triggerAction) { } - this.closeGrabbing = function() { + this.nearGrabbing = function() { if (!this.triggerSmoothedSqueezed()) { this.state = STATE_RELEASE; return; @@ -302,15 +310,20 @@ function controller(hand, triggerAction) { this.actionID = Entities.addAction("hold", this.grabbedEntity, { hand: this.hand == RIGHT_HAND ? "right" : "left", - timeScale: CLOSE_GRABBING_ACTION_TIMEFRAME, + timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, relativePosition: offsetPosition, relativeRotation: offsetRotation }); if (this.actionID == NULL_ACTION_ID) { this.actionID = null; } else { - this.state = STATE_CONTINUE_CLOSE_GRABBING; - Entities.callEntityMethod(this.grabbedEntity, "closeGrabbing"); + this.state = STATE_CONTINUE_NEAR_GRABBING; + Entities.callEntityMethod(this.grabbedEntity, "startNearGrab"); + if (this.hand === RIGHT_HAND) { + Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); + } else { + Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); + } } this.currentHandControllerPosition = Controller.getSpatialControlPosition(this.palm); @@ -318,7 +331,7 @@ function controller(hand, triggerAction) { } - this.continueCloseGrabbing = function() { + this.continueNearGrabbing = function() { if (!this.triggerSmoothedSqueezed()) { this.state = STATE_RELEASE; return; @@ -338,14 +351,14 @@ function controller(hand, triggerAction) { if (this.triggerSqueezed()) { this.grabbedVelocity = Vec3.sum(Vec3.multiply(this.grabbedVelocity, - (1.0 - CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)), - Vec3.multiply(grabbedVelocity, CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)); + (1.0 - NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)), + Vec3.multiply(grabbedVelocity, NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)); } } this.currentHandControllerPosition = handControllerPosition; this.currentObjectTime = now; - Entities.callEntityMethod(this.grabbedEntity, "continueCloseGrabbing"); + Entities.callEntityMethod(this.grabbedEntity, "continueNearGrab"); } @@ -361,7 +374,7 @@ function controller(hand, triggerAction) { Entities.editEntity(this.grabbedEntity, {velocity: Vec3.multiply(this.grabbedVelocity, RELEASE_VELOCITY_MULTIPLIER)} ); - Entities.callEntityMethod(this.grabbedEntity, "release"); + Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); this.deactivateEntity(this.grabbedEntity); this.grabbedVelocity = ZERO_VEC; From 3d18edd9d10f646d08ae954a367a2e2b2b1272cf Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 15:13:09 -0700 Subject: [PATCH 105/192] update detectGrabExample.js --- examples/entityScripts/detectGrabExample.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/entityScripts/detectGrabExample.js b/examples/entityScripts/detectGrabExample.js index c84d3250cc..7e97572159 100644 --- a/examples/entityScripts/detectGrabExample.js +++ b/examples/entityScripts/detectGrabExample.js @@ -23,16 +23,27 @@ DetectGrabbed.prototype = { - distanceHolding: function () { - print("I am being distance held... entity:" + this.entityID); + setRightHand: function () { + print("I am being held in a right hand... entity:" + this.entityID); + }, + setLeftHand: function () { + print("I am being held in a left hand... entity:" + this.entityID); }, - closeGrabbing: function () { + startDistantGrab: function () { + print("I am being distance held... entity:" + this.entityID); + }, + continueDistantGrab: function () { + print("I continue to be distance held... entity:" + this.entityID); + }, + + startNearGrab: function () { print("I was just grabbed... entity:" + this.entityID); }, - continueCloseGrabbing: function () { + continueNearGrab: function () { print("I am still being grabbed... entity:" + this.entityID); }, + release: function () { print("I was released... entity:" + this.entityID); }, From f851c6204f1402ecc4792fe4c3ce02cf72d39e40 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 18 Sep 2015 15:14:04 -0700 Subject: [PATCH 106/192] Remove overcalling of glVertexAttrib4f --- libraries/gpu/src/gpu/GLBackend.cpp | 11 ++++++++--- libraries/gpu/src/gpu/GLBackend.h | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index a8c21125b5..336178542a 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -603,11 +603,16 @@ void Batch::_glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) DO_IT_NOW(_glColor4f, 4); } void GLBackend::do_glColor4f(Batch& batch, uint32 paramOffset) { - // TODO Replace this with a proper sticky Input attribute buffer with frequency 0 - glVertexAttrib4f( gpu::Stream::COLOR, + + glm::vec4 newColor( batch._params[paramOffset + 3]._float, batch._params[paramOffset + 2]._float, batch._params[paramOffset + 1]._float, - batch._params[paramOffset + 0]._float); + batch._params[paramOffset + 0]._float); + + if (_input._colorAttribute != newColor) { + _input._colorAttribute = newColor; + glVertexAttrib4fv(gpu::Stream::COLOR, &_input._colorAttribute.r); + } (void) CHECK_GL_ERROR(); } diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 6d806a7f06..dabc69dedb 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -278,6 +278,8 @@ protected: Offsets _bufferStrides; std::vector _bufferVBOs; + glm::vec4 _colorAttribute{ 0.0f }; + BufferPointer _indexBuffer; Offset _indexBufferOffset; Type _indexBufferType; From dc17985e291ce406d91b5c92a64ae524c8ca3f9d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 18 Sep 2015 15:24:44 -0700 Subject: [PATCH 107/192] Add some logic for turning off the flashlight when squeeze trigger pressure is low, and turning it back on when squeeze pressure goes over certain amount --- examples/toys/flashlight/flashlight.js | 90 ++++++++++++++++++++------ 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/examples/toys/flashlight/flashlight.js b/examples/toys/flashlight/flashlight.js index d5bdb4a630..cc708beccf 100644 --- a/examples/toys/flashlight/flashlight.js +++ b/examples/toys/flashlight/flashlight.js @@ -12,9 +12,10 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +// TODO: update to use new grab signals, which will include handedness. (function() { - + function debugPrint(message) { //print(message); } @@ -25,24 +26,35 @@ // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) - Flashlight = function() { + Flashlight = function() { _this = this; _this._hasSpotlight = false; _this._spotlight = null; }; + var DISABLE_LIGHT_THRESHOLD = 0.5; // These constants define the Spotlight position and orientation relative to the model - var MODEL_LIGHT_POSITION = {x: 0, y: 0, z: 0}; - var MODEL_LIGHT_ROTATION = Quat.angleAxis (-90, {x: 1, y: 0, z: 0}); + var MODEL_LIGHT_POSITION = { + x: 0, + y: 0, + z: 0 + }; + var MODEL_LIGHT_ROTATION = Quat.angleAxis(-90, { + x: 1, + y: 0, + z: 0 + }); // Evaluate the world light entity position and orientation from the model ones function evalLightWorldTransform(modelPos, modelRot) { - return {p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)), - q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION)}; + return { + p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)), + q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION) + }; }; Flashlight.prototype = { - + lightOn: false, // update() will be called regulary, because we've hooked the update signal in our preload() function @@ -56,7 +68,10 @@ var entityID = _this.entityID; // we want to assume that if there is no grab data, then we are not being grabbed - var defaultGrabData = { activated: false, avatarId: null }; + var defaultGrabData = { + activated: false, + avatarId: null + }; // this handy function getEntityCustomData() is available in utils.js and it will return just the specific section // of user data we asked for. If it's not available it returns our default data. @@ -81,8 +96,16 @@ position: lightTransform.p, rotation: lightTransform.q, isSpotlight: true, - dimensions: { x: 2, y: 2, z: 20 }, - color: { red: 255, green: 255, blue: 255 }, + dimensions: { + x: 2, + y: 2, + z: 20 + }, + color: { + red: 255, + green: 255, + blue: 255 + }, intensity: 2, exponent: 0.3, cutoff: 20 @@ -93,8 +116,11 @@ debugPrint("Flashlight:: creating a spotlight"); } else { // Updating the spotlight - Entities.editEntity(_this._spotlight, {position: lightTransform.p, rotation: lightTransform.q}); - + Entities.editEntity(_this._spotlight, { + position: lightTransform.p, + rotation: lightTransform.q + }); + _this.changeLightWithTriggerPressure(); debugPrint("Flashlight:: updating the spotlight"); } @@ -115,19 +141,47 @@ debugPrint("I'm was released..."); } }, + changeLightWithTriggerPressure: function(flashLightHand) { + var handClickString = flashLightHand + "_HAND_CLICK"; + + var handClick = Controller.findAction(handClickString); + + this.triggerValue = Controller.getActionValue(handClick); + + if (this.triggerValue < DISABLE_LIGHT_THRESHOLD && this.lightOn === true) { + this.turnLightOff(); + } else if (this.triggerValue >= DISABLE_LIGHT_THRESHOLD && this.lightOn === false) { + this.turnLightOn(); + } + + return triggerValue + }, + turnLightOff: function() { + Entities.editEntity(_this._spotlight, { + intensity: 0 + }); + this.lightOn = false + }, + turnLightOn: function() { + Entities.editEntity(_this._spotlight, { + intensity: 2 + }); + this.lightOn = true + }, + // preload() will be called when the entity has become visible (or known) to the interface // it gives us a chance to set our local JavaScript object up. In this case it means: // * remembering our entityID, so we can access it in cases where we're called without an entityID // * connecting to the update signal so we can check our grabbed state preload: function(entityID) { _this.entityID = entityID; - - var modelProperties = Entities.getEntityProperties(entityID); - - - Script.update.connect(this.update); + var modelProperties = Entities.getEntityProperties(entityID); + + + + Script.update.connect(this.update); }, // unload() will be called when our entity is no longer available. It may be because we were deleted, @@ -147,4 +201,4 @@ // entity scripts always need to return a newly constructed object of our type return new Flashlight(); -}) +}) \ No newline at end of file From c364189bc858f914121b3f98841fd88697ce32a7 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 18 Sep 2015 15:26:34 -0700 Subject: [PATCH 108/192] Update file header --- examples/toys/flashlight/flashlight.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/toys/flashlight/flashlight.js b/examples/toys/flashlight/flashlight.js index cc708beccf..9aa64a4435 100644 --- a/examples/toys/flashlight/flashlight.js +++ b/examples/toys/flashlight/flashlight.js @@ -1,6 +1,7 @@ // -// flashligh.js -// examples/entityScripts +// flashlight.js +// +// Script Type: Entity // // Created by Sam Gateau on 9/9/15. // Copyright 2015 High Fidelity, Inc. @@ -13,6 +14,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // // TODO: update to use new grab signals, which will include handedness. +// BONUS: dim the light with pressure instead of binary on/off (function() { @@ -169,7 +171,7 @@ }); this.lightOn = true }, - + // preload() will be called when the entity has become visible (or known) to the interface // it gives us a chance to set our local JavaScript object up. In this case it means: // * remembering our entityID, so we can access it in cases where we're called without an entityID From ccf125c047426a2c481d3ee8c58a05fc6048fdde Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 18 Sep 2015 15:40:39 -0700 Subject: [PATCH 109/192] Cleanup comments --- examples/toys/bubblewand/bubble.js | 15 +++---------- examples/toys/bubblewand/createWand.js | 2 +- examples/toys/bubblewand/wand.js | 30 +++++++------------------- 3 files changed, 12 insertions(+), 35 deletions(-) diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js index b52f0d326a..b6356d4025 100644 --- a/examples/toys/bubblewand/bubble.js +++ b/examples/toys/bubblewand/bubble.js @@ -23,7 +23,6 @@ var properties; this.preload = function(entityID) { - // print('bubble preload') _this.entityID = entityID; Script.update.connect(_this.update); }; @@ -35,28 +34,20 @@ properties = tmpProperties; } - // var defaultBubbleData={ - // avatarID:'noAvatar' - // } - + //we want to play the particle burst exactly once, so we make sure that this is a bubble we own. var entityData = getEntityCustomData(BUBBLE_USER_DATA_KEY, _this.entityID); if (entityData && entityData.avatarID && entityData.avatarID === MyAvatar.sessionUUID) { _this.bubbleCreator = true - } - - }; this.unload = function(entityID) { Script.update.disconnect(this.update); - print('bubble unload') - + //only play particle burst for our bubbles if (this.bubbleCreator) { - print('PLAYING BURST') this.createBurstParticles(); } @@ -84,7 +75,7 @@ emitRate: 100, animationIsPlaying: true, position: position, - lifespan: 1, + lifespan: 0.2, dimensions: { x: 1, y: 1, diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index a50b7185f6..c99f648e04 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -17,7 +17,7 @@ Script.include("../../libraries/utils.js"); var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; -var WAND_SCRIPT_URL = Script.resolvePath("wand.js?"+randInt(0,500)); +var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); //create the wand in front of the avatar blahy var center = Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0, y: 0.5, z: 0}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 3d574d89ca..67b5b43573 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -18,7 +18,7 @@ var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; var BUBBLE_SCRIPT = Script.resolvePath('bubble.js'); -//test + var BUBBLE_USER_DATA_KEY = "BubbleKey"; var BUBBLE_INITIAL_DIMENSIONS = { x: 0.01, @@ -36,10 +36,10 @@ var SHRINK_FACTOR = 0.001; var SHRINK_LOWER_LIMIT = 0.02; var WAND_TIP_OFFSET = 0.095; - var VELOCITY_STRENGTH_LOWER_LIMIT = 0.01; - var VELOCITY_STRENGH_MAX = 10; var VELOCITY_THRESHOLD = 0.5; + var GRAB_USER_DATA_KEY = "grabKey"; + var _this; var BubbleWand = function() { @@ -58,19 +58,16 @@ Script.update.disconnect(this.update); }, update: function(deltaTime) { - print('BW update') - var GRAB_USER_DATA_KEY = "grabKey"; var defaultGrabData = { activated: false, avatarId: null }; var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, _this.entityID, defaultGrabData); - print('grabData'+JSON.stringify(grabData)) - if (grabData.activated && grabData.avatarId === MyAvatar.sessionUUID) { + if (grabData.activated && grabData.avatarId === MyAvatar.sessionUUID) { // remember we're being grabbed so we can detect being released _this.beingGrabbed = true; - print('being grabbed') + //the first time we want to make a bubble if (_this.currentBubble === null) { _this.createBubbleAtTipOfWand(); @@ -90,7 +87,6 @@ } else if (_this.beingGrabbed) { // if we are not being grabbed, and we previously were, then we were just released, remember that - print('let go') _this.beingGrabbed = false; //remove the current bubble when the wand is released @@ -98,7 +94,7 @@ _this.currentBubble = null return } - print('not grabbed') + }, getWandTipPosition: function(properties) { @@ -114,7 +110,6 @@ }, addCollisionsToBubbleAfterCreation: function(bubble) { - print('adding collisions to bubble' + bubble); Entities.editEntity(bubble, { collisionsWillMove: true }) @@ -131,7 +126,6 @@ return gravity }, growBubbleWithWandVelocity: function(properties, deltaTime) { - print('grow bubble') var wandPosition = properties.position; var wandTipPosition = this.getWandTipPosition(properties) @@ -142,13 +136,6 @@ var velocityStrength = Vec3.length(velocity); velocityStrength = velocityStrength; - // if (velocityStrength < VELOCITY_STRENGTH_LOWER_LIMIT) { - // velocityStrength = 0 - // } - - // if (velocityStrength > VELOCITY_STRENGTH_MAX) { - // velocityStrength = VELOCITY_STRENGTH_MAX - // } //store the last position of the wand for velocity calculations this.lastPosition = wandPosition; @@ -157,7 +144,6 @@ var dimensions = Entities.getEntityProperties(this.currentBubble).dimensions; if (velocityStrength > VELOCITY_THRESHOLD) { - print('velocity over threshold') //add some variation in bubble sizes var bubbleSize = randInt(BUBBLE_SIZE_MIN, BUBBLE_SIZE_MAX); bubbleSize = bubbleSize / BUBBLE_DIVISOR; @@ -165,7 +151,6 @@ //release the bubble if its dimensions are bigger than the bubble size if (dimensions.x > bubbleSize) { - print('release the bubble') //bubbles pop after existing for a bit -- so set a random lifetime var lifetime = randInt(BUBBLE_LIFETIME_MIN, BUBBLE_LIFETIME_MAX); @@ -178,7 +163,9 @@ //wait to make the bubbles collidable, so that they dont hit each other and the wand Script.setTimeout(this.addCollisionsToBubbleAfterCreation(this.currentBubble), lifetime / 2); + //we want to pop the bubble for just one person this.setBubbleOwner(this.currentBubble); + //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble this.createBubbleAtTipOfWand(); return @@ -206,7 +193,6 @@ }); }, setBubbleOwner: function(bubble) { - print('SET BUBBLE OWNER', bubble) setEntityCustomData(BUBBLE_USER_DATA_KEY, bubble, { avatarID: MyAvatar.sessionUUID, }); From 5367de35c11538ff3656ce81a918aa3c6eea9de4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 15:45:36 -0700 Subject: [PATCH 110/192] fix distance-grab throwing --- examples/controllers/handControllerGrab.js | 25 ++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 20b0003cc7..041651f786 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -220,6 +220,7 @@ function controller(hand, triggerAction) { // add the action and initialize some variables this.currentObjectPosition = grabbedProperties.position; this.currentObjectRotation = grabbedProperties.rotation; + this.currentObjectTime = Date.now(); this.handPreviousPosition = handControllerPosition; this.handPreviousRotation = handRotation; @@ -268,7 +269,15 @@ function controller(hand, triggerAction) { var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition); this.handPreviousPosition = handControllerPosition; var superHandMoved = Vec3.multiply(handMoved, radius); - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); + + var newObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved); + var deltaPosition = Vec3.subtract(newObjectPosition, this.currentObjectPosition); // meters + var now = Date.now(); + var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds + this.computeReleaseVelocity(deltaPosition, deltaTime); + + this.currentObjectPosition = newObjectPosition; + this.currentObjectTime = now; // this doubles hand rotation var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, @@ -343,8 +352,16 @@ function controller(hand, triggerAction) { var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerPosition); // meters var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds + this.computeReleaseVelocity(deltaPosition, deltaTime); - if (deltaTime > 0.0 && !vec3equal(this.currentHandControllerPosition, handControllerPosition)) { + this.currentHandControllerPosition = handControllerPosition; + this.currentObjectTime = now; + Entities.callEntityMethod(this.grabbedEntity, "continueNearGrab"); + } + + + this.computeReleaseVelocity = function(deltaPosition, deltaTime) { + if (deltaTime > 0.0 && !vec3equal(deltaPosition, ZERO_VEC)) { var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger // value would otherwise give the held object time to slow down. @@ -355,10 +372,6 @@ function controller(hand, triggerAction) { Vec3.multiply(grabbedVelocity, NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)); } } - - this.currentHandControllerPosition = handControllerPosition; - this.currentObjectTime = now; - Entities.callEntityMethod(this.grabbedEntity, "continueNearGrab"); } From 5db3af3f4c4b4d83776c1ffd0d64d30046c2cde0 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 15:58:47 -0700 Subject: [PATCH 111/192] formatting --- examples/controllers/handControllerGrab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 041651f786..bea96ab496 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -257,7 +257,7 @@ function controller(hand, triggerAction) { var handPosition = this.getHandPosition(); var handControllerPosition = Controller.getSpatialControlPosition(this.palm); var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position","rotation"]); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]); this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR); @@ -408,7 +408,7 @@ function controller(hand, triggerAction) { }; setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); } - + this.deactivateEntity = function(entity) { var data = { activated: false, From 236831d385989c88c0b6a9dff72e6a5fc82db3ea Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 16:28:15 -0700 Subject: [PATCH 112/192] fix when releaseGrab is called --- examples/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index bea96ab496..6fff9df0f8 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -380,6 +380,7 @@ function controller(hand, triggerAction) { if (this.grabbedEntity != null && this.actionID != null) { Entities.deleteAction(this.grabbedEntity, this.actionID); + Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); } // the action will tend to quickly bring an object's velocity to zero. now that @@ -387,7 +388,6 @@ function controller(hand, triggerAction) { Entities.editEntity(this.grabbedEntity, {velocity: Vec3.multiply(this.grabbedVelocity, RELEASE_VELOCITY_MULTIPLIER)} ); - Entities.callEntityMethod(this.grabbedEntity, "releaseGrab"); this.deactivateEntity(this.grabbedEntity); this.grabbedVelocity = ZERO_VEC; From 8a703d036331cf952c8d29d97b5ece409596a317 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 16 Sep 2015 23:42:20 -0700 Subject: [PATCH 113/192] Instanced rendering, first pass --- examples/cubePerfTest.js | 6 +- .../src/RenderableBoxEntityItem.cpp | 9 +- libraries/gpu/src/gpu/Batch.cpp | 30 +- libraries/gpu/src/gpu/Batch.h | 36 + libraries/gpu/src/gpu/Format.h | 17 +- libraries/gpu/src/gpu/GLBackend.cpp | 17 +- libraries/gpu/src/gpu/GLBackendInput.cpp | 18 +- libraries/gpu/src/gpu/GLBackendShader.cpp | 5 + libraries/gpu/src/gpu/Inputs.slh | 1 + libraries/gpu/src/gpu/Resource.h | 5 + libraries/gpu/src/gpu/Stream.h | 9 +- libraries/gpu/src/gpu/Transform.slh | 43 + .../src/DeferredLightingEffect.cpp | 1652 +++++++++-------- .../render-utils/src/DeferredLightingEffect.h | 9 +- libraries/render-utils/src/GeometryCache.cpp | 164 +- libraries/render-utils/src/GeometryCache.h | 6 + libraries/render-utils/src/simple.slv | 10 +- 17 files changed, 1139 insertions(+), 898 deletions(-) diff --git a/examples/cubePerfTest.js b/examples/cubePerfTest.js index bdf123ae33..699472edd9 100644 --- a/examples/cubePerfTest.js +++ b/examples/cubePerfTest.js @@ -16,7 +16,7 @@ var PARTICLE_MAX_SIZE = 2.50; var LIFETIME = 600; var boxes = []; -var ids = Entities.findEntities({ x: 512, y: 512, z: 512 }, 50); +var ids = Entities.findEntities(MyAvatar.position, 50); for (var i = 0; i < ids.length; i++) { var id = ids[i]; var properties = Entities.getEntityProperties(id); @@ -33,10 +33,10 @@ for (var x = 0; x < SIDE_SIZE; x++) { var gray = Math.random() * 155; var cube = Math.random() > 0.5; var color = { red: 100 + gray, green: 100 + gray, blue: 100 + gray }; - var position = { x: 512 + x * 0.2, y: 512 + y * 0.2, z: 512 + z * 0.2}; + var position = Vec3.sum(MyAvatar.position, { x: x * 0.2, y: y * 0.2, z: z * 0.2}); var radius = Math.random() * 0.1; boxes.push(Entities.addEntity({ - type: cube ? "Box" : "Sphere", + type: cube ? "Box" : "Box", name: "PerfTest", position: position, dimensions: { x: radius, y: radius, z: radius }, diff --git a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp b/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp index 525226b0e8..b9ff69af52 100644 --- a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp @@ -39,9 +39,6 @@ void RenderableBoxEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableBoxEntityItem::render"); Q_ASSERT(getType() == EntityTypes::Box); Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - batch.setModelTransform(getTransformToCenter()); // we want to include the scale as well - glm::vec4 cubeColor(toGlm(getXColor()), getLocalRenderAlpha()); if (!_procedural) { _procedural.reset(new Procedural(this->getUserData())); @@ -54,11 +51,15 @@ void RenderableBoxEntityItem::render(RenderArgs* args) { gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); } + gpu::Batch& batch = *args->_batch; + glm::vec4 cubeColor(toGlm(getXColor()), getLocalRenderAlpha()); + if (_procedural->ready()) { + batch.setModelTransform(getTransformToCenter()); // we want to include the scale as well _procedural->prepare(batch, this->getDimensions()); DependencyManager::get()->renderSolidCube(batch, 1.0f, _procedural->getColor(cubeColor)); } else { - DependencyManager::get()->renderSolidCube(batch, 1.0f, cubeColor); + DependencyManager::get()->renderSolidCubeInstance(batch, getTransformToCenter(), cubeColor); } RenderableDebugableEntityItem::render(this, args); diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index fb6618e953..e5ec8525b6 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -8,10 +8,10 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include - #include "Batch.h" +#include + #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" @@ -302,4 +302,28 @@ void Batch::enableSkybox(bool enable) { bool Batch::isSkyboxEnabled() const { return _enableSkybox; -} \ No newline at end of file +} + +void Batch::setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function) { + NamedBatchData& instance = _namedData[instanceName]; + ++instance._count; + instance._function = function; +} + +BufferPointer Batch::getNamedBuffer(const std::string& instanceName, uint8_t index) { + NamedBatchData& instance = _namedData[instanceName]; + if (instance._buffers.size() <= index) { + instance._buffers.resize(index + 1); + } + if (!instance._buffers[index]) { + instance._buffers[index].reset(new Buffer()); + } + return instance._buffers[index]; +} + +void Batch::preExecute() { + for (auto& mapItem : _namedData) { + mapItem.second.process(*this); + } + _namedData.clear(); +} diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 0ecfde44f1..c3bf6250c5 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -12,6 +12,8 @@ #define hifi_gpu_Batch_h #include +#include +#include #include "Framebuffer.h" #include "Pipeline.h" @@ -38,16 +40,42 @@ enum ReservedSlot { TRANSFORM_CAMERA_SLOT = 7, }; +// The named batch data provides a mechanism for accumulating data into buffers over the course +// of many independent calls. For instance, two objects in the scene might both want to render +// a simple box, but are otherwise unaware of each other. The common code that they call to render +// the box can create buffers to store the rendering parameters for each box and register a function +// that will be called with the accumulated buffer data when the batch commands are finally +// executed against the backend + + class Batch { public: typedef Stream::Slot Slot; + struct NamedBatchData { + using BufferPointers = std::vector; + using Function = std::function; + + std::once_flag _once; + BufferPointers _buffers; + size_t _count{ 0 }; + Function _function; + + void process(Batch& batch) { + _function(batch, *this); + } + }; + + using NamedBatchDataMap = std::map; + Batch(); explicit Batch(const Batch& batch); ~Batch(); void clear(); + void preExecute(); + // Batches may need to override the context level stereo settings // if they're performing framebuffer copy operations, like the // deferred lighting resolution mechanism @@ -67,6 +95,12 @@ public: void drawInstanced(uint32 nbInstances, Primitive primitiveType, uint32 nbVertices, uint32 startVertex = 0, uint32 startInstance = 0); void drawIndexedInstanced(uint32 nbInstances, Primitive primitiveType, uint32 nbIndices, uint32 startIndex = 0, uint32 startInstance = 0); + + void setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function); + BufferPointer getNamedBuffer(const std::string& instanceName, uint8_t index = 0); + + + // Input Stage // InputFormat // InputBuffers @@ -291,6 +325,8 @@ public: FramebufferCaches _framebuffers; QueryCaches _queries; + NamedBatchDataMap _namedData; + bool _enableStereo{ true }; bool _enableSkybox{ false }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 8cd16e0be4..e16256574b 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -120,6 +120,18 @@ enum Dimension { MAT4, NUM_DIMENSIONS, }; + +// Count (of scalars) in an Element for a given Dimension +static const int LOCATION_COUNT[NUM_DIMENSIONS] = { + 1, + 1, + 1, + 1, + 1, + 3, + 4, +}; + // Count (of scalars) in an Element for a given Dimension static const int DIMENSION_COUNT[NUM_DIMENSIONS] = { 1, @@ -127,8 +139,8 @@ static const int DIMENSION_COUNT[NUM_DIMENSIONS] = { 3, 4, 4, - 9, - 16, + 3, + 4, }; // Semantic of an Element @@ -184,6 +196,7 @@ public: Dimension getDimension() const { return (Dimension)_dimension; } uint8 getDimensionCount() const { return DIMENSION_COUNT[(Dimension)_dimension]; } + uint8 getLocationCount() const { return LOCATION_COUNT[(Dimension)_dimension]; } Type getType() const { return (Type)_type; } bool isNormalized() const { return (getType() >= NFLOAT); } diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index a8c21125b5..d2e8155ba1 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -191,6 +191,9 @@ void GLBackend::renderPassDraw(Batch& batch) { } void GLBackend::render(Batch& batch) { + // Finalize the batch by moving all the instanced rendering into the command buffer + batch.preExecute(); + _stereo._skybox = batch.isSkyboxEnabled(); // Allow the batch to override the rendering stereo settings // for things like full framebuffer copy operations (deferred lighting passes) @@ -316,7 +319,19 @@ void GLBackend::do_drawInstanced(Batch& batch, uint32 paramOffset) { } void GLBackend::do_drawIndexedInstanced(Batch& batch, uint32 paramOffset) { - (void) CHECK_GL_ERROR(); + updateInput(); + updateTransform(); + updatePipeline(); + + GLint numInstances = batch._params[paramOffset + 4]._uint; + GLenum mode = _primitiveToGLmode[(Primitive)batch._params[paramOffset + 3]._uint]; + uint32 numIndices = batch._params[paramOffset + 2]._uint; + uint32 startIndex = batch._params[paramOffset + 1]._uint; + uint32 startInstance = batch._params[paramOffset + 0]._uint; + GLenum glType = _elementTypeToGLType[_input._indexBufferType]; + + glDrawElementsInstanced(mode, numIndices, glType, nullptr, numInstances); + (void)CHECK_GL_ERROR(); } void GLBackend::do_resetStages(Batch& batch, uint32 paramOffset) { diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index efbead5da2..7f021fd5c5 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -160,7 +160,10 @@ void GLBackend::updateInput() { if (_input._format) { for (auto& it : _input._format->getAttributes()) { const Stream::Attribute& attrib = (it).second; - newActivation.set(attrib._slot); + uint8_t locationCount = attrib._element.getLocationCount(); + for (int i = 0; i < locationCount; ++i) { + newActivation.set(attrib._slot + i); + } } } @@ -211,14 +214,19 @@ void GLBackend::updateInput() { const Stream::Attribute& attrib = attributes.at(channel._slots[i]); GLuint slot = attrib._slot; GLuint count = attrib._element.getDimensionCount(); + uint8_t locationCount = attrib._element.getLocationCount(); GLenum type = _elementTypeToGLType[attrib._element.getType()]; - GLuint stride = strides[bufferNum]; + GLenum perLocationStride = strides[bufferNum]; + GLuint stride = perLocationStride * locationCount; GLuint pointer = attrib._offset + offsets[bufferNum]; GLboolean isNormalized = attrib._element.isNormalized(); - glVertexAttribPointer(slot, count, type, isNormalized, stride, - reinterpret_cast(pointer)); - + for (int j = 0; j < locationCount; ++j) { + glVertexAttribPointer(slot + j, count, type, isNormalized, stride, + reinterpret_cast(pointer + perLocationStride * j)); + glVertexAttribDivisor(slot + j, attrib._frequency); + } + // TODO: Support properly the IAttrib version (void) CHECK_GL_ERROR(); diff --git a/libraries/gpu/src/gpu/GLBackendShader.cpp b/libraries/gpu/src/gpu/GLBackendShader.cpp index cd90da483b..5a0ab93ec5 100755 --- a/libraries/gpu/src/gpu/GLBackendShader.cpp +++ b/libraries/gpu/src/gpu/GLBackendShader.cpp @@ -75,6 +75,11 @@ void makeBindings(GLBackend::GLShader* shader) { glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_WEIGHT, "inSkinClusterWeight"); } + loc = glGetAttribLocation(glprogram, "inInstanceTransform"); + if (loc >= 0 && loc != gpu::Stream::INSTANCE_XFM) { + glBindAttribLocation(glprogram, gpu::Stream::INSTANCE_XFM, "inInstanceTransform"); + } + // Link again to take into account the assigned attrib location glLinkProgram(glprogram); diff --git a/libraries/gpu/src/gpu/Inputs.slh b/libraries/gpu/src/gpu/Inputs.slh index 8f90b6ebee..99e1e1d6d5 100644 --- a/libraries/gpu/src/gpu/Inputs.slh +++ b/libraries/gpu/src/gpu/Inputs.slh @@ -18,4 +18,5 @@ in vec4 inTangent; in vec4 inSkinClusterIndex; in vec4 inSkinClusterWeight; in vec4 inTexCoord1; +in mat4 inInstanceTransform; <@endif@> diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 177c798e2c..de5e4a7242 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -139,6 +139,11 @@ public: // \return the number of bytes copied Size append(Size size, const Byte* data); + template + Size append(const T& t) { + return append(sizeof(t), reinterpret_cast(&t)); + } + // Access the sysmem object. const Sysmem& getSysmem() const { assert(_sysmem); return (*_sysmem); } Sysmem& editSysmem() { assert(_sysmem); return (*_sysmem); } diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h index 46ea1574ed..c0ad1ebe46 100644 --- a/libraries/gpu/src/gpu/Stream.h +++ b/libraries/gpu/src/gpu/Stream.h @@ -35,11 +35,12 @@ public: SKIN_CLUSTER_INDEX = 5, SKIN_CLUSTER_WEIGHT = 6, TEXCOORD1 = 7, - INSTANCE_XFM = 8, - INSTANCE_SCALE = 9, - INSTANCE_TRANSLATE = 10, + INSTANCE_SCALE = 8, + INSTANCE_TRANSLATE = 9, + INSTANCE_XFM = 10, - NUM_INPUT_SLOTS, + // Instance XFM is a mat4, and as such takes up 4 slots + NUM_INPUT_SLOTS = INSTANCE_XFM + 4, }; typedef uint8 Slot; diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index b492b4ef24..b766cc88d4 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -53,6 +53,15 @@ TransformCamera getTransformCamera() { } <@endfunc@> +<@func transformInstancedModelToClipPos(cameraTransform, objectTransform, modelPos, clipPos)@> + + { // transformModelToClipPos + vec4 _eyepos = (inInstanceTransform * <$modelPos$>) + vec4(-<$modelPos$>.w * <$cameraTransform$>._viewInverse[3].xyz, 0.0); + <$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * _eyepos; + } +<@endfunc@> + <@func $transformModelToEyeAndClipPos(cameraTransform, objectTransform, modelPos, eyePos, clipPos)@> @@ -65,12 +74,31 @@ TransformCamera getTransformCamera() { } <@endfunc@> +<@func $transformInstancedModelToEyeAndClipPos(cameraTransform, objectTransform, modelPos, eyePos, clipPos)@> + + { // transformModelToClipPos + vec4 _worldpos = (inInstanceTransform * <$modelPos$>); + <$eyePos$> = (<$cameraTransform$>._view * _worldpos); + vec4 _eyepos =(inInstanceTransform * <$modelPos$>) + vec4(-<$modelPos$>.w * <$cameraTransform$>._viewInverse[3].xyz, 0.0); + <$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * _eyepos; + // <$eyePos$> = (<$cameraTransform$>._projectionInverse * <$clipPos$>); + } +<@endfunc@> + + <@func transformModelToWorldPos(objectTransform, modelPos, worldPos)@> { // transformModelToWorldPos <$worldPos$> = (<$objectTransform$>._model * <$modelPos$>); } <@endfunc@> +<@func transformInstancedModelToWorldPos(objectTransform, modelPos, worldPos)@> + { // transformModelToWorldPos + <$worldPos$> = (inInstanceTransform * <$modelPos$>); + } +<@endfunc@> + <@func transformModelToEyeDir(cameraTransform, objectTransform, modelDir, eyeDir)@> { // transformModelToEyeDir vec3 mr0 = vec3(<$objectTransform$>._modelInverse[0].x, <$objectTransform$>._modelInverse[1].x, <$objectTransform$>._modelInverse[2].x); @@ -85,6 +113,21 @@ TransformCamera getTransformCamera() { } <@endfunc@> +<@func transformInstancedModelToEyeDir(cameraTransform, objectTransform, modelDir, eyeDir)@> + { // transformModelToEyeDir + mat4 modelInverse = inverse(inInstanceTransform); + vec3 mr0 = vec3(modelInverse[0].x, modelInverse[1].x, modelInverse[2].x); + vec3 mr1 = vec3(modelInverse[0].y, modelInverse[1].y, modelInverse[2].y); + vec3 mr2 = vec3(modelInverse[0].z, modelInverse[1].z, modelInverse[2].z); + + vec3 mvc0 = vec3(dot(<$cameraTransform$>._viewInverse[0].xyz, mr0), dot(<$cameraTransform$>._viewInverse[0].xyz, mr1), dot(<$cameraTransform$>._viewInverse[0].xyz, mr2)); + vec3 mvc1 = vec3(dot(<$cameraTransform$>._viewInverse[1].xyz, mr0), dot(<$cameraTransform$>._viewInverse[1].xyz, mr1), dot(<$cameraTransform$>._viewInverse[1].xyz, mr2)); + vec3 mvc2 = vec3(dot(<$cameraTransform$>._viewInverse[2].xyz, mr0), dot(<$cameraTransform$>._viewInverse[2].xyz, mr1), dot(<$cameraTransform$>._viewInverse[2].xyz, mr2)); + + <$eyeDir$> = vec3(dot(mvc0, <$modelDir$>), dot(mvc1, <$modelDir$>), dot(mvc2, <$modelDir$>)); + } +<@endfunc@> + <@func transformEyeToWorldDir(cameraTransform, eyeDir, worldDir)@> { // transformEyeToWorldDir <$worldDir$> = vec3(<$cameraTransform$>._viewInverse * vec4(<$eyeDir$>.xyz, 0.0)); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index ce387e648b..6c7310509a 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -1,809 +1,845 @@ -// -// DeferredLightingEffect.cpp -// interface/src/renderer -// -// Created by Andrzej Kapolka on 9/11/14. -// Copyright 2014 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 "DeferredLightingEffect.h" - -#include -#include -#include - -#include -#include -#include - -#include "AbstractViewStateInterface.h" -#include "GeometryCache.h" -#include "TextureCache.h" -#include "FramebufferCache.h" - - -#include "simple_vert.h" -#include "simple_textured_frag.h" -#include "simple_textured_emisive_frag.h" - -#include "deferred_light_vert.h" -#include "deferred_light_limited_vert.h" -#include "deferred_light_spot_vert.h" - -#include "directional_light_frag.h" -#include "directional_light_shadow_map_frag.h" -#include "directional_light_cascaded_shadow_map_frag.h" - -#include "directional_ambient_light_frag.h" -#include "directional_ambient_light_shadow_map_frag.h" -#include "directional_ambient_light_cascaded_shadow_map_frag.h" - -#include "directional_skybox_light_frag.h" -#include "directional_skybox_light_shadow_map_frag.h" -#include "directional_skybox_light_cascaded_shadow_map_frag.h" - -#include "point_light_frag.h" -#include "spot_light_frag.h" - -static const std::string glowIntensityShaderHandle = "glowIntensity"; - -struct LightLocations { - int shadowDistances; - int shadowScale; - int radius; - int ambientSphere; - int lightBufferUnit; - int atmosphereBufferUnit; - int texcoordMat; - int coneParam; - int deferredTransformBuffer; -}; - -static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& program, LightLocationsPtr& locations); - - -gpu::PipelinePointer DeferredLightingEffect::getPipeline(SimpleProgramKey config) { - auto it = _simplePrograms.find(config); - if (it != _simplePrograms.end()) { - return it.value(); - } - - auto state = std::make_shared(); - if (config.isCulled()) { - state->setCullMode(gpu::State::CULL_BACK); - } else { - state->setCullMode(gpu::State::CULL_NONE); - } - state->setDepthTest(true, true, gpu::LESS_EQUAL); - if (config.hasDepthBias()) { - state->setDepthBias(1.0f); - state->setDepthBiasSlopeScale(1.0f); - } - state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - gpu::ShaderPointer program = (config.isEmissive()) ? _emissiveShader : _simpleShader; - gpu::PipelinePointer pipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); - _simplePrograms.insert(config, pipeline); - return pipeline; -} - -void DeferredLightingEffect::init(AbstractViewStateInterface* viewState) { - auto VS = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(simple_vert))); - auto PS = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(simple_textured_frag))); - auto PSEmissive = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(simple_textured_emisive_frag))); - - _simpleShader = gpu::ShaderPointer(gpu::Shader::createProgram(VS, PS)); - _emissiveShader = gpu::ShaderPointer(gpu::Shader::createProgram(VS, PSEmissive)); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), DeferredLightingEffect::NORMAL_FITTING_MAP_SLOT)); - gpu::Shader::makeProgram(*_simpleShader, slotBindings); - gpu::Shader::makeProgram(*_emissiveShader, slotBindings); - - _viewState = viewState; - _directionalLightLocations = std::make_shared(); - _directionalLightShadowMapLocations = std::make_shared(); - _directionalLightCascadedShadowMapLocations = std::make_shared(); - _directionalAmbientSphereLightLocations = std::make_shared(); - _directionalAmbientSphereLightShadowMapLocations = std::make_shared(); - _directionalAmbientSphereLightCascadedShadowMapLocations = std::make_shared(); - _directionalSkyboxLightLocations = std::make_shared(); - _directionalSkyboxLightShadowMapLocations = std::make_shared(); - _directionalSkyboxLightCascadedShadowMapLocations = std::make_shared(); - _pointLightLocations = std::make_shared(); - _spotLightLocations = std::make_shared(); - - loadLightProgram(deferred_light_vert, directional_light_frag, false, _directionalLight, _directionalLightLocations); - loadLightProgram(deferred_light_vert, directional_light_shadow_map_frag, false, _directionalLightShadowMap, - _directionalLightShadowMapLocations); - loadLightProgram(deferred_light_vert, directional_light_cascaded_shadow_map_frag, false, _directionalLightCascadedShadowMap, - _directionalLightCascadedShadowMapLocations); - - loadLightProgram(deferred_light_vert, directional_ambient_light_frag, false, _directionalAmbientSphereLight, _directionalAmbientSphereLightLocations); - loadLightProgram(deferred_light_vert, directional_ambient_light_shadow_map_frag, false, _directionalAmbientSphereLightShadowMap, - _directionalAmbientSphereLightShadowMapLocations); - loadLightProgram(deferred_light_vert, directional_ambient_light_cascaded_shadow_map_frag, false, _directionalAmbientSphereLightCascadedShadowMap, - _directionalAmbientSphereLightCascadedShadowMapLocations); - - loadLightProgram(deferred_light_vert, directional_skybox_light_frag, false, _directionalSkyboxLight, _directionalSkyboxLightLocations); - loadLightProgram(deferred_light_vert, directional_skybox_light_shadow_map_frag, false, _directionalSkyboxLightShadowMap, - _directionalSkyboxLightShadowMapLocations); - loadLightProgram(deferred_light_vert, directional_skybox_light_cascaded_shadow_map_frag, false, _directionalSkyboxLightCascadedShadowMap, - _directionalSkyboxLightCascadedShadowMapLocations); - - - loadLightProgram(deferred_light_limited_vert, point_light_frag, true, _pointLight, _pointLightLocations); - loadLightProgram(deferred_light_spot_vert, spot_light_frag, true, _spotLight, _spotLightLocations); - - { - //auto VSFS = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); - //auto PSBlit = gpu::StandardShaderLib::getDrawTexturePS(); - auto blitProgram = gpu::StandardShaderLib::getProgram(gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS, gpu::StandardShaderLib::getDrawTexturePS); - gpu::Shader::makeProgram(*blitProgram); - auto blitState = std::make_shared(); - blitState->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - blitState->setColorWriteMask(true, true, true, false); - _blitLightBuffer = gpu::PipelinePointer(gpu::Pipeline::create(blitProgram, blitState)); - } - - // Allocate a global light representing the Global Directional light casting shadow (the sun) and the ambient light - _globalLights.push_back(0); - _allocatedLights.push_back(std::make_shared()); - - model::LightPointer lp = _allocatedLights[0]; - - lp->setDirection(-glm::vec3(1.0f, 1.0f, 1.0f)); - lp->setColor(glm::vec3(1.0f)); - lp->setIntensity(1.0f); - lp->setType(model::Light::SUN); - lp->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset(_ambientLightMode % gpu::SphericalHarmonics::NUM_PRESET)); -} - - - -void DeferredLightingEffect::bindSimpleProgram(gpu::Batch& batch, bool textured, bool culled, - bool emmisive, bool depthBias) { - SimpleProgramKey config{textured, culled, emmisive, depthBias}; - batch.setPipeline(getPipeline(config)); - - gpu::ShaderPointer program = (config.isEmissive()) ? _emissiveShader : _simpleShader; - int glowIntensity = program->getUniforms().findLocation("glowIntensity"); - batch._glUniform1f(glowIntensity, 1.0f); - - if (!config.isTextured()) { - // If it is not textured, bind white texture and keep using textured pipeline - batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); - } - - batch.setResourceTexture(NORMAL_FITTING_MAP_SLOT, DependencyManager::get()->getNormalFittingTexture()); -} - -void DeferredLightingEffect::renderSolidSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderSphere(batch, radius, slices, stacks, color); -} - -void DeferredLightingEffect::renderWireSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderSphere(batch, radius, slices, stacks, color, false); -} - -void DeferredLightingEffect::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderSolidCube(batch, size, color); -} - -void DeferredLightingEffect::renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderWireCube(batch, size, color); -} - -void DeferredLightingEffect::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, - const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderQuad(batch, minCorner, maxCorner, color); -} - -void DeferredLightingEffect::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color1, const glm::vec4& color2) { - bindSimpleProgram(batch); - DependencyManager::get()->renderLine(batch, p1, p2, color1, color2); -} - -void DeferredLightingEffect::addPointLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity) { - addSpotLight(position, radius, color, intensity); -} - -void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity, const glm::quat& orientation, float exponent, float cutoff) { - - unsigned int lightID = _pointLights.size() + _spotLights.size() + _globalLights.size(); - if (lightID >= _allocatedLights.size()) { - _allocatedLights.push_back(std::make_shared()); - } - model::LightPointer lp = _allocatedLights[lightID]; - - lp->setPosition(position); - lp->setMaximumRadius(radius); - lp->setColor(color); - lp->setIntensity(intensity); - //lp->setShowContour(quadraticAttenuation); - - if (exponent == 0.0f && cutoff == PI) { - lp->setType(model::Light::POINT); - _pointLights.push_back(lightID); - - } else { - lp->setOrientation(orientation); - lp->setSpotAngle(cutoff); - lp->setSpotExponent(exponent); - lp->setType(model::Light::SPOT); - _spotLights.push_back(lightID); - } -} - -void DeferredLightingEffect::prepare(RenderArgs* args) { - gpu::Batch batch; - batch.enableStereo(false); - - batch.setStateScissorRect(args->_viewport); - - auto primaryFbo = DependencyManager::get()->getPrimaryFramebuffer(); - - batch.setFramebuffer(primaryFbo); - // clear the normal and specular buffers - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR1, glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), true); - const float MAX_SPECULAR_EXPONENT = 128.0f; - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR2, glm::vec4(0.0f, 0.0f, 0.0f, 1.0f / MAX_SPECULAR_EXPONENT), true); - - args->_context->render(batch); -} - -gpu::FramebufferPointer _copyFBO; - -void DeferredLightingEffect::render(RenderArgs* args) { - gpu::Batch batch; - - // Allocate the parameters buffer used by all the deferred shaders - if (!_deferredTransformBuffer[0]._buffer) { - DeferredTransform parameters; - _deferredTransformBuffer[0] = gpu::BufferView(std::make_shared(sizeof(DeferredTransform), (const gpu::Byte*) ¶meters)); - _deferredTransformBuffer[1] = gpu::BufferView(std::make_shared(sizeof(DeferredTransform), (const gpu::Byte*) ¶meters)); - } - - // Framebuffer copy operations cannot function as multipass stereo operations. - batch.enableStereo(false); - - // perform deferred lighting, rendering to free fbo - auto framebufferCache = DependencyManager::get(); - - QSize framebufferSize = framebufferCache->getFrameBufferSize(); - - // binding the first framebuffer - _copyFBO = framebufferCache->getFramebuffer(); - batch.setFramebuffer(_copyFBO); - - // Clearing it - batch.setViewportTransform(args->_viewport); - batch.setStateScissorRect(args->_viewport); - batch.clearColorFramebuffer(_copyFBO->getBufferMask(), glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), true); - - // BInd the G-Buffer surfaces - batch.setResourceTexture(0, framebufferCache->getPrimaryColorTexture()); - batch.setResourceTexture(1, framebufferCache->getPrimaryNormalTexture()); - batch.setResourceTexture(2, framebufferCache->getPrimarySpecularTexture()); - batch.setResourceTexture(3, framebufferCache->getPrimaryDepthTexture()); - - // THe main viewport is assumed to be the mono viewport (or the 2 stereo faces side by side within that viewport) - auto monoViewport = args->_viewport; - float sMin = args->_viewport.x / (float)framebufferSize.width(); - float sWidth = args->_viewport.z / (float)framebufferSize.width(); - float tMin = args->_viewport.y / (float)framebufferSize.height(); - float tHeight = args->_viewport.w / (float)framebufferSize.height(); - - // The view frustum is the mono frustum base - auto viewFrustum = args->_viewFrustum; - - // Eval the mono projection - mat4 monoProjMat; - viewFrustum->evalProjectionMatrix(monoProjMat); - - // The mono view transform - Transform monoViewTransform; - viewFrustum->evalViewTransform(monoViewTransform); - - // THe mono view matrix coming from the mono view transform - glm::mat4 monoViewMat; - monoViewTransform.getMatrix(monoViewMat); - - // Running in stero ? - bool isStereo = args->_context->isStereo(); - int numPasses = 1; - - mat4 projMats[2]; - Transform viewTransforms[2]; - ivec4 viewports[2]; - vec4 clipQuad[2]; - vec2 screenBottomLeftCorners[2]; - vec2 screenTopRightCorners[2]; - vec4 fetchTexcoordRects[2]; - - DeferredTransform deferredTransforms[2]; - auto geometryCache = DependencyManager::get(); - - if (isStereo) { - numPasses = 2; - - mat4 eyeViews[2]; - args->_context->getStereoProjections(projMats); - args->_context->getStereoViews(eyeViews); - - float halfWidth = 0.5 * sWidth; - - for (int i = 0; i < numPasses; i++) { - // In stereo, the 2 sides are layout side by side in the mono viewport and their width is half - int sideWidth = monoViewport.z >> 1; - viewports[i] = ivec4(monoViewport.x + (i * sideWidth), monoViewport.y, sideWidth, monoViewport.w); - - deferredTransforms[i].projection = projMats[i]; - +// +// DeferredLightingEffect.cpp +// interface/src/renderer +// +// Created by Andrzej Kapolka on 9/11/14. +// Copyright 2014 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 "DeferredLightingEffect.h" + +#include +#include +#include + +#include +#include +#include + +#include "AbstractViewStateInterface.h" +#include "GeometryCache.h" +#include "TextureCache.h" +#include "FramebufferCache.h" + + +#include "simple_vert.h" +#include "simple_textured_frag.h" +#include "simple_textured_emisive_frag.h" + +#include "deferred_light_vert.h" +#include "deferred_light_limited_vert.h" +#include "deferred_light_spot_vert.h" + +#include "directional_light_frag.h" +#include "directional_light_shadow_map_frag.h" +#include "directional_light_cascaded_shadow_map_frag.h" + +#include "directional_ambient_light_frag.h" +#include "directional_ambient_light_shadow_map_frag.h" +#include "directional_ambient_light_cascaded_shadow_map_frag.h" + +#include "directional_skybox_light_frag.h" +#include "directional_skybox_light_shadow_map_frag.h" +#include "directional_skybox_light_cascaded_shadow_map_frag.h" + +#include "point_light_frag.h" +#include "spot_light_frag.h" + +static const std::string glowIntensityShaderHandle = "glowIntensity"; + +struct LightLocations { + int shadowDistances; + int shadowScale; + int radius; + int ambientSphere; + int lightBufferUnit; + int atmosphereBufferUnit; + int texcoordMat; + int coneParam; + int deferredTransformBuffer; +}; + +static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& program, LightLocationsPtr& locations); + + +gpu::PipelinePointer DeferredLightingEffect::getPipeline(SimpleProgramKey config) { + auto it = _simplePrograms.find(config); + if (it != _simplePrograms.end()) { + return it.value(); + } + + auto state = std::make_shared(); + if (config.isCulled()) { + state->setCullMode(gpu::State::CULL_BACK); + } else { + state->setCullMode(gpu::State::CULL_NONE); + } + state->setDepthTest(true, true, gpu::LESS_EQUAL); + if (config.hasDepthBias()) { + state->setDepthBias(1.0f); + state->setDepthBiasSlopeScale(1.0f); + } + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + + gpu::ShaderPointer program = (config.isEmissive()) ? _emissiveShader : _simpleShader; + gpu::PipelinePointer pipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); + _simplePrograms.insert(config, pipeline); + return pipeline; +} + +void DeferredLightingEffect::init(AbstractViewStateInterface* viewState) { + auto VS = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(simple_vert))); + auto PS = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(simple_textured_frag))); + auto PSEmissive = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(simple_textured_emisive_frag))); + + _simpleShader = gpu::ShaderPointer(gpu::Shader::createProgram(VS, PS)); + _emissiveShader = gpu::ShaderPointer(gpu::Shader::createProgram(VS, PSEmissive)); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), DeferredLightingEffect::NORMAL_FITTING_MAP_SLOT)); + gpu::Shader::makeProgram(*_simpleShader, slotBindings); + gpu::Shader::makeProgram(*_emissiveShader, slotBindings); + + _viewState = viewState; + _directionalLightLocations = std::make_shared(); + _directionalLightShadowMapLocations = std::make_shared(); + _directionalLightCascadedShadowMapLocations = std::make_shared(); + _directionalAmbientSphereLightLocations = std::make_shared(); + _directionalAmbientSphereLightShadowMapLocations = std::make_shared(); + _directionalAmbientSphereLightCascadedShadowMapLocations = std::make_shared(); + _directionalSkyboxLightLocations = std::make_shared(); + _directionalSkyboxLightShadowMapLocations = std::make_shared(); + _directionalSkyboxLightCascadedShadowMapLocations = std::make_shared(); + _pointLightLocations = std::make_shared(); + _spotLightLocations = std::make_shared(); + + loadLightProgram(deferred_light_vert, directional_light_frag, false, _directionalLight, _directionalLightLocations); + loadLightProgram(deferred_light_vert, directional_light_shadow_map_frag, false, _directionalLightShadowMap, + _directionalLightShadowMapLocations); + loadLightProgram(deferred_light_vert, directional_light_cascaded_shadow_map_frag, false, _directionalLightCascadedShadowMap, + _directionalLightCascadedShadowMapLocations); + + loadLightProgram(deferred_light_vert, directional_ambient_light_frag, false, _directionalAmbientSphereLight, _directionalAmbientSphereLightLocations); + loadLightProgram(deferred_light_vert, directional_ambient_light_shadow_map_frag, false, _directionalAmbientSphereLightShadowMap, + _directionalAmbientSphereLightShadowMapLocations); + loadLightProgram(deferred_light_vert, directional_ambient_light_cascaded_shadow_map_frag, false, _directionalAmbientSphereLightCascadedShadowMap, + _directionalAmbientSphereLightCascadedShadowMapLocations); + + loadLightProgram(deferred_light_vert, directional_skybox_light_frag, false, _directionalSkyboxLight, _directionalSkyboxLightLocations); + loadLightProgram(deferred_light_vert, directional_skybox_light_shadow_map_frag, false, _directionalSkyboxLightShadowMap, + _directionalSkyboxLightShadowMapLocations); + loadLightProgram(deferred_light_vert, directional_skybox_light_cascaded_shadow_map_frag, false, _directionalSkyboxLightCascadedShadowMap, + _directionalSkyboxLightCascadedShadowMapLocations); + + + loadLightProgram(deferred_light_limited_vert, point_light_frag, true, _pointLight, _pointLightLocations); + loadLightProgram(deferred_light_spot_vert, spot_light_frag, true, _spotLight, _spotLightLocations); + + { + //auto VSFS = gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS(); + //auto PSBlit = gpu::StandardShaderLib::getDrawTexturePS(); + auto blitProgram = gpu::StandardShaderLib::getProgram(gpu::StandardShaderLib::getDrawViewportQuadTransformTexcoordVS, gpu::StandardShaderLib::getDrawTexturePS); + gpu::Shader::makeProgram(*blitProgram); + auto blitState = std::make_shared(); + blitState->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + blitState->setColorWriteMask(true, true, true, false); + _blitLightBuffer = gpu::PipelinePointer(gpu::Pipeline::create(blitProgram, blitState)); + } + + // Allocate a global light representing the Global Directional light casting shadow (the sun) and the ambient light + _globalLights.push_back(0); + _allocatedLights.push_back(std::make_shared()); + + model::LightPointer lp = _allocatedLights[0]; + + lp->setDirection(-glm::vec3(1.0f, 1.0f, 1.0f)); + lp->setColor(glm::vec3(1.0f)); + lp->setIntensity(1.0f); + lp->setType(model::Light::SUN); + lp->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset(_ambientLightMode % gpu::SphericalHarmonics::NUM_PRESET)); +} + + + +gpu::PipelinePointer DeferredLightingEffect::bindSimpleProgram(gpu::Batch& batch, bool textured, bool culled, + bool emmisive, bool depthBias) { + SimpleProgramKey config{textured, culled, emmisive, depthBias}; + gpu::PipelinePointer pipeline = getPipeline(config); + batch.setPipeline(pipeline); + + gpu::ShaderPointer program = (config.isEmissive()) ? _emissiveShader : _simpleShader; + int glowIntensity = program->getUniforms().findLocation("glowIntensity"); + batch._glUniform1f(glowIntensity, 1.0f); + + if (!config.isTextured()) { + // If it is not textured, bind white texture and keep using textured pipeline + batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); + } + + batch.setResourceTexture(NORMAL_FITTING_MAP_SLOT, DependencyManager::get()->getNormalFittingTexture()); + return pipeline; +} + + +void DeferredLightingEffect::renderSolidSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) { + bindSimpleProgram(batch); + DependencyManager::get()->renderSphere(batch, radius, slices, stacks, color); +} + +void DeferredLightingEffect::renderWireSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) { + bindSimpleProgram(batch); + DependencyManager::get()->renderSphere(batch, radius, slices, stacks, color, false); +} + +uint32_t toCompactColor(const glm::vec4& color) { + uint32_t compactColor = ((int(color.x * 255.0f) & 0xFF)) | + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); + return compactColor; +} + +void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { + static const std::string INSTANCE_NAME = __FUNCTION__; + static const size_t TRANSFORM_BUFFER = 0; + static const size_t COLOR_BUFFER = 1; + { + gpu::BufferPointer instanceTransformBuffer = batch.getNamedBuffer(INSTANCE_NAME, TRANSFORM_BUFFER); + glm::mat4 xfmMat4; + instanceTransformBuffer->append(xfm.getMatrix(xfmMat4)); + + gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(INSTANCE_NAME, COLOR_BUFFER); + auto compactColor = toCompactColor(color); + instanceColorBuffer->append(compactColor); + } + + batch.setupNamedCalls(INSTANCE_NAME, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + auto pipeline = bindSimpleProgram(batch); + auto location = pipeline->getProgram()->getUniforms().findLocation("Instanced"); + + batch._glUniform1i(location, 1); + DependencyManager::get()->renderSolidCubeInstances(batch, data._count, + data._buffers[TRANSFORM_BUFFER], data._buffers[COLOR_BUFFER]); + batch._glUniform1i(location, 0); + }); +} + +void DeferredLightingEffect::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) { + bindSimpleProgram(batch); + DependencyManager::get()->renderSolidCube(batch, size, color); +} + +void DeferredLightingEffect::renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color) { + bindSimpleProgram(batch); + DependencyManager::get()->renderWireCube(batch, size, color); +} + +void DeferredLightingEffect::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, + const glm::vec4& color) { + bindSimpleProgram(batch); + DependencyManager::get()->renderQuad(batch, minCorner, maxCorner, color); +} + +void DeferredLightingEffect::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, + const glm::vec4& color1, const glm::vec4& color2) { + bindSimpleProgram(batch); + DependencyManager::get()->renderLine(batch, p1, p2, color1, color2); +} + +void DeferredLightingEffect::addPointLight(const glm::vec3& position, float radius, const glm::vec3& color, + float intensity) { + addSpotLight(position, radius, color, intensity); +} + +void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color, + float intensity, const glm::quat& orientation, float exponent, float cutoff) { + + unsigned int lightID = _pointLights.size() + _spotLights.size() + _globalLights.size(); + if (lightID >= _allocatedLights.size()) { + _allocatedLights.push_back(std::make_shared()); + } + model::LightPointer lp = _allocatedLights[lightID]; + + lp->setPosition(position); + lp->setMaximumRadius(radius); + lp->setColor(color); + lp->setIntensity(intensity); + //lp->setShowContour(quadraticAttenuation); + + if (exponent == 0.0f && cutoff == PI) { + lp->setType(model::Light::POINT); + _pointLights.push_back(lightID); + + } else { + lp->setOrientation(orientation); + lp->setSpotAngle(cutoff); + lp->setSpotExponent(exponent); + lp->setType(model::Light::SPOT); + _spotLights.push_back(lightID); + } +} + +void DeferredLightingEffect::prepare(RenderArgs* args) { + gpu::Batch batch; + batch.enableStereo(false); + + batch.setStateScissorRect(args->_viewport); + + auto primaryFbo = DependencyManager::get()->getPrimaryFramebuffer(); + + batch.setFramebuffer(primaryFbo); + // clear the normal and specular buffers + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR1, glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), true); + const float MAX_SPECULAR_EXPONENT = 128.0f; + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR2, glm::vec4(0.0f, 0.0f, 0.0f, 1.0f / MAX_SPECULAR_EXPONENT), true); + + args->_context->render(batch); +} + +gpu::FramebufferPointer _copyFBO; + +void DeferredLightingEffect::render(RenderArgs* args) { + gpu::Batch batch; + + // Allocate the parameters buffer used by all the deferred shaders + if (!_deferredTransformBuffer[0]._buffer) { + DeferredTransform parameters; + _deferredTransformBuffer[0] = gpu::BufferView(std::make_shared(sizeof(DeferredTransform), (const gpu::Byte*) ¶meters)); + _deferredTransformBuffer[1] = gpu::BufferView(std::make_shared(sizeof(DeferredTransform), (const gpu::Byte*) ¶meters)); + } + + // Framebuffer copy operations cannot function as multipass stereo operations. + batch.enableStereo(false); + + // perform deferred lighting, rendering to free fbo + auto framebufferCache = DependencyManager::get(); + + QSize framebufferSize = framebufferCache->getFrameBufferSize(); + + // binding the first framebuffer + _copyFBO = framebufferCache->getFramebuffer(); + batch.setFramebuffer(_copyFBO); + + // Clearing it + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + batch.clearColorFramebuffer(_copyFBO->getBufferMask(), glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), true); + + // BInd the G-Buffer surfaces + batch.setResourceTexture(0, framebufferCache->getPrimaryColorTexture()); + batch.setResourceTexture(1, framebufferCache->getPrimaryNormalTexture()); + batch.setResourceTexture(2, framebufferCache->getPrimarySpecularTexture()); + batch.setResourceTexture(3, framebufferCache->getPrimaryDepthTexture()); + + // THe main viewport is assumed to be the mono viewport (or the 2 stereo faces side by side within that viewport) + auto monoViewport = args->_viewport; + float sMin = args->_viewport.x / (float)framebufferSize.width(); + float sWidth = args->_viewport.z / (float)framebufferSize.width(); + float tMin = args->_viewport.y / (float)framebufferSize.height(); + float tHeight = args->_viewport.w / (float)framebufferSize.height(); + + // The view frustum is the mono frustum base + auto viewFrustum = args->_viewFrustum; + + // Eval the mono projection + mat4 monoProjMat; + viewFrustum->evalProjectionMatrix(monoProjMat); + + // The mono view transform + Transform monoViewTransform; + viewFrustum->evalViewTransform(monoViewTransform); + + // THe mono view matrix coming from the mono view transform + glm::mat4 monoViewMat; + monoViewTransform.getMatrix(monoViewMat); + + // Running in stero ? + bool isStereo = args->_context->isStereo(); + int numPasses = 1; + + mat4 projMats[2]; + Transform viewTransforms[2]; + ivec4 viewports[2]; + vec4 clipQuad[2]; + vec2 screenBottomLeftCorners[2]; + vec2 screenTopRightCorners[2]; + vec4 fetchTexcoordRects[2]; + + DeferredTransform deferredTransforms[2]; + auto geometryCache = DependencyManager::get(); + + if (isStereo) { + numPasses = 2; + + mat4 eyeViews[2]; + args->_context->getStereoProjections(projMats); + args->_context->getStereoViews(eyeViews); + + float halfWidth = 0.5 * sWidth; + + for (int i = 0; i < numPasses; i++) { + // In stereo, the 2 sides are layout side by side in the mono viewport and their width is half + int sideWidth = monoViewport.z >> 1; + viewports[i] = ivec4(monoViewport.x + (i * sideWidth), monoViewport.y, sideWidth, monoViewport.w); + + deferredTransforms[i].projection = projMats[i]; + auto sideViewMat = eyeViews[i] * monoViewMat; - viewTransforms[i].evalFromRawMatrix(sideViewMat); - deferredTransforms[i].viewInverse = sideViewMat; - - deferredTransforms[i].stereoSide = (i == 0 ? -1.0f : 1.0f); - - clipQuad[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); - screenBottomLeftCorners[i] = glm::vec2(-1.0f + i * 1.0f, -1.0f); - screenTopRightCorners[i] = glm::vec2(i * 1.0f, 1.0f); - - fetchTexcoordRects[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); - } - } else { - - viewports[0] = monoViewport; - projMats[0] = monoProjMat; - - deferredTransforms[0].projection = monoProjMat; - - deferredTransforms[0].viewInverse = monoViewMat; - viewTransforms[0] = monoViewTransform; - - deferredTransforms[0].stereoSide = 0.0f; - - clipQuad[0] = glm::vec4(sMin, tMin, sWidth, tHeight); - screenBottomLeftCorners[0] = glm::vec2(-1.0f, -1.0f); - screenTopRightCorners[0] = glm::vec2(1.0f, 1.0f); - - fetchTexcoordRects[0] = glm::vec4(sMin, tMin, sWidth, tHeight); - } - - auto eyePoint = viewFrustum->getPosition(); - float nearRadius = glm::distance(eyePoint, viewFrustum->getNearTopLeft()); - - - for (int side = 0; side < numPasses; side++) { - // Render in this side's viewport - batch.setViewportTransform(viewports[side]); - batch.setStateScissorRect(viewports[side]); - - // Sync and Bind the correct DeferredTransform ubo - _deferredTransformBuffer[side]._buffer->setSubData(0, sizeof(DeferredTransform), (const gpu::Byte*) &deferredTransforms[side]); - batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, _deferredTransformBuffer[side]); - - glm::vec2 topLeft(-1.0f, -1.0f); - glm::vec2 bottomRight(1.0f, 1.0f); - glm::vec2 texCoordTopLeft(clipQuad[side].x, clipQuad[side].y); - glm::vec2 texCoordBottomRight(clipQuad[side].x + clipQuad[side].z, clipQuad[side].y + clipQuad[side].w); - - // First Global directional light and ambient pass - { - bool useSkyboxCubemap = (_skybox) && (_skybox->getCubemap()); - - auto& program = _directionalLight; - LightLocationsPtr locations = _directionalLightLocations; - - // TODO: At some point bring back the shadows... - // Setup the global directional pass pipeline - { - if (useSkyboxCubemap) { - program = _directionalSkyboxLight; - locations = _directionalSkyboxLightLocations; - } else if (_ambientLightMode > -1) { - program = _directionalAmbientSphereLight; - locations = _directionalAmbientSphereLightLocations; - } - batch.setPipeline(program); - } - - { // Setup the global lighting - auto globalLight = _allocatedLights[_globalLights.front()]; - - if (locations->ambientSphere >= 0) { - gpu::SphericalHarmonics sh = globalLight->getAmbientSphere(); - if (useSkyboxCubemap && _skybox->getCubemap()->getIrradiance()) { - sh = (*_skybox->getCubemap()->getIrradiance()); - } - for (int i =0; i ambientSphere + i, 1, (const float*) (&sh) + i * 4); - } - } - - if (useSkyboxCubemap) { - batch.setResourceTexture(5, _skybox->getCubemap()); - } - - if (locations->lightBufferUnit >= 0) { - batch.setUniformBuffer(locations->lightBufferUnit, globalLight->getSchemaBuffer()); - } - - if (_atmosphere && (locations->atmosphereBufferUnit >= 0)) { - batch.setUniformBuffer(locations->atmosphereBufferUnit, _atmosphere->getDataBuffer()); - } - } - - { - batch.setModelTransform(Transform()); - batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); - - glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); - geometryCache->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); - } - - if (useSkyboxCubemap) { - batch.setResourceTexture(5, nullptr); - } - } - - auto texcoordMat = glm::mat4(); - /* texcoordMat[0] = glm::vec4(sWidth / 2.0f, 0.0f, 0.0f, sMin + sWidth / 2.0f); - texcoordMat[1] = glm::vec4(0.0f, tHeight / 2.0f, 0.0f, tMin + tHeight / 2.0f); - */ texcoordMat[0] = glm::vec4(fetchTexcoordRects[side].z / 2.0f, 0.0f, 0.0f, fetchTexcoordRects[side].x + fetchTexcoordRects[side].z / 2.0f); - texcoordMat[1] = glm::vec4(0.0f, fetchTexcoordRects[side].w / 2.0f, 0.0f, fetchTexcoordRects[side].y + fetchTexcoordRects[side].w / 2.0f); - texcoordMat[2] = glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); - texcoordMat[3] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - - // enlarge the scales slightly to account for tesselation - const float SCALE_EXPANSION = 0.05f; - - - batch.setProjectionTransform(projMats[side]); - batch.setViewTransform(viewTransforms[side]); - - // Splat Point lights - if (!_pointLights.empty()) { - batch.setPipeline(_pointLight); - - batch._glUniformMatrix4fv(_pointLightLocations->texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); - - for (auto lightID : _pointLights) { - auto& light = _allocatedLights[lightID]; - // IN DEBUG: light->setShowContour(true); - if (_pointLightLocations->lightBufferUnit >= 0) { - batch.setUniformBuffer(_pointLightLocations->lightBufferUnit, light->getSchemaBuffer()); - } - - float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); - // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, - // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... - if (glm::distance(eyePoint, glm::vec3(light->getPosition())) < expandedRadius + nearRadius) { - Transform model; - model.setTranslation(glm::vec3(0.0f, 0.0f, -1.0f)); - batch.setModelTransform(model); - batch.setViewTransform(Transform()); - batch.setProjectionTransform(glm::mat4()); - - glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); - - batch.setProjectionTransform(projMats[side]); - batch.setViewTransform(viewTransforms[side]); - } else { - Transform model; - model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z)); - batch.setModelTransform(model); - geometryCache->renderSphere(batch, expandedRadius, 32, 32, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); - } - } - } - - // Splat spot lights - if (!_spotLights.empty()) { - batch.setPipeline(_spotLight); - - batch._glUniformMatrix4fv(_spotLightLocations->texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); - - for (auto lightID : _spotLights) { - auto light = _allocatedLights[lightID]; - // IN DEBUG: light->setShowContour(true); - - batch.setUniformBuffer(_spotLightLocations->lightBufferUnit, light->getSchemaBuffer()); - - auto eyeLightPos = eyePoint - light->getPosition(); - auto eyeHalfPlaneDistance = glm::dot(eyeLightPos, light->getDirection()); - - const float TANGENT_LENGTH_SCALE = 0.666f; - glm::vec4 coneParam(light->getSpotAngleCosSin(), TANGENT_LENGTH_SCALE * tanf(0.5f * light->getSpotAngle()), 1.0f); - - float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); - // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, - // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... - if ((eyeHalfPlaneDistance > -nearRadius) && - (glm::distance(eyePoint, glm::vec3(light->getPosition())) < expandedRadius + nearRadius)) { - coneParam.w = 0.0f; - batch._glUniform4fv(_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); - - Transform model; - model.setTranslation(glm::vec3(0.0f, 0.0f, -1.0f)); - batch.setModelTransform(model); - batch.setViewTransform(Transform()); - batch.setProjectionTransform(glm::mat4()); - - glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); - - batch.setProjectionTransform( projMats[side]); - batch.setViewTransform(viewTransforms[side]); - } else { - coneParam.w = 1.0f; - batch._glUniform4fv(_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); - - Transform model; - model.setTranslation(light->getPosition()); - model.postRotate(light->getOrientation()); - model.postScale(glm::vec3(expandedRadius, expandedRadius, expandedRadius)); - - batch.setModelTransform(model); - auto mesh = getSpotLightMesh(); - - batch.setIndexBuffer(mesh->getIndexBuffer()); - batch.setInputBuffer(0, mesh->getVertexBuffer()); - batch.setInputFormat(mesh->getVertexFormat()); - - auto& part = mesh->getPartBuffer().get(); - - batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); - } - } - } - } - - // Probably not necessary in the long run because the gpu layer would unbound this texture if used as render target - batch.setResourceTexture(0, nullptr); - batch.setResourceTexture(1, nullptr); - batch.setResourceTexture(2, nullptr); - batch.setResourceTexture(3, nullptr); - batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, nullptr); - - args->_context->render(batch); - - // End of the Lighting pass - if (!_pointLights.empty()) { - _pointLights.clear(); - } - if (!_spotLights.empty()) { - _spotLights.clear(); - } -} - - -void DeferredLightingEffect::copyBack(RenderArgs* args) { - gpu::Batch batch; - batch.enableStereo(false); - auto framebufferCache = DependencyManager::get(); - QSize framebufferSize = framebufferCache->getFrameBufferSize(); - - // TODO why doesn't this blit work? It only seems to affect a small area below the rear view mirror. - // auto destFbo = framebufferCache->getPrimaryFramebuffer(); - auto destFbo = framebufferCache->getPrimaryFramebufferDepthColor(); -// gpu::Vec4i vp = args->_viewport; -// batch.blit(_copyFBO, vp, framebufferCache->getPrimaryFramebuffer(), vp); - batch.setFramebuffer(destFbo); - batch.setViewportTransform(args->_viewport); - batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); - { - float sMin = args->_viewport.x / (float)framebufferSize.width(); - float sWidth = args->_viewport.z / (float)framebufferSize.width(); - float tMin = args->_viewport.y / (float)framebufferSize.height(); - float tHeight = args->_viewport.w / (float)framebufferSize.height(); - Transform model; - batch.setPipeline(_blitLightBuffer); - model.setTranslation(glm::vec3(sMin, tMin, 0.0)); - model.setScale(glm::vec3(sWidth, tHeight, 1.0)); - batch.setModelTransform(model); - } - - batch.setResourceTexture(0, _copyFBO->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - - args->_context->render(batch); - framebufferCache->releaseFramebuffer(_copyFBO); -} - -void DeferredLightingEffect::setupTransparent(RenderArgs* args, int lightBufferUnit) { - auto globalLight = _allocatedLights[_globalLights.front()]; - args->_batch->setUniformBuffer(lightBufferUnit, globalLight->getSchemaBuffer()); -} - -static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& pipeline, LightLocationsPtr& locations) { - auto VS = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(vertSource))); - auto PS = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(fragSource))); - - gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(VS, PS)); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("diffuseMap"), 0)); - slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), 1)); - slotBindings.insert(gpu::Shader::Binding(std::string("specularMap"), 2)); - slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), 3)); - slotBindings.insert(gpu::Shader::Binding(std::string("shadowMap"), 4)); - slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), 5)); - const int LIGHT_GPU_SLOT = 3; - slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); - const int ATMOSPHERE_GPU_SLOT = 4; - slotBindings.insert(gpu::Shader::Binding(std::string("atmosphereBufferUnit"), ATMOSPHERE_GPU_SLOT)); - - slotBindings.insert(gpu::Shader::Binding(std::string("deferredTransformBuffer"), DeferredLightingEffect::DEFERRED_TRANSFORM_BUFFER_SLOT)); - - gpu::Shader::makeProgram(*program, slotBindings); - - locations->shadowDistances = program->getUniforms().findLocation("shadowDistances"); - locations->shadowScale = program->getUniforms().findLocation("shadowScale"); - - locations->radius = program->getUniforms().findLocation("radius"); - locations->ambientSphere = program->getUniforms().findLocation("ambientSphere.L00"); - - locations->texcoordMat = program->getUniforms().findLocation("texcoordMat"); - locations->coneParam = program->getUniforms().findLocation("coneParam"); - - locations->lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); - locations->atmosphereBufferUnit = program->getBuffers().findLocation("atmosphereBufferUnit"); - locations->deferredTransformBuffer = program->getBuffers().findLocation("deferredTransformBuffer"); - - auto state = std::make_shared(); - if (lightVolume) { - state->setCullMode(gpu::State::CULL_BACK); - - // No need for z test since the depth buffer is not bound state->setDepthTest(true, false, gpu::LESS_EQUAL); - // TODO: We should bind the true depth buffer both as RT and texture for the depth test - // TODO: We should use DepthClamp and avoid changing geometry for inside /outside cases - state->setDepthClampEnable(true); - - // additive blending - state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - } else { - state->setCullMode(gpu::State::CULL_BACK); - } - pipeline.reset(gpu::Pipeline::create(program, state)); - -} - -void DeferredLightingEffect::setAmbientLightMode(int preset) { - if ((preset >= 0) && (preset < gpu::SphericalHarmonics::NUM_PRESET)) { - _ambientLightMode = preset; - auto light = _allocatedLights.front(); - light->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset(preset % gpu::SphericalHarmonics::NUM_PRESET)); - } else { - // force to preset 0 - setAmbientLightMode(0); - } -} - -void DeferredLightingEffect::setGlobalLight(const glm::vec3& direction, const glm::vec3& diffuse, float intensity, float ambientIntensity) { - auto light = _allocatedLights.front(); - light->setDirection(direction); - light->setColor(diffuse); - light->setIntensity(intensity); - light->setAmbientIntensity(ambientIntensity); -} - -void DeferredLightingEffect::setGlobalSkybox(const model::SkyboxPointer& skybox) { - _skybox = skybox; -} - -model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { - if (!_spotLightMesh) { - _spotLightMesh = std::make_shared(); - - int slices = 32; - int rings = 3; - int vertices = 2 + rings * slices; - int originVertex = vertices - 2; - int capVertex = vertices - 1; - int verticesSize = vertices * 3 * sizeof(float); - int indices = 3 * slices * (1 + 1 + 2 * (rings -1)); - int ringFloatOffset = slices * 3; - - - float* vertexData = new float[verticesSize]; - float* vertexRing0 = vertexData; - float* vertexRing1 = vertexRing0 + ringFloatOffset; - float* vertexRing2 = vertexRing1 + ringFloatOffset; - - for (int i = 0; i < slices; i++) { - float theta = TWO_PI * i / slices; - auto cosin = glm::vec2(cosf(theta), sinf(theta)); - - *(vertexRing0++) = cosin.x; - *(vertexRing0++) = cosin.y; - *(vertexRing0++) = 0.0f; - - *(vertexRing1++) = cosin.x; - *(vertexRing1++) = cosin.y; - *(vertexRing1++) = 0.33f; - - *(vertexRing2++) = cosin.x; - *(vertexRing2++) = cosin.y; - *(vertexRing2++) = 0.66f; - } - - *(vertexRing2++) = 0.0f; - *(vertexRing2++) = 0.0f; - *(vertexRing2++) = -1.0f; - - *(vertexRing2++) = 0.0f; - *(vertexRing2++) = 0.0f; - *(vertexRing2++) = 1.0f; - - _spotLightMesh->setVertexBuffer(gpu::BufferView(new gpu::Buffer(verticesSize, (gpu::Byte*) vertexData), gpu::Element::VEC3F_XYZ)); - delete[] vertexData; - - gpu::uint16* indexData = new gpu::uint16[indices]; - gpu::uint16* index = indexData; - for (int i = 0; i < slices; i++) { - *(index++) = originVertex; - - int s0 = i; - int s1 = ((i + 1) % slices); - *(index++) = s0; - *(index++) = s1; - - int s2 = s0 + slices; - int s3 = s1 + slices; - *(index++) = s1; - *(index++) = s0; - *(index++) = s2; - - *(index++) = s1; - *(index++) = s2; - *(index++) = s3; - - int s4 = s2 + slices; - int s5 = s3 + slices; - *(index++) = s3; - *(index++) = s2; - *(index++) = s4; - - *(index++) = s3; - *(index++) = s4; - *(index++) = s5; - - - *(index++) = s5; - *(index++) = s4; - *(index++) = capVertex; - } - - _spotLightMesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(sizeof(unsigned short) * indices, (gpu::Byte*) indexData), gpu::Element::INDEX_UINT16)); - delete[] indexData; - - model::Mesh::Part part(0, indices, 0, model::Mesh::TRIANGLES); - //DEBUG: model::Mesh::Part part(0, indices, 0, model::Mesh::LINE_STRIP); - - _spotLightMesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(sizeof(part), (gpu::Byte*) &part), gpu::Element::PART_DRAWCALL)); - - _spotLightMesh->makeBufferStream(); - } - return _spotLightMesh; -} - + viewTransforms[i].evalFromRawMatrix(sideViewMat); + deferredTransforms[i].viewInverse = sideViewMat; + + deferredTransforms[i].stereoSide = (i == 0 ? -1.0f : 1.0f); + + clipQuad[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); + screenBottomLeftCorners[i] = glm::vec2(-1.0f + i * 1.0f, -1.0f); + screenTopRightCorners[i] = glm::vec2(i * 1.0f, 1.0f); + + fetchTexcoordRects[i] = glm::vec4(sMin + i * halfWidth, tMin, halfWidth, tHeight); + } + } else { + + viewports[0] = monoViewport; + projMats[0] = monoProjMat; + + deferredTransforms[0].projection = monoProjMat; + + deferredTransforms[0].viewInverse = monoViewMat; + viewTransforms[0] = monoViewTransform; + + deferredTransforms[0].stereoSide = 0.0f; + + clipQuad[0] = glm::vec4(sMin, tMin, sWidth, tHeight); + screenBottomLeftCorners[0] = glm::vec2(-1.0f, -1.0f); + screenTopRightCorners[0] = glm::vec2(1.0f, 1.0f); + + fetchTexcoordRects[0] = glm::vec4(sMin, tMin, sWidth, tHeight); + } + + auto eyePoint = viewFrustum->getPosition(); + float nearRadius = glm::distance(eyePoint, viewFrustum->getNearTopLeft()); + + + for (int side = 0; side < numPasses; side++) { + // Render in this side's viewport + batch.setViewportTransform(viewports[side]); + batch.setStateScissorRect(viewports[side]); + + // Sync and Bind the correct DeferredTransform ubo + _deferredTransformBuffer[side]._buffer->setSubData(0, sizeof(DeferredTransform), (const gpu::Byte*) &deferredTransforms[side]); + batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, _deferredTransformBuffer[side]); + + glm::vec2 topLeft(-1.0f, -1.0f); + glm::vec2 bottomRight(1.0f, 1.0f); + glm::vec2 texCoordTopLeft(clipQuad[side].x, clipQuad[side].y); + glm::vec2 texCoordBottomRight(clipQuad[side].x + clipQuad[side].z, clipQuad[side].y + clipQuad[side].w); + + // First Global directional light and ambient pass + { + bool useSkyboxCubemap = (_skybox) && (_skybox->getCubemap()); + + auto& program = _directionalLight; + LightLocationsPtr locations = _directionalLightLocations; + + // TODO: At some point bring back the shadows... + // Setup the global directional pass pipeline + { + if (useSkyboxCubemap) { + program = _directionalSkyboxLight; + locations = _directionalSkyboxLightLocations; + } else if (_ambientLightMode > -1) { + program = _directionalAmbientSphereLight; + locations = _directionalAmbientSphereLightLocations; + } + batch.setPipeline(program); + } + + { // Setup the global lighting + auto globalLight = _allocatedLights[_globalLights.front()]; + + if (locations->ambientSphere >= 0) { + gpu::SphericalHarmonics sh = globalLight->getAmbientSphere(); + if (useSkyboxCubemap && _skybox->getCubemap()->getIrradiance()) { + sh = (*_skybox->getCubemap()->getIrradiance()); + } + for (int i =0; i ambientSphere + i, 1, (const float*) (&sh) + i * 4); + } + } + + if (useSkyboxCubemap) { + batch.setResourceTexture(5, _skybox->getCubemap()); + } + + if (locations->lightBufferUnit >= 0) { + batch.setUniformBuffer(locations->lightBufferUnit, globalLight->getSchemaBuffer()); + } + + if (_atmosphere && (locations->atmosphereBufferUnit >= 0)) { + batch.setUniformBuffer(locations->atmosphereBufferUnit, _atmosphere->getDataBuffer()); + } + } + + { + batch.setModelTransform(Transform()); + batch.setProjectionTransform(glm::mat4()); + batch.setViewTransform(Transform()); + + glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); + geometryCache->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); + } + + if (useSkyboxCubemap) { + batch.setResourceTexture(5, nullptr); + } + } + + auto texcoordMat = glm::mat4(); + /* texcoordMat[0] = glm::vec4(sWidth / 2.0f, 0.0f, 0.0f, sMin + sWidth / 2.0f); + texcoordMat[1] = glm::vec4(0.0f, tHeight / 2.0f, 0.0f, tMin + tHeight / 2.0f); + */ texcoordMat[0] = glm::vec4(fetchTexcoordRects[side].z / 2.0f, 0.0f, 0.0f, fetchTexcoordRects[side].x + fetchTexcoordRects[side].z / 2.0f); + texcoordMat[1] = glm::vec4(0.0f, fetchTexcoordRects[side].w / 2.0f, 0.0f, fetchTexcoordRects[side].y + fetchTexcoordRects[side].w / 2.0f); + texcoordMat[2] = glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); + texcoordMat[3] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); + + // enlarge the scales slightly to account for tesselation + const float SCALE_EXPANSION = 0.05f; + + + batch.setProjectionTransform(projMats[side]); + batch.setViewTransform(viewTransforms[side]); + + // Splat Point lights + if (!_pointLights.empty()) { + batch.setPipeline(_pointLight); + + batch._glUniformMatrix4fv(_pointLightLocations->texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); + + for (auto lightID : _pointLights) { + auto& light = _allocatedLights[lightID]; + // IN DEBUG: light->setShowContour(true); + if (_pointLightLocations->lightBufferUnit >= 0) { + batch.setUniformBuffer(_pointLightLocations->lightBufferUnit, light->getSchemaBuffer()); + } + + float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); + // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, + // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... + if (glm::distance(eyePoint, glm::vec3(light->getPosition())) < expandedRadius + nearRadius) { + Transform model; + model.setTranslation(glm::vec3(0.0f, 0.0f, -1.0f)); + batch.setModelTransform(model); + batch.setViewTransform(Transform()); + batch.setProjectionTransform(glm::mat4()); + + glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); + DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); + + batch.setProjectionTransform(projMats[side]); + batch.setViewTransform(viewTransforms[side]); + } else { + Transform model; + model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z)); + batch.setModelTransform(model); + geometryCache->renderSphere(batch, expandedRadius, 32, 32, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); + } + } + } + + // Splat spot lights + if (!_spotLights.empty()) { + batch.setPipeline(_spotLight); + + batch._glUniformMatrix4fv(_spotLightLocations->texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); + + for (auto lightID : _spotLights) { + auto light = _allocatedLights[lightID]; + // IN DEBUG: light->setShowContour(true); + + batch.setUniformBuffer(_spotLightLocations->lightBufferUnit, light->getSchemaBuffer()); + + auto eyeLightPos = eyePoint - light->getPosition(); + auto eyeHalfPlaneDistance = glm::dot(eyeLightPos, light->getDirection()); + + const float TANGENT_LENGTH_SCALE = 0.666f; + glm::vec4 coneParam(light->getSpotAngleCosSin(), TANGENT_LENGTH_SCALE * tanf(0.5f * light->getSpotAngle()), 1.0f); + + float expandedRadius = light->getMaximumRadius() * (1.0f + SCALE_EXPANSION); + // TODO: We shouldn;t have to do that test and use a different volume geometry for when inside the vlight volume, + // we should be able to draw thre same geometry use DepthClamp but for unknown reason it's s not working... + if ((eyeHalfPlaneDistance > -nearRadius) && + (glm::distance(eyePoint, glm::vec3(light->getPosition())) < expandedRadius + nearRadius)) { + coneParam.w = 0.0f; + batch._glUniform4fv(_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); + + Transform model; + model.setTranslation(glm::vec3(0.0f, 0.0f, -1.0f)); + batch.setModelTransform(model); + batch.setViewTransform(Transform()); + batch.setProjectionTransform(glm::mat4()); + + glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); + DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); + + batch.setProjectionTransform( projMats[side]); + batch.setViewTransform(viewTransforms[side]); + } else { + coneParam.w = 1.0f; + batch._glUniform4fv(_spotLightLocations->coneParam, 1, reinterpret_cast< const float* >(&coneParam)); + + Transform model; + model.setTranslation(light->getPosition()); + model.postRotate(light->getOrientation()); + model.postScale(glm::vec3(expandedRadius, expandedRadius, expandedRadius)); + + batch.setModelTransform(model); + auto mesh = getSpotLightMesh(); + + batch.setIndexBuffer(mesh->getIndexBuffer()); + batch.setInputBuffer(0, mesh->getVertexBuffer()); + batch.setInputFormat(mesh->getVertexFormat()); + + auto& part = mesh->getPartBuffer().get(); + + batch.drawIndexed(model::Mesh::topologyToPrimitive(part._topology), part._numIndices, part._startIndex); + } + } + } + } + + // Probably not necessary in the long run because the gpu layer would unbound this texture if used as render target + batch.setResourceTexture(0, nullptr); + batch.setResourceTexture(1, nullptr); + batch.setResourceTexture(2, nullptr); + batch.setResourceTexture(3, nullptr); + batch.setUniformBuffer(_directionalLightLocations->deferredTransformBuffer, nullptr); + + args->_context->render(batch); + + // End of the Lighting pass + if (!_pointLights.empty()) { + _pointLights.clear(); + } + if (!_spotLights.empty()) { + _spotLights.clear(); + } +} + + +void DeferredLightingEffect::copyBack(RenderArgs* args) { + gpu::Batch batch; + batch.enableStereo(false); + auto framebufferCache = DependencyManager::get(); + QSize framebufferSize = framebufferCache->getFrameBufferSize(); + + // TODO why doesn't this blit work? It only seems to affect a small area below the rear view mirror. + // auto destFbo = framebufferCache->getPrimaryFramebuffer(); + auto destFbo = framebufferCache->getPrimaryFramebufferDepthColor(); +// gpu::Vec4i vp = args->_viewport; +// batch.blit(_copyFBO, vp, framebufferCache->getPrimaryFramebuffer(), vp); + batch.setFramebuffer(destFbo); + batch.setViewportTransform(args->_viewport); + batch.setProjectionTransform(glm::mat4()); + batch.setViewTransform(Transform()); + { + float sMin = args->_viewport.x / (float)framebufferSize.width(); + float sWidth = args->_viewport.z / (float)framebufferSize.width(); + float tMin = args->_viewport.y / (float)framebufferSize.height(); + float tHeight = args->_viewport.w / (float)framebufferSize.height(); + Transform model; + batch.setPipeline(_blitLightBuffer); + model.setTranslation(glm::vec3(sMin, tMin, 0.0)); + model.setScale(glm::vec3(sWidth, tHeight, 1.0)); + batch.setModelTransform(model); + } + + batch.setResourceTexture(0, _copyFBO->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + + args->_context->render(batch); + framebufferCache->releaseFramebuffer(_copyFBO); +} + +void DeferredLightingEffect::setupTransparent(RenderArgs* args, int lightBufferUnit) { + auto globalLight = _allocatedLights[_globalLights.front()]; + args->_batch->setUniformBuffer(lightBufferUnit, globalLight->getSchemaBuffer()); +} + +static void loadLightProgram(const char* vertSource, const char* fragSource, bool lightVolume, gpu::PipelinePointer& pipeline, LightLocationsPtr& locations) { + auto VS = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(vertSource))); + auto PS = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(fragSource))); + + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(VS, PS)); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("diffuseMap"), 0)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), 1)); + slotBindings.insert(gpu::Shader::Binding(std::string("specularMap"), 2)); + slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), 3)); + slotBindings.insert(gpu::Shader::Binding(std::string("shadowMap"), 4)); + slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), 5)); + const int LIGHT_GPU_SLOT = 3; + slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); + const int ATMOSPHERE_GPU_SLOT = 4; + slotBindings.insert(gpu::Shader::Binding(std::string("atmosphereBufferUnit"), ATMOSPHERE_GPU_SLOT)); + + slotBindings.insert(gpu::Shader::Binding(std::string("deferredTransformBuffer"), DeferredLightingEffect::DEFERRED_TRANSFORM_BUFFER_SLOT)); + + gpu::Shader::makeProgram(*program, slotBindings); + + locations->shadowDistances = program->getUniforms().findLocation("shadowDistances"); + locations->shadowScale = program->getUniforms().findLocation("shadowScale"); + + locations->radius = program->getUniforms().findLocation("radius"); + locations->ambientSphere = program->getUniforms().findLocation("ambientSphere.L00"); + + locations->texcoordMat = program->getUniforms().findLocation("texcoordMat"); + locations->coneParam = program->getUniforms().findLocation("coneParam"); + + locations->lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); + locations->atmosphereBufferUnit = program->getBuffers().findLocation("atmosphereBufferUnit"); + locations->deferredTransformBuffer = program->getBuffers().findLocation("deferredTransformBuffer"); + + auto state = std::make_shared(); + if (lightVolume) { + state->setCullMode(gpu::State::CULL_BACK); + + // No need for z test since the depth buffer is not bound state->setDepthTest(true, false, gpu::LESS_EQUAL); + // TODO: We should bind the true depth buffer both as RT and texture for the depth test + // TODO: We should use DepthClamp and avoid changing geometry for inside /outside cases + state->setDepthClampEnable(true); + + // additive blending + state->setBlendFunction(true, gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + } else { + state->setCullMode(gpu::State::CULL_BACK); + } + pipeline.reset(gpu::Pipeline::create(program, state)); + +} + +void DeferredLightingEffect::setAmbientLightMode(int preset) { + if ((preset >= 0) && (preset < gpu::SphericalHarmonics::NUM_PRESET)) { + _ambientLightMode = preset; + auto light = _allocatedLights.front(); + light->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset(preset % gpu::SphericalHarmonics::NUM_PRESET)); + } else { + // force to preset 0 + setAmbientLightMode(0); + } +} + +void DeferredLightingEffect::setGlobalLight(const glm::vec3& direction, const glm::vec3& diffuse, float intensity, float ambientIntensity) { + auto light = _allocatedLights.front(); + light->setDirection(direction); + light->setColor(diffuse); + light->setIntensity(intensity); + light->setAmbientIntensity(ambientIntensity); +} + +void DeferredLightingEffect::setGlobalSkybox(const model::SkyboxPointer& skybox) { + _skybox = skybox; +} + +model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { + if (!_spotLightMesh) { + _spotLightMesh = std::make_shared(); + + int slices = 32; + int rings = 3; + int vertices = 2 + rings * slices; + int originVertex = vertices - 2; + int capVertex = vertices - 1; + int verticesSize = vertices * 3 * sizeof(float); + int indices = 3 * slices * (1 + 1 + 2 * (rings -1)); + int ringFloatOffset = slices * 3; + + + float* vertexData = new float[verticesSize]; + float* vertexRing0 = vertexData; + float* vertexRing1 = vertexRing0 + ringFloatOffset; + float* vertexRing2 = vertexRing1 + ringFloatOffset; + + for (int i = 0; i < slices; i++) { + float theta = TWO_PI * i / slices; + auto cosin = glm::vec2(cosf(theta), sinf(theta)); + + *(vertexRing0++) = cosin.x; + *(vertexRing0++) = cosin.y; + *(vertexRing0++) = 0.0f; + + *(vertexRing1++) = cosin.x; + *(vertexRing1++) = cosin.y; + *(vertexRing1++) = 0.33f; + + *(vertexRing2++) = cosin.x; + *(vertexRing2++) = cosin.y; + *(vertexRing2++) = 0.66f; + } + + *(vertexRing2++) = 0.0f; + *(vertexRing2++) = 0.0f; + *(vertexRing2++) = -1.0f; + + *(vertexRing2++) = 0.0f; + *(vertexRing2++) = 0.0f; + *(vertexRing2++) = 1.0f; + + _spotLightMesh->setVertexBuffer(gpu::BufferView(new gpu::Buffer(verticesSize, (gpu::Byte*) vertexData), gpu::Element::VEC3F_XYZ)); + delete[] vertexData; + + gpu::uint16* indexData = new gpu::uint16[indices]; + gpu::uint16* index = indexData; + for (int i = 0; i < slices; i++) { + *(index++) = originVertex; + + int s0 = i; + int s1 = ((i + 1) % slices); + *(index++) = s0; + *(index++) = s1; + + int s2 = s0 + slices; + int s3 = s1 + slices; + *(index++) = s1; + *(index++) = s0; + *(index++) = s2; + + *(index++) = s1; + *(index++) = s2; + *(index++) = s3; + + int s4 = s2 + slices; + int s5 = s3 + slices; + *(index++) = s3; + *(index++) = s2; + *(index++) = s4; + + *(index++) = s3; + *(index++) = s4; + *(index++) = s5; + + + *(index++) = s5; + *(index++) = s4; + *(index++) = capVertex; + } + + _spotLightMesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(sizeof(unsigned short) * indices, (gpu::Byte*) indexData), gpu::Element::INDEX_UINT16)); + delete[] indexData; + + model::Mesh::Part part(0, indices, 0, model::Mesh::TRIANGLES); + //DEBUG: model::Mesh::Part part(0, indices, 0, model::Mesh::LINE_STRIP); + + _spotLightMesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(sizeof(part), (gpu::Byte*) &part), gpu::Element::PART_DRAWCALL)); + + _spotLightMesh->makeBufferStream(); + } + return _spotLightMesh; +} + diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index ea6f2f0ce0..83bb4c215f 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -37,15 +37,22 @@ public: void init(AbstractViewStateInterface* viewState); /// Sets up the state necessary to render static untextured geometry with the simple program. - void bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool culled = true, + gpu::PipelinePointer bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool culled = true, bool emmisive = false, bool depthBias = false); + /// Sets up the state necessary to render static untextured geometry with the simple program. + void bindInstanceProgram(gpu::Batch& batch, bool textured = false, bool culled = true, + bool emmisive = false, bool depthBias = false); + //// Renders a solid sphere with the simple program. void renderSolidSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color); //// Renders a wireframe sphere with the simple program. void renderWireSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color); + //// Renders a solid cube using instancing. Transform should include scaling. + void renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color); + //// Renders a solid cube with the simple program. void renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index a58df10cc6..ea05df84ef 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -689,28 +689,31 @@ void GeometryCache::renderVertices(gpu::Batch& batch, gpu::Primitive primitiveTy } } -void GeometryCache::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) { - Vec2Pair colorKey(glm::vec2(color.x, color.y), glm::vec2(color.z, color.y)); - const int FLOATS_PER_VERTEX = 3; - const int VERTICES_PER_FACE = 4; - const int NUMBER_OF_FACES = 6; - const int TRIANGLES_PER_FACE = 2; - const int VERTICES_PER_TRIANGLE = 3; - const int vertices = NUMBER_OF_FACES * VERTICES_PER_FACE; - const int indices = NUMBER_OF_FACES * TRIANGLES_PER_FACE * VERTICES_PER_TRIANGLE; - const int vertexPoints = vertices * FLOATS_PER_VERTEX; - const int VERTEX_STRIDE = sizeof(GLfloat) * FLOATS_PER_VERTEX * 2; // vertices and normals - const int NORMALS_OFFSET = sizeof(GLfloat) * FLOATS_PER_VERTEX; +static const int FLOATS_PER_VERTEX = 3; +static const int VERTICES_PER_TRIANGLE = 3; +static const int CUBE_NUMBER_OF_FACES = 6; +static const int CUBE_VERTICES_PER_FACE = 4; +static const int CUBE_TRIANGLES_PER_FACE = 2; +static const int CUBE_VERTICES = CUBE_NUMBER_OF_FACES * CUBE_VERTICES_PER_FACE; +static const int CUBE_VERTEX_POINTS = CUBE_VERTICES * FLOATS_PER_VERTEX; +static const int CUBE_INDICES = CUBE_NUMBER_OF_FACES * CUBE_TRIANGLES_PER_FACE * VERTICES_PER_TRIANGLE; + +static const gpu::Element CUBE_POSITION_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element CUBE_NORMAL_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element CUBE_COLOR_ELEMENT{ gpu::VEC4, gpu::NUINT8, gpu::RGBA }; +static const gpu::Element INSTANCE_XFM_ELEMENT{ gpu::MAT4, gpu::FLOAT, gpu::XYZW }; + +gpu::BufferPointer GeometryCache::getCubeVertices(float size) { if (!_solidCubeVertices.contains(size)) { auto verticesBuffer = std::make_shared(); _solidCubeVertices[size] = verticesBuffer; - GLfloat* vertexData = new GLfloat[vertexPoints * 2]; // vertices and normals + GLfloat* vertexData = new GLfloat[CUBE_VERTEX_POINTS * 2]; // vertices and normals GLfloat* vertex = vertexData; float halfSize = size / 2.0f; - static GLfloat cannonicalVertices[vertexPoints] = + static GLfloat cannonicalVertices[CUBE_VERTEX_POINTS] = { 1, 1, 1, -1, 1, 1, -1,-1, 1, 1,-1, 1, // v0,v1,v2,v3 (front) 1, 1, 1, 1,-1, 1, 1,-1,-1, 1, 1,-1, // v0,v3,v4,v5 (right) 1, 1, 1, 1, 1,-1, -1, 1,-1, -1, 1, 1, // v0,v5,v6,v1 (top) @@ -719,7 +722,7 @@ void GeometryCache::renderSolidCube(gpu::Batch& batch, float size, const glm::ve 1,-1,-1, -1,-1,-1, -1, 1,-1, 1, 1,-1 }; // v4,v7,v6,v5 (back) // normal array - static GLfloat cannonicalNormals[vertexPoints] = + static GLfloat cannonicalNormals[CUBE_VERTEX_POINTS] = { 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // v0,v1,v2,v3 (front) 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0,v3,v4,v5 (right) 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // v0,v5,v6,v1 (top) @@ -731,7 +734,7 @@ void GeometryCache::renderSolidCube(gpu::Batch& batch, float size, const glm::ve GLfloat* cannonicalVertex = &cannonicalVertices[0]; GLfloat* cannonicalNormal = &cannonicalNormals[0]; - for (int i = 0; i < vertices; i++) { + for (int i = 0; i < CUBE_VERTICES; i++) { // vertices *(vertex++) = halfSize * *cannonicalVertex++; *(vertex++) = halfSize * *cannonicalVertex++; @@ -742,90 +745,121 @@ void GeometryCache::renderSolidCube(gpu::Batch& batch, float size, const glm::ve *(vertex++) = *cannonicalNormal++; *(vertex++) = *cannonicalNormal++; } - - verticesBuffer->append(sizeof(GLfloat) * vertexPoints * 2, (gpu::Byte*) vertexData); + verticesBuffer->append(sizeof(GLfloat) * CUBE_VERTEX_POINTS * 2, (gpu::Byte*) vertexData); } + return _solidCubeVertices[size]; +} + +gpu::BufferPointer GeometryCache::getSolidCubeIndices() { if (!_solidCubeIndexBuffer) { - static GLubyte cannonicalIndices[indices] = - { 0, 1, 2, 2, 3, 0, // front + static GLubyte cannonicalIndices[CUBE_INDICES] = { 0, 1, 2, 2, 3, 0, // front 4, 5, 6, 6, 7, 4, // right 8, 9,10, 10,11, 8, // top 12,13,14, 14,15,12, // left 16,17,18, 18,19,16, // bottom 20,21,22, 22,23,20 }; // back - + auto indexBuffer = std::make_shared(); _solidCubeIndexBuffer = indexBuffer; - + _solidCubeIndexBuffer->append(sizeof(cannonicalIndices), (gpu::Byte*) cannonicalIndices); } + return _solidCubeIndexBuffer; +} + +void GeometryCache::setupCubeVertices(gpu::Batch& batch, gpu::BufferPointer& verticesBuffer) { + static const int VERTEX_STRIDE = sizeof(GLfloat) * FLOATS_PER_VERTEX * 2; // vertices and normals + static const int NORMALS_OFFSET = sizeof(GLfloat) * FLOATS_PER_VERTEX; + + gpu::BufferView verticesView(verticesBuffer, 0, verticesBuffer->getSize(), VERTEX_STRIDE, CUBE_POSITION_ELEMENT); + gpu::BufferView normalsView(verticesBuffer, NORMALS_OFFSET, verticesBuffer->getSize(), VERTEX_STRIDE, CUBE_NORMAL_ELEMENT); + batch.setInputBuffer(gpu::Stream::POSITION, verticesView); + batch.setInputBuffer(gpu::Stream::NORMAL, normalsView); +} + +void GeometryCache::renderSolidCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) { + static gpu::Stream::FormatPointer streamFormat; + if (!streamFormat) { + streamFormat = std::make_shared(); // 1 for everyone + streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, CUBE_POSITION_ELEMENT, 0); + streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, CUBE_NORMAL_ELEMENT); + streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, CUBE_COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + streamFormat->setAttribute(gpu::Stream::INSTANCE_XFM, gpu::Stream::INSTANCE_XFM, INSTANCE_XFM_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + } + batch.setInputFormat(streamFormat); + + gpu::BufferView colorView(colorBuffer, CUBE_COLOR_ELEMENT); + batch.setInputBuffer(gpu::Stream::COLOR, colorView); + + gpu::BufferView instanceXfmView(transformBuffer, 0, transformBuffer->getSize(), INSTANCE_XFM_ELEMENT); + batch.setInputBuffer(gpu::Stream::INSTANCE_XFM, instanceXfmView); + + gpu::BufferPointer verticesBuffer = getCubeVertices(1.0); + setupCubeVertices(batch, verticesBuffer); + batch.setIndexBuffer(gpu::UINT8, getSolidCubeIndices(), 0); + batch.drawIndexedInstanced(count, gpu::TRIANGLES, CUBE_INDICES); +} + + +void GeometryCache::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) { + Vec2Pair colorKey(glm::vec2(color.x, color.y), glm::vec2(color.z, color.y)); if (!_solidCubeColors.contains(colorKey)) { auto colorBuffer = std::make_shared(); _solidCubeColors[colorKey] = colorBuffer; - const int NUM_COLOR_SCALARS_PER_CUBE = 24; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | ((int(color.y * 255.0f) & 0xFF) << 8) | ((int(color.z * 255.0f) & 0xFF) << 16) | ((int(color.w * 255.0f) & 0xFF) << 24); - int colors[NUM_COLOR_SCALARS_PER_CUBE] = { compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor }; - + int colors[CUBE_VERTICES] = { + compactColor, compactColor, compactColor, compactColor, + compactColor, compactColor, compactColor, compactColor, + compactColor, compactColor, compactColor, compactColor, + compactColor, compactColor, compactColor, compactColor, + compactColor, compactColor, compactColor, compactColor, + compactColor, compactColor, compactColor, compactColor + }; colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); } - gpu::BufferPointer verticesBuffer = _solidCubeVertices[size]; gpu::BufferPointer colorBuffer = _solidCubeColors[colorKey]; - const int VERTICES_SLOT = 0; - const int NORMALS_SLOT = 1; - const int COLOR_SLOT = 2; static gpu::Stream::FormatPointer streamFormat; - static gpu::Element positionElement, normalElement, colorElement; if (!streamFormat) { streamFormat = std::make_shared(); // 1 for everyone - streamFormat->setAttribute(gpu::Stream::POSITION, VERTICES_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - streamFormat->setAttribute(gpu::Stream::NORMAL, NORMALS_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - streamFormat->setAttribute(gpu::Stream::COLOR, COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element; - normalElement = streamFormat->getAttributes().at(gpu::Stream::NORMAL)._element; - colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element; + streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, CUBE_POSITION_ELEMENT, 0); + streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, CUBE_NORMAL_ELEMENT); + streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, CUBE_COLOR_ELEMENT); } - - - gpu::BufferView verticesView(verticesBuffer, 0, verticesBuffer->getSize(), VERTEX_STRIDE, positionElement); - gpu::BufferView normalsView(verticesBuffer, NORMALS_OFFSET, verticesBuffer->getSize(), VERTEX_STRIDE, normalElement); - gpu::BufferView colorView(colorBuffer, streamFormat->getAttributes().at(gpu::Stream::COLOR)._element); - batch.setInputFormat(streamFormat); - batch.setInputBuffer(VERTICES_SLOT, verticesView); - batch.setInputBuffer(NORMALS_SLOT, normalsView); - batch.setInputBuffer(COLOR_SLOT, colorView); - batch.setIndexBuffer(gpu::UINT8, _solidCubeIndexBuffer, 0); - batch.drawIndexed(gpu::TRIANGLES, indices); + + gpu::BufferView colorView(colorBuffer, CUBE_COLOR_ELEMENT); + batch.setInputBuffer(gpu::Stream::COLOR, colorView); + + gpu::BufferPointer verticesBuffer = getCubeVertices(size); + setupCubeVertices(batch, verticesBuffer); + + batch.setIndexBuffer(gpu::UINT8, getSolidCubeIndices(), 0); + batch.drawIndexed(gpu::TRIANGLES, CUBE_INDICES); } + void GeometryCache::renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color) { Vec2Pair colorKey(glm::vec2(color.x, color.y),glm::vec2(color.z, color.y)); - const int FLOATS_PER_VERTEX = 3; - const int VERTICES_PER_EDGE = 2; - const int TOP_EDGES = 4; - const int BOTTOM_EDGES = 4; - const int SIDE_EDGES = 4; - const int vertices = 8; - const int indices = (TOP_EDGES + BOTTOM_EDGES + SIDE_EDGES) * VERTICES_PER_EDGE; + static const int WIRE_CUBE_VERTICES_PER_EDGE = 2; + static const int WIRE_CUBE_TOP_EDGES = 4; + static const int WIRE_CUBE_BOTTOM_EDGES = 4; + static const int WIRE_CUBE_SIDE_EDGES = 4; + static const int WIRE_CUBE_VERTICES = 8; + static const int WIRE_CUBE_INDICES = (WIRE_CUBE_TOP_EDGES + WIRE_CUBE_BOTTOM_EDGES + WIRE_CUBE_SIDE_EDGES) * WIRE_CUBE_VERTICES_PER_EDGE; if (!_cubeVerticies.contains(size)) { auto verticesBuffer = std::make_shared(); _cubeVerticies[size] = verticesBuffer; - int vertexPoints = vertices * FLOATS_PER_VERTEX; - GLfloat* vertexData = new GLfloat[vertexPoints]; // only vertices, no normals because we're a wire cube + static const int WIRE_CUBE_VERTEX_POINTS = WIRE_CUBE_VERTICES * FLOATS_PER_VERTEX; + GLfloat* vertexData = new GLfloat[WIRE_CUBE_VERTEX_POINTS]; // only vertices, no normals because we're a wire cube GLfloat* vertex = vertexData; float halfSize = size / 2.0f; @@ -834,15 +868,15 @@ void GeometryCache::renderWireCube(gpu::Batch& batch, float size, const glm::vec 1,-1, 1, 1,-1,-1, -1,-1,-1, -1,-1, 1 // v4, v5, v6, v7 (bottom) }; - for (int i = 0; i < vertexPoints; i++) { + for (int i = 0; i < WIRE_CUBE_VERTEX_POINTS; i++) { vertex[i] = cannonicalVertices[i] * halfSize; } - verticesBuffer->append(sizeof(GLfloat) * vertexPoints, (gpu::Byte*) vertexData); // I'm skeptical that this is right + verticesBuffer->append(sizeof(GLfloat) * WIRE_CUBE_VERTEX_POINTS, (gpu::Byte*) vertexData); // I'm skeptical that this is right } if (!_wireCubeIndexBuffer) { - static GLubyte cannonicalIndices[indices] = { + static GLubyte cannonicalIndices[WIRE_CUBE_INDICES] = { 0, 1, 1, 2, 2, 3, 3, 0, // (top) 4, 5, 5, 6, 6, 7, 7, 4, // (bottom) 0, 4, 1, 5, 2, 6, 3, 7, // (side edges) @@ -890,7 +924,7 @@ void GeometryCache::renderWireCube(gpu::Batch& batch, float size, const glm::vec batch.setInputBuffer(VERTICES_SLOT, verticesView); batch.setInputBuffer(COLOR_SLOT, colorView); batch.setIndexBuffer(gpu::UINT8, _wireCubeIndexBuffer, 0); - batch.drawIndexed(gpu::LINES, indices); + batch.drawIndexed(gpu::LINES, WIRE_CUBE_INDICES); } void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id) { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 3820b58baf..9ba2658a9c 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -131,6 +131,11 @@ public: virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra); + gpu::BufferPointer getCubeVertices(float size); + void setupCubeVertices(gpu::Batch& batch, gpu::BufferPointer& verticesBuffer); + + gpu::BufferPointer getSolidCubeIndices(); + void renderSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec3& color, bool solid = true, int id = UNKNOWN_ID) { renderSphere(batch, radius, slices, stacks, glm::vec4(color, 1.0f), solid, id); } @@ -139,6 +144,7 @@ public: void renderGrid(gpu::Batch& batch, int xDivisions, int yDivisions, const glm::vec4& color); void renderGrid(gpu::Batch& batch, int x, int y, int width, int height, int rows, int cols, const glm::vec4& color, int id = UNKNOWN_ID); + void renderSolidCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer); void renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color); void renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color); void renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id = UNKNOWN_ID); diff --git a/libraries/render-utils/src/simple.slv b/libraries/render-utils/src/simple.slv index b22bb36d83..e7fed4a6b4 100644 --- a/libraries/render-utils/src/simple.slv +++ b/libraries/render-utils/src/simple.slv @@ -18,6 +18,7 @@ <$declareStandardTransform()$> +uniform bool Instanced = false; // the interpolated normal out vec3 _normal; @@ -33,6 +34,11 @@ void main(void) { // standard transform TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + if (Instanced) { + <$transformInstancedModelToClipPos(cam, obj, inPosition, gl_Position)$> + <$transformInstancedModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + } else { + <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> + <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + } } \ No newline at end of file From cfde86a7ea2edee2ee7eab1f8530564226a2396d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 17:07:10 -0700 Subject: [PATCH 114/192] tone down distance throwing --- examples/controllers/handControllerGrab.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 6fff9df0f8..56b710c5dd 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -18,7 +18,7 @@ Script.include("../libraries/utils.js"); // these tune time-averaging and "on" value for analog trigger // -var TRIGGER_SMOOTH_RATIO = 0.0; // 0.0 disables smoothing of trigger value +var TRIGGER_SMOOTH_RATIO = 0.1; // 0.0 disables smoothing of trigger value var TRIGGER_ON_VALUE = 0.2; ///////////////////////////////////////////////////////////////// @@ -274,7 +274,7 @@ function controller(hand, triggerAction) { var deltaPosition = Vec3.subtract(newObjectPosition, this.currentObjectPosition); // meters var now = Date.now(); var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds - this.computeReleaseVelocity(deltaPosition, deltaTime); + this.computeReleaseVelocity(deltaPosition, deltaTime, false); this.currentObjectPosition = newObjectPosition; this.currentObjectTime = now; @@ -352,7 +352,7 @@ function controller(hand, triggerAction) { var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerPosition); // meters var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds - this.computeReleaseVelocity(deltaPosition, deltaTime); + this.computeReleaseVelocity(deltaPosition, deltaTime, true); this.currentHandControllerPosition = handControllerPosition; this.currentObjectTime = now; @@ -360,7 +360,7 @@ function controller(hand, triggerAction) { } - this.computeReleaseVelocity = function(deltaPosition, deltaTime) { + this.computeReleaseVelocity = function(deltaPosition, deltaTime, useMultiplier) { if (deltaTime > 0.0 && !vec3equal(deltaPosition, ZERO_VEC)) { var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime); // don't update grabbedVelocity if the trigger is off. the smoothing of the trigger @@ -371,6 +371,10 @@ function controller(hand, triggerAction) { (1.0 - NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)), Vec3.multiply(grabbedVelocity, NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)); } + + if (useMultiplier) { + this.grabbedVelocity = Vec3.multiply(this.grabbedVelocity, RELEASE_VELOCITY_MULTIPLIER); + } } } @@ -385,9 +389,7 @@ function controller(hand, triggerAction) { // the action will tend to quickly bring an object's velocity to zero. now that // the action is gone, set the objects velocity to something the holder might expect. - Entities.editEntity(this.grabbedEntity, - {velocity: Vec3.multiply(this.grabbedVelocity, RELEASE_VELOCITY_MULTIPLIER)} - ); + Entities.editEntity(this.grabbedEntity, {velocity: this.grabbedVelocity}); this.deactivateEntity(this.grabbedEntity); this.grabbedVelocity = ZERO_VEC; From fd1c8f638906c5dbbed60b87bf6a94806b3a5f9a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Sep 2015 17:39:50 -0700 Subject: [PATCH 115/192] fix releaseGrab in test script --- examples/entityScripts/detectGrabExample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/entityScripts/detectGrabExample.js b/examples/entityScripts/detectGrabExample.js index 7e97572159..3ff5ba8da2 100644 --- a/examples/entityScripts/detectGrabExample.js +++ b/examples/entityScripts/detectGrabExample.js @@ -44,7 +44,7 @@ print("I am still being grabbed... entity:" + this.entityID); }, - release: function () { + releaseGrab: function () { print("I was released... entity:" + this.entityID); }, From 44eb448cbe0f259f52fc161125e73d7f7f9864a3 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 18 Sep 2015 18:05:51 -0700 Subject: [PATCH 116/192] Hooked up isTalking flag to Rig and AnimGraph. --- interface/src/avatar/Head.cpp | 3 ++- interface/src/avatar/Head.h | 2 ++ interface/src/avatar/SkeletonModel.cpp | 2 ++ libraries/animation/src/Rig.cpp | 5 +++++ libraries/animation/src/Rig.h | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 09ac893e65..a514eb4e8d 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -158,10 +158,11 @@ void Head::simulate(float deltaTime, bool isMine, bool billboard) { bool forceBlink = false; const float TALKING_LOUDNESS = 100.0f; const float BLINK_AFTER_TALKING = 0.25f; + _timeWithoutTalking += deltaTime; if ((_averageLoudness - _longTermAverageLoudness) > TALKING_LOUDNESS) { _timeWithoutTalking = 0.0f; - } else if (_timeWithoutTalking < BLINK_AFTER_TALKING && (_timeWithoutTalking += deltaTime) >= BLINK_AFTER_TALKING) { + } else if (_timeWithoutTalking < BLINK_AFTER_TALKING && _timeWithoutTalking >= BLINK_AFTER_TALKING) { forceBlink = true; } diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index 691775b029..1fbfceca92 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -102,6 +102,8 @@ public: void relaxLean(float deltaTime); void addLeanDeltas(float sideways, float forward); + + float getTimeWithoutTalking() const { return _timeWithoutTalking; } private: glm::vec3 calculateAverageEyePosition() const { return _leftEyePosition + (_rightEyePosition - _leftEyePosition ) * 0.5f; } diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 3894a0ade9..27e61175eb 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -146,6 +146,8 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { headParams.leftEyeJointIndex = geometry.leftEyeJointIndex; headParams.rightEyeJointIndex = geometry.rightEyeJointIndex; + headParams.isTalking = head->getTimeWithoutTalking() <= 1.5f; + _rig->updateFromHeadParameters(headParams, deltaTime); Rig::HandParameters handParams; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 025cb5f3d1..1210313bdb 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -44,6 +44,7 @@ void Rig::HeadParameters::dump() const { qCDebug(animation, " neckJointIndex = %.d", neckJointIndex); qCDebug(animation, " leftEyeJointIndex = %.d", leftEyeJointIndex); qCDebug(animation, " rightEyeJointIndex = %.d", rightEyeJointIndex); + qCDebug(animation, " isTalking = %s", isTalking ? "true" : "false"); } void insertSorted(QList& handles, const AnimationHandlePointer& handle) { @@ -951,6 +952,10 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { updateNeckJoint(params.neckJointIndex, params); updateEyeJoints(params.leftEyeJointIndex, params.rightEyeJointIndex, params.modelTranslation, params.modelRotation, params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); + + if (_enableAnimGraph) { + _animVars.set("isTalking", params.isTalking); + } } static const glm::vec3 X_AXIS(1.0f, 0.0f, 0.0f); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 9939f383b7..37e1d51a0a 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -72,6 +72,7 @@ public: int neckJointIndex = -1; int leftEyeJointIndex = -1; int rightEyeJointIndex = -1; + bool isTalking = false; void dump() const; }; From eadf212418cd58329e76d318bdd4fdf63511fee9 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 18 Sep 2015 18:31:53 -0700 Subject: [PATCH 117/192] Updated avatar.json with talking idle animation. --- interface/src/avatar/MyAvatar.cpp | 5 ++- libraries/animation/src/Rig.cpp | 1 + tests/animation/src/data/avatar.json | 53 ++++++++++++++++++++++++---- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 69f7516430..ae483988e3 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1299,10 +1299,13 @@ void MyAvatar::initAnimGraph() { // ik-avatar-hands.json // https://gist.githubusercontent.com/hyperlogic/04a02c47eb56d8bfaebb // + // ik-avatar-hands-idle.json + // https://gist.githubusercontent.com/hyperlogic/d951c78532e7a20557ad + // // or run a local web-server // python -m SimpleHTTPServer& //auto graphUrl = QUrl("http://localhost:8000/avatar.json"); - auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/04a02c47eb56d8bfaebb/raw/72517b231f606b724c5169e02642e401f9af5a54/ik-avatar-hands.json"); + auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/d951c78532e7a20557ad/raw/8275a99a859bbb9b42530c1c7ebfd024e63ba250/ik-avatar-hands-idle.json"); _rig->initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 1210313bdb..b0ffd081c2 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -955,6 +955,7 @@ void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { if (_enableAnimGraph) { _animVars.set("isTalking", params.isTalking); + _animVars.set("notIsTalking", !params.isTalking); } } diff --git a/tests/animation/src/data/avatar.json b/tests/animation/src/data/avatar.json index d1f6166b3d..9c357ac845 100644 --- a/tests/animation/src/data/avatar.json +++ b/tests/animation/src/data/avatar.json @@ -451,15 +451,54 @@ "children": [ { "id": "idle", - "type": "clip", + "type": "stateMachine", "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", - "startFrame": 0.0, - "endFrame": 90.0, - "timeScale": 1.0, - "loopFlag": true + "currentState": "idleStand", + "states": [ + { + "id": "idleStand", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isTalking", "state": "idleTalk" } + ] + }, + { + "id": "idleTalk", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "notIsTalking", "state": "idleStand" } + ] + } + ] }, - "children": [] + "children": [ + { + "id": "idleStand", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "idleTalk", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/talk/talk.fbx", + "startFrame": 0.0, + "endFrame": 801.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] }, { "id": "walkFwd", From 89417415088a6aa3dec042d6fe49ad11d745dbe5 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 18 Sep 2015 18:47:18 -0700 Subject: [PATCH 118/192] Migrating the rendering code to the Material Maps and adding the simplae variables to the the TextutreMap --- interface/src/avatar/Avatar.cpp | 2 +- .../src/gpu-networking/TextureCache.cpp | 93 ++---------- .../src/gpu-networking/TextureCache.h | 30 +--- libraries/gpu/src/gpu/Texture.h | 3 +- libraries/model/src/model/Material.cpp | 2 +- libraries/model/src/model/Material.h | 9 ++ libraries/model/src/model/TextureStorage.cpp | 31 +++- libraries/model/src/model/TextureStorage.h | 22 ++- libraries/render-utils/src/GeometryCache.cpp | 36 +++-- libraries/render-utils/src/GeometryCache.h | 5 - libraries/render-utils/src/Model.cpp | 132 ++++++++++-------- 11 files changed, 157 insertions(+), 208 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 5ac830baf3..e6bcca1f87 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -645,7 +645,7 @@ void Avatar::renderBillboard(RenderArgs* renderArgs) { // Using a unique URL ensures we don't get another avatar's texture from TextureCache QUrl uniqueUrl = QUrl(QUuid::createUuid().toString()); _billboardTexture = DependencyManager::get()->getTexture( - uniqueUrl, DEFAULT_TEXTURE, false, _billboard); + uniqueUrl, DEFAULT_TEXTURE, _billboard); } if (!_billboardTexture || !_billboardTexture->isLoaded()) { return; diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp index 8e268d9f04..12b9ce423d 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp @@ -143,21 +143,9 @@ public: const QByteArray& content; }; -NetworkTexturePointer TextureCache::getTexture(const QUrl& url, TextureType type, bool dilatable, const QByteArray& content) { - if (!dilatable) { - TextureExtra extra = { type, content }; - return ResourceCache::getResource(url, QUrl(), false, &extra).staticCast(); - } - NetworkTexturePointer texture = _dilatableNetworkTextures.value(url); - if (texture.isNull()) { - texture = NetworkTexturePointer(new DilatableNetworkTexture(url, content), &Resource::allReferencesCleared); - texture->setSelf(texture); - texture->setCache(this); - _dilatableNetworkTextures.insert(url, texture); - } else { - removeUnusedResource(texture); - } - return texture; +NetworkTexturePointer TextureCache::getTexture(const QUrl& url, TextureType type, const QByteArray& content) { + TextureExtra extra = { type, content }; + return ResourceCache::getResource(url, QUrl(), false, &extra).staticCast(); } /// Returns a texture version of an image file @@ -567,12 +555,13 @@ void NetworkTexture::setImage(const QImage& image, void* voidTexture, bool trans gpu::Texture* texture = static_cast(voidTexture); // Passing ownership - _gpuTexture.reset(texture); + // _gpuTexture.reset(texture); _textureStorage->resetTexture(texture); + auto gpuTexture = _textureStorage->getGPUTexture(); - if (_gpuTexture) { - _width = _gpuTexture->getWidth(); - _height = _gpuTexture->getHeight(); + if (gpuTexture) { + _width = gpuTexture->getWidth(); + _height = gpuTexture->getHeight(); } else { _width = _height = 0; } @@ -586,69 +575,3 @@ void NetworkTexture::imageLoaded(const QImage& image) { // nothing by default } -DilatableNetworkTexture::DilatableNetworkTexture(const QUrl& url, const QByteArray& content) : - NetworkTexture(url, DEFAULT_TEXTURE, content), - _innerRadius(0), - _outerRadius(0) -{ -} - -QSharedPointer DilatableNetworkTexture::getDilatedTexture(float dilation) { - QSharedPointer texture = _dilatedTextures.value(dilation); - if (texture.isNull()) { - texture = QSharedPointer::create(); - - if (!_image.isNull()) { - QImage dilatedImage = _image; - QPainter painter; - painter.begin(&dilatedImage); - QPainterPath path; - qreal radius = glm::mix((float) _innerRadius, (float) _outerRadius, dilation); - path.addEllipse(QPointF(_image.width() / 2.0, _image.height() / 2.0), radius, radius); - painter.fillPath(path, Qt::black); - painter.end(); - - bool isLinearRGB = true;// (_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); - gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); - gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); - if (dilatedImage.hasAlphaChannel()) { - formatGPU = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::RGBA : gpu::SRGBA)); - // FIXME either remove the ?: operator or provide different arguments depending on linear - formatMip = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::BGRA : gpu::BGRA)); - } - texture->_gpuTexture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, dilatedImage.width(), dilatedImage.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - texture->_gpuTexture->assignStoredMip(0, formatMip, dilatedImage.byteCount(), dilatedImage.constBits()); - texture->_gpuTexture->autoGenerateMips(-1); - - } - - _dilatedTextures.insert(dilation, texture); - } - return texture; -} - -void DilatableNetworkTexture::imageLoaded(const QImage& image) { - _image = image; - - // scan out from the center to find inner and outer radii - int halfWidth = image.width() / 2; - int halfHeight = image.height() / 2; - const int BLACK_THRESHOLD = 32; - while (_innerRadius < halfWidth && qGray(image.pixel(halfWidth + _innerRadius, halfHeight)) < BLACK_THRESHOLD) { - _innerRadius++; - } - _outerRadius = _innerRadius; - const int TRANSPARENT_THRESHOLD = 32; - while (_outerRadius < halfWidth && qAlpha(image.pixel(halfWidth + _outerRadius, halfHeight)) > TRANSPARENT_THRESHOLD) { - _outerRadius++; - } - - // clear out any textures we generated before loading - _dilatedTextures.clear(); -} - -void DilatableNetworkTexture::reinsert() { - static_cast(_cache.data())->_dilatableNetworkTextures.insert(_url, - qWeakPointerCast(_self)); -} - diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.h b/libraries/gpu-networking/src/gpu-networking/TextureCache.h index ad18550541..d17691a484 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.h +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.h @@ -61,7 +61,7 @@ public: static gpu::TexturePointer getImageTexture(const QString& path); /// Loads a texture from the specified URL. - NetworkTexturePointer getTexture(const QUrl& url, TextureType type = DEFAULT_TEXTURE, bool dilatable = false, + NetworkTexturePointer getTexture(const QUrl& url, TextureType type = DEFAULT_TEXTURE, const QByteArray& content = QByteArray()); protected: @@ -88,7 +88,6 @@ private: class Texture { public: friend class TextureCache; - friend class DilatableNetworkTexture; Texture(); ~Texture(); @@ -97,8 +96,6 @@ public: model::TextureStoragePointer _textureStorage; protected: - gpu::TexturePointer _gpuTexture; - private: }; @@ -146,29 +143,4 @@ private: int _height; }; -/// Caches derived, dilated textures. -class DilatableNetworkTexture : public NetworkTexture { - Q_OBJECT - -public: - - DilatableNetworkTexture(const QUrl& url, const QByteArray& content); - - /// Returns a pointer to a texture with the requested amount of dilation. - QSharedPointer getDilatedTexture(float dilation); - -protected: - - virtual void imageLoaded(const QImage& image); - virtual void reinsert(); - -private: - - QImage _image; - int _innerRadius; - int _outerRadius; - - QMap > _dilatedTextures; -}; - #endif // hifi_TextureCache_h diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index d5c3635816..65a0439864 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -356,7 +356,7 @@ public: protected: std::unique_ptr< Storage > _storage; - + Stamp _stamp = 0; Sampler _sampler; @@ -380,7 +380,6 @@ protected: bool _autoGenerateMips = false; bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index a013fcd845..679e22c9e7 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -82,7 +82,7 @@ void Material::setOpacity(float opacity) { } void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textureMap) { - if (textureMap && !textureMap->isNull()) { + if (textureMap) { _key.setMapChannel(channel, (true)); _textureMaps[channel] = textureMap; } else { diff --git a/libraries/model/src/model/Material.h b/libraries/model/src/model/Material.h index 0bb79fa7e1..0e7388f722 100755 --- a/libraries/model/src/model/Material.h +++ b/libraries/model/src/model/Material.h @@ -41,6 +41,7 @@ public: GLOSS_MAP_BIT, TRANSPARENT_MAP_BIT, NORMAL_MAP_BIT, + LIGHTMAP_MAP_BIT, NUM_FLAGS, }; @@ -53,6 +54,7 @@ public: GLOSS_MAP, TRANSPARENT_MAP, NORMAL_MAP, + LIGHTMAP_MAP, NUM_MAP_CHANNELS, }; @@ -83,6 +85,7 @@ public: Builder& withTransparentMap() { _flags.set(TRANSPARENT_MAP_BIT); return (*this); } Builder& withNormalMap() { _flags.set(NORMAL_MAP_BIT); return (*this); } + Builder& withLightmapMap() { _flags.set(LIGHTMAP_MAP_BIT); return (*this); } // Convenient standard keys that we will keep on using all over the place static MaterialKey opaqueDiffuse() { return Builder().withDiffuse().build(); } @@ -122,6 +125,9 @@ public: void setNormalMap(bool value) { _flags.set(NORMAL_MAP_BIT, value); } bool isNormalMap() const { return _flags[NORMAL_MAP_BIT]; } + void setLightmapMap(bool value) { _flags.set(LIGHTMAP_MAP_BIT, value); } + bool isLightmapMap() const { return _flags[LIGHTMAP_MAP_BIT]; } + void setMapChannel(MapChannel channel, bool value) { _flags.set(EMISSIVE_MAP_BIT + channel, value); } bool isMapChannel(MapChannel channel) const { return _flags[EMISSIVE_MAP_BIT + channel]; } @@ -177,6 +183,9 @@ public: Builder& withoutNormalMap() { _value.reset(MaterialKey::NORMAL_MAP_BIT); _mask.set(MaterialKey::NORMAL_MAP_BIT); return (*this); } Builder& withNormalMap() { _value.set(MaterialKey::NORMAL_MAP_BIT); _mask.set(MaterialKey::NORMAL_MAP_BIT); return (*this); } + Builder& withoutLightmapMap() { _value.reset(MaterialKey::LIGHTMAP_MAP_BIT); _mask.set(MaterialKey::LIGHTMAP_MAP_BIT); return (*this); } + Builder& withLightmapMap() { _value.set(MaterialKey::LIGHTMAP_MAP_BIT); _mask.set(MaterialKey::LIGHTMAP_MAP_BIT); return (*this); } + // Convenient standard keys that we will keep on using all over the place static MaterialFilter opaqueDiffuse() { return Builder().withDiffuse().withoutTransparent().build(); } }; diff --git a/libraries/model/src/model/TextureStorage.cpp b/libraries/model/src/model/TextureStorage.cpp index a0c64dda4a..cc098e7356 100755 --- a/libraries/model/src/model/TextureStorage.cpp +++ b/libraries/model/src/model/TextureStorage.cpp @@ -14,9 +14,10 @@ using namespace model; using namespace gpu; // TextureStorage -TextureStorage::TextureStorage() : Texture::Storage()//, - // _gpuTexture(Texture::createFromStorage(this)) -{} +TextureStorage::TextureStorage() +{/* : Texture::Storage()//, + // _gpuTexture(Texture::createFromStorage(this))*/ +} TextureStorage::~TextureStorage() { } @@ -30,16 +31,22 @@ void TextureStorage::resetTexture(gpu::Texture* texture) { _gpuTexture.reset(texture); } - +bool TextureStorage::isDefined() const { + if (_gpuTexture) { + return _gpuTexture->isDefined(); + } else { + return false; + } +} void TextureMap::setTextureStorage(TextureStoragePointer& texStorage) { _textureStorage = texStorage; } -bool TextureMap::isNull() const { +bool TextureMap::isDefined() const { if (_textureStorage) { - return _textureStorage->isMipAvailable(0); + return _textureStorage->isDefined(); } else { return false; } @@ -51,4 +58,14 @@ gpu::TextureView TextureMap::getTextureView() const { } else { return gpu::TextureView(); } -} \ No newline at end of file +} + +void TextureMap::setTextureTransform(const Transform& texcoordTransform) { + _texcoordTransform = texcoordTransform; +} + +void TextureMap::setLightmapOffsetScale(float offset, float scale) { + _lightmapOffsetScale.x = offset; + _lightmapOffsetScale.y = scale; +} + diff --git a/libraries/model/src/model/TextureStorage.h b/libraries/model/src/model/TextureStorage.h index 250217fb0b..c879918ea9 100755 --- a/libraries/model/src/model/TextureStorage.h +++ b/libraries/model/src/model/TextureStorage.h @@ -1,5 +1,5 @@ // -// TextureStorage.h +// TextureMap.h // libraries/model/src/model // // Created by Sam Gateau on 5/6/2015. @@ -14,6 +14,7 @@ #include "gpu/Texture.h" #include "Material.h" +#include "Transform.h" #include @@ -32,7 +33,7 @@ public: // TextureStorage is a specialized version of the gpu::Texture::Storage // It provides the mechanism to create a texture from a Url and the intended usage // that guides the internal format used -class TextureStorage : public gpu::Texture::Storage { +class TextureStorage { public: TextureStorage(); ~TextureStorage(); @@ -40,12 +41,13 @@ public: const QUrl& getUrl() const { return _imageUrl; } gpu::Texture::Type getType() const { return _usage._type; } const gpu::TexturePointer getGPUTexture() const { return _gpuTexture; } - - virtual void reset() { Storage::reset(); } + void reset(const QUrl& url, const TextureUsage& usage); void resetTexture(gpu::Texture* texture); + bool isDefined() const; + protected: gpu::TexturePointer _gpuTexture; TextureUsage _usage; @@ -59,12 +61,20 @@ public: void setTextureStorage(TextureStoragePointer& texStorage); - bool isNull() const; - + bool isDefined() const; gpu::TextureView getTextureView() const; + void setTextureTransform(const Transform& texcoordTransform); + const Transform& getTextureTransform() const { return _texcoordTransform; } + + void setLightmapOffsetScale(float offset, float scale); + const glm::vec2& getLightmapOffsetScale() const { return _lightmapOffsetScale; } + protected: TextureStoragePointer _textureStorage; + + Transform _texcoordTransform; + glm::vec2 _lightmapOffsetScale{ 0.0f, 1.0f }; }; typedef std::shared_ptr< TextureMap > TextureMapPointer; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 69e08c353c..3b68e82339 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2035,33 +2035,45 @@ static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const networkMaterial->_material = material._material; if (!material.diffuseTexture.filename.isEmpty()) { - // TODO: SOlve the eye case - networkMaterial->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.diffuseTexture.filename)), DEFAULT_TEXTURE, - /* mesh.isEye*/ false, material.diffuseTexture.content); + networkMaterial->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.diffuseTexture.filename)), DEFAULT_TEXTURE, material.diffuseTexture.content); networkMaterial->diffuseTextureName = material.diffuseTexture.name; - networkMaterial->_diffuseTexTransform = material.diffuseTexture.transform; auto diffuseMap = model::TextureMapPointer(new model::TextureMap()); diffuseMap->setTextureStorage(networkMaterial->diffuseTexture->_textureStorage); + diffuseMap->setTextureTransform(material.diffuseTexture.transform); + material._material->setTextureMap(model::MaterialKey::DIFFUSE_MAP, diffuseMap); } if (!material.normalTexture.filename.isEmpty()) { - networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), NORMAL_TEXTURE, - false, material.normalTexture.content); + networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), NORMAL_TEXTURE, material.normalTexture.content); networkMaterial->normalTextureName = material.normalTexture.name; + + auto normalMap = model::TextureMapPointer(new model::TextureMap()); + normalMap->setTextureStorage(networkMaterial->normalTexture->_textureStorage); + + material._material->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap); } if (!material.specularTexture.filename.isEmpty()) { - networkMaterial->specularTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.specularTexture.filename)), SPECULAR_TEXTURE, - false, material.specularTexture.content); + networkMaterial->specularTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.specularTexture.filename)), SPECULAR_TEXTURE, material.specularTexture.content); networkMaterial->specularTextureName = material.specularTexture.name; + + auto glossMap = model::TextureMapPointer(new model::TextureMap()); + glossMap->setTextureStorage(networkMaterial->specularTexture->_textureStorage); + + material._material->setTextureMap(model::MaterialKey::GLOSS_MAP, glossMap); } if (!material.emissiveTexture.filename.isEmpty()) { - networkMaterial->emissiveTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.emissiveTexture.filename)), EMISSIVE_TEXTURE, - false, material.emissiveTexture.content); + networkMaterial->emissiveTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.emissiveTexture.filename)), EMISSIVE_TEXTURE, material.emissiveTexture.content); networkMaterial->emissiveTextureName = material.emissiveTexture.name; - networkMaterial->_emissiveTexTransform = material.emissiveTexture.transform; - networkMaterial->_emissiveParams = material.emissiveParams; + checkForTexcoordLightmap = true; + + auto lightmapMap = model::TextureMapPointer(new model::TextureMap()); + lightmapMap->setTextureStorage(networkMaterial->emissiveTexture->_textureStorage); + lightmapMap->setTextureTransform(material.emissiveTexture.transform); + lightmapMap->setLightmapOffsetScale(material.emissiveParams.x, material.emissiveParams.y); + + material._material->setTextureMap(model::MaterialKey::LIGHTMAP_MAP, lightmapMap); } return networkMaterial; diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index b0e5346261..38e9fa54c2 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -438,11 +438,6 @@ public: QSharedPointer specularTexture; QString emissiveTextureName; QSharedPointer emissiveTexture; - - Transform _diffuseTexTransform; - Transform _emissiveTexTransform; - - glm::vec2 _emissiveParams; }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index cd407766fa..c87be06dba 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -97,6 +97,11 @@ Model::~Model() { Model::RenderPipelineLib Model::_renderPipelineLib; const int MATERIAL_GPU_SLOT = 3; +const int DIFFUSE_MAP_SLOT = 0; +const int NORMAL_MAP_SLOT = 1; +const int SPECULAR_MAP_SLOT = 2; +const int LIGHTMAP_MAP_SLOT = 3; +const int LIGHT_BUFFER_SLOT = 4; void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, gpu::ShaderPointer& vertexShader, @@ -104,11 +109,11 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), MATERIAL_GPU_SLOT)); - slotBindings.insert(gpu::Shader::Binding(std::string("diffuseMap"), 0)); - slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), 1)); - slotBindings.insert(gpu::Shader::Binding(std::string("specularMap"), 2)); - slotBindings.insert(gpu::Shader::Binding(std::string("emissiveMap"), 3)); - slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), 4)); + slotBindings.insert(gpu::Shader::Binding(std::string("diffuseMap"), DIFFUSE_MAP_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), NORMAL_MAP_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("specularMap"), SPECULAR_MAP_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("emissiveMap"), LIGHTMAP_MAP_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), DeferredLightingEffect::NORMAL_FITTING_MAP_SLOT)); gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vertexShader, pixelShader)); @@ -1642,75 +1647,82 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape batch.setUniformBuffer(locations->materialBufferUnit, material->getSchemaBuffer()); } - auto textureMaps = drawMaterial->_material->getTextureMaps(); + auto materialKey = material->getKey(); + auto textureMaps = material->getTextureMaps(); + glm::mat4 texcoordTransform[2]; - auto diffuseMap2 = textureMaps[model::MaterialKey::DIFFUSE_MAP]; - Texture* diffuseMap = drawMaterial->diffuseTexture.data(); - if (mesh.isEye && diffuseMap) { - // FIXME - guard against out of bounds here - if (meshIndex < _dilatedTextures.size()) { - if (partIndex < _dilatedTextures[meshIndex].size()) { - diffuseMap = (_dilatedTextures[meshIndex][partIndex] = - static_cast(diffuseMap)->getDilatedTexture(_pupilDilation)).data(); + // Diffuse + if (materialKey.isDiffuseMap()) { + auto diffuseMap = textureMaps[model::MaterialKey::DIFFUSE_MAP]; + + if (diffuseMap && diffuseMap->isDefined()) { + batch.setResourceTexture(DIFFUSE_MAP_SLOT, diffuseMap->getTextureView()); + + if (!diffuseMap->getTextureTransform().isIdentity()) { + diffuseMap->getTextureTransform().getMatrix(texcoordTransform[0]); + } + } else { + batch.setResourceTexture(DIFFUSE_MAP_SLOT, textureCache->getGrayTexture()); + } + } + + // Normal map + if (materialKey.isNormalMap()) { + auto normalMap = textureMaps[model::MaterialKey::NORMAL_MAP]; + if (normalMap && normalMap->isDefined()) { + batch.setResourceTexture(NORMAL_MAP_SLOT, normalMap->getTextureView()); + + // texcoord are assumed to be the same has diffuse + } else { + batch.setResourceTexture(NORMAL_MAP_SLOT, textureCache->getBlueTexture()); + } + } + + // TODO: For now gloss map is used as the "specular map in the shading, we ll need to fix that + if ((locations->specularTextureUnit >= 0) && materialKey.isGlossMap()) { + auto specularMap = textureMaps[model::MaterialKey::GLOSS_MAP]; + if (specularMap && specularMap->isDefined()) { + batch.setResourceTexture(SPECULAR_MAP_SLOT, specularMap->getTextureView()); + + // texcoord are assumed to be the same has diffuse + } else { + batch.setResourceTexture(SPECULAR_MAP_SLOT, textureCache->getBlackTexture()); + } + } + + // TODO: For now lightmaop is piped into the emissive map unit, we need to fix that and support for real emissive too + if ((locations->emissiveTextureUnit >= 0) && materialKey.isLightmapMap()) { + auto lightmapMap = textureMaps[model::MaterialKey::LIGHTMAP_MAP]; + + if (lightmapMap && lightmapMap->isDefined()) { + batch.setResourceTexture(LIGHTMAP_MAP_SLOT, lightmapMap->getTextureView()); + + auto lightmapOffsetScale = lightmapMap->getLightmapOffsetScale(); + batch._glUniform2f(locations->emissiveParams, lightmapOffsetScale.x, lightmapOffsetScale.y); + + if (!lightmapMap->getTextureTransform().isIdentity()) { + lightmapMap->getTextureTransform().getMatrix(texcoordTransform[1]); } } - } - //if (diffuseMap && static_cast(diffuseMap)->isLoaded()) { - - if (diffuseMap2 && !diffuseMap2->isNull()) { - - batch.setResourceTexture(0, diffuseMap2->getTextureView()); - // batch.setResourceTexture(0, diffuseMap->getGPUTexture()); - } else { - batch.setResourceTexture(0, textureCache->getGrayTexture()); + else { + batch.setResourceTexture(LIGHTMAP_MAP_SLOT, textureCache->getGrayTexture()); + } } + // Texcoord transforms ? if (locations->texcoordMatrices >= 0) { - glm::mat4 texcoordTransform[2]; - if (!drawMaterial->_diffuseTexTransform.isIdentity()) { - drawMaterial->_diffuseTexTransform.getMatrix(texcoordTransform[0]); - } - if (!drawMaterial->_emissiveTexTransform.isIdentity()) { - drawMaterial->_emissiveTexTransform.getMatrix(texcoordTransform[1]); - } - - batch._glUniformMatrix4fv(locations->texcoordMatrices, 2, false, (const float*) &texcoordTransform); + batch._glUniformMatrix4fv(locations->texcoordMatrices, 2, false, (const float*)&texcoordTransform); } - if (!mesh.tangents.isEmpty()) { - NetworkTexture* normalMap = drawMaterial->normalTexture.data(); - batch.setResourceTexture(1, (!normalMap || !normalMap->isLoaded()) ? - textureCache->getBlueTexture() : normalMap->getGPUTexture()); + // TODO: We should be able to do that just in the renderTransparentJob + if (translucentMesh && locations->lightBufferUnit >= 0) { + DependencyManager::get()->setupTransparent(args, locations->lightBufferUnit); } - if (locations->specularTextureUnit >= 0) { - NetworkTexture* specularMap = drawMaterial->specularTexture.data(); - batch.setResourceTexture(locations->specularTextureUnit, (!specularMap || !specularMap->isLoaded()) ? - textureCache->getBlackTexture() : specularMap->getGPUTexture()); - } if (args) { args->_details._materialSwitches++; } - - // HACK: For unknown reason (yet!) this code that should be assigned only if the material changes need to be called for every - // drawcall with an emissive, so let's do it for now. - if (locations->emissiveTextureUnit >= 0) { - // assert(locations->emissiveParams >= 0); // we should have the emissiveParams defined in the shader - //float emissiveOffset = part.emissiveParams.x; - //float emissiveScale = part.emissiveParams.y; - float emissiveOffset = drawMaterial->_emissiveParams.x; - float emissiveScale = drawMaterial->_emissiveParams.y; - batch._glUniform2f(locations->emissiveParams, emissiveOffset, emissiveScale); - - NetworkTexture* emissiveMap = drawMaterial->emissiveTexture.data(); - batch.setResourceTexture(locations->emissiveTextureUnit, (!emissiveMap || !emissiveMap->isLoaded()) ? - textureCache->getGrayTexture() : emissiveMap->getGPUTexture()); - } - - if (translucentMesh && locations->lightBufferUnit >= 0) { - DependencyManager::get()->setupTransparent(args, locations->lightBufferUnit); - } } } From 9a8dc6ca5daf8dde4b30773e9a1f17053639707e Mon Sep 17 00:00:00 2001 From: samcake Date: Sat, 19 Sep 2015 08:59:47 -0700 Subject: [PATCH 119/192] Fix compilation issues on mac --- libraries/model/src/model/Asset.h | 4 ++-- libraries/render-utils/src/GeometryCache.cpp | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/model/src/model/Asset.h b/libraries/model/src/model/Asset.h index a7b06b1239..51fc177538 100644 --- a/libraries/model/src/model/Asset.h +++ b/libraries/model/src/model/Asset.h @@ -35,9 +35,9 @@ public: }; static Version evalVersionFromID(ID id) { - if (ID <= 0) { + if (id <= 0) { return DRAFT; - } else (ID > 0) { + } else { return FINAL; } } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 3b68e82339..d19ed485d1 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1786,8 +1786,7 @@ void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& u for (auto&& material : _materials) { QSharedPointer matchingTexture = QSharedPointer(); if (material->diffuseTextureName == name) { - // TODO: Find a solution to the eye case - material->diffuseTexture = textureCache->getTexture(url, DEFAULT_TEXTURE, /* _geometry->meshes[i].isEye*/ false); + material->diffuseTexture = textureCache->getTexture(url, DEFAULT_TEXTURE); } else if (material->normalTextureName == name) { material->normalTexture = textureCache->getTexture(url); } else if (material->specularTextureName == name) { From 4d2048c6f6779a8e917f158b542586f24ec2ed63 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 20 Sep 2015 13:12:13 -0700 Subject: [PATCH 120/192] Let other users see your default avatar. Fixes https://app.asana.com/0/26225263936266/51001490412567 --- libraries/avatars/src/AvatarData.cpp | 5 +++-- libraries/avatars/src/AvatarData.h | 2 +- libraries/avatars/src/AvatarHashMap.cpp | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index e1bb6ae8a0..104b592c1a 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -73,7 +73,7 @@ AvatarData::~AvatarData() { // We cannot have a file-level variable (const or otherwise) in the header if it uses PathUtils, because that references Application, which will not yet initialized. // Thus we have a static class getter, referencing a static class var. QUrl AvatarData::_defaultFullAvatarModelUrl = {}; // In C++, if this initialization were in the header, every file would have it's own copy, even for class vars. -const QUrl AvatarData::defaultFullAvatarModelUrl() { +const QUrl& AvatarData::defaultFullAvatarModelUrl() { if (_defaultFullAvatarModelUrl.isEmpty()) { _defaultFullAvatarModelUrl = QUrl::fromLocalFile(PathUtils::resourcesPath() + "meshes/defaultAvatar_full.fst"); } @@ -966,8 +966,9 @@ bool AvatarData::hasIdentityChangedAfterParsing(NLPacket& packet) { QByteArray AvatarData::identityByteArray() { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); + const QUrl& urlToSend = (_skeletonModelURL == AvatarData::defaultFullAvatarModelUrl()) ? QUrl("") : _skeletonModelURL; - identityStream << QUuid() << _faceModelURL << _skeletonModelURL << _attachmentData << _displayName; + identityStream << QUuid() << _faceModelURL << urlToSend << _attachmentData << _displayName; return identityData; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 81d252c622..a934b98037 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -166,7 +166,7 @@ public: AvatarData(); virtual ~AvatarData(); - static const QUrl defaultFullAvatarModelUrl(); + static const QUrl& defaultFullAvatarModelUrl(); virtual bool isMyAvatar() const { return false; } diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 4378e818ec..520bb34887 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -99,8 +99,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer packet, avatar->setFaceModelURL(faceMeshURL); } - if (avatar->getSkeletonModelURL() != skeletonURL) { - avatar->setSkeletonModelURL(skeletonURL); + if (avatar->getSkeletonModelURL().isEmpty() || (avatar->getSkeletonModelURL() != skeletonURL)) { + avatar->setSkeletonModelURL(skeletonURL); // Will expand "" to default and so will not continuously fire } if (avatar->getAttachmentData() != attachmentData) { From a41c20a1b798a0770f9a1a3216bda8e2d915ad06 Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 20 Sep 2015 14:17:55 -0700 Subject: [PATCH 121/192] Fixing buggy hash map look up and cleaning of name --- libraries/fbx/src/FBXReader.cpp | 27 +++++++++++---- libraries/fbx/src/FBXReader.h | 26 +++++++++++--- libraries/fbx/src/FBXReader_Material.cpp | 44 +++++++++++------------- 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 216f560fab..9c79bb5ff5 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -817,12 +817,12 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (subobject.name == "RelativeFilename") { QByteArray filename = subobject.properties.at(0).toByteArray(); filename = fileOnUrl(filename, url); - textureFilenames.insert(getID(object.properties), filename); + _textureFilenames.insert(getID(object.properties), filename); } else if (subobject.name == "TextureName") { // trim the name from the timestamp QString name = QString(subobject.properties.at(0).toByteArray()); name = name.left(name.indexOf('[')); - textureNames.insert(getID(object.properties), name); + _textureNames.insert(getID(object.properties), name); } else if (subobject.name == "Texture_Alpha_Source") { tex.assign(tex.alphaSource, subobject.properties.at(0).value()); } else if (subobject.name == "ModelUVTranslation") { @@ -831,6 +831,12 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } else if (subobject.name == "ModelUVScaling") { tex.assign(tex.UVScaling, glm::vec2(subobject.properties.at(0).value(), subobject.properties.at(1).value())); + if (tex.UVScaling.x == 0.0f) { + tex.UVScaling.x = 1.0f; + } + if (tex.UVScaling.y == 0.0f) { + tex.UVScaling.y = 1.0f; + } } else if (subobject.name == "Cropping") { tex.assign(tex.cropping, glm::vec4(subobject.properties.at(0).value(), subobject.properties.at(1).value(), @@ -857,6 +863,15 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS tex.assign(tex.rotation, getVec3(property.properties, index)); } else if (property.properties.at(0) == "Scaling") { tex.assign(tex.scaling, getVec3(property.properties, index)); + if (tex.scaling.x == 0.0f) { + tex.scaling.x = 1.0f; + } + if (tex.scaling.y == 0.0f) { + tex.scaling.y = 1.0f; + } + if (tex.scaling.z == 0.0f) { + tex.scaling.z = 1.0f; + } } #if defined(DEBUG_FBXREADER) else { @@ -882,7 +897,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } if (!tex.isDefault) { - textureParams.insert(getID(object.properties), tex); + _textureParams.insert(getID(object.properties), tex); } } else if (object.name == "Video") { QByteArray filename; @@ -897,7 +912,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } if (!content.isEmpty()) { - textureContent.insert(filename, content); + _textureContent.insert(filename, content); } } else if (object.name == "Material") { FBXMaterial material = { glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(), glm::vec2(0.f, 1.0f), 96.0f, 1.0f, @@ -1299,7 +1314,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.materials = _fbxMaterials; // see if any materials have texture children - bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, textureFilenames, _connectionChildMap); + bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, _textureFilenames, _connectionChildMap); for (QHash::iterator it = meshes.begin(); it != meshes.end(); it++) { ExtractedMesh& extracted = it.value(); @@ -1344,7 +1359,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS materialIndex++; - } else if (textureFilenames.contains(childID)) { + } else if (_textureFilenames.contains(childID)) { FBXTexture texture = getTexture(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index d29963cd81..0dc8d7ece3 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -312,7 +312,8 @@ FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const /// \exception QString if an error occurs in parsing FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); -struct TextureParam { +class TextureParam { +public: glm::vec2 UVTranslation; glm::vec2 UVScaling; glm::vec4 cropping; @@ -351,6 +352,21 @@ struct TextureParam { useMaterial(true), isDefault(true) {} + + TextureParam(const TextureParam& src) : + UVTranslation(src.UVTranslation), + UVScaling(src.UVScaling), + cropping(src.cropping), + UVSet(src.UVSet), + translation(src.translation), + rotation(src.rotation), + scaling(src.scaling), + alphaSource(src.alphaSource), + currentTextureBlendMode(src.currentTextureBlendMode), + useMaterial(src.useMaterial), + isDefault(src.isDefault) + {} + }; class ExtractedMesh; @@ -370,10 +386,10 @@ public: FBXTexture getTexture(const QString& textureID); - QHash textureNames; - QHash textureFilenames; - QHash textureContent; - QHash textureParams; + QHash _textureNames; + QHash _textureFilenames; + QHash _textureContent; + QHash _textureParams; QHash diffuseTextures; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index b634886079..6d0195bac5 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -28,17 +28,30 @@ bool FBXMaterial::needTangentSpace() const { FBXTexture FBXReader::getTexture(const QString& textureID) { FBXTexture texture; - texture.filename = textureFilenames.value(textureID); - texture.name = textureNames.value(textureID); - texture.content = textureContent.value(texture.filename); + texture.filename = _textureFilenames.value(textureID); + texture.name = _textureNames.value(textureID); + texture.content = _textureContent.value(texture.filename); texture.transform.setIdentity(); texture.texcoordSet = 0; - QHash::const_iterator it = textureParams.constFind(textureID); - if (it != textureParams.end()) { - const TextureParam& p = (*it); + if (_textureParams.contains(textureID)) { + auto p = _textureParams.value(textureID); + texture.transform.setTranslation(p.translation); texture.transform.setRotation(glm::quat(glm::radians(p.rotation))); - texture.transform.setScale(p.scaling); + + auto scaling = p.scaling; + // Protect from bad scaling which should never happen + if (scaling.x == 0.0f) { + scaling.x = 1.0f; + } + if (scaling.y == 0.0f) { + scaling.y = 1.0f; + } + if (scaling.z == 0.0f) { + scaling.z = 1.0f; + } + texture.transform.setScale(scaling); + if ((p.UVSet != "map1") && (p.UVSet != "UVSet0")) { texture.texcoordSet = 1; } @@ -61,14 +74,11 @@ void FBXReader::consolidateFBXMaterials() { // FBX files generated by 3DSMax have an intermediate texture parent, apparently foreach (const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { - if (textureFilenames.contains(childTextureID)) { + if (_textureFilenames.contains(childTextureID)) { diffuseTexture = getTexture(diffuseTextureID); } } - // TODO associate this per part - //diffuseTexture.texcoordSet = matchTextureUVSetToAttributeChannel(diffuseTexture.texcoordSetName, extracted.texcoordSetMap); - material.diffuseTexture = diffuseTexture; detectDifferentUVs = (diffuseTexture.texcoordSet != 0) || (!diffuseTexture.transform.isIdentity()); @@ -78,12 +88,6 @@ void FBXReader::consolidateFBXMaterials() { QString bumpTextureID = bumpTextures.value(material.materialID); if (!bumpTextureID.isNull()) { normalTexture = getTexture(bumpTextureID); - - // TODO Need to generate tangent space at association per part - //generateTangents = true; - - // TODO at per part association time - // normalTexture.texcoordSet = matchTextureUVSetToAttributeChannel(normalTexture.texcoordSetName, extracted.texcoordSetMap); material.normalTexture = normalTexture; @@ -94,8 +98,6 @@ void FBXReader::consolidateFBXMaterials() { QString specularTextureID = specularTextures.value(material.materialID); if (!specularTextureID.isNull()) { specularTexture = getTexture(specularTextureID); - // TODO at per part association time - // specularTexture.texcoordSet = matchTextureUVSetToAttributeChannel(specularTexture.texcoordSetName, extracted.texcoordSetMap); detectDifferentUVs |= (specularTexture.texcoordSet != 0) || (!specularTexture.transform.isIdentity()); } @@ -115,13 +117,9 @@ void FBXReader::consolidateFBXMaterials() { emissiveTexture = getTexture(ambientTextureID); } - // TODO : do this at per part association - //emissiveTexture.texcoordSet = matchTextureUVSetToAttributeChannel(emissiveTexture.texcoordSetName, extracted.texcoordSetMap); - material.emissiveParams = emissiveParams; material.emissiveTexture = emissiveTexture; - detectDifferentUVs |= (emissiveTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); } From 6914caac9db54f7ec4765e5519570a85a3d5353b Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 20 Sep 2015 18:14:03 -0700 Subject: [PATCH 122/192] Renaming TextureStorage.x to TextureMap.x --- libraries/model/src/model/{TextureStorage.cpp => TextureMap.cpp} | 0 libraries/model/src/model/{TextureStorage.h => TextureMap.h} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename libraries/model/src/model/{TextureStorage.cpp => TextureMap.cpp} (100%) rename libraries/model/src/model/{TextureStorage.h => TextureMap.h} (100%) diff --git a/libraries/model/src/model/TextureStorage.cpp b/libraries/model/src/model/TextureMap.cpp similarity index 100% rename from libraries/model/src/model/TextureStorage.cpp rename to libraries/model/src/model/TextureMap.cpp diff --git a/libraries/model/src/model/TextureStorage.h b/libraries/model/src/model/TextureMap.h similarity index 100% rename from libraries/model/src/model/TextureStorage.h rename to libraries/model/src/model/TextureMap.h From 3614812681f7b81fbaa28c699834a7907e602899 Mon Sep 17 00:00:00 2001 From: samcake Date: Sun, 20 Sep 2015 23:31:59 -0700 Subject: [PATCH 123/192] MOving the actual creation of the texture and the pixel manipulation required from TextureCache to TextureSOurce --- .../src/gpu-networking/TextureCache.cpp | 70 +++- .../src/gpu-networking/TextureCache.h | 25 +- libraries/model/src/model/Material.cpp | 2 +- libraries/model/src/model/ModelLogging.cpp | 11 + libraries/model/src/model/ModelLogging.h | 14 + libraries/model/src/model/TextureMap.cpp | 356 +++++++++++++++++- libraries/model/src/model/TextureMap.h | 11 +- libraries/render-utils/src/GeometryCache.cpp | 2 +- 8 files changed, 465 insertions(+), 26 deletions(-) create mode 100644 libraries/model/src/model/ModelLogging.cpp create mode 100644 libraries/model/src/model/ModelLogging.h diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp index 12b9ce423d..e77c7c1145 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp @@ -200,34 +200,66 @@ NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArr } } +NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content) : + Resource(url, !content.isEmpty()), + // _type(type), + _textureLoader(textureLoader), + _translucent(false), + _width(0), + _height(0) { + + _textureStorage.reset(new model::TextureStorage()); + + if (!url.isValid()) { + _loaded = true; + } + + std::string theName = url.toString().toStdString(); + // if we have content, load it after we have our self pointer + if (!content.isEmpty()) { + _startedLoading = true; + QMetaObject::invokeMethod(this, "loadContent", Qt::QueuedConnection, Q_ARG(const QByteArray&, content)); + } +} + +NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { + if (_type != CUBE_TEXTURE) { + + return TextureLoaderFunc(model::TextureStorage::create2DTextureFromImage); + } else { + return TextureLoaderFunc(model::TextureStorage::createCubeTextureFromImage); + } +} + + class ImageReader : public QRunnable { public: - ImageReader(const QWeakPointer& texture, TextureType type, const QByteArray& data, const QUrl& url = QUrl()); + ImageReader(const QWeakPointer& texture, const NetworkTexture::TextureLoaderFunc& textureLoader, const QByteArray& data, const QUrl& url = QUrl()); virtual void run(); private: QWeakPointer _texture; - TextureType _type; + NetworkTexture::TextureLoaderFunc _textureLoader; QUrl _url; QByteArray _content; }; void NetworkTexture::downloadFinished(const QByteArray& data) { // send the reader off to the thread pool - QThreadPool::globalInstance()->start(new ImageReader(_self, _type, data, _url)); + QThreadPool::globalInstance()->start(new ImageReader(_self, getTextureLoader(), data, _url)); } void NetworkTexture::loadContent(const QByteArray& content) { - QThreadPool::globalInstance()->start(new ImageReader(_self, _type, content, _url)); + QThreadPool::globalInstance()->start(new ImageReader(_self, getTextureLoader(), content, _url)); } -ImageReader::ImageReader(const QWeakPointer& texture, TextureType type, const QByteArray& data, +ImageReader::ImageReader(const QWeakPointer& texture, const NetworkTexture::TextureLoaderFunc& textureLoader, const QByteArray& data, const QUrl& url) : _texture(texture), - _type(type), + _textureLoader(textureLoader), _url(url), _content(data) { @@ -246,7 +278,7 @@ void listSupportedImageFormats() { }); } - +/* class CubeLayout { public: int _widthRatio = 1; @@ -280,6 +312,7 @@ public: _faceZPos(fZP), _faceZNeg(fZN) {} }; +*/ void ImageReader::run() { QSharedPointer texture = _texture.toStrongRef(); @@ -309,8 +342,17 @@ void ImageReader::run() { return; } - int imageArea = image.width() * image.height(); + gpu::Texture* theTexture = nullptr; auto ntex = dynamic_cast(&*texture); + if (ntex) { + theTexture = ntex->getTextureLoader()(image, _url.toString().toStdString()); + } + +/* + int imageArea = image.width() * image.height(); + + gpu::Texture* theTexture = nullptr; + if (ntex && (ntex->getType() == CUBE_TEXTURE)) { qCDebug(gpunetwork) << "Cube map size:" << _url << image.width() << image.height(); } @@ -535,21 +577,21 @@ void ImageReader::run() { theTexture->autoGenerateMips(-1); } } - +*/ QMetaObject::invokeMethod(texture.data(), "setImage", Q_ARG(const QImage&, image), Q_ARG(void*, theTexture), - Q_ARG(bool, isTransparent), - Q_ARG(const QColor&, averageColor), + // Q_ARG(bool, isTransparent), + // Q_ARG(const QColor&, averageColor), Q_ARG(int, originalWidth), Q_ARG(int, originalHeight)); } -void NetworkTexture::setImage(const QImage& image, void* voidTexture, bool translucent, const QColor& averageColor, int originalWidth, +void NetworkTexture::setImage(const QImage& image, void* voidTexture,/* bool translucent, const QColor& averageColor, */ int originalWidth, int originalHeight) { - _translucent = translucent; - _averageColor = averageColor; + // _translucent = translucent; + // _averageColor = averageColor; _originalWidth = originalWidth; _originalHeight = originalHeight; diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.h b/libraries/gpu-networking/src/gpu-networking/TextureCache.h index d17691a484..b33ede975b 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.h +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.h @@ -20,7 +20,7 @@ #include #include -#include +#include namespace gpu { class Batch; @@ -63,7 +63,13 @@ public: /// Loads a texture from the specified URL. NetworkTexturePointer getTexture(const QUrl& url, TextureType type = DEFAULT_TEXTURE, const QByteArray& content = QByteArray()); - + + typedef gpu::Texture* TextureLoader(const QImage& image, const std::string& srcImageName); + + typedef std::function TextureLoaderFunc; + + NetworkTexturePointer getTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, + const QByteArray& content = QByteArray()); protected: virtual QSharedPointer createResource(const QUrl& url, @@ -107,27 +113,33 @@ class NetworkTexture : public Resource, public Texture { public: + typedef TextureCache::TextureLoaderFunc TextureLoaderFunc; + NetworkTexture(const QUrl& url, TextureType type, const QByteArray& content); + NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); /// Checks whether it "looks like" this texture is translucent /// (majority of pixels neither fully opaque or fully transparent). - bool isTranslucent() const { return _translucent; } + // bool isTranslucent() const { return _translucent; } /// Returns the lazily-computed average texture color. - const QColor& getAverageColor() const { return _averageColor; } + // const QColor& getAverageColor() const { return _averageColor; } int getOriginalWidth() const { return _originalWidth; } int getOriginalHeight() const { return _originalHeight; } int getWidth() const { return _width; } int getHeight() const { return _height; } - TextureType getType() const { return _type; } + // TextureType getType() const { return _type; } + + TextureLoaderFunc getTextureLoader() const; + protected: virtual void downloadFinished(const QByteArray& data) override; Q_INVOKABLE void loadContent(const QByteArray& content); // FIXME: This void* should be a gpu::Texture* but i cannot get it to work for now, moving on... - Q_INVOKABLE void setImage(const QImage& image, void* texture, bool translucent, const QColor& averageColor, int originalWidth, + Q_INVOKABLE void setImage(const QImage& image, void* texture, /*bool translucent, const QColor& averageColor, */int originalWidth, int originalHeight); virtual void imageLoaded(const QImage& image); @@ -135,6 +147,7 @@ protected: TextureType _type; private: + TextureLoaderFunc _textureLoader; bool _translucent; QColor _averageColor; int _originalWidth; diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index 679e22c9e7..cf91836254 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -10,7 +10,7 @@ // #include "Material.h" -#include "TextureStorage.h" +#include "TextureMap.h" using namespace model; using namespace gpu; diff --git a/libraries/model/src/model/ModelLogging.cpp b/libraries/model/src/model/ModelLogging.cpp new file mode 100644 index 0000000000..3b3fbed82c --- /dev/null +++ b/libraries/model/src/model/ModelLogging.cpp @@ -0,0 +1,11 @@ +// +// Created by Sam Gateau on 2015/09/21 +// Copyright 2013-2015 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 "ModelLogging.h" + +Q_LOGGING_CATEGORY(modelLog, "hifi.model") \ No newline at end of file diff --git a/libraries/model/src/model/ModelLogging.h b/libraries/model/src/model/ModelLogging.h new file mode 100644 index 0000000000..33ed6fb059 --- /dev/null +++ b/libraries/model/src/model/ModelLogging.h @@ -0,0 +1,14 @@ +// +// ModelLogging.h +// hifi +// +// Created by Sam Gateau on 9/20/15. +// Copyright 2013-2015 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 + +Q_DECLARE_LOGGING_CATEGORY(modelLog) diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index cc098e7356..70e855bb15 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -1,5 +1,5 @@ // -// TextureStorage.cpp +// TextureMap.cpp // libraries/model/src/model // // Created by Sam Gateau on 5/6/2015. @@ -8,7 +8,13 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "TextureStorage.h" +#include "TextureMap.h" + +#include +#include +#include + +#include "ModelLogging.h" using namespace model; using namespace gpu; @@ -69,3 +75,349 @@ void TextureMap::setLightmapOffsetScale(float offset, float scale) { _lightmapOffsetScale.y = scale; } + + + +gpu::Texture* TextureStorage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + QImage image = srcImage; + + int imageArea = image.width() * image.height(); + + int opaquePixels = 0; + int translucentPixels = 0; + bool isTransparent = false; + int redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0; + const int EIGHT_BIT_MAXIMUM = 255; + QColor averageColor(EIGHT_BIT_MAXIMUM, EIGHT_BIT_MAXIMUM, EIGHT_BIT_MAXIMUM); + + if (!image.hasAlphaChannel()) { + if (image.format() != QImage::Format_RGB888) { + image = image.convertToFormat(QImage::Format_RGB888); + } + // int redTotal = 0, greenTotal = 0, blueTotal = 0; + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + QRgb rgb = image.pixel(x, y); + redTotal += qRed(rgb); + greenTotal += qGreen(rgb); + blueTotal += qBlue(rgb); + } + } + if (imageArea > 0) { + averageColor.setRgb(redTotal / imageArea, greenTotal / imageArea, blueTotal / imageArea); + } + } else { + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + // check for translucency/false transparency + // int opaquePixels = 0; + // int translucentPixels = 0; + // int redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0; + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + QRgb rgb = image.pixel(x, y); + redTotal += qRed(rgb); + greenTotal += qGreen(rgb); + blueTotal += qBlue(rgb); + int alpha = qAlpha(rgb); + alphaTotal += alpha; + if (alpha == EIGHT_BIT_MAXIMUM) { + opaquePixels++; + } else if (alpha != 0) { + translucentPixels++; + } + } + } + if (opaquePixels == imageArea) { + qCDebug(modelLog) << "Image with alpha channel is completely opaque:" << QString(srcImageName.c_str()); + image = image.convertToFormat(QImage::Format_RGB888); + } + + averageColor = QColor(redTotal / imageArea, + greenTotal / imageArea, blueTotal / imageArea, alphaTotal / imageArea); + + isTransparent = (translucentPixels >= imageArea / 2); + } + + gpu::Texture* theTexture = nullptr; + if ((image.width() > 0) && (image.height() > 0)) { + + // bool isLinearRGB = true; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); + bool isLinearRGB = false; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); + + gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); + gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); + if (image.hasAlphaChannel()) { + formatGPU = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::RGBA : gpu::SRGBA)); + formatMip = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::BGRA : gpu::SBGRA)); + } + + + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + theTexture->autoGenerateMips(-1); + } + + return theTexture; +} + +class CubeLayout { +public: + int _widthRatio = 1; + int _heightRatio = 1; + + class Face { + public: + int _x = 0; + int _y = 0; + bool _horizontalMirror = false; + bool _verticalMirror = false; + + Face() {} + Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} + }; + + Face _faceXPos; + Face _faceXNeg; + Face _faceYPos; + Face _faceYNeg; + Face _faceZPos; + Face _faceZNeg; + + CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : + _widthRatio(wr), + _heightRatio(hr), + _faceXPos(fXP), + _faceXNeg(fXN), + _faceYPos(fYP), + _faceYNeg(fYN), + _faceZPos(fZP), + _faceZNeg(fZN) {} +}; + +gpu::Texture* TextureStorage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + QImage image = srcImage; + + int imageArea = image.width() * image.height(); + + + qCDebug(modelLog) << "Cube map size:" << QString(srcImageName.c_str()) << image.width() << image.height(); + + int opaquePixels = 0; + int translucentPixels = 0; + bool isTransparent = false; + int redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0; + const int EIGHT_BIT_MAXIMUM = 255; + QColor averageColor(EIGHT_BIT_MAXIMUM, EIGHT_BIT_MAXIMUM, EIGHT_BIT_MAXIMUM); + + if (!image.hasAlphaChannel()) { + if (image.format() != QImage::Format_RGB888) { + image = image.convertToFormat(QImage::Format_RGB888); + } + // int redTotal = 0, greenTotal = 0, blueTotal = 0; + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + QRgb rgb = image.pixel(x, y); + redTotal += qRed(rgb); + greenTotal += qGreen(rgb); + blueTotal += qBlue(rgb); + } + } + if (imageArea > 0) { + averageColor.setRgb(redTotal / imageArea, greenTotal / imageArea, blueTotal / imageArea); + } + } else { + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + // check for translucency/false transparency + // int opaquePixels = 0; + // int translucentPixels = 0; + // int redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0; + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + QRgb rgb = image.pixel(x, y); + redTotal += qRed(rgb); + greenTotal += qGreen(rgb); + blueTotal += qBlue(rgb); + int alpha = qAlpha(rgb); + alphaTotal += alpha; + if (alpha == EIGHT_BIT_MAXIMUM) { + opaquePixels++; + } else if (alpha != 0) { + translucentPixels++; + } + } + } + if (opaquePixels == imageArea) { + qCDebug(modelLog) << "Image with alpha channel is completely opaque:" << QString(srcImageName.c_str()); + image = image.convertToFormat(QImage::Format_RGB888); + } + + averageColor = QColor(redTotal / imageArea, + greenTotal / imageArea, blueTotal / imageArea, alphaTotal / imageArea); + + isTransparent = (translucentPixels >= imageArea / 2); + } + + gpu::Texture* theTexture = nullptr; + if ((image.width() > 0) && (image.height() > 0)) { + + // bool isLinearRGB = true; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); + bool isLinearRGB = false; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); + + gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); + gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); + if (image.hasAlphaChannel()) { + formatGPU = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::RGBA : gpu::SRGBA)); + formatMip = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::BGRA : gpu::SBGRA)); + } + + + const CubeLayout CUBEMAP_LAYOUTS[] = { + // Here is the expected layout for the faces in an image with the 1/6 aspect ratio: + // + // WIDTH + // <------> + // ^ +------+ + // | | | + // | | +X | + // | | | + // H +------+ + // E | | + // I | -X | + // G | | + // H +------+ + // T | | + // | | +Y | + // | | | + // | +------+ + // | | | + // | | -Y | + // | | | + // H +------+ + // E | | + // I | +Z | + // G | | + // H +------+ + // T | | + // | | -Z | + // | | | + // V +------+ + // + // FaceWidth = width = height / 6 + { 1, 6, + {0, 0, true, false}, + {0, 1, true, false}, + {0, 2, false, true}, + {0, 3, false, true}, + {0, 4, true, false}, + {0, 5, true, false} + }, + + // Here is the expected layout for the faces in an image with the 3/4 aspect ratio: + // + // <-----------WIDTH-----------> + // ^ +------+------+------+------+ + // | | | | | | + // | | | +Y | | | + // | | | | | | + // H +------+------+------+------+ + // E | | | | | + // I | -X | -Z | +X | +Z | + // G | | | | | + // H +------+------+------+------+ + // T | | | | | + // | | | -Y | | | + // | | | | | | + // V +------+------+------+------+ + // + // FaceWidth = width / 4 = height / 3 + { 4, 3, + {2, 1, true, false}, + {0, 1, true, false}, + {1, 0, false, true}, + {1, 2, false, true}, + {3, 1, true, false}, + {1, 1, true, false} + }, + + // Here is the expected layout for the faces in an image with the 4/3 aspect ratio: + // + // <-------WIDTH--------> + // ^ +------+------+------+ + // | | | | | + // | | | +Y | | + // | | | | | + // H +------+------+------+ + // E | | | | + // I | -X | -Z | +X | + // G | | | | + // H +------+------+------+ + // T | | | | + // | | | -Y | | + // | | | | | + // | +------+------+------+ + // | | | | | + // | | | +Z! | | <+Z is upside down! + // | | | | | + // V +------+------+------+ + // + // FaceWidth = width / 3 = height / 4 + { 3, 4, + {2, 1, true, false}, + {0, 1, true, false}, + {1, 0, false, true}, + {1, 2, false, true}, + {1, 3, false, true}, + {1, 1, true, false} + } + }; + const int NUM_CUBEMAP_LAYOUTS = sizeof(CUBEMAP_LAYOUTS) / sizeof(CubeLayout); + + // Find the layout of the cubemap in the 2D image + int foundLayout = -1; + for (int i = 0; i < NUM_CUBEMAP_LAYOUTS; i++) { + if ((image.height() * CUBEMAP_LAYOUTS[i]._widthRatio) == (image.width() * CUBEMAP_LAYOUTS[i]._heightRatio)) { + foundLayout = i; + break; + } + } + + std::vector faces; + // If found, go extract the faces as separate images + if (foundLayout >= 0) { + auto& layout = CUBEMAP_LAYOUTS[foundLayout]; + int faceWidth = image.width() / layout._widthRatio; + + faces.push_back(image.copy(QRect(layout._faceXPos._x * faceWidth, layout._faceXPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXPos._horizontalMirror, layout._faceXPos._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceXNeg._x * faceWidth, layout._faceXNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXNeg._horizontalMirror, layout._faceXNeg._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceYPos._x * faceWidth, layout._faceYPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYPos._horizontalMirror, layout._faceYPos._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceYNeg._x * faceWidth, layout._faceYNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYNeg._horizontalMirror, layout._faceYNeg._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceZPos._x * faceWidth, layout._faceZPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZPos._horizontalMirror, layout._faceZPos._verticalMirror)); + faces.push_back(image.copy(QRect(layout._faceZNeg._x * faceWidth, layout._faceZNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZNeg._horizontalMirror, layout._faceZNeg._verticalMirror)); + } else { + qCDebug(modelLog) << "Failed to find a known cube map layout from this image:" << QString(srcImageName.c_str()); + return; + } + + // If the 6 faces have been created go on and define the true Texture + if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { + theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); + theTexture->autoGenerateMips(-1); + int f = 0; + for (auto& face : faces) { + theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); + f++; + } + + // GEnerate irradiance while we are at it + theTexture->generateIrradiance(); + } + } + + return theTexture; +} diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index c879918ea9..cb5f510043 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -8,8 +8,8 @@ // 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_model_TextureStorage_h -#define hifi_model_TextureStorage_h +#ifndef hifi_model_TextureMap_h +#define hifi_model_TextureMap_h #include "gpu/Texture.h" @@ -18,6 +18,8 @@ #include +class QImage; + namespace model { typedef glm::vec3 Color; @@ -47,6 +49,9 @@ public: void resetTexture(gpu::Texture* texture); bool isDefined() const; + + static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); + static gpu::Texture* createCubeTextureFromImage(const QImage& image, const std::string& srcImageName); protected: gpu::TexturePointer _gpuTexture; @@ -55,6 +60,8 @@ protected: }; typedef std::shared_ptr< TextureStorage > TextureStoragePointer; + + class TextureMap { public: TextureMap() {} diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index f56c2cb191..40857dedf2 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -29,7 +29,7 @@ #include "gpu/StandardShaderLib.h" -#include "model/TextureStorage.h" +#include "model/TextureMap.h" //#define WANT_DEBUG From 6a714f405c31c10bb3342a6b6924dd2c6b3024bb Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 00:53:48 -0700 Subject: [PATCH 124/192] Integrate the PR from ALessandro through a different Texture Type and a different TextureLoader to transform a bump map into a normal map --- libraries/fbx/src/FBXReader.cpp | 5 +- libraries/fbx/src/FBXReader.h | 5 +- libraries/fbx/src/FBXReader_Material.cpp | 16 +- .../src/gpu-networking/TextureCache.cpp | 312 ++---------------- .../src/gpu-networking/TextureCache.h | 20 +- libraries/model/src/model/TextureMap.cpp | 118 ++++++- libraries/model/src/model/TextureMap.h | 17 +- libraries/render-utils/src/GeometryCache.cpp | 10 +- 8 files changed, 168 insertions(+), 335 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 9c79bb5ff5..b4c15ac2a8 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1080,9 +1080,10 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } else if (type.contains("transparentcolor")) { // it should be TransparentColor... // THis is how Maya assign a texture that affect diffuse color AND transparency ? diffuseTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); - } else if (type.contains("bump") || type.contains("normal")) { + } else if (type.contains("bump")) { bumpTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); - + } else if (type.contains("normal")) { + normalTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("specular") || type.contains("reflection")) { specularTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 0dc8d7ece3..ddcff8224b 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -112,7 +112,9 @@ public: Transform transform; int texcoordSet; QString texcoordSetName; - + + bool isBumpmap{ false }; + bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); } }; @@ -394,6 +396,7 @@ public: QHash diffuseTextures; QHash bumpTextures; + QHash normalTextures; QHash specularTextures; QHash emissiveTextures; QHash ambientTextures; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index 6d0195bac5..c29c64030e 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -86,13 +86,21 @@ void FBXReader::consolidateFBXMaterials() { FBXTexture normalTexture; QString bumpTextureID = bumpTextures.value(material.materialID); - if (!bumpTextureID.isNull()) { - normalTexture = getTexture(bumpTextureID); - + QString normalTextureID = normalTextures.value(material.materialID); + if (!normalTextureID.isNull()) { + normalTexture = getTexture(normalTextureID); + normalTexture.isBumpmap = false; + + material.normalTexture = normalTexture; + detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); + } else if (!bumpTextureID.isNull()) { + normalTexture = getTexture(bumpTextureID); + normalTexture.isBumpmap = true; + material.normalTexture = normalTexture; - detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); } + FBXTexture specularTexture; QString specularTextureID = specularTextures.value(material.materialID); diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp index e77c7c1145..fd69a2ab04 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp @@ -182,11 +182,10 @@ Texture::~Texture() { NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArray& content) : Resource(url, !content.isEmpty()), _type(type), - _translucent(false), _width(0), _height(0) { - _textureStorage.reset(new model::TextureStorage()); + _textureSource.reset(new model::TextureSource()); if (!url.isValid()) { _loaded = true; @@ -202,13 +201,12 @@ NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArr NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content) : Resource(url, !content.isEmpty()), - // _type(type), + _type(CUSTOM_TEXTURE), _textureLoader(textureLoader), - _translucent(false), _width(0), _height(0) { - _textureStorage.reset(new model::TextureStorage()); + _textureSource.reset(new model::TextureSource()); if (!url.isValid()) { _loaded = true; @@ -223,11 +221,27 @@ NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& texture } NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { - if (_type != CUBE_TEXTURE) { - - return TextureLoaderFunc(model::TextureStorage::create2DTextureFromImage); - } else { - return TextureLoaderFunc(model::TextureStorage::createCubeTextureFromImage); + switch (_type) { + case CUBE_TEXTURE: { + return TextureLoaderFunc(model::TextureSource::createCubeTextureFromImage); + break; + } + case BUMP_TEXTURE: { + return TextureLoaderFunc(model::TextureSource::createNormalTextureFromBumpImage); + break; + } + case CUSTOM_TEXTURE: { + return _textureLoader; + break; + } + case DEFAULT_TEXTURE: + case NORMAL_TEXTURE: + case SPECULAR_TEXTURE: + case EMISSIVE_TEXTURE: + default: { + return TextureLoaderFunc(model::TextureSource::create2DTextureFromImage); + break; + } } } @@ -278,42 +292,6 @@ void listSupportedImageFormats() { }); } -/* -class CubeLayout { -public: - int _widthRatio = 1; - int _heightRatio = 1; - - class Face { - public: - int _x = 0; - int _y = 0; - bool _horizontalMirror = false; - bool _verticalMirror = false; - - Face() {} - Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} - }; - - Face _faceXPos; - Face _faceXNeg; - Face _faceYPos; - Face _faceYNeg; - Face _faceZPos; - Face _faceZNeg; - - CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : - _widthRatio(wr), - _heightRatio(hr), - _faceXPos(fXP), - _faceXNeg(fXN), - _faceYPos(fYP), - _faceYNeg(fYN), - _faceZPos(fZP), - _faceZNeg(fZN) {} -}; -*/ - void ImageReader::run() { QSharedPointer texture = _texture.toStrongRef(); if (texture.isNull()) { @@ -347,259 +325,25 @@ void ImageReader::run() { if (ntex) { theTexture = ntex->getTextureLoader()(image, _url.toString().toStdString()); } - -/* - int imageArea = image.width() * image.height(); - - gpu::Texture* theTexture = nullptr; - - if (ntex && (ntex->getType() == CUBE_TEXTURE)) { - qCDebug(gpunetwork) << "Cube map size:" << _url << image.width() << image.height(); - } - - int opaquePixels = 0; - int translucentPixels = 0; - bool isTransparent = false; - int redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0; - const int EIGHT_BIT_MAXIMUM = 255; - QColor averageColor(EIGHT_BIT_MAXIMUM, EIGHT_BIT_MAXIMUM, EIGHT_BIT_MAXIMUM); - if (!image.hasAlphaChannel()) { - if (image.format() != QImage::Format_RGB888) { - image = image.convertToFormat(QImage::Format_RGB888); - } - // int redTotal = 0, greenTotal = 0, blueTotal = 0; - for (int y = 0; y < image.height(); y++) { - for (int x = 0; x < image.width(); x++) { - QRgb rgb = image.pixel(x, y); - redTotal += qRed(rgb); - greenTotal += qGreen(rgb); - blueTotal += qBlue(rgb); - } - } - if (imageArea > 0) { - averageColor.setRgb(redTotal / imageArea, greenTotal / imageArea, blueTotal / imageArea); - } - } else { - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); - } - - // check for translucency/false transparency - // int opaquePixels = 0; - // int translucentPixels = 0; - // int redTotal = 0, greenTotal = 0, blueTotal = 0, alphaTotal = 0; - for (int y = 0; y < image.height(); y++) { - for (int x = 0; x < image.width(); x++) { - QRgb rgb = image.pixel(x, y); - redTotal += qRed(rgb); - greenTotal += qGreen(rgb); - blueTotal += qBlue(rgb); - int alpha = qAlpha(rgb); - alphaTotal += alpha; - if (alpha == EIGHT_BIT_MAXIMUM) { - opaquePixels++; - } else if (alpha != 0) { - translucentPixels++; - } - } - } - if (opaquePixels == imageArea) { - qCDebug(gpunetwork) << "Image with alpha channel is completely opaque:" << _url; - image = image.convertToFormat(QImage::Format_RGB888); - } - - averageColor = QColor(redTotal / imageArea, - greenTotal / imageArea, blueTotal / imageArea, alphaTotal / imageArea); - - isTransparent = (translucentPixels >= imageArea / 2); - } - - gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { - - // bool isLinearRGB = true; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); - bool isLinearRGB = !(_type == CUBE_TEXTURE); //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); - - gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); - gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); - if (image.hasAlphaChannel()) { - formatGPU = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::RGBA : gpu::SRGBA)); - formatMip = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::BGRA : gpu::SBGRA)); - } - - if (_type == CUBE_TEXTURE) { - - const CubeLayout CUBEMAP_LAYOUTS[] = { - // Here is the expected layout for the faces in an image with the 1/6 aspect ratio: - // - // WIDTH - // <------> - // ^ +------+ - // | | | - // | | +X | - // | | | - // H +------+ - // E | | - // I | -X | - // G | | - // H +------+ - // T | | - // | | +Y | - // | | | - // | +------+ - // | | | - // | | -Y | - // | | | - // H +------+ - // E | | - // I | +Z | - // G | | - // H +------+ - // T | | - // | | -Z | - // | | | - // V +------+ - // - // FaceWidth = width = height / 6 - { 1, 6, - {0, 0, true, false}, - {0, 1, true, false}, - {0, 2, false, true}, - {0, 3, false, true}, - {0, 4, true, false}, - {0, 5, true, false} - }, - - // Here is the expected layout for the faces in an image with the 3/4 aspect ratio: - // - // <-----------WIDTH-----------> - // ^ +------+------+------+------+ - // | | | | | | - // | | | +Y | | | - // | | | | | | - // H +------+------+------+------+ - // E | | | | | - // I | -X | -Z | +X | +Z | - // G | | | | | - // H +------+------+------+------+ - // T | | | | | - // | | | -Y | | | - // | | | | | | - // V +------+------+------+------+ - // - // FaceWidth = width / 4 = height / 3 - { 4, 3, - {2, 1, true, false}, - {0, 1, true, false}, - {1, 0, false, true}, - {1, 2, false, true}, - {3, 1, true, false}, - {1, 1, true, false} - }, - - // Here is the expected layout for the faces in an image with the 4/3 aspect ratio: - // - // <-------WIDTH--------> - // ^ +------+------+------+ - // | | | | | - // | | | +Y | | - // | | | | | - // H +------+------+------+ - // E | | | | - // I | -X | -Z | +X | - // G | | | | - // H +------+------+------+ - // T | | | | - // | | | -Y | | - // | | | | | - // | +------+------+------+ - // | | | | | - // | | | +Z! | | <+Z is upside down! - // | | | | | - // V +------+------+------+ - // - // FaceWidth = width / 3 = height / 4 - { 3, 4, - {2, 1, true, false}, - {0, 1, true, false}, - {1, 0, false, true}, - {1, 2, false, true}, - {1, 3, false, true}, - {1, 1, true, false} - } - }; - const int NUM_CUBEMAP_LAYOUTS = sizeof(CUBEMAP_LAYOUTS) / sizeof(CubeLayout); - - // Find the layout of the cubemap in the 2D image - int foundLayout = -1; - for (int i = 0; i < NUM_CUBEMAP_LAYOUTS; i++) { - if ((image.height() * CUBEMAP_LAYOUTS[i]._widthRatio) == (image.width() * CUBEMAP_LAYOUTS[i]._heightRatio)) { - foundLayout = i; - break; - } - } - - std::vector faces; - // If found, go extract the faces as separate images - if (foundLayout >= 0) { - auto& layout = CUBEMAP_LAYOUTS[foundLayout]; - int faceWidth = image.width() / layout._widthRatio; - - faces.push_back(image.copy(QRect(layout._faceXPos._x * faceWidth, layout._faceXPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXPos._horizontalMirror, layout._faceXPos._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceXNeg._x * faceWidth, layout._faceXNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceXNeg._horizontalMirror, layout._faceXNeg._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceYPos._x * faceWidth, layout._faceYPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYPos._horizontalMirror, layout._faceYPos._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceYNeg._x * faceWidth, layout._faceYNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceYNeg._horizontalMirror, layout._faceYNeg._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceZPos._x * faceWidth, layout._faceZPos._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZPos._horizontalMirror, layout._faceZPos._verticalMirror)); - faces.push_back(image.copy(QRect(layout._faceZNeg._x * faceWidth, layout._faceZNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZNeg._horizontalMirror, layout._faceZNeg._verticalMirror)); - } else { - qCDebug(gpunetwork) << "Failed to find a known cube map layout from this image:" << _url; - return; - } - - // If the 6 faces have been created go on and define the true Texture - if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { - theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); - theTexture->autoGenerateMips(-1); - int f = 0; - for (auto& face : faces) { - theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); - f++; - } - - // GEnerate irradiance while we are at it - theTexture->generateIrradiance(); - } - - } else { - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - theTexture->autoGenerateMips(-1); - } - } -*/ QMetaObject::invokeMethod(texture.data(), "setImage", Q_ARG(const QImage&, image), Q_ARG(void*, theTexture), - // Q_ARG(bool, isTransparent), - // Q_ARG(const QColor&, averageColor), Q_ARG(int, originalWidth), Q_ARG(int, originalHeight)); } -void NetworkTexture::setImage(const QImage& image, void* voidTexture,/* bool translucent, const QColor& averageColor, */ int originalWidth, +void NetworkTexture::setImage(const QImage& image, void* voidTexture, int originalWidth, int originalHeight) { - // _translucent = translucent; - // _averageColor = averageColor; _originalWidth = originalWidth; _originalHeight = originalHeight; gpu::Texture* texture = static_cast(voidTexture); + // Passing ownership - // _gpuTexture.reset(texture); - _textureStorage->resetTexture(texture); - auto gpuTexture = _textureStorage->getGPUTexture(); + _textureSource->resetTexture(texture); + auto gpuTexture = _textureSource->getGPUTexture(); if (gpuTexture) { _width = gpuTexture->getWidth(); diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.h b/libraries/gpu-networking/src/gpu-networking/TextureCache.h index b33ede975b..335ea2d89c 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.h +++ b/libraries/gpu-networking/src/gpu-networking/TextureCache.h @@ -29,7 +29,7 @@ class NetworkTexture; typedef QSharedPointer NetworkTexturePointer; -enum TextureType { DEFAULT_TEXTURE, NORMAL_TEXTURE, SPECULAR_TEXTURE, EMISSIVE_TEXTURE, SPLAT_TEXTURE, CUBE_TEXTURE }; +enum TextureType { DEFAULT_TEXTURE, NORMAL_TEXTURE, BUMP_TEXTURE, SPECULAR_TEXTURE, EMISSIVE_TEXTURE, CUBE_TEXTURE, CUSTOM_TEXTURE }; /// Stores cached textures, including render-to-texture targets. class TextureCache : public ResourceCache, public Dependency { @@ -97,9 +97,8 @@ public: Texture(); ~Texture(); - //const gpu::TexturePointer& getGPUTexture() const { return _gpuTexture; } - const gpu::TexturePointer getGPUTexture() const { return _textureStorage->getGPUTexture(); } - model::TextureStoragePointer _textureStorage; + const gpu::TexturePointer getGPUTexture() const { return _textureSource->getGPUTexture(); } + model::TextureSourcePointer _textureSource; protected: @@ -118,18 +117,10 @@ public: NetworkTexture(const QUrl& url, TextureType type, const QByteArray& content); NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); - /// Checks whether it "looks like" this texture is translucent - /// (majority of pixels neither fully opaque or fully transparent). - // bool isTranslucent() const { return _translucent; } - - /// Returns the lazily-computed average texture color. - // const QColor& getAverageColor() const { return _averageColor; } - int getOriginalWidth() const { return _originalWidth; } int getOriginalHeight() const { return _originalHeight; } int getWidth() const { return _width; } int getHeight() const { return _height; } - // TextureType getType() const { return _type; } TextureLoaderFunc getTextureLoader() const; @@ -139,8 +130,7 @@ protected: Q_INVOKABLE void loadContent(const QByteArray& content); // FIXME: This void* should be a gpu::Texture* but i cannot get it to work for now, moving on... - Q_INVOKABLE void setImage(const QImage& image, void* texture, /*bool translucent, const QColor& averageColor, */int originalWidth, - int originalHeight); + Q_INVOKABLE void setImage(const QImage& image, void* texture, int originalWidth, int originalHeight); virtual void imageLoaded(const QImage& image); @@ -148,8 +138,6 @@ protected: private: TextureLoaderFunc _textureLoader; - bool _translucent; - QColor _averageColor; int _originalWidth; int _originalHeight; int _width; diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 70e855bb15..f055f5e6fd 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -19,25 +19,25 @@ using namespace model; using namespace gpu; -// TextureStorage -TextureStorage::TextureStorage() +// TextureSource +TextureSource::TextureSource() {/* : Texture::Storage()//, // _gpuTexture(Texture::createFromStorage(this))*/ } -TextureStorage::~TextureStorage() { +TextureSource::~TextureSource() { } -void TextureStorage::reset(const QUrl& url, const TextureUsage& usage) { +void TextureSource::reset(const QUrl& url, const TextureUsage& usage) { _imageUrl = url; _usage = usage; } -void TextureStorage::resetTexture(gpu::Texture* texture) { +void TextureSource::resetTexture(gpu::Texture* texture) { _gpuTexture.reset(texture); } -bool TextureStorage::isDefined() const { +bool TextureSource::isDefined() const { if (_gpuTexture) { return _gpuTexture->isDefined(); } else { @@ -46,21 +46,21 @@ bool TextureStorage::isDefined() const { } -void TextureMap::setTextureStorage(TextureStoragePointer& texStorage) { - _textureStorage = texStorage; +void TextureMap::setTextureSource(TextureSourcePointer& texStorage) { + _textureSource = texStorage; } bool TextureMap::isDefined() const { - if (_textureStorage) { - return _textureStorage->isDefined(); + if (_textureSource) { + return _textureSource->isDefined(); } else { return false; } } gpu::TextureView TextureMap::getTextureView() const { - if (_textureStorage) { - return gpu::TextureView(_textureStorage->getGPUTexture(), 0); + if (_textureSource) { + return gpu::TextureView(_textureSource->getGPUTexture(), 0); } else { return gpu::TextureView(); } @@ -78,7 +78,7 @@ void TextureMap::setLightmapOffsetScale(float offset, float scale) { -gpu::Texture* TextureStorage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { +gpu::Texture* TextureSource::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { QImage image = srcImage; int imageArea = image.width() * image.height(); @@ -145,7 +145,7 @@ gpu::Texture* TextureStorage::create2DTextureFromImage(const QImage& srcImage, c if ((image.width() > 0) && (image.height() > 0)) { // bool isLinearRGB = true; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); - bool isLinearRGB = false; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); + bool isLinearRGB = true; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); @@ -163,6 +163,94 @@ gpu::Texture* TextureStorage::create2DTextureFromImage(const QImage& srcImage, c return theTexture; } +int clampPixelCoordinate(int coordinate, int maxCoordinate) { + return coordinate - ((int)(coordinate < 0) * coordinate) + ((int)(coordinate > maxCoordinate) * (maxCoordinate - coordinate)); +} + +const int RGBA_MAX = 255; + +// transform -1 - 1 to 0 - 255 (from sobel value to rgb) +double mapComponent(double sobelValue) { + const double factor = RGBA_MAX / 2.0; + return (sobelValue + 1.0) * factor; +} + +gpu::Texture* TextureSource::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) { + QImage image = srcImage; + + // PR 5540 by AlessandroSigna + // integrated here as a specialized TextureLoader for bumpmaps + // The conversion is done using the Sobel Filter to calculate the derivatives from the grayscale image + const double pStrength = 2.0; + int width = image.width(); + int height = image.height(); + QImage result(width, height, image.format()); + + for (int i = 0; i < width; i++) { + const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); + const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); + + for (int j = 0; j < height; j++) { + const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); + const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); + + // surrounding pixels + const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); + const QRgb top = image.pixel(iPrevClamped, j); + const QRgb topRight = image.pixel(iPrevClamped, jNextClamped); + const QRgb right = image.pixel(i, jNextClamped); + const QRgb bottomRight = image.pixel(iNextClamped, jNextClamped); + const QRgb bottom = image.pixel(iNextClamped, j); + const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); + const QRgb left = image.pixel(i, jPrevClamped); + + // take their gray intensities + // since it's a grayscale image, the value of each component RGB is the same + const double tl = qRed(topLeft); + const double t = qRed(top); + const double tr = qRed(topRight); + const double r = qRed(right); + const double br = qRed(bottomRight); + const double b = qRed(bottom); + const double bl = qRed(bottomLeft); + const double l = qRed(left); + + // apply the sobel filter + const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); + const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); + const double dZ = RGBA_MAX / pStrength; + + glm::vec3 v(dX, dY, dZ); + glm::normalize(v); + + // convert to rgb from the value obtained computing the filter + QRgb qRgbValue = qRgb(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z)); + result.setPixel(i, j, qRgbValue); + } + } + + gpu::Texture* theTexture = nullptr; + if ((image.width() > 0) && (image.height() > 0)) { + + // bool isLinearRGB = true; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); + bool isLinearRGB = true; //(_type == NORMAL_TEXTURE) || (_type == EMISSIVE_TEXTURE); + + gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); + gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::UINT8, (isLinearRGB ? gpu::RGB : gpu::SRGB)); + if (image.hasAlphaChannel()) { + formatGPU = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::RGBA : gpu::SRGBA)); + formatMip = gpu::Element(gpu::VEC4, gpu::UINT8, (isLinearRGB ? gpu::BGRA : gpu::SBGRA)); + } + + + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + theTexture->autoGenerateMips(-1); + } + + return theTexture; +} + class CubeLayout { public: int _widthRatio = 1; @@ -197,7 +285,7 @@ public: _faceZNeg(fZN) {} }; -gpu::Texture* TextureStorage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { +gpu::Texture* TextureSource::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { QImage image = srcImage; int imageArea = image.width() * image.height(); diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index cb5f510043..197172358b 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -32,13 +32,13 @@ public: int _environmentUsage = 0; }; -// TextureStorage is a specialized version of the gpu::Texture::Storage +// TextureSource is a specialized version of the gpu::Texture::Storage // It provides the mechanism to create a texture from a Url and the intended usage // that guides the internal format used -class TextureStorage { +class TextureSource { public: - TextureStorage(); - ~TextureStorage(); + TextureSource(); + ~TextureSource(); const QUrl& getUrl() const { return _imageUrl; } gpu::Texture::Type getType() const { return _usage._type; } @@ -51,6 +51,7 @@ public: bool isDefined() const; static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); + static gpu::Texture* createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createCubeTextureFromImage(const QImage& image, const std::string& srcImageName); protected: @@ -58,7 +59,7 @@ protected: TextureUsage _usage; QUrl _imageUrl; }; -typedef std::shared_ptr< TextureStorage > TextureStoragePointer; +typedef std::shared_ptr< TextureSource > TextureSourcePointer; @@ -66,7 +67,7 @@ class TextureMap { public: TextureMap() {} - void setTextureStorage(TextureStoragePointer& texStorage); + void setTextureSource(TextureSourcePointer& texStorage); bool isDefined() const; gpu::TextureView getTextureView() const; @@ -78,7 +79,7 @@ public: const glm::vec2& getLightmapOffsetScale() const { return _lightmapOffsetScale; } protected: - TextureStoragePointer _textureStorage; + TextureSourcePointer _textureSource; Transform _texcoordTransform; glm::vec2 _lightmapOffsetScale{ 0.0f, 1.0f }; @@ -87,5 +88,5 @@ typedef std::shared_ptr< TextureMap > TextureMapPointer; }; -#endif // hifi_model_TextureStorage_h +#endif // hifi_model_TextureMap_h diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 40857dedf2..77f9b2cece 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2072,17 +2072,17 @@ static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const networkMaterial->diffuseTextureName = material.diffuseTexture.name; auto diffuseMap = model::TextureMapPointer(new model::TextureMap()); - diffuseMap->setTextureStorage(networkMaterial->diffuseTexture->_textureStorage); + diffuseMap->setTextureSource(networkMaterial->diffuseTexture->_textureSource); diffuseMap->setTextureTransform(material.diffuseTexture.transform); material._material->setTextureMap(model::MaterialKey::DIFFUSE_MAP, diffuseMap); } if (!material.normalTexture.filename.isEmpty()) { - networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), NORMAL_TEXTURE, material.normalTexture.content); + networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE), material.normalTexture.content); networkMaterial->normalTextureName = material.normalTexture.name; auto normalMap = model::TextureMapPointer(new model::TextureMap()); - normalMap->setTextureStorage(networkMaterial->normalTexture->_textureStorage); + normalMap->setTextureSource(networkMaterial->normalTexture->_textureSource); material._material->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap); } @@ -2091,7 +2091,7 @@ static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const networkMaterial->specularTextureName = material.specularTexture.name; auto glossMap = model::TextureMapPointer(new model::TextureMap()); - glossMap->setTextureStorage(networkMaterial->specularTexture->_textureStorage); + glossMap->setTextureSource(networkMaterial->specularTexture->_textureSource); material._material->setTextureMap(model::MaterialKey::GLOSS_MAP, glossMap); } @@ -2102,7 +2102,7 @@ static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const checkForTexcoordLightmap = true; auto lightmapMap = model::TextureMapPointer(new model::TextureMap()); - lightmapMap->setTextureStorage(networkMaterial->emissiveTexture->_textureStorage); + lightmapMap->setTextureSource(networkMaterial->emissiveTexture->_textureSource); lightmapMap->setTextureTransform(material.emissiveTexture.transform); lightmapMap->setLightmapOffsetScale(material.emissiveParams.x, material.emissiveParams.y); From 2875cb99bb9dc7991aec976475c7acbe48168cca Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 09:47:00 -0700 Subject: [PATCH 125/192] Fixing the compilation issue --- libraries/model/src/model/TextureMap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index f055f5e6fd..0dd7bca754 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -489,7 +489,7 @@ gpu::Texture* TextureSource::createCubeTextureFromImage(const QImage& srcImage, faces.push_back(image.copy(QRect(layout._faceZNeg._x * faceWidth, layout._faceZNeg._y * faceWidth, faceWidth, faceWidth)).mirrored(layout._faceZNeg._horizontalMirror, layout._faceZNeg._verticalMirror)); } else { qCDebug(modelLog) << "Failed to find a known cube map layout from this image:" << QString(srcImageName.c_str()); - return; + return nullptr; } // If the 6 faces have been created go on and define the true Texture From 52f6fe1d041912321eed3aa8e56dfdc2819a702c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Sep 2015 09:59:00 -0700 Subject: [PATCH 126/192] Clear entity list when you change domains --- examples/edit.js | 5 +++++ examples/html/entityList.html | 4 +++- examples/libraries/entityList.js | 9 ++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/examples/edit.js b/examples/edit.js index d778ff324d..d28d51df9d 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -245,6 +245,10 @@ var toolBar = (function () { that.setActive(false); } + that.clearEntityList = function() { + entityListTool.clearEntityList(); + }; + that.setActive = function(active) { if (active != isActive) { if (active && !Entities.canAdjustLocks()) { @@ -510,6 +514,7 @@ var toolBar = (function () { Window.domainChanged.connect(function() { that.setActive(false); + that.clearEntityList(); }); Entities.canAdjustLocksChanged.connect(function(canAdjustLocks) { diff --git a/examples/html/entityList.html b/examples/html/entityList.html index a1ba167652..3a1eeedf95 100644 --- a/examples/html/entityList.html +++ b/examples/html/entityList.html @@ -201,7 +201,9 @@ EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - if (data.type == "selectionUpdate") { + if (data.type === "clearEntityList") { + clearEntities(); + } else if (data.type == "selectionUpdate") { var notFound = updateSelectedEntities(data.selectedIDs); if (notFound) { refreshEntities(); diff --git a/examples/libraries/entityList.js b/examples/libraries/entityList.js index 66dc9f336f..bab256dd5b 100644 --- a/examples/libraries/entityList.js +++ b/examples/libraries/entityList.js @@ -26,13 +26,20 @@ EntityListTool = function(opts) { selectedIDs.push(selectionManager.selections[i]); } - data = { + var data = { type: 'selectionUpdate', selectedIDs: selectedIDs, }; webView.eventBridge.emitScriptEvent(JSON.stringify(data)); }); + that.clearEntityList = function () { + var data = { + type: 'clearEntityList' + } + webView.eventBridge.emitScriptEvent(JSON.stringify(data)); + }; + that.sendUpdate = function() { var entities = []; var ids = Entities.findEntities(MyAvatar.position, searchRadius); From 5ebc8d603644f2d21748fce7be97573ebb08ab21 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 21 Sep 2015 10:24:11 -0700 Subject: [PATCH 127/192] fix for file upload to ATP on Windows --- interface/src/ui/AssetUploadDialogFactory.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/AssetUploadDialogFactory.cpp b/interface/src/ui/AssetUploadDialogFactory.cpp index 175fc9a12d..a4426d0c1a 100644 --- a/interface/src/ui/AssetUploadDialogFactory.cpp +++ b/interface/src/ui/AssetUploadDialogFactory.cpp @@ -39,13 +39,13 @@ void AssetUploadDialogFactory::showDialog() { auto nodeList = DependencyManager::get(); if (nodeList->getThisNodeCanRez()) { - auto filename = QFileDialog::getOpenFileUrl(_dialogParent, "Select a file to upload"); + auto filename = QFileDialog::getOpenFileName(_dialogParent, "Select a file to upload"); if (!filename.isEmpty()) { qDebug() << "Selected filename for upload to asset-server: " << filename; auto assetClient = DependencyManager::get(); - auto upload = assetClient->createUpload(filename.path()); + auto upload = assetClient->createUpload(filename); if (upload) { // connect to the finished signal so we know when the AssetUpload is done @@ -56,7 +56,7 @@ void AssetUploadDialogFactory::showDialog() { } else { // show a QMessageBox to say that there is no local asset server QString messageBoxText = QString("Could not upload \n\n%1\n\nbecause you are currently not connected" \ - " to a local asset-server.").arg(QFileInfo(filename.toString()).fileName()); + " to a local asset-server.").arg(QFileInfo(filename).fileName()); QMessageBox::information(_dialogParent, "Failed to Upload", messageBoxText); } From 8b701b8a5b75f7f897ee5bc101e9829afbf59c6a Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 21 Sep 2015 10:26:04 -0700 Subject: [PATCH 128/192] Provide a place in the advanced avatar section of preferences in which users can specify a different animation graph spec. Takes effect when you save (if you have enabled the anim graph under developer->avatar). An empty string gives you the default behavior. --- .../defaultAvatar_full/avatar-animation.json | 587 ++++++++++++++++++ interface/src/avatar/MyAvatar.cpp | 4 +- interface/src/avatar/MyAvatar.h | 3 + interface/src/ui/PreferencesDialog.cpp | 11 +- interface/ui/preferencesDialog.ui | 65 ++ 5 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 interface/resources/meshes/defaultAvatar_full/avatar-animation.json diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json new file mode 100644 index 0000000000..9c357ac845 --- /dev/null +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -0,0 +1,587 @@ +{ + "version": "1.0", + "root": { + "id": "ikOverlay", + "type": "overlay", + "data": { + "alpha": 1.0, + "boneSet": "fullBody" + }, + "children": [ + { + "id": "ik", + "type": "inverseKinematics", + "data": { + "targets": [ + { + "jointName": "RightHand", + "positionVar": "rightHandPosition", + "rotationVar": "rightHandRotation" + }, + { + "jointName": "LeftHand", + "positionVar": "leftHandPosition", + "rotationVar": "leftHandRotation" + }, + { + "jointName": "Head", + "positionVar": "headPosition", + "rotationVar": "headRotation" + } + ] + }, + "children": [] + }, + { + "id": "manipulatorOverlay", + "type": "overlay", + "data": { + "alpha": 1.0, + "boneSet": "spineOnly" + }, + "children": [ + { + "id": "spineLean", + "type": "manipulator", + "data": { + "alpha": 1.0, + "joints": [ + { "var": "lean", "jointName": "Spine" } + ] + }, + "children": [] + }, + { + "id": "rightHandOverlay", + "type": "overlay", + "data": { + "alpha": 1.0, + "boneSet": "rightHand", + "alphaVar": "rightHandOverlayAlpha" + }, + "children": [ + { + "id": "rightHandStateMachine", + "type": "stateMachine", + "data": { + "currentState": "rightHandIdle", + "states": [ + { + "id": "rightHandIdle", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandPoint", "state": "rightHandPointIntro" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" } + ] + }, + { + "id": "rightHandPointIntro", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandIdle", "state": "rightHandIdle" }, + { "var": "isRightHandPointIntroOnDone", "state": "rightHandPointHold" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" } + ] + }, + { + "id": "rightHandPointHold", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandIdle", "state": "rightHandPointOutro" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" } + ] + }, + { + "id": "rightHandPointOutro", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandPointOutroOnDone", "state": "rightHandIdle" }, + { "var": "isRightHandGrab", "state": "rightHandGrab" }, + { "var": "isRightHandPoint", "state": "rightHandPointHold" } + ] + }, + { + "id": "rightHandGrab", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandIdle", "state": "rightHandIdle" }, + { "var": "isRightHandPoint_DISABLED", "state": "rightHandPointHold" } + ] + } + ] + }, + "children": [ + { + "id": "rightHandIdle", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_right_hand.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightHandPointHold", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_right_hand.fbx", + "startFrame": 12.0, + "endFrame": 12.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightHandPointIntro", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_right_hand.fbx", + "startFrame": 0.0, + "endFrame": 12.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "rightHandPointOutro", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_right_hand.fbx", + "startFrame": 0.0, + "endFrame": 65.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "rightHandGrab", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGrabBlend" + }, + "children": [ + { + "id": "rightHandOpen", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_right_hand.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightHandClose", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/right_hand_anim.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] + }, + { + "id": "leftHandOverlay", + "type": "overlay", + "data": { + "alpha": 1.0, + "boneSet": "leftHand", + "alphaVar" : "leftHandOverlay" + }, + "children": [ + { + "id": "leftHandStateMachine", + "type": "stateMachine", + "data": { + "currentState": "leftHandIdle", + "states": [ + { + "id": "leftHandIdle", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandPoint", "state": "leftHandPointIntro" }, + { "var": "isLeftHandGrab", "state": "leftHandGrab" } + ] + }, + { + "id": "leftHandPointIntro", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandIdle", "state": "leftHandIdle" }, + { "var": "isLeftHandPointIntroOnDone", "state": "leftHandPointHold" }, + { "var": "isLeftHandGrab", "state": "leftHandGrab" } + ] + }, + { + "id": "leftHandPointHold", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandIdle", "state": "leftHandPointOutro" }, + { "var": "isLeftHandGrab", "state": "leftHandGrab" } + ] + }, + { + "id": "leftHandPointOutro", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandPointOutroOnDone", "state": "leftHandIdle" }, + { "var": "isLeftHandGrab", "state": "leftHandGrab" }, + { "var": "isLeftHandPoint", "state": "leftHandPointHold" } + ] + }, + { + "id": "leftHandGrab", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandIdle", "state": "leftHandIdle" }, + { "var": "isLeftHandPoint_DISABLED", "state": "leftHandPointHold" } + ] + } + ] + }, + "children": [ + { + "id": "leftHandIdle", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_left_hand.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandPointHold", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_left_hand.fbx", + "startFrame": 12.0, + "endFrame": 12.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandPointIntro", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_left_hand.fbx", + "startFrame": 0.0, + "endFrame": 12.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "leftHandPointOutro", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_left_hand.fbx", + "startFrame": 0.0, + "endFrame": 65.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "leftHandGrab", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGrabBlend" + }, + "children": [ + { + "id": "leftHandOpen", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/point_left_hand.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandClose", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/left_hand_anim.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] + }, + { + "id": "mainStateMachine", + "type": "stateMachine", + "data": { + "currentState": "idle", + "states": [ + { + "id": "idle", + "interpTarget": 6, + "interpDuration": 6, + "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" } + ] + }, + { + "id": "walkFwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkBwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnRight", + "interpTarget": 6, + "interpDuration": 6, + "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" } + ] + }, + { + "id": "turnLeft", + "interpTarget": 6, + "interpDuration": 6, + "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" } + ] + } + ] + }, + "children": [ + { + "id": "idle", + "type": "stateMachine", + "data": { + "currentState": "idleStand", + "states": [ + { + "id": "idleStand", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isTalking", "state": "idleTalk" } + ] + }, + { + "id": "idleTalk", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "notIsTalking", "state": "idleStand" } + ] + } + ] + }, + "children": [ + { + "id": "idleStand", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "idleTalk", + "type": "clip", + "data": { + "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/talk/talk.fbx", + "startFrame": 0.0, + "endFrame": 801.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "walkFwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "walkBwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx", + "startFrame": 0.0, + "endFrame": 37.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "turnLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 28.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "turnRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } +} diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ae483988e3..dfaf3c5239 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1305,7 +1305,9 @@ void MyAvatar::initAnimGraph() { // or run a local web-server // python -m SimpleHTTPServer& //auto graphUrl = QUrl("http://localhost:8000/avatar.json"); - auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/d951c78532e7a20557ad/raw/8275a99a859bbb9b42530c1c7ebfd024e63ba250/ik-avatar-hands-idle.json"); + auto graphUrl = QUrl(_animGraphUrl.isEmpty() ? + QUrl::fromLocalFile(PathUtils::resourcesPath() + "meshes/defaultAvatar_full/avatar-animation.json") : + _animGraphUrl); _rig->initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 6989ea3969..7293b06c32 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -195,10 +195,12 @@ public slots: bool getEnableRigAnimations() const { return _rig->getEnableRig(); } void setEnableRigAnimations(bool isEnabled); bool getEnableAnimGraph() const { return _rig->getEnableAnimGraph(); } + const QString& getAnimGraphUrl() const { return _animGraphUrl; } void setEnableAnimGraph(bool isEnabled); void setEnableDebugDrawBindPose(bool isEnabled); void setEnableDebugDrawAnimPose(bool isEnabled); void setEnableMeshVisible(bool isEnabled); + void setAnimGraphUrl(const QString& url) { _animGraphUrl = url; } signals: void transformChanged(); @@ -298,6 +300,7 @@ private: // Avatar Preferences QUrl _fullAvatarURLFromPreferences; QString _fullAvatarModelName; + QString _animGraphUrl {""}; // cache of the current HMD sensor position and orientation // in sensor space. diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 37e33c07a7..81e4d61b5e 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -190,6 +190,7 @@ void PreferencesDialog::loadPreferences() { ui.leanScaleSpin->setValue(myAvatar->getLeanScale()); ui.avatarScaleSpin->setValue(myAvatar->getScale()); + ui.avatarAnimationEdit->setText(myAvatar->getAnimGraphUrl()); ui.maxOctreePPSSpin->setValue(qApp->getMaxOctreePacketsPerSecond()); @@ -248,7 +249,15 @@ void PreferencesDialog::savePreferences() { myAvatar->getHead()->setPupilDilation(ui.pupilDilationSlider->value() / (float)ui.pupilDilationSlider->maximum()); myAvatar->setLeanScale(ui.leanScaleSpin->value()); myAvatar->setClampedTargetScale(ui.avatarScaleSpin->value()); - + if (myAvatar->getAnimGraphUrl() != ui.avatarAnimationEdit->text()) { // If changed, destroy the old and start with the new + bool isEnabled = myAvatar->getEnableAnimGraph(); + myAvatar->setEnableAnimGraph(false); + myAvatar->setAnimGraphUrl(ui.avatarAnimationEdit->text()); + if (isEnabled) { + myAvatar->setEnableAnimGraph(true); + } + } + DependencyManager::get()->getMyAvatar()->setRealWorldFieldOfView(ui.realWorldFieldOfViewSpin->value()); qApp->setFieldOfView(ui.fieldOfViewSpin->value()); diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index b91367dcf1..53b71fb507 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -1587,6 +1587,71 @@ + + + + 0 + + + 7 + + + 0 + + + 7 + + + + + + Arial + + + + Avatar Animation JSON + + + avatarAnimationEdit + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + Arial + + + + Qt::LeftToRight + + + + + + default + + + + + From c2a2abe615b7ba3298547ca189403269f4eeb0de Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 11:05:51 -0700 Subject: [PATCH 129/192] Remove nested bubble entities and related code --- examples/toys/bubblewand/bubble.js | 112 ------------------------- examples/toys/bubblewand/createWand.js | 23 +++-- examples/toys/bubblewand/wand.js | 19 +---- 3 files changed, 12 insertions(+), 142 deletions(-) delete mode 100644 examples/toys/bubblewand/bubble.js diff --git a/examples/toys/bubblewand/bubble.js b/examples/toys/bubblewand/bubble.js deleted file mode 100644 index b6356d4025..0000000000 --- a/examples/toys/bubblewand/bubble.js +++ /dev/null @@ -1,112 +0,0 @@ -// bubble.js -// part of bubblewand -// -// Script Type: Entity -// Created by James B. Pollack @imgntn -- 09/03/2015 -// Copyright 2015 High Fidelity, Inc. -// -// example of a nested entity. plays a particle burst at the location where its deleted. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -(function() { - Script.include("../../utilities.js"); - Script.include("../../libraries/utils.js"); - - var BUBBLE_PARTICLE_TEXTURE = "http://hifi-public.s3.amazonaws.com/james/bubblewand/textures/bubble_particle.png" - - var BUBBLE_USER_DATA_KEY = "BubbleKey"; - - var _this = this; - - var properties; - - this.preload = function(entityID) { - _this.entityID = entityID; - Script.update.connect(_this.update); - }; - - this.update = function() { - // we want the position at unload but for some reason it keeps getting set to 0,0,0 -- so i just exclude that location. sorry origin bubbles. - var tmpProperties = Entities.getEntityProperties(_this.entityID); - if (tmpProperties.position.x !== 0 && tmpProperties.position.y !== 0 && tmpProperties.position.z !== 0) { - properties = tmpProperties; - } - - //we want to play the particle burst exactly once, so we make sure that this is a bubble we own. - var entityData = getEntityCustomData(BUBBLE_USER_DATA_KEY, _this.entityID); - - if (entityData && entityData.avatarID && entityData.avatarID === MyAvatar.sessionUUID) { - _this.bubbleCreator = true - } - - }; - - this.unload = function(entityID) { - Script.update.disconnect(this.update); - - //only play particle burst for our bubbles - if (this.bubbleCreator) { - this.createBurstParticles(); - } - - }; - - - this.createBurstParticles = function() { - //get the current position and dimensions of the bubble - var position = properties.position; - var dimensions = properties.dimensions; - - - var animationSettings = JSON.stringify({ - fps: 30, - frameIndex: 0, - running: true, - firstFrame: 0, - lastFrame: 30, - loop: false - }); - - var particleBurst = Entities.addEntity({ - type: "ParticleEffect", - animationSettings: animationSettings, - emitRate: 100, - animationIsPlaying: true, - position: position, - lifespan: 0.2, - dimensions: { - x: 1, - y: 1, - z: 1 - }, - emitVelocity: { - x: 1, - y: 1, - z: 1 - }, - velocitySpread: { - x: 1, - y: 1, - z: 1 - }, - emitAcceleration: { - x: 0.25, - y: 0.25, - z: 0.25 - }, - radiusSpread: 0.01, - particleRadius: 0.02, - alphaStart: 1.0, - alpha: 0.5, - alphaFinish: 0, - textures: BUBBLE_PARTICLE_TEXTURE, - visible: true, - locked: false - }); - - }; - - -}); \ No newline at end of file diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index c99f648e04..9ca018ea16 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -11,31 +11,28 @@ var IN_TOYBOX = false; -Script.include("../../utilities.js"); +Script.include("../../utilities.js"); Script.include("../../libraries/utils.js"); - var WAND_MODEL = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/wand.fbx'; var WAND_COLLISION_SHAPE = 'http://hifi-public.s3.amazonaws.com/james/bubblewand/models/wand/collisionHull.obj'; var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); -//create the wand in front of the avatar blahy -var center = Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0, y: 0.5, z: 0}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); -var tablePosition = { - x:546.48, - y:495.63, - z:506.25 -} +//create the wand in front of the avatar +var center = Vec3.sum(Vec3.sum(MyAvatar.position, { + x: 0, + y: 0.5, + z: 0 +}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); var wand = Entities.addEntity({ - name:'Bubble Wand', + name: 'Bubble Wand', type: "Model", modelURL: WAND_MODEL, - position: IN_TOYBOX? tablePosition: center, + position: center, gravity: { x: 0, - y:0, - // y: -9.8, + y: -9.8, z: 0, }, dimensions: { diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 67b5b43573..064d045f35 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -17,9 +17,7 @@ Script.include("../../libraries/utils.js"); var BUBBLE_MODEL = "http://hifi-public.s3.amazonaws.com/james/bubblewand/models/bubble/bubble.fbx"; - var BUBBLE_SCRIPT = Script.resolvePath('bubble.js'); - var BUBBLE_USER_DATA_KEY = "BubbleKey"; var BUBBLE_INITIAL_DIMENSIONS = { x: 0.01, y: 0.01, @@ -47,7 +45,6 @@ } BubbleWand.prototype = { - bubbles: [], currentBubble: null, preload: function(entityID) { this.entityID = entityID; @@ -129,11 +126,9 @@ var wandPosition = properties.position; var wandTipPosition = this.getWandTipPosition(properties) - var distance = Vec3.subtract(wandPosition, this.lastPosition); var velocity = Vec3.multiply(distance, 1 / deltaTime); - var velocityStrength = Vec3.length(velocity); velocityStrength = velocityStrength; @@ -163,9 +158,6 @@ //wait to make the bubbles collidable, so that they dont hit each other and the wand Script.setTimeout(this.addCollisionsToBubbleAfterCreation(this.currentBubble), lifetime / 2); - //we want to pop the bubble for just one person - this.setBubbleOwner(this.currentBubble); - //release the bubble -- when we create a new bubble, it will carry on and this update loop will affect the new bubble this.createBubbleAtTipOfWand(); return @@ -192,11 +184,6 @@ dimensions: dimensions }); }, - setBubbleOwner: function(bubble) { - setEntityCustomData(BUBBLE_USER_DATA_KEY, bubble, { - avatarID: MyAvatar.sessionUUID, - }); - }, createBubbleAtTipOfWand: function() { //create a new bubble at the tip of the wand @@ -219,11 +206,9 @@ collisionsWillMove: false, ignoreForCollisions: false, linearDamping: BUBBLE_LINEAR_DAMPING, - shapeType: "sphere", - script: BUBBLE_SCRIPT, + shapeType: "sphere" }); - //add this bubble to an array of bubbles so we can keep track of them - this.bubbles.push(this.currentBubble) + } From f3eef9322c73824a1f48d65c80a42f17e196c07c Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 11:10:42 -0700 Subject: [PATCH 130/192] Remove toybox reference in spawner script --- examples/toys/bubblewand/createWand.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 9ca018ea16..76681a50d7 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -9,8 +9,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -var IN_TOYBOX = false; - Script.include("../../utilities.js"); Script.include("../../libraries/utils.js"); From b88c8e507b56985d34da33774839c88691563d3f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 21 Sep 2015 11:35:26 -0700 Subject: [PATCH 131/192] fix a couple of properties --- libraries/entities/src/EntityItemProperties.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 65b41b5734..13138e5c10 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -498,18 +498,21 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool // Models only if (_type == EntityTypes::Model) { COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_URL, modelURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_URL, animationURL); - COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); } - if (_type == EntityTypes::Model || _type == EntityTypes::Zone || _type == EntityTypes::ParticleEffect) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString()); } + // FIXME - it seems like ParticleEffect should also support this + if (_type == EntityTypes::Model || _type == EntityTypes::Zone) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); + } + // Models & Particles if (_type == EntityTypes::Model || _type == EntityTypes::ParticleEffect) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_PLAYING, animationIsPlaying); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FPS, animationFPS); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FRAME_INDEX, animationFrameIndex); From e21c1cb67cbb94fd9efb627457b9159ba633d180 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 21 Sep 2015 11:51:53 -0700 Subject: [PATCH 132/192] make sure server/mixers are first in FIFO --- domain-server/src/DomainServer.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 49b7f2e183..7a57780fd1 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1744,9 +1744,22 @@ void DomainServer::addStaticAssignmentsToQueue() { // if the domain-server has just restarted, // check if there are static assignments that we need to throw into the assignment queue - QHash staticHashCopy = _allAssignments; - QHash::iterator staticAssignment = staticHashCopy.begin(); - while (staticAssignment != staticHashCopy.end()) { + auto sharedAssignments = _allAssignments.values(); + + // sort the assignments to put the server/mixer assignments first + qSort(sharedAssignments.begin(), sharedAssignments.end(), [](SharedAssignmentPointer a, SharedAssignmentPointer b){ + if (a->getType() == b->getType()) { + return true; + } else if (a->getType() != Assignment::AgentType && b->getType() != Assignment::AgentType) { + return a->getType() < b->getType(); + } else { + return a->getType() != Assignment::AgentType; + } + }); + + auto staticAssignment = sharedAssignments.begin(); + + while (staticAssignment != sharedAssignments.end()) { // add any of the un-matched static assignments to the queue // enumerate the nodes and check if there is one with an attached assignment with matching UUID From 030404157e9f144c8d723ba102026568e68f588e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 21 Sep 2015 11:54:41 -0700 Subject: [PATCH 133/192] cleanup Assignment grab from iterator --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7a57780fd1..614b0a1528 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1763,7 +1763,7 @@ void DomainServer::addStaticAssignmentsToQueue() { // add any of the un-matched static assignments to the queue // enumerate the nodes and check if there is one with an attached assignment with matching UUID - if (!DependencyManager::get()->nodeWithUUID(staticAssignment->data()->getUUID())) { + if (!DependencyManager::get()->nodeWithUUID((*staticAssignment)->getUUID())) { // this assignment has not been fulfilled - reset the UUID and add it to the assignment queue refreshStaticAssignmentAndAddToQueue(*staticAssignment); } From dc9c774eb541439ea5064843abb39d8979987ab2 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 21 Sep 2015 13:09:43 -0700 Subject: [PATCH 134/192] merge model part quads and triangles together to reduce the number of draw calls --- interface/resources/qml/Stats.qml | 2 +- interface/src/ui/Stats.cpp | 1 - libraries/fbx/src/FBXReader.cpp | 53 ++++++++++++++++-------- libraries/fbx/src/FBXReader.h | 10 ++--- libraries/render-utils/src/Model.cpp | 61 ++-------------------------- libraries/render-utils/src/Model.h | 4 -- libraries/shared/src/RenderArgs.h | 1 - 7 files changed, 46 insertions(+), 86 deletions(-) diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index c3829c7be2..eb39fbc70f 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -168,7 +168,7 @@ Item { color: root.fontColor; font.pixelSize: root.fontSize text: "Triangles: " + root.triangles + - " / Quads: " + root.quads + " / Material Switches: " + root.materialSwitches + " / Material Switches: " + root.materialSwitches } Text { color: root.fontColor; diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 8ddb767537..4dd552210c 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -337,7 +337,6 @@ void Stats::updateStats() { void Stats::setRenderDetails(const RenderDetails& details) { STAT_UPDATE(triangles, details._trianglesRendered); - STAT_UPDATE(quads, details._quadsRendered); STAT_UPDATE(materialSwitches, details._materialSwitches); if (_expanded) { STAT_UPDATE(meshOpaque, details._opaque._rendered); diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 6f69e8befc..2fbca10915 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -829,24 +829,30 @@ public: std::vector attributes; }; -gpu::BufferPointer FBXMeshPart::getTrianglesForQuads() const { +gpu::BufferPointer FBXMeshPart::getMergedTriangles() const { // if we've been asked for our triangulation of the original quads, but we don't yet have them // then create them now. - if (!trianglesForQuadsAvailable) { - trianglesForQuadsAvailable = true; + if (!mergedTrianglesAvailable) { + mergedTrianglesAvailable = true; - quadsAsTrianglesIndicesBuffer = std::make_shared(); + mergedTrianglesIndicesBuffer = std::make_shared(); // QVector quadIndices; // original indices from the FBX mesh - QVector quadsAsTrianglesIndices; // triangle versions of quads converted when first needed + QVector mergedTrianglesIndices; // triangle versions of quads converted when first needed + const int INDICES_PER_ORIGINAL_TRIANGLE = 3; const int INDICES_PER_ORIGINAL_QUAD = 4; const int INDICES_PER_TRIANGULATED_QUAD = 6; int numberOfQuads = quadIndices.size() / INDICES_PER_ORIGINAL_QUAD; - - quadsAsTrianglesIndices.resize(numberOfQuads * INDICES_PER_TRIANGULATED_QUAD); + int numberOfTriangles = triangleIndices.size() / INDICES_PER_ORIGINAL_TRIANGLE; + int mergedNumberOfIndices = (numberOfQuads * INDICES_PER_TRIANGULATED_QUAD) + triangleIndices.size(); + + // resized our merged indices to be enough room for our triangulated quads and our original triangles + mergedTrianglesIndices.resize(mergedNumberOfIndices); int originalIndex = 0; int triangulatedIndex = 0; + + // triangulate our quads for (int fromQuad = 0; fromQuad < numberOfQuads; fromQuad++) { int i0 = quadIndices[originalIndex + 0]; int i1 = quadIndices[originalIndex + 1]; @@ -860,23 +866,38 @@ gpu::BufferPointer FBXMeshPart::getTrianglesForQuads() const { // Triangle tri1 = { v0, v1, v2 }; // Triangle tri2 = { v2, v3, v0 }; - quadsAsTrianglesIndices[triangulatedIndex + 0] = i0; - quadsAsTrianglesIndices[triangulatedIndex + 1] = i1; - quadsAsTrianglesIndices[triangulatedIndex + 2] = i3; + mergedTrianglesIndices[triangulatedIndex + 0] = i0; + mergedTrianglesIndices[triangulatedIndex + 1] = i1; + mergedTrianglesIndices[triangulatedIndex + 2] = i3; - quadsAsTrianglesIndices[triangulatedIndex + 3] = i1; - quadsAsTrianglesIndices[triangulatedIndex + 4] = i2; - quadsAsTrianglesIndices[triangulatedIndex + 5] = i3; + mergedTrianglesIndices[triangulatedIndex + 3] = i1; + mergedTrianglesIndices[triangulatedIndex + 4] = i2; + mergedTrianglesIndices[triangulatedIndex + 5] = i3; originalIndex += INDICES_PER_ORIGINAL_QUAD; triangulatedIndex += INDICES_PER_TRIANGULATED_QUAD; } - trianglesForQuadsIndicesCount = INDICES_PER_TRIANGULATED_QUAD * numberOfQuads; - quadsAsTrianglesIndicesBuffer->append(quadsAsTrianglesIndices.size() * sizeof(quint32), (gpu::Byte*)quadsAsTrianglesIndices.data()); + // add our original triangs + originalIndex = 0; + for (int fromTriangle = 0; fromTriangle < numberOfTriangles; fromTriangle++) { + int i0 = triangleIndices[originalIndex + 0]; + int i1 = triangleIndices[originalIndex + 1]; + int i2 = triangleIndices[originalIndex + 2]; + + mergedTrianglesIndices[triangulatedIndex + 0] = i0; + mergedTrianglesIndices[triangulatedIndex + 1] = i1; + mergedTrianglesIndices[triangulatedIndex + 2] = i2; + + originalIndex += INDICES_PER_ORIGINAL_TRIANGLE; + triangulatedIndex += INDICES_PER_ORIGINAL_TRIANGLE; + } + + mergedTrianglesIndicesCount = mergedNumberOfIndices; + mergedTrianglesIndicesBuffer->append(mergedNumberOfIndices * sizeof(quint32), (gpu::Byte*)mergedTrianglesIndices.data()); } - return quadsAsTrianglesIndicesBuffer; + return mergedTrianglesIndicesBuffer; } void appendIndex(MeshData& data, QVector& indices, int index) { diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index cff22676c8..f2fd9fdd15 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -117,10 +117,10 @@ public: /// A single part of a mesh (with the same material). class FBXMeshPart { public: - + QVector quadIndices; // original indices from the FBX mesh QVector triangleIndices; // original indices from the FBX mesh - mutable gpu::BufferPointer quadsAsTrianglesIndicesBuffer; + mutable gpu::BufferPointer mergedTrianglesIndicesBuffer; // both the quads and the triangles merged into a single set of triangles glm::vec3 diffuseColor; glm::vec3 specularColor; @@ -136,10 +136,10 @@ public: QString materialID; model::MaterialPointer _material; - mutable bool trianglesForQuadsAvailable = false; - mutable int trianglesForQuadsIndicesCount = 0; + mutable bool mergedTrianglesAvailable = false; + mutable int mergedTrianglesIndicesCount = 0; - gpu::BufferPointer getTrianglesForQuads() const; + gpu::BufferPointer getMergedTriangles() const; }; /// A single mesh (with optional blendshapes) extracted from an FBX document. diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index a4dc1b1de3..a7910a7857 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -75,7 +75,6 @@ Model::Model(RigPointer rig, QObject* parent) : _isVisible(true), _blendNumber(0), _appliedBlendNumber(0), - _calculatedMeshPartOffsetValid(false), _calculatedMeshPartBoxesValid(false), _calculatedMeshBoxesValid(false), _calculatedMeshTrianglesValid(false), @@ -601,25 +600,6 @@ bool Model::convexHullContains(glm::vec3 point) { return false; } -void Model::recalculateMeshPartOffsets() { - if (!_calculatedMeshPartOffsetValid) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - int numberOfMeshes = geometry.meshes.size(); - _calculatedMeshPartOffset.clear(); - for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& mesh = geometry.meshes.at(i); - qint64 partOffset = 0; - for (int j = 0; j < mesh.parts.size(); j++) { - const FBXMeshPart& part = mesh.parts.at(j); - _calculatedMeshPartOffset[QPair(i, j)] = partOffset; - partOffset += part.quadIndices.size() * sizeof(int); - partOffset += part.triangleIndices.size() * sizeof(int); - - } - } - _calculatedMeshPartOffsetValid = true; - } -} // TODO: we seem to call this too often when things haven't actually changed... look into optimizing this // Any script might trigger findRayIntersectionAgainstSubMeshes (and maybe convexHullContains), so these // can occur multiple times. In addition, rendering does it's own ray picking in order to decide which @@ -636,8 +616,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { _calculatedMeshTriangles.clear(); _calculatedMeshTriangles.resize(numberOfMeshes); _calculatedMeshPartBoxes.clear(); - _calculatedMeshPartOffset.clear(); - _calculatedMeshPartOffsetValid = false; for (int i = 0; i < numberOfMeshes; i++) { const FBXMesh& mesh = geometry.meshes.at(i); Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents); @@ -646,7 +624,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { if (pickAgainstTriangles) { QVector thisMeshTriangles; - qint64 partOffset = 0; for (int j = 0; j < mesh.parts.size(); j++) { const FBXMeshPart& part = mesh.parts.at(j); @@ -732,15 +709,9 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { } } _calculatedMeshPartBoxes[QPair(i, j)] = thisPartBounds; - _calculatedMeshPartOffset[QPair(i, j)] = partOffset; - - partOffset += part.quadIndices.size() * sizeof(int); - partOffset += part.triangleIndices.size() * sizeof(int); - } _calculatedMeshTriangles[i] = thisMeshTriangles; _calculatedMeshPartBoxesValid = true; - _calculatedMeshPartOffsetValid = true; } } _calculatedMeshBoxesValid = true; @@ -1480,12 +1451,6 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran return; // bail asap } - // We need to make sure we have valid offsets calculated before we can render - if (!_calculatedMeshPartOffsetValid) { - _mutex.lock(); - recalculateMeshPartOffsets(); - _mutex.unlock(); - } auto textureCache = DependencyManager::get(); gpu::Batch& batch = *(args->_batch); @@ -1703,32 +1668,12 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran } } - qint64 offset; - { - // FIXME_STUTTER: We should n't have any lock here - _mutex.lock(); - offset = _calculatedMeshPartOffset[QPair(meshIndex, partIndex)]; - _mutex.unlock(); - } - - if (part.quadIndices.size() > 0) { - batch.setIndexBuffer(gpu::UINT32, part.getTrianglesForQuads(), 0); - batch.drawIndexed(gpu::TRIANGLES, part.trianglesForQuadsIndicesCount, 0); - - offset += part.quadIndices.size() * sizeof(int); - batch.setIndexBuffer(gpu::UINT32, (networkMesh._indexBuffer), 0); // restore this in case there are triangles too - } - - if (part.triangleIndices.size() > 0) { - batch.drawIndexed(gpu::TRIANGLES, part.triangleIndices.size(), offset); - offset += part.triangleIndices.size() * sizeof(int); - } + batch.setIndexBuffer(gpu::UINT32, part.getMergedTriangles(), 0); + batch.drawIndexed(gpu::TRIANGLES, part.mergedTrianglesIndicesCount, 0); if (args) { const int INDICES_PER_TRIANGLE = 3; - const int INDICES_PER_QUAD = 4; - args->_details._trianglesRendered += part.triangleIndices.size() / INDICES_PER_TRIANGLE; - args->_details._quadsRendered += part.quadIndices.size() / INDICES_PER_QUAD; + args->_details._trianglesRendered += part.mergedTrianglesIndicesCount / INDICES_PER_TRIANGLE; } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 93b98da8b5..fd9a9e6353 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -352,9 +352,6 @@ private: }; QHash, AABox> _calculatedMeshPartBoxes; // world coordinate AABoxes for all sub mesh part boxes - QHash, qint64> _calculatedMeshPartOffset; - bool _calculatedMeshPartOffsetValid; - bool _calculatedMeshPartBoxesValid; QVector _calculatedMeshBoxes; // world coordinate AABoxes for all sub mesh boxes @@ -365,7 +362,6 @@ private: QMutex _mutex; void recalculateMeshBoxes(bool pickAgainstTriangles = false); - void recalculateMeshPartOffsets(); void segregateMeshGroups(); // used to calculate our list of translucent vs opaque meshes diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index 7a5daf7a17..25eed96490 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -43,7 +43,6 @@ public: int _materialSwitches = 0; int _trianglesRendered = 0; - int _quadsRendered = 0; Item _opaque; Item _translucent; From d3b1bcb86d24a96333103921f27de095b4b555bb Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 13:21:35 -0700 Subject: [PATCH 135/192] Redistributing the files to create the model-networking lib and separate that from the redner-utils --- interface/CMakeLists.txt | 2 +- interface/src/Application.cpp | 14 +- interface/src/Stars.cpp | 4 +- interface/src/ui/CachesSizeDialog.cpp | 4 +- libraries/entities-renderer/CMakeLists.txt | 2 +- libraries/gpu/src/gpu/Shader.cpp | 25 + libraries/gpu/src/gpu/Shader.h | 23 + libraries/gpu/src/gpu/Texture.cpp | 25 + libraries/gpu/src/gpu/Texture.h | 24 + .../CMakeLists.txt | 2 +- .../src/model-networking/ModelCache.cpp | 506 ++++++++++++++++++ .../src/model-networking/ModelCache.h | 205 +++++++ .../ModelNetworkingLogging.cpp} | 4 +- .../ModelNetworkingLogging.h} | 2 +- .../src/model-networking}/ShaderCache.cpp | 0 .../src/model-networking}/ShaderCache.h | 0 .../src/model-networking}/TextureCache.cpp | 20 +- .../src/model-networking}/TextureCache.h | 4 +- libraries/model/CMakeLists.txt | 2 +- libraries/model/src/model/Skybox.cpp | 23 +- libraries/model/src/model/Skybox.h | 7 +- libraries/model/src/model/TextureMap.cpp | 36 +- libraries/model/src/model/TextureMap.h | 29 +- libraries/procedural/CMakeLists.txt | 4 +- .../procedural/src/procedural/Procedural.cpp | 1 - .../procedural/src/procedural/Procedural.h | 3 +- .../src/procedural/ProceduralSkybox.cpp | 71 +++ .../src/procedural/ProceduralSkybox.h | 35 ++ .../src/procedural/ProceduralSkybox.slf | 50 ++ .../src/procedural/ProceduralSkybox.slv | 34 ++ libraries/render-utils/CMakeLists.txt | 2 +- libraries/render-utils/src/GeometryCache.cpp | 469 ---------------- libraries/render-utils/src/GeometryCache.h | 165 +----- libraries/render-utils/src/TextureCache.h | 2 +- libraries/script-engine/CMakeLists.txt | 2 +- tests/gpu-test/CMakeLists.txt | 2 +- 36 files changed, 1055 insertions(+), 748 deletions(-) rename libraries/{gpu-networking => model-networking}/CMakeLists.txt (89%) create mode 100644 libraries/model-networking/src/model-networking/ModelCache.cpp create mode 100644 libraries/model-networking/src/model-networking/ModelCache.h rename libraries/{gpu-networking/src/gpu-networking/GpuNetworkingLogging.cpp => model-networking/src/model-networking/ModelNetworkingLogging.cpp} (73%) rename libraries/{gpu-networking/src/gpu-networking/GpuNetworkingLogging.h => model-networking/src/model-networking/ModelNetworkingLogging.h} (86%) rename libraries/{gpu-networking/src/gpu-networking => model-networking/src/model-networking}/ShaderCache.cpp (100%) rename libraries/{gpu-networking/src/gpu-networking => model-networking/src/model-networking}/ShaderCache.h (100%) rename libraries/{gpu-networking/src/gpu-networking => model-networking/src/model-networking}/TextureCache.cpp (94%) rename libraries/{gpu-networking/src/gpu-networking => model-networking/src/model-networking}/TextureCache.h (98%) create mode 100644 libraries/procedural/src/procedural/ProceduralSkybox.cpp create mode 100644 libraries/procedural/src/procedural/ProceduralSkybox.h create mode 100644 libraries/procedural/src/procedural/ProceduralSkybox.slf create mode 100644 libraries/procedural/src/procedural/ProceduralSkybox.slv diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 05a937eef8..e3118c0048 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -113,7 +113,7 @@ endif() target_link_libraries(${TARGET_NAME} ${BULLET_LIBRARIES}) # link required hifi libraries -link_hifi_libraries(shared octree environment gpu gpu-networking procedural model render fbx networking entities avatars +link_hifi_libraries(shared octree environment gpu procedural model render fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater plugins display-plugins input-plugins) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1b6be53e83..798b1ab9a2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -278,6 +278,7 @@ bool setupEssentials(int& argc, char** argv) { auto addressManager = DependencyManager::set(); auto nodeList = DependencyManager::set(NodeType::Agent, listenPort); auto geometryCache = DependencyManager::set(); + auto modelCache = DependencyManager::set(); auto scriptCache = DependencyManager::set(); auto soundCache = DependencyManager::set(); auto faceshift = DependencyManager::set(); @@ -418,12 +419,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // put the NodeList and datagram processing on the node thread nodeList->moveToThread(nodeThread); - // geometry background downloads need to happen on the Datagram Processor Thread. The idle loop will - // emit checkBackgroundDownloads to cause the GeometryCache to check it's queue for requested background + // Model background downloads need to happen on the Datagram Processor Thread. The idle loop will + // emit checkBackgroundDownloads to cause the ModelCache to check it's queue for requested background // downloads. - QSharedPointer geometryCacheP = DependencyManager::get(); - ResourceCache* geometryCache = geometryCacheP.data(); - connect(this, &Application::checkBackgroundDownloads, geometryCache, &ResourceCache::checkAsynchronousGets); + QSharedPointer modelCacheP = DependencyManager::get(); + ResourceCache* modelCache = modelCacheP.data(); + connect(this, &Application::checkBackgroundDownloads, modelCache, &ResourceCache::checkAsynchronousGets); // put the audio processing on a separate thread QThread* audioThread = new QThread(); @@ -892,6 +893,7 @@ Application::~Application() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); @@ -2718,7 +2720,7 @@ void Application::reloadResourceCaches() { emptyLocalCache(); DependencyManager::get()->refreshAll(); - DependencyManager::get()->refreshAll(); + DependencyManager::get()->refreshAll(); DependencyManager::get()->refreshAll(); DependencyManager::get()->refreshAll(); } diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index 119b9ed1a2..e2b64869ec 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -188,8 +188,10 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element; }); - auto geometryCache = DependencyManager::get(); + auto modelCache = DependencyManager::get(); auto textureCache = DependencyManager::get(); + auto geometryCache = DependencyManager::get(); + gpu::Batch& batch = *renderArgs->_batch; batch.setViewTransform(Transform()); diff --git a/interface/src/ui/CachesSizeDialog.cpp b/interface/src/ui/CachesSizeDialog.cpp index a29793349f..dee9452c73 100644 --- a/interface/src/ui/CachesSizeDialog.cpp +++ b/interface/src/ui/CachesSizeDialog.cpp @@ -56,7 +56,7 @@ CachesSizeDialog::CachesSizeDialog(QWidget* parent) : void CachesSizeDialog::confirmClicked(bool checked) { DependencyManager::get()->setUnusedResourceCacheSize(_animations->value() * BYTES_PER_MEGABYTES); - DependencyManager::get()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES); + DependencyManager::get()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES); DependencyManager::get()->setUnusedResourceCacheSize(_sounds->value() * BYTES_PER_MEGABYTES); DependencyManager::get()->setUnusedResourceCacheSize(_textures->value() * BYTES_PER_MEGABYTES); @@ -65,7 +65,7 @@ void CachesSizeDialog::confirmClicked(bool checked) { void CachesSizeDialog::resetClicked(bool checked) { _animations->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _geometries->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); + _geometries->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); _sounds->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); _textures->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); } diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index 3787beb32b..695ca1feb5 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -26,4 +26,4 @@ find_package(PolyVox REQUIRED) target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${POLYVOX_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${POLYVOX_LIBRARIES}) -link_hifi_libraries(shared gpu gpu-networking procedural script-engine render render-utils) +link_hifi_libraries(shared gpu procedural model model-networking script-engine render render-utils) diff --git a/libraries/gpu/src/gpu/Shader.cpp b/libraries/gpu/src/gpu/Shader.cpp index 59838fae9c..223a78ed93 100755 --- a/libraries/gpu/src/gpu/Shader.cpp +++ b/libraries/gpu/src/gpu/Shader.cpp @@ -71,3 +71,28 @@ bool Shader::makeProgram(Shader& shader, const Shader::BindingSet& bindings) { } return false; } + + +// ShaderSource +ShaderSource::ShaderSource() { +} + +ShaderSource::~ShaderSource() { +} + +void ShaderSource::reset(const QUrl& url) { + _shaderUrl = url; + _gpuShader.reset(); +} + +void ShaderSource::resetShader(gpu::Shader* shader) { + _gpuShader.reset(shader); +} + +bool ShaderSource::isDefined() const { + if (_gpuShader) { + return true; + } else { + return false; + } +} diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index 9c3953bff5..d3c3992e6d 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -15,6 +15,8 @@ #include #include #include + +#include namespace gpu { @@ -187,6 +189,27 @@ protected: typedef Shader::Pointer ShaderPointer; typedef std::vector< ShaderPointer > Shaders; +// ShaderSource is the bridge between a URL or a a way to produce the final gpu::Shader that will be used to render it. +class ShaderSource { +public: + ShaderSource(); + ~ShaderSource(); + + const QUrl& getUrl() const { return _shaderUrl; } + const gpu::ShaderPointer getGPUShader() const { return _gpuShader; } + + void reset(const QUrl& url); + + void resetShader(gpu::Shader* texture); + + bool isDefined() const; + +protected: + gpu::ShaderPointer _gpuShader; + QUrl _shaderUrl; +}; +typedef std::shared_ptr< ShaderSource > ShaderSourcePointer; + }; diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 100ad28053..57df7b8ec0 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -768,3 +768,28 @@ void SphericalHarmonics::evalFromTexture(const Texture& texture) { L22 = coefs[8]; } } + + +// TextureSource +TextureSource::TextureSource() { +} + +TextureSource::~TextureSource() { +} + +void TextureSource::reset(const QUrl& url) { + _imageUrl = url; +} + +void TextureSource::resetTexture(gpu::Texture* texture) { + _gpuTexture.reset(texture); +} + +bool TextureSource::isDefined() const { + if (_gpuTexture) { + return _gpuTexture->isDefined(); + } else { + return false; + } +} + diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 65a0439864..e1855e0848 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -15,6 +15,8 @@ #include //min max and more +#include + namespace gpu { // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated @@ -441,6 +443,28 @@ public: }; typedef std::vector TextureViews; +// TextureSource is the bridge between a URL or a a way to produce an image and the final gpu::Texture that will be used to render it. +// It provides the mechanism to create a texture using a customizable TextureLoader +class TextureSource { +public: + TextureSource(); + ~TextureSource(); + + const QUrl& getUrl() const { return _imageUrl; } + const gpu::TexturePointer getGPUTexture() const { return _gpuTexture; } + + void reset(const QUrl& url); + + void resetTexture(gpu::Texture* texture); + + bool isDefined() const; + +protected: + gpu::TexturePointer _gpuTexture; + QUrl _imageUrl; +}; +typedef std::shared_ptr< TextureSource > TextureSourcePointer; + }; diff --git a/libraries/gpu-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt similarity index 89% rename from libraries/gpu-networking/CMakeLists.txt rename to libraries/model-networking/CMakeLists.txt index 75e0f61b3e..3613f76cff 100644 --- a/libraries/gpu-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,4 +1,4 @@ -set(TARGET_NAME gpu-networking) +set(TARGET_NAME model-networking) # use setup_hifi_library macro to setup our project and link appropriate Qt modules setup_hifi_library() diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp new file mode 100644 index 0000000000..8fa3a9a234 --- /dev/null +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -0,0 +1,506 @@ +// +// ModelCache.cpp +// interface/src/renderer +// +// Created by Andrzej Kapolka on 6/21/13. +// Copyright 2013 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 "ModelCache.h" + +#include + +#include +#include + +#include +#include + +#include "TextureCache.h" +#include "ModelNetworkingLogging.h" + +#include "gpu/StandardShaderLib.h" + +#include "model/TextureMap.h" + +//#define WANT_DEBUG + +ModelCache::ModelCache() +{ + const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; + setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE); +} + +ModelCache::~ModelCache() { +} + +QSharedPointer ModelCache::createResource(const QUrl& url, const QSharedPointer& fallback, + bool delayLoad, const void* extra) { + // NetworkGeometry is no longer a subclass of Resource, but requires this method because, it is pure virtual. + assert(false); + return QSharedPointer(); +} + + +GeometryReader::GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping) : + _url(url), + _data(data), + _mapping(mapping) { +} + +void GeometryReader::run() { + try { + if (_data.isEmpty()) { + throw QString("Reply is NULL ?!"); + } + QString urlname = _url.path().toLower(); + bool urlValid = true; + urlValid &= !urlname.isEmpty(); + urlValid &= !_url.path().isEmpty(); + urlValid &= _url.path().toLower().endsWith(".fbx") || _url.path().toLower().endsWith(".obj"); + + if (urlValid) { + // Let's read the binaries from the network + FBXGeometry* fbxgeo = nullptr; + if (_url.path().toLower().endsWith(".fbx")) { + const bool grabLightmaps = true; + const float lightmapLevel = 1.0f; + fbxgeo = readFBX(_data, _mapping, _url.path(), grabLightmaps, lightmapLevel); + } else if (_url.path().toLower().endsWith(".obj")) { + fbxgeo = OBJReader().readOBJ(_data, _mapping, _url); + } else { + QString errorStr("usupported format"); + emit onError(NetworkGeometry::ModelParseError, errorStr); + } + emit onSuccess(fbxgeo); + } else { + throw QString("url is invalid"); + } + + } catch (const QString& error) { + qCDebug(modelnetworking) << "Error reading " << _url << ": " << error; + emit onError(NetworkGeometry::ModelParseError, error); + } +} + +NetworkGeometry::NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl) : + _url(url), + _mapping(mapping), + _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url) { + + if (delayLoad) { + _state = DelayState; + } else { + attemptRequestInternal(); + } +} + +NetworkGeometry::~NetworkGeometry() { + if (_resource) { + _resource->deleteLater(); + } +} + +void NetworkGeometry::attemptRequest() { + if (_state == DelayState) { + attemptRequestInternal(); + } +} + +void NetworkGeometry::attemptRequestInternal() { + if (_url.path().toLower().endsWith(".fst")) { + _mappingUrl = _url; + requestMapping(_url); + } else { + _modelUrl = _url; + requestModel(_url); + } +} + +bool NetworkGeometry::isLoaded() const { + return _state == SuccessState; +} + +bool NetworkGeometry::isLoadedWithTextures() const { + if (!isLoaded()) { + return false; + } + + if (!_isLoadedWithTextures) { + for (auto&& material : _materials) { + if ((material->diffuseTexture && !material->diffuseTexture->isLoaded()) || + (material->normalTexture && !material->normalTexture->isLoaded()) || + (material->specularTexture && !material->specularTexture->isLoaded()) || + (material->emissiveTexture && !material->emissiveTexture->isLoaded())) { + return false; + } + } + _isLoadedWithTextures = true; + } + return true; +} + +void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& url) { + + + if (_meshes.size() > 0) { + auto textureCache = DependencyManager::get(); + for (auto&& material : _materials) { + QSharedPointer matchingTexture = QSharedPointer(); + if (material->diffuseTextureName == name) { + material->diffuseTexture = textureCache->getTexture(url, DEFAULT_TEXTURE); + } else if (material->normalTextureName == name) { + material->normalTexture = textureCache->getTexture(url); + } else if (material->specularTextureName == name) { + material->specularTexture = textureCache->getTexture(url); + } else if (material->emissiveTextureName == name) { + material->emissiveTexture = textureCache->getTexture(url); + } + } + } else { + qCWarning(modelnetworking) << "Ignoring setTextureWirthNameToURL() geometry not ready." << name << url; + } + _isLoadedWithTextures = false; +} + +QStringList NetworkGeometry::getTextureNames() const { + QStringList result; + for (auto&& material : _materials) { + if (!material->diffuseTextureName.isEmpty() && material->diffuseTexture) { + QString textureURL = material->diffuseTexture->getURL().toString(); + result << material->diffuseTextureName + ":" + textureURL; + } + + if (!material->normalTextureName.isEmpty() && material->normalTexture) { + QString textureURL = material->normalTexture->getURL().toString(); + result << material->normalTextureName + ":" + textureURL; + } + + if (!material->specularTextureName.isEmpty() && material->specularTexture) { + QString textureURL = material->specularTexture->getURL().toString(); + result << material->specularTextureName + ":" + textureURL; + } + + if (!material->emissiveTextureName.isEmpty() && material->emissiveTexture) { + QString textureURL = material->emissiveTexture->getURL().toString(); + result << material->emissiveTextureName + ":" + textureURL; + } + } + + return result; +} + +void NetworkGeometry::requestMapping(const QUrl& url) { + _state = RequestMappingState; + if (_resource) { + _resource->deleteLater(); + } + _resource = new Resource(url, false); + connect(_resource, &Resource::loaded, this, &NetworkGeometry::mappingRequestDone); + connect(_resource, &Resource::failed, this, &NetworkGeometry::mappingRequestError); +} + +void NetworkGeometry::requestModel(const QUrl& url) { + _state = RequestModelState; + if (_resource) { + _resource->deleteLater(); + } + _modelUrl = url; + _resource = new Resource(url, false); + connect(_resource, &Resource::loaded, this, &NetworkGeometry::modelRequestDone); + connect(_resource, &Resource::failed, this, &NetworkGeometry::modelRequestError); +} + +void NetworkGeometry::mappingRequestDone(const QByteArray& data) { + assert(_state == RequestMappingState); + + // parse the mapping file + _mapping = FSTReader::readMapping(data); + + QUrl replyUrl = _mappingUrl; + QString modelUrlStr = _mapping.value("filename").toString(); + if (modelUrlStr.isNull()) { + qCDebug(modelnetworking) << "Mapping file " << _url << "has no \"filename\" entry"; + emit onFailure(*this, MissingFilenameInMapping); + } else { + // read _textureBase from mapping file, if present + QString texdir = _mapping.value("texdir").toString(); + if (!texdir.isNull()) { + if (!texdir.endsWith('/')) { + texdir += '/'; + } + _textureBaseUrl = replyUrl.resolved(texdir); + } + + _modelUrl = replyUrl.resolved(modelUrlStr); + requestModel(_modelUrl); + } +} + +void NetworkGeometry::mappingRequestError(QNetworkReply::NetworkError error) { + assert(_state == RequestMappingState); + _state = ErrorState; + emit onFailure(*this, MappingRequestError); +} + +void NetworkGeometry::modelRequestDone(const QByteArray& data) { + assert(_state == RequestModelState); + + _state = ParsingModelState; + + // asynchronously parse the model file. + GeometryReader* geometryReader = new GeometryReader(_modelUrl, data, _mapping); + connect(geometryReader, SIGNAL(onSuccess(FBXGeometry*)), SLOT(modelParseSuccess(FBXGeometry*))); + connect(geometryReader, SIGNAL(onError(int, QString)), SLOT(modelParseError(int, QString))); + + QThreadPool::globalInstance()->start(geometryReader); +} + +void NetworkGeometry::modelRequestError(QNetworkReply::NetworkError error) { + assert(_state == RequestModelState); + _state = ErrorState; + emit onFailure(*this, ModelRequestError); +} + +static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBaseUrl) { + auto textureCache = DependencyManager::get(); + NetworkMesh* networkMesh = new NetworkMesh(); + + int totalIndices = 0; + bool checkForTexcoordLightmap = false; + + + + // process network parts + foreach (const FBXMeshPart& part, mesh.parts) { + totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); + } + + // initialize index buffer + { + networkMesh->_indexBuffer = std::make_shared(); + networkMesh->_indexBuffer->resize(totalIndices * sizeof(int)); + int offset = 0; + foreach(const FBXMeshPart& part, mesh.parts) { + networkMesh->_indexBuffer->setSubData(offset, part.quadIndices.size() * sizeof(int), + (gpu::Byte*) part.quadIndices.constData()); + offset += part.quadIndices.size() * sizeof(int); + networkMesh->_indexBuffer->setSubData(offset, part.triangleIndices.size() * sizeof(int), + (gpu::Byte*) part.triangleIndices.constData()); + offset += part.triangleIndices.size() * sizeof(int); + } + } + + // initialize vertex buffer + { + networkMesh->_vertexBuffer = std::make_shared(); + // if we don't need to do any blending, the positions/normals can be static + if (mesh.blendshapes.isEmpty()) { + int normalsOffset = mesh.vertices.size() * sizeof(glm::vec3); + int tangentsOffset = normalsOffset + mesh.normals.size() * sizeof(glm::vec3); + int colorsOffset = tangentsOffset + mesh.tangents.size() * sizeof(glm::vec3); + int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); + int texCoords1Offset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); + int clusterIndicesOffset = texCoords1Offset + mesh.texCoords1.size() * sizeof(glm::vec2); + int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); + + networkMesh->_vertexBuffer->resize(clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4)); + + networkMesh->_vertexBuffer->setSubData(0, mesh.vertices.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.vertices.constData()); + networkMesh->_vertexBuffer->setSubData(normalsOffset, mesh.normals.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.normals.constData()); + networkMesh->_vertexBuffer->setSubData(tangentsOffset, + mesh.tangents.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.tangents.constData()); + networkMesh->_vertexBuffer->setSubData(colorsOffset, mesh.colors.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.colors.constData()); + networkMesh->_vertexBuffer->setSubData(texCoordsOffset, + mesh.texCoords.size() * sizeof(glm::vec2), (gpu::Byte*) mesh.texCoords.constData()); + networkMesh->_vertexBuffer->setSubData(texCoords1Offset, + mesh.texCoords1.size() * sizeof(glm::vec2), (gpu::Byte*) mesh.texCoords1.constData()); + networkMesh->_vertexBuffer->setSubData(clusterIndicesOffset, + mesh.clusterIndices.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterIndices.constData()); + networkMesh->_vertexBuffer->setSubData(clusterWeightsOffset, + mesh.clusterWeights.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterWeights.constData()); + + // otherwise, at least the cluster indices/weights can be static + networkMesh->_vertexStream = std::make_shared(); + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3)); + if (mesh.normals.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, normalsOffset, sizeof(glm::vec3)); + if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, tangentsOffset, sizeof(glm::vec3)); + if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); + if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); + if (mesh.texCoords1.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoords1Offset, sizeof(glm::vec2)); + if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); + if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); + + int channelNum = 0; + networkMesh->_vertexFormat = std::make_shared(); + networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); + if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + if (mesh.texCoords1.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + // } else if (checkForTexcoordLightmap && mesh.texCoords.size()) { + } else if (mesh.texCoords.size()) { + // need lightmap texcoord UV but doesn't have uv#1 so just reuse the same channel + networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum - 1, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + } + if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); + if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); + } + else { + int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); + int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); + int clusterIndicesOffset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); + int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); + + networkMesh->_vertexBuffer->resize(clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4)); + networkMesh->_vertexBuffer->setSubData(0, mesh.tangents.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.tangents.constData()); + networkMesh->_vertexBuffer->setSubData(colorsOffset, mesh.colors.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.colors.constData()); + networkMesh->_vertexBuffer->setSubData(texCoordsOffset, + mesh.texCoords.size() * sizeof(glm::vec2), (gpu::Byte*) mesh.texCoords.constData()); + networkMesh->_vertexBuffer->setSubData(clusterIndicesOffset, + mesh.clusterIndices.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterIndices.constData()); + networkMesh->_vertexBuffer->setSubData(clusterWeightsOffset, + mesh.clusterWeights.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterWeights.constData()); + + networkMesh->_vertexStream = std::make_shared(); + if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3)); + if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); + if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); + if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); + if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); + + int channelNum = 0; + networkMesh->_vertexFormat = std::make_shared(); + networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); + if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); + if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); + } + } + + return networkMesh; +} + +static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) { + auto textureCache = DependencyManager::get(); + NetworkMaterial* networkMaterial = new NetworkMaterial(); + + int totalIndices = 0; + bool checkForTexcoordLightmap = false; + + networkMaterial->_material = material._material; + + if (!material.diffuseTexture.filename.isEmpty()) { + networkMaterial->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.diffuseTexture.filename)), DEFAULT_TEXTURE, material.diffuseTexture.content); + networkMaterial->diffuseTextureName = material.diffuseTexture.name; + + auto diffuseMap = model::TextureMapPointer(new model::TextureMap()); + diffuseMap->setTextureSource(networkMaterial->diffuseTexture->_textureSource); + diffuseMap->setTextureTransform(material.diffuseTexture.transform); + + material._material->setTextureMap(model::MaterialKey::DIFFUSE_MAP, diffuseMap); + } + if (!material.normalTexture.filename.isEmpty()) { + networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE), material.normalTexture.content); + networkMaterial->normalTextureName = material.normalTexture.name; + + auto normalMap = model::TextureMapPointer(new model::TextureMap()); + normalMap->setTextureSource(networkMaterial->normalTexture->_textureSource); + + material._material->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap); + } + if (!material.specularTexture.filename.isEmpty()) { + networkMaterial->specularTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.specularTexture.filename)), SPECULAR_TEXTURE, material.specularTexture.content); + networkMaterial->specularTextureName = material.specularTexture.name; + + auto glossMap = model::TextureMapPointer(new model::TextureMap()); + glossMap->setTextureSource(networkMaterial->specularTexture->_textureSource); + + material._material->setTextureMap(model::MaterialKey::GLOSS_MAP, glossMap); + } + if (!material.emissiveTexture.filename.isEmpty()) { + networkMaterial->emissiveTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.emissiveTexture.filename)), EMISSIVE_TEXTURE, material.emissiveTexture.content); + networkMaterial->emissiveTextureName = material.emissiveTexture.name; + + checkForTexcoordLightmap = true; + + auto lightmapMap = model::TextureMapPointer(new model::TextureMap()); + lightmapMap->setTextureSource(networkMaterial->emissiveTexture->_textureSource); + lightmapMap->setTextureTransform(material.emissiveTexture.transform); + lightmapMap->setLightmapOffsetScale(material.emissiveParams.x, material.emissiveParams.y); + + material._material->setTextureMap(model::MaterialKey::LIGHTMAP_MAP, lightmapMap); + } + + return networkMaterial; +} + + +void NetworkGeometry::modelParseSuccess(FBXGeometry* geometry) { + // assume owner ship of geometry pointer + _geometry.reset(geometry); + + + + foreach(const FBXMesh& mesh, _geometry->meshes) { + _meshes.emplace_back(buildNetworkMesh(mesh, _textureBaseUrl)); + } + + QHash fbxMatIDToMatID; + foreach(const FBXMaterial& material, _geometry->materials) { + fbxMatIDToMatID[material.materialID] = _materials.size(); + _materials.emplace_back(buildNetworkMaterial(material, _textureBaseUrl)); + } + + + int meshID = 0; + foreach(const FBXMesh& mesh, _geometry->meshes) { + int partID = 0; + foreach (const FBXMeshPart& part, mesh.parts) { + NetworkShape* networkShape = new NetworkShape(); + networkShape->_meshID = meshID; + networkShape->_partID = partID; + networkShape->_materialID = fbxMatIDToMatID[part.materialID]; + _shapes.emplace_back(networkShape); + partID++; + } + meshID++; + } + + _state = SuccessState; + emit onSuccess(*this, *_geometry.get()); + + delete _resource; + _resource = nullptr; +} + +void NetworkGeometry::modelParseError(int error, QString str) { + _state = ErrorState; + emit onFailure(*this, (NetworkGeometry::Error)error); + + delete _resource; + _resource = nullptr; +} + + +const NetworkMaterial* NetworkGeometry::getShapeMaterial(int shapeID) { + if ((shapeID >= 0) && (shapeID < _shapes.size())) { + int materialID = _shapes[shapeID]->_materialID; + if ((materialID >= 0) && (materialID < _materials.size())) { + return _materials[materialID].get(); + } else { + return 0; + } + } else { + return 0; + } +} + diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h new file mode 100644 index 0000000000..1110d36e3e --- /dev/null +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -0,0 +1,205 @@ +// +// ModelCache.h +// libraries/model-networking/src/model-networking +// +// Created by Sam Gateau on 9/21/15. +// Copyright 2013 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_ModelCache_h +#define hifi_ModelCache_h + +#include +#include + +#include +#include + +#include "FBXReader.h" +#include "OBJReader.h" + +#include +#include + + +#include +#include + +class NetworkGeometry; +class NetworkMesh; +class NetworkTexture; +class NetworkMaterial; +class NetworkShape; + +/// Stores cached geometry. +class ModelCache : public ResourceCache, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, + bool delayLoad, const void* extra); + + /// Loads geometry from the specified URL. + /// \param fallback a fallback URL to load if the desired one is unavailable + /// \param delayLoad if true, don't load the geometry immediately; wait until load is first requested + QSharedPointer getGeometry(const QUrl& url, const QUrl& fallback = QUrl(), bool delayLoad = false); + + /// Set a batch to the simple pipeline, returning the previous pipeline + void useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend = false); + +private: + ModelCache(); + virtual ~ModelCache(); + + QHash > _networkGeometry; +}; + +class NetworkGeometry : public QObject { + Q_OBJECT + +public: + // mapping is only used if url is a .fbx or .obj file, it is essentially the content of an fst file. + // if delayLoad is true, the url will not be immediately downloaded. + // use the attemptRequest method to initiate the download. + NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl = QUrl()); + ~NetworkGeometry(); + + const QUrl& getURL() const { return _url; } + + void attemptRequest(); + + // true when the geometry is loaded (but maybe not it's associated textures) + bool isLoaded() const; + + // true when the requested geometry and its textures are loaded. + bool isLoadedWithTextures() const; + + // WARNING: only valid when isLoaded returns true. + const FBXGeometry& getFBXGeometry() const { return *_geometry; } + const std::vector>& getMeshes() const { return _meshes; } + // const model::AssetPointer getAsset() const { return _asset; } + + // model::MeshPointer getShapeMesh(int shapeID); + // int getShapePart(int shapeID); + + // This would be the final verison + // model::MaterialPointer getShapeMaterial(int shapeID); + const NetworkMaterial* getShapeMaterial(int shapeID); + + + void setTextureWithNameToURL(const QString& name, const QUrl& url); + QStringList getTextureNames() const; + + enum Error { + MissingFilenameInMapping = 0, + MappingRequestError, + ModelRequestError, + ModelParseError + }; + +signals: + // Fired when everything has downloaded and parsed successfully. + void onSuccess(NetworkGeometry& networkGeometry, FBXGeometry& fbxGeometry); + + // Fired when something went wrong. + void onFailure(NetworkGeometry& networkGeometry, Error error); + +protected slots: + void mappingRequestDone(const QByteArray& data); + void mappingRequestError(QNetworkReply::NetworkError error); + + void modelRequestDone(const QByteArray& data); + void modelRequestError(QNetworkReply::NetworkError error); + + void modelParseSuccess(FBXGeometry* geometry); + void modelParseError(int error, QString str); + +protected: + void attemptRequestInternal(); + void requestMapping(const QUrl& url); + void requestModel(const QUrl& url); + + enum State { DelayState, + RequestMappingState, + RequestModelState, + ParsingModelState, + SuccessState, + ErrorState }; + State _state; + + QUrl _url; + QUrl _mappingUrl; + QUrl _modelUrl; + QVariantHash _mapping; + QUrl _textureBaseUrl; + + Resource* _resource = nullptr; + std::unique_ptr _geometry; // This should go away evenutally once we can put everything we need in the model::AssetPointer + std::vector> _meshes; + std::vector> _materials; + std::vector> _shapes; + + + // The model asset created from this NetworkGeometry + // model::AssetPointer _asset; + + // cache for isLoadedWithTextures() + mutable bool _isLoadedWithTextures = false; +}; + +/// Reads geometry in a worker thread. +class GeometryReader : public QObject, public QRunnable { + Q_OBJECT +public: + GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping); + virtual void run(); +signals: + void onSuccess(FBXGeometry* geometry); + void onError(int error, QString str); +private: + QUrl _url; + QByteArray _data; + QVariantHash _mapping; +}; + + +class NetworkShape { +public: + int _meshID{ -1 }; + int _partID{ -1 }; + int _materialID{ -1 }; +}; + +class NetworkMaterial { +public: + model::MaterialPointer _material; + QString diffuseTextureName; + QSharedPointer diffuseTexture; + QString normalTextureName; + QSharedPointer normalTexture; + QString specularTextureName; + QSharedPointer specularTexture; + QString emissiveTextureName; + QSharedPointer emissiveTexture; +}; + + +/// The state associated with a single mesh. +class NetworkMesh { +public: + gpu::BufferPointer _indexBuffer; + gpu::BufferPointer _vertexBuffer; + + gpu::BufferStreamPointer _vertexStream; + + gpu::Stream::FormatPointer _vertexFormat; + + int getTranslucentPartCount(const FBXMesh& fbxMesh) const; + bool isPartTranslucent(const FBXMesh& fbxMesh, int partIndex) const; +}; + +#endif // hifi_GeometryCache_h diff --git a/libraries/gpu-networking/src/gpu-networking/GpuNetworkingLogging.cpp b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp similarity index 73% rename from libraries/gpu-networking/src/gpu-networking/GpuNetworkingLogging.cpp rename to libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp index 38da22969b..0c44fa33eb 100644 --- a/libraries/gpu-networking/src/gpu-networking/GpuNetworkingLogging.cpp +++ b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp @@ -6,6 +6,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GpuNetworkingLogging.h" +#include "ModelNetworkingLogging.h" -Q_LOGGING_CATEGORY(gpunetwork, "hifi.gpu-network") +Q_LOGGING_CATEGORY(modelnetworking, "hifi.gpu-network") diff --git a/libraries/gpu-networking/src/gpu-networking/GpuNetworkingLogging.h b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.h similarity index 86% rename from libraries/gpu-networking/src/gpu-networking/GpuNetworkingLogging.h rename to libraries/model-networking/src/model-networking/ModelNetworkingLogging.h index 7499340a9b..caf997bad0 100644 --- a/libraries/gpu-networking/src/gpu-networking/GpuNetworkingLogging.h +++ b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.h @@ -8,4 +8,4 @@ #include -Q_DECLARE_LOGGING_CATEGORY(gpunetwork) +Q_DECLARE_LOGGING_CATEGORY(modelnetworking) diff --git a/libraries/gpu-networking/src/gpu-networking/ShaderCache.cpp b/libraries/model-networking/src/model-networking/ShaderCache.cpp similarity index 100% rename from libraries/gpu-networking/src/gpu-networking/ShaderCache.cpp rename to libraries/model-networking/src/model-networking/ShaderCache.cpp diff --git a/libraries/gpu-networking/src/gpu-networking/ShaderCache.h b/libraries/model-networking/src/model-networking/ShaderCache.h similarity index 100% rename from libraries/gpu-networking/src/gpu-networking/ShaderCache.h rename to libraries/model-networking/src/model-networking/ShaderCache.h diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp similarity index 94% rename from libraries/gpu-networking/src/gpu-networking/TextureCache.cpp rename to libraries/model-networking/src/model-networking/TextureCache.cpp index fd69a2ab04..0bdc0749f6 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -1,6 +1,6 @@ // // TextureCache.cpp -// libraries/gpu-networking/src +// libraries/model-networking/src // // Created by Andrzej Kapolka on 8/6/13. // Copyright 2013 High Fidelity, Inc. @@ -25,7 +25,7 @@ #include -#include "GpuNetworkingLogging.h" +#include "ModelNetworkingLogging.h" TextureCache::TextureCache() { const qint64 TEXTURE_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; @@ -185,7 +185,7 @@ NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArr _width(0), _height(0) { - _textureSource.reset(new model::TextureSource()); + _textureSource.reset(new gpu::TextureSource()); if (!url.isValid()) { _loaded = true; @@ -206,7 +206,7 @@ NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& texture _width(0), _height(0) { - _textureSource.reset(new model::TextureSource()); + _textureSource.reset(new gpu::TextureSource()); if (!url.isValid()) { _loaded = true; @@ -223,11 +223,11 @@ NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& texture NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { switch (_type) { case CUBE_TEXTURE: { - return TextureLoaderFunc(model::TextureSource::createCubeTextureFromImage); + return TextureLoaderFunc(model::TextureUsage::createCubeTextureFromImage); break; } case BUMP_TEXTURE: { - return TextureLoaderFunc(model::TextureSource::createNormalTextureFromBumpImage); + return TextureLoaderFunc(model::TextureUsage::createNormalTextureFromBumpImage); break; } case CUSTOM_TEXTURE: { @@ -239,7 +239,7 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { case SPECULAR_TEXTURE: case EMISSIVE_TEXTURE: default: { - return TextureLoaderFunc(model::TextureSource::create2DTextureFromImage); + return TextureLoaderFunc(model::TextureUsage::create2DTextureFromImage); break; } } @@ -288,7 +288,7 @@ void listSupportedImageFormats() { foreach(const QByteArray& f, supportedFormats) { formats += QString(f) + ","; } - qCDebug(gpunetwork) << "List of supported Image formats:" << formats; + qCDebug(modelnetworking) << "List of supported Image formats:" << formats; }); } @@ -313,9 +313,9 @@ void ImageReader::run() { if (originalWidth == 0 || originalHeight == 0 || imageFormat == QImage::Format_Invalid) { if (filenameExtension.empty()) { - qCDebug(gpunetwork) << "QImage failed to create from content, no file extension:" << _url; + qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url; } else { - qCDebug(gpunetwork) << "QImage failed to create from content" << _url; + qCDebug(modelnetworking) << "QImage failed to create from content" << _url; } return; } diff --git a/libraries/gpu-networking/src/gpu-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h similarity index 98% rename from libraries/gpu-networking/src/gpu-networking/TextureCache.h rename to libraries/model-networking/src/model-networking/TextureCache.h index 335ea2d89c..cc22509631 100644 --- a/libraries/gpu-networking/src/gpu-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -1,6 +1,6 @@ // // TextureCache.h -// libraries/gpu-networking/src +// libraries/model-networking/src // // Created by Andrzej Kapolka on 8/6/13. // Copyright 2013 High Fidelity, Inc. @@ -98,7 +98,7 @@ public: ~Texture(); const gpu::TexturePointer getGPUTexture() const { return _textureSource->getGPUTexture(); } - model::TextureSourcePointer _textureSource; + gpu::TextureSourcePointer _textureSource; protected: diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 5e8ebb247c..c30ffb7238 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -9,4 +9,4 @@ add_dependency_external_projects(glm) find_package(GLM REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) -link_hifi_libraries(shared networking gpu octree) +link_hifi_libraries(shared gpu) diff --git a/libraries/model/src/model/Skybox.cpp b/libraries/model/src/model/Skybox.cpp index 7e3af09a1f..c931b78128 100755 --- a/libraries/model/src/model/Skybox.cpp +++ b/libraries/model/src/model/Skybox.cpp @@ -13,7 +13,6 @@ #include #include -// #include #include #include "Skybox_vert.h" @@ -39,16 +38,7 @@ Skybox::Skybox() { void Skybox::setColor(const Color& color) { _color = color; } -/* -void Skybox::setProcedural(QSharedPointer procedural) { - _procedural = procedural; - if (_procedural) { - _procedural->_vertexSource = Skybox_vert; - _procedural->_fragmentSource = Skybox_frag; - // No pipeline state customization - } -} -*/ + void Skybox::setCubemap(const gpu::TexturePointer& cubemap) { _cubemap = cubemap; } @@ -58,7 +48,7 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky static gpu::BufferPointer theBuffer; static gpu::Stream::FormatPointer theFormat; - if (/*skybox._procedural || */skybox.getCubemap()) { + if (skybox._procedural || skybox.getCubemap()) { if (!theBuffer) { const float CLIP = 1.0f; const glm::vec2 vertices[4] = { { -CLIP, -CLIP }, { CLIP, -CLIP }, { -CLIP, CLIP }, { CLIP, CLIP } }; @@ -78,14 +68,7 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky batch.setInputBuffer(gpu::Stream::POSITION, theBuffer, 0, 8); batch.setInputFormat(theFormat); - /*if (skybox._procedural && skybox._procedural->_enabled && skybox._procedural->ready()) { - if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { - batch.setResourceTexture(0, skybox.getCubemap()); - } - - skybox._procedural->prepare(batch, glm::vec3(1)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } else*/ if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { + if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { static gpu::BufferPointer theConstants; static gpu::PipelinePointer thePipeline; static int SKYBOX_CONSTANTS_SLOT = 0; // need to be defined by the compilation of the shader diff --git a/libraries/model/src/model/Skybox.h b/libraries/model/src/model/Skybox.h index 887f1ff80b..68352e4309 100755 --- a/libraries/model/src/model/Skybox.h +++ b/libraries/model/src/model/Skybox.h @@ -11,13 +11,14 @@ #ifndef hifi_model_Skybox_h #define hifi_model_Skybox_h -//#include #include #include "Light.h" class ViewFrustum; struct Procedural; +typedef std::shared_ptr ProceduralPointer; + namespace gpu { class Batch; } namespace model { @@ -36,13 +37,13 @@ public: void setCubemap(const gpu::TexturePointer& cubemap); const gpu::TexturePointer& getCubemap() const { return _cubemap; } - // void setProcedural(QSharedPointer procedural); + void setProcedural(const ProceduralPointer& procedural); static void render(gpu::Batch& batch, const ViewFrustum& frustum, const Skybox& skybox); protected: gpu::TexturePointer _cubemap; - // QSharedPointer _procedural; + ProceduralPointer _procedural; Color _color{1.0f, 1.0f, 1.0f}; }; typedef std::shared_ptr< Skybox > SkyboxPointer; diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 0dd7bca754..c2551d276d 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -19,35 +19,9 @@ using namespace model; using namespace gpu; -// TextureSource -TextureSource::TextureSource() -{/* : Texture::Storage()//, - // _gpuTexture(Texture::createFromStorage(this))*/ -} -TextureSource::~TextureSource() { -} - -void TextureSource::reset(const QUrl& url, const TextureUsage& usage) { - _imageUrl = url; - _usage = usage; -} - -void TextureSource::resetTexture(gpu::Texture* texture) { - _gpuTexture.reset(texture); -} - -bool TextureSource::isDefined() const { - if (_gpuTexture) { - return _gpuTexture->isDefined(); - } else { - return false; - } -} - - -void TextureMap::setTextureSource(TextureSourcePointer& texStorage) { - _textureSource = texStorage; +void TextureMap::setTextureSource(TextureSourcePointer& textureSource) { + _textureSource = textureSource; } bool TextureMap::isDefined() const { @@ -78,7 +52,7 @@ void TextureMap::setLightmapOffsetScale(float offset, float scale) { -gpu::Texture* TextureSource::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { +gpu::Texture* TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { QImage image = srcImage; int imageArea = image.width() * image.height(); @@ -175,7 +149,7 @@ double mapComponent(double sobelValue) { return (sobelValue + 1.0) * factor; } -gpu::Texture* TextureSource::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) { +gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcImage, const std::string& srcImageName) { QImage image = srcImage; // PR 5540 by AlessandroSigna @@ -285,7 +259,7 @@ public: _faceZNeg(fZN) {} }; -gpu::Texture* TextureSource::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { +gpu::Texture* TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { QImage image = srcImage; int imageArea = image.width() * image.height(); diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index 197172358b..343ef1f481 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -30,36 +30,11 @@ public: Material::MapFlags _materialUsage{ MaterialKey::DIFFUSE_MAP }; int _environmentUsage = 0; -}; -// TextureSource is a specialized version of the gpu::Texture::Storage -// It provides the mechanism to create a texture from a Url and the intended usage -// that guides the internal format used -class TextureSource { -public: - TextureSource(); - ~TextureSource(); - - const QUrl& getUrl() const { return _imageUrl; } - gpu::Texture::Type getType() const { return _usage._type; } - const gpu::TexturePointer getGPUTexture() const { return _gpuTexture; } - - void reset(const QUrl& url, const TextureUsage& usage); - - void resetTexture(gpu::Texture* texture); - - bool isDefined() const; - static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createNormalTextureFromBumpImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createCubeTextureFromImage(const QImage& image, const std::string& srcImageName); - -protected: - gpu::TexturePointer _gpuTexture; - TextureUsage _usage; - QUrl _imageUrl; }; -typedef std::shared_ptr< TextureSource > TextureSourcePointer; @@ -67,7 +42,7 @@ class TextureMap { public: TextureMap() {} - void setTextureSource(TextureSourcePointer& texStorage); + void setTextureSource(gpu::TextureSourcePointer& textureSource); bool isDefined() const; gpu::TextureView getTextureView() const; @@ -79,7 +54,7 @@ public: const glm::vec2& getLightmapOffsetScale() const { return _lightmapOffsetScale; } protected: - TextureSourcePointer _textureSource; + gpu::TextureSourcePointer _textureSource; Transform _texcoordTransform; glm::vec2 _lightmapOffsetScale{ 0.0f, 1.0f }; diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt index bd53f0abb9..9595ef5f7a 100644 --- a/libraries/procedural/CMakeLists.txt +++ b/libraries/procedural/CMakeLists.txt @@ -1,5 +1,7 @@ set(TARGET_NAME procedural) +AUTOSCRIBE_SHADER_LIB(gpu model) + # use setup_hifi_library macro to setup our project and link appropriate Qt modules setup_hifi_library() @@ -7,4 +9,4 @@ add_dependency_external_projects(glm) find_package(GLM REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) -link_hifi_libraries(shared gpu networking gpu-networking) +link_hifi_libraries(shared gpu model model-networking) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index c46add8412..aa8946f62b 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index bb6a0ad44d..5b3f2b4742 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -18,8 +18,7 @@ #include #include #include -#include - +#include // FIXME better encapsulation // FIXME better mechanism for extending to things rendered using shaders other than simple.slv diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp new file mode 100644 index 0000000000..e237bd3d7d --- /dev/null +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -0,0 +1,71 @@ +// +// ProceduralSkybox.cpp +// libraries/procedural/src/procedural +// +// Created by Sam Gateau on 9/21/2015. +// Copyright 2015 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 "ProceduralSkybox.h" + + +#include +#include +#include + +#include "ProceduralSkybox_vert.h" +#include "ProceduralSkybox_frag.h" + +ProceduralSkybox::ProceduralSkybox() : model::Skybox() { +} + +void ProceduralSkybox::setProcedural(const ProceduralPointer& procedural) { + _procedural = procedural; + if (_procedural) { + _procedural->_vertexSource = ProceduralSkybox_vert; + _procedural->_fragmentSource = ProceduralSkybox_frag; + // No pipeline state customization + } +} + +void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const ProceduralSkybox& skybox) { + static gpu::BufferPointer theBuffer; + static gpu::Stream::FormatPointer theFormat; + + if (skybox._procedural || skybox.getCubemap()) { + if (!theBuffer) { + const float CLIP = 1.0f; + const glm::vec2 vertices[4] = { { -CLIP, -CLIP }, { CLIP, -CLIP }, { -CLIP, CLIP }, { CLIP, CLIP } }; + theBuffer = std::make_shared(sizeof(vertices), (const gpu::Byte*) vertices); + theFormat = std::make_shared(); + theFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ)); + } + + glm::mat4 projMat; + viewFrustum.evalProjectionMatrix(projMat); + + Transform viewTransform; + viewFrustum.evalViewTransform(viewTransform); + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewTransform); + batch.setModelTransform(Transform()); // only for Mac + batch.setInputBuffer(gpu::Stream::POSITION, theBuffer, 0, 8); + batch.setInputFormat(theFormat); + + if (skybox._procedural && skybox._procedural->_enabled && skybox._procedural->ready()) { + if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { + batch.setResourceTexture(0, skybox.getCubemap()); + } + + skybox._procedural->prepare(batch, glm::vec3(1)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } else { + // skybox has no cubemap, just clear the color buffer + auto color = skybox.getColor(); + batch.clearFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(color, 0.0f), 0.0f, 0, true); + } +} + diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h new file mode 100644 index 0000000000..32d8200079 --- /dev/null +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -0,0 +1,35 @@ +// +// ProceduralSkybox.h +// libraries/procedural/src/procedural +// +// Created by Sam Gateau on 9/21/15. +// Copyright 2015 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 +// + +#pragma once +#ifndef hifi_ProceduralSkybox_h +#define hifi_ProceduralSkybox_h + +#include + +#include "Procedural.h" + +class ProceduralSkybox: public model::Skybox { +public: + ProceduralSkybox(); + ProceduralSkybox& operator= (const ProceduralSkybox& skybox); + virtual ~ProceduralSkybox() {}; + + void setProcedural(const ProceduralPointer& procedural); + + static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); + +protected: + ProceduralPointer _procedural; +}; +typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; + +#endif diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.slf b/libraries/procedural/src/procedural/ProceduralSkybox.slf new file mode 100644 index 0000000000..382801f52d --- /dev/null +++ b/libraries/procedural/src/procedural/ProceduralSkybox.slf @@ -0,0 +1,50 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// skybox.frag +// fragment shader +// +// Created by Sam Gateau on 5/5/2015. +// Copyright 2015 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 +// + +uniform samplerCube cubeMap; + +struct Skybox { + vec4 _color; +}; + +uniform skyboxBuffer { + Skybox _skybox; +}; + +in vec3 _normal; +out vec4 _fragColor; + +//PROCEDURAL_COMMON_BLOCK + +#line 1001 +//PROCEDURAL_BLOCK + +#line 2033 +void main(void) { + +#ifdef PROCEDURAL + + vec3 color = getSkyboxColor(); + _fragColor = vec4(color, 0.0); + +#else + + vec3 coord = normalize(_normal); + vec3 texel = texture(cubeMap, coord).rgb; + vec3 color = texel * _skybox._color.rgb; + vec3 pixel = pow(color, vec3(1.0/2.2)); // manual Gamma correction + _fragColor = vec4(pixel, 0.0); + +#endif + +} diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.slv b/libraries/procedural/src/procedural/ProceduralSkybox.slv new file mode 100644 index 0000000000..d1b9a20a66 --- /dev/null +++ b/libraries/procedural/src/procedural/ProceduralSkybox.slv @@ -0,0 +1,34 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// skybox.vert +// vertex shader +// +// Created by Sam Gateau on 5/5/2015. +// Copyright 2015 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 gpu/Inputs.slh@> + +<@include gpu/Transform.slh@> + +<$declareStandardTransform()$> + +out vec3 _normal; + +void main(void) { + // standard transform + TransformCamera cam = getTransformCamera(); + vec3 clipDir = vec3(inPosition.xy, 0.0); + vec3 eyeDir; + + <$transformClipToEyeDir(cam, clipDir, eyeDir)$> + <$transformEyeToWorldDir(cam, eyeDir, _normal)$> + + // Position is supposed to come in clip space + gl_Position = vec4(inPosition.xy, 0.0, 1.0); +} \ No newline at end of file diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 4d33c6f1c1..6a0f69dd8d 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -40,4 +40,4 @@ add_dependency_external_projects(oglplus) find_package(OGLPLUS REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${OGLPLUS_INCLUDE_DIRS}) -link_hifi_libraries(shared gpu gpu-networking procedural model render environment animation fbx) +link_hifi_libraries(shared gpu procedural model model-networking render environment animation fbx) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 77f9b2cece..89ac761a7a 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -38,8 +38,6 @@ const int GeometryCache::UNKNOWN_ID = -1; GeometryCache::GeometryCache() : _nextID(0) { - const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; - setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE); } GeometryCache::~GeometryCache() { @@ -51,13 +49,6 @@ GeometryCache::~GeometryCache() { #endif //def WANT_DEBUG } -QSharedPointer GeometryCache::createResource(const QUrl& url, const QSharedPointer& fallback, - bool delayLoad, const void* extra) { - // NetworkGeometry is no longer a subclass of Resource, but requires this method because, it is pure virtual. - assert(false); - return QSharedPointer(); -} - const int NUM_VERTICES_PER_TRIANGLE = 3; const int NUM_TRIANGLES_PER_QUAD = 2; const int NUM_VERTICES_PER_TRIANGULATED_QUAD = NUM_VERTICES_PER_TRIANGLE * NUM_TRIANGLES_PER_QUAD; @@ -1713,463 +1704,3 @@ void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) { batch.setPipeline(_standardDrawPipeline); } } - -GeometryReader::GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping) : - _url(url), - _data(data), - _mapping(mapping) { -} - -void GeometryReader::run() { - try { - if (_data.isEmpty()) { - throw QString("Reply is NULL ?!"); - } - QString urlname = _url.path().toLower(); - bool urlValid = true; - urlValid &= !urlname.isEmpty(); - urlValid &= !_url.path().isEmpty(); - urlValid &= _url.path().toLower().endsWith(".fbx") || _url.path().toLower().endsWith(".obj"); - - if (urlValid) { - // Let's read the binaries from the network - FBXGeometry* fbxgeo = nullptr; - if (_url.path().toLower().endsWith(".fbx")) { - const bool grabLightmaps = true; - const float lightmapLevel = 1.0f; - fbxgeo = readFBX(_data, _mapping, _url.path(), grabLightmaps, lightmapLevel); - } else if (_url.path().toLower().endsWith(".obj")) { - fbxgeo = OBJReader().readOBJ(_data, _mapping, _url); - } else { - QString errorStr("usupported format"); - emit onError(NetworkGeometry::ModelParseError, errorStr); - } - emit onSuccess(fbxgeo); - } else { - throw QString("url is invalid"); - } - - } catch (const QString& error) { - qCDebug(renderutils) << "Error reading " << _url << ": " << error; - emit onError(NetworkGeometry::ModelParseError, error); - } -} - -NetworkGeometry::NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl) : - _url(url), - _mapping(mapping), - _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url) { - - if (delayLoad) { - _state = DelayState; - } else { - attemptRequestInternal(); - } -} - -NetworkGeometry::~NetworkGeometry() { - if (_resource) { - _resource->deleteLater(); - } -} - -void NetworkGeometry::attemptRequest() { - if (_state == DelayState) { - attemptRequestInternal(); - } -} - -void NetworkGeometry::attemptRequestInternal() { - if (_url.path().toLower().endsWith(".fst")) { - _mappingUrl = _url; - requestMapping(_url); - } else { - _modelUrl = _url; - requestModel(_url); - } -} - -bool NetworkGeometry::isLoaded() const { - return _state == SuccessState; -} - -bool NetworkGeometry::isLoadedWithTextures() const { - if (!isLoaded()) { - return false; - } - - if (!_isLoadedWithTextures) { - for (auto&& material : _materials) { - if ((material->diffuseTexture && !material->diffuseTexture->isLoaded()) || - (material->normalTexture && !material->normalTexture->isLoaded()) || - (material->specularTexture && !material->specularTexture->isLoaded()) || - (material->emissiveTexture && !material->emissiveTexture->isLoaded())) { - return false; - } - } - _isLoadedWithTextures = true; - } - return true; -} - -void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& url) { - - - if (_meshes.size() > 0) { - auto textureCache = DependencyManager::get(); - for (auto&& material : _materials) { - QSharedPointer matchingTexture = QSharedPointer(); - if (material->diffuseTextureName == name) { - material->diffuseTexture = textureCache->getTexture(url, DEFAULT_TEXTURE); - } else if (material->normalTextureName == name) { - material->normalTexture = textureCache->getTexture(url); - } else if (material->specularTextureName == name) { - material->specularTexture = textureCache->getTexture(url); - } else if (material->emissiveTextureName == name) { - material->emissiveTexture = textureCache->getTexture(url); - } - } - } else { - qCWarning(renderutils) << "Ignoring setTextureWirthNameToURL() geometry not ready." << name << url; - } - _isLoadedWithTextures = false; -} - -QStringList NetworkGeometry::getTextureNames() const { - QStringList result; - for (auto&& material : _materials) { - if (!material->diffuseTextureName.isEmpty() && material->diffuseTexture) { - QString textureURL = material->diffuseTexture->getURL().toString(); - result << material->diffuseTextureName + ":" + textureURL; - } - - if (!material->normalTextureName.isEmpty() && material->normalTexture) { - QString textureURL = material->normalTexture->getURL().toString(); - result << material->normalTextureName + ":" + textureURL; - } - - if (!material->specularTextureName.isEmpty() && material->specularTexture) { - QString textureURL = material->specularTexture->getURL().toString(); - result << material->specularTextureName + ":" + textureURL; - } - - if (!material->emissiveTextureName.isEmpty() && material->emissiveTexture) { - QString textureURL = material->emissiveTexture->getURL().toString(); - result << material->emissiveTextureName + ":" + textureURL; - } - } - - return result; -} - -void NetworkGeometry::requestMapping(const QUrl& url) { - _state = RequestMappingState; - if (_resource) { - _resource->deleteLater(); - } - _resource = new Resource(url, false); - connect(_resource, &Resource::loaded, this, &NetworkGeometry::mappingRequestDone); - connect(_resource, &Resource::failed, this, &NetworkGeometry::mappingRequestError); -} - -void NetworkGeometry::requestModel(const QUrl& url) { - _state = RequestModelState; - if (_resource) { - _resource->deleteLater(); - } - _modelUrl = url; - _resource = new Resource(url, false); - connect(_resource, &Resource::loaded, this, &NetworkGeometry::modelRequestDone); - connect(_resource, &Resource::failed, this, &NetworkGeometry::modelRequestError); -} - -void NetworkGeometry::mappingRequestDone(const QByteArray& data) { - assert(_state == RequestMappingState); - - // parse the mapping file - _mapping = FSTReader::readMapping(data); - - QUrl replyUrl = _mappingUrl; - QString modelUrlStr = _mapping.value("filename").toString(); - if (modelUrlStr.isNull()) { - qCDebug(renderutils) << "Mapping file " << _url << "has no \"filename\" entry"; - emit onFailure(*this, MissingFilenameInMapping); - } else { - // read _textureBase from mapping file, if present - QString texdir = _mapping.value("texdir").toString(); - if (!texdir.isNull()) { - if (!texdir.endsWith('/')) { - texdir += '/'; - } - _textureBaseUrl = replyUrl.resolved(texdir); - } - - _modelUrl = replyUrl.resolved(modelUrlStr); - requestModel(_modelUrl); - } -} - -void NetworkGeometry::mappingRequestError(QNetworkReply::NetworkError error) { - assert(_state == RequestMappingState); - _state = ErrorState; - emit onFailure(*this, MappingRequestError); -} - -void NetworkGeometry::modelRequestDone(const QByteArray& data) { - assert(_state == RequestModelState); - - _state = ParsingModelState; - - // asynchronously parse the model file. - GeometryReader* geometryReader = new GeometryReader(_modelUrl, data, _mapping); - connect(geometryReader, SIGNAL(onSuccess(FBXGeometry*)), SLOT(modelParseSuccess(FBXGeometry*))); - connect(geometryReader, SIGNAL(onError(int, QString)), SLOT(modelParseError(int, QString))); - - QThreadPool::globalInstance()->start(geometryReader); -} - -void NetworkGeometry::modelRequestError(QNetworkReply::NetworkError error) { - assert(_state == RequestModelState); - _state = ErrorState; - emit onFailure(*this, ModelRequestError); -} - -static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBaseUrl) { - auto textureCache = DependencyManager::get(); - NetworkMesh* networkMesh = new NetworkMesh(); - - int totalIndices = 0; - bool checkForTexcoordLightmap = false; - - - - // process network parts - foreach (const FBXMeshPart& part, mesh.parts) { - totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); - } - - // initialize index buffer - { - networkMesh->_indexBuffer = std::make_shared(); - networkMesh->_indexBuffer->resize(totalIndices * sizeof(int)); - int offset = 0; - foreach(const FBXMeshPart& part, mesh.parts) { - networkMesh->_indexBuffer->setSubData(offset, part.quadIndices.size() * sizeof(int), - (gpu::Byte*) part.quadIndices.constData()); - offset += part.quadIndices.size() * sizeof(int); - networkMesh->_indexBuffer->setSubData(offset, part.triangleIndices.size() * sizeof(int), - (gpu::Byte*) part.triangleIndices.constData()); - offset += part.triangleIndices.size() * sizeof(int); - } - } - - // initialize vertex buffer - { - networkMesh->_vertexBuffer = std::make_shared(); - // if we don't need to do any blending, the positions/normals can be static - if (mesh.blendshapes.isEmpty()) { - int normalsOffset = mesh.vertices.size() * sizeof(glm::vec3); - int tangentsOffset = normalsOffset + mesh.normals.size() * sizeof(glm::vec3); - int colorsOffset = tangentsOffset + mesh.tangents.size() * sizeof(glm::vec3); - int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); - int texCoords1Offset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); - int clusterIndicesOffset = texCoords1Offset + mesh.texCoords1.size() * sizeof(glm::vec2); - int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); - - networkMesh->_vertexBuffer->resize(clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4)); - - networkMesh->_vertexBuffer->setSubData(0, mesh.vertices.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.vertices.constData()); - networkMesh->_vertexBuffer->setSubData(normalsOffset, mesh.normals.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.normals.constData()); - networkMesh->_vertexBuffer->setSubData(tangentsOffset, - mesh.tangents.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.tangents.constData()); - networkMesh->_vertexBuffer->setSubData(colorsOffset, mesh.colors.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.colors.constData()); - networkMesh->_vertexBuffer->setSubData(texCoordsOffset, - mesh.texCoords.size() * sizeof(glm::vec2), (gpu::Byte*) mesh.texCoords.constData()); - networkMesh->_vertexBuffer->setSubData(texCoords1Offset, - mesh.texCoords1.size() * sizeof(glm::vec2), (gpu::Byte*) mesh.texCoords1.constData()); - networkMesh->_vertexBuffer->setSubData(clusterIndicesOffset, - mesh.clusterIndices.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterIndices.constData()); - networkMesh->_vertexBuffer->setSubData(clusterWeightsOffset, - mesh.clusterWeights.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterWeights.constData()); - - // otherwise, at least the cluster indices/weights can be static - networkMesh->_vertexStream = std::make_shared(); - networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3)); - if (mesh.normals.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, normalsOffset, sizeof(glm::vec3)); - if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, tangentsOffset, sizeof(glm::vec3)); - if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); - if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); - if (mesh.texCoords1.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoords1Offset, sizeof(glm::vec2)); - if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); - if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); - - int channelNum = 0; - networkMesh->_vertexFormat = std::make_shared(); - networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); - if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); - if (mesh.texCoords1.size()) { - networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); - // } else if (checkForTexcoordLightmap && mesh.texCoords.size()) { - } else if (mesh.texCoords.size()) { - // need lightmap texcoord UV but doesn't have uv#1 so just reuse the same channel - networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum - 1, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); - } - if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); - if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); - } - else { - int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); - int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); - int clusterIndicesOffset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); - int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); - - networkMesh->_vertexBuffer->resize(clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4)); - networkMesh->_vertexBuffer->setSubData(0, mesh.tangents.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.tangents.constData()); - networkMesh->_vertexBuffer->setSubData(colorsOffset, mesh.colors.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.colors.constData()); - networkMesh->_vertexBuffer->setSubData(texCoordsOffset, - mesh.texCoords.size() * sizeof(glm::vec2), (gpu::Byte*) mesh.texCoords.constData()); - networkMesh->_vertexBuffer->setSubData(clusterIndicesOffset, - mesh.clusterIndices.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterIndices.constData()); - networkMesh->_vertexBuffer->setSubData(clusterWeightsOffset, - mesh.clusterWeights.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterWeights.constData()); - - networkMesh->_vertexStream = std::make_shared(); - if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3)); - if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); - if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); - if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); - if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); - - int channelNum = 0; - networkMesh->_vertexFormat = std::make_shared(); - networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); - if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); - if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); - if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); - } - } - - return networkMesh; -} - -static NetworkMaterial* buildNetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) { - auto textureCache = DependencyManager::get(); - NetworkMaterial* networkMaterial = new NetworkMaterial(); - - int totalIndices = 0; - bool checkForTexcoordLightmap = false; - - networkMaterial->_material = material._material; - - if (!material.diffuseTexture.filename.isEmpty()) { - networkMaterial->diffuseTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.diffuseTexture.filename)), DEFAULT_TEXTURE, material.diffuseTexture.content); - networkMaterial->diffuseTextureName = material.diffuseTexture.name; - - auto diffuseMap = model::TextureMapPointer(new model::TextureMap()); - diffuseMap->setTextureSource(networkMaterial->diffuseTexture->_textureSource); - diffuseMap->setTextureTransform(material.diffuseTexture.transform); - - material._material->setTextureMap(model::MaterialKey::DIFFUSE_MAP, diffuseMap); - } - if (!material.normalTexture.filename.isEmpty()) { - networkMaterial->normalTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.normalTexture.filename)), (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE), material.normalTexture.content); - networkMaterial->normalTextureName = material.normalTexture.name; - - auto normalMap = model::TextureMapPointer(new model::TextureMap()); - normalMap->setTextureSource(networkMaterial->normalTexture->_textureSource); - - material._material->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap); - } - if (!material.specularTexture.filename.isEmpty()) { - networkMaterial->specularTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.specularTexture.filename)), SPECULAR_TEXTURE, material.specularTexture.content); - networkMaterial->specularTextureName = material.specularTexture.name; - - auto glossMap = model::TextureMapPointer(new model::TextureMap()); - glossMap->setTextureSource(networkMaterial->specularTexture->_textureSource); - - material._material->setTextureMap(model::MaterialKey::GLOSS_MAP, glossMap); - } - if (!material.emissiveTexture.filename.isEmpty()) { - networkMaterial->emissiveTexture = textureCache->getTexture(textureBaseUrl.resolved(QUrl(material.emissiveTexture.filename)), EMISSIVE_TEXTURE, material.emissiveTexture.content); - networkMaterial->emissiveTextureName = material.emissiveTexture.name; - - checkForTexcoordLightmap = true; - - auto lightmapMap = model::TextureMapPointer(new model::TextureMap()); - lightmapMap->setTextureSource(networkMaterial->emissiveTexture->_textureSource); - lightmapMap->setTextureTransform(material.emissiveTexture.transform); - lightmapMap->setLightmapOffsetScale(material.emissiveParams.x, material.emissiveParams.y); - - material._material->setTextureMap(model::MaterialKey::LIGHTMAP_MAP, lightmapMap); - } - - return networkMaterial; -} - - -void NetworkGeometry::modelParseSuccess(FBXGeometry* geometry) { - // assume owner ship of geometry pointer - _geometry.reset(geometry); - - - - foreach(const FBXMesh& mesh, _geometry->meshes) { - _meshes.emplace_back(buildNetworkMesh(mesh, _textureBaseUrl)); - } - - QHash fbxMatIDToMatID; - foreach(const FBXMaterial& material, _geometry->materials) { - fbxMatIDToMatID[material.materialID] = _materials.size(); - _materials.emplace_back(buildNetworkMaterial(material, _textureBaseUrl)); - } - - - int meshID = 0; - foreach(const FBXMesh& mesh, _geometry->meshes) { - int partID = 0; - foreach (const FBXMeshPart& part, mesh.parts) { - NetworkShape* networkShape = new NetworkShape(); - networkShape->_meshID = meshID; - networkShape->_partID = partID; - networkShape->_materialID = fbxMatIDToMatID[part.materialID]; - _shapes.emplace_back(networkShape); - partID++; - } - meshID++; - } - - _state = SuccessState; - emit onSuccess(*this, *_geometry.get()); - - delete _resource; - _resource = nullptr; -} - -void NetworkGeometry::modelParseError(int error, QString str) { - _state = ErrorState; - emit onFailure(*this, (NetworkGeometry::Error)error); - - delete _resource; - _resource = nullptr; -} - - -const NetworkMaterial* NetworkGeometry::getShapeMaterial(int shapeID) { - if ((shapeID >= 0) && (shapeID < _shapes.size())) { - int materialID = _shapes[shapeID]->_materialID; - if ((materialID >= 0) && (materialID < _materials.size())) { - return _materials[materialID].get(); - } else { - return 0; - } - } else { - return 0; - } -} - diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index a411d83c18..6102489b5c 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -12,14 +12,12 @@ #ifndef hifi_GeometryCache_h #define hifi_GeometryCache_h +#include "model-networking/ModelCache.h" + #include #include #include -#include - -#include "FBXReader.h" -#include "OBJReader.h" #include #include @@ -28,13 +26,6 @@ #include #include -class NetworkGeometry; -class NetworkMesh; -class NetworkTexture; -class NetworkMaterial; -class NetworkShape; - - typedef glm::vec3 Vec3Key; typedef QPair Vec2Pair; @@ -125,17 +116,13 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) { } /// Stores cached geometry. -class GeometryCache : public ResourceCache, public Dependency { - Q_OBJECT +class GeometryCache : public Dependency { SINGLETON_DEPENDENCY public: int allocateID() { return _nextID++; } static const int UNKNOWN_ID; - virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, - bool delayLoad, const void* extra); - gpu::BufferPointer getCubeVertices(float size); void setupCubeVertices(gpu::Batch& batch, gpu::BufferPointer& verticesBuffer); @@ -313,152 +300,6 @@ private: QHash _sphereColors; QHash _registeredSphereColors; QHash _lastRegisteredSphereColors; - - QHash > _networkGeometry; -}; - -class NetworkGeometry : public QObject { - Q_OBJECT - -public: - // mapping is only used if url is a .fbx or .obj file, it is essentially the content of an fst file. - // if delayLoad is true, the url will not be immediately downloaded. - // use the attemptRequest method to initiate the download. - NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl = QUrl()); - ~NetworkGeometry(); - - const QUrl& getURL() const { return _url; } - - void attemptRequest(); - - // true when the geometry is loaded (but maybe not it's associated textures) - bool isLoaded() const; - - // true when the requested geometry and its textures are loaded. - bool isLoadedWithTextures() const; - - // WARNING: only valid when isLoaded returns true. - const FBXGeometry& getFBXGeometry() const { return *_geometry; } - const std::vector>& getMeshes() const { return _meshes; } - // const model::AssetPointer getAsset() const { return _asset; } - - // model::MeshPointer getShapeMesh(int shapeID); - // int getShapePart(int shapeID); - - // This would be the final verison - // model::MaterialPointer getShapeMaterial(int shapeID); - const NetworkMaterial* getShapeMaterial(int shapeID); - - - void setTextureWithNameToURL(const QString& name, const QUrl& url); - QStringList getTextureNames() const; - - enum Error { - MissingFilenameInMapping = 0, - MappingRequestError, - ModelRequestError, - ModelParseError - }; - -signals: - // Fired when everything has downloaded and parsed successfully. - void onSuccess(NetworkGeometry& networkGeometry, FBXGeometry& fbxGeometry); - - // Fired when something went wrong. - void onFailure(NetworkGeometry& networkGeometry, Error error); - -protected slots: - void mappingRequestDone(const QByteArray& data); - void mappingRequestError(QNetworkReply::NetworkError error); - - void modelRequestDone(const QByteArray& data); - void modelRequestError(QNetworkReply::NetworkError error); - - void modelParseSuccess(FBXGeometry* geometry); - void modelParseError(int error, QString str); - -protected: - void attemptRequestInternal(); - void requestMapping(const QUrl& url); - void requestModel(const QUrl& url); - - enum State { DelayState, - RequestMappingState, - RequestModelState, - ParsingModelState, - SuccessState, - ErrorState }; - State _state; - - QUrl _url; - QUrl _mappingUrl; - QUrl _modelUrl; - QVariantHash _mapping; - QUrl _textureBaseUrl; - - Resource* _resource = nullptr; - std::unique_ptr _geometry; // This should go away evenutally once we can put everything we need in the model::AssetPointer - std::vector> _meshes; - std::vector> _materials; - std::vector> _shapes; - - - // The model asset created from this NetworkGeometry - // model::AssetPointer _asset; - - // cache for isLoadedWithTextures() - mutable bool _isLoadedWithTextures = false; -}; - -/// Reads geometry in a worker thread. -class GeometryReader : public QObject, public QRunnable { - Q_OBJECT -public: - GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping); - virtual void run(); -signals: - void onSuccess(FBXGeometry* geometry); - void onError(int error, QString str); -private: - QUrl _url; - QByteArray _data; - QVariantHash _mapping; -}; - - -class NetworkShape { -public: - int _meshID{ -1 }; - int _partID{ -1 }; - int _materialID{ -1 }; -}; - -class NetworkMaterial { -public: - model::MaterialPointer _material; - QString diffuseTextureName; - QSharedPointer diffuseTexture; - QString normalTextureName; - QSharedPointer normalTexture; - QString specularTextureName; - QSharedPointer specularTexture; - QString emissiveTextureName; - QSharedPointer emissiveTexture; -}; - - -/// The state associated with a single mesh. -class NetworkMesh { -public: - gpu::BufferPointer _indexBuffer; - gpu::BufferPointer _vertexBuffer; - - gpu::BufferStreamPointer _vertexStream; - - gpu::Stream::FormatPointer _vertexFormat; - - int getTranslucentPartCount(const FBXMesh& fbxMesh) const; - bool isPartTranslucent(const FBXMesh& fbxMesh, int partIndex) const; }; #endif // hifi_GeometryCache_h diff --git a/libraries/render-utils/src/TextureCache.h b/libraries/render-utils/src/TextureCache.h index 23ac11d7b0..d6c1e419b9 100644 --- a/libraries/render-utils/src/TextureCache.h +++ b/libraries/render-utils/src/TextureCache.h @@ -1,2 +1,2 @@ // Compatibility -#include +#include diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 1acfb57829..a458d4de51 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -7,4 +7,4 @@ add_dependency_external_projects(glm) find_package(GLM REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) -link_hifi_libraries(shared networking octree gpu gpu-networking procedural model fbx entities animation audio physics) +link_hifi_libraries(shared networking octree gpu procedural model model-networking fbx entities animation audio physics) diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt index 3d42364132..1cb9e9f78e 100644 --- a/tests/gpu-test/CMakeLists.txt +++ b/tests/gpu-test/CMakeLists.txt @@ -10,6 +10,6 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") #include_oglplus() # link in the shared libraries -link_hifi_libraries(networking gpu gpu-networking procedural shared fbx model animation script-engine render-utils ) +link_hifi_libraries(networking gpu procedural shared fbx model model-networking animation script-engine render-utils ) copy_dlls_beside_windows_executable() \ No newline at end of file From e024d23366fa7e818f392c41afaebf5e03d0d7e3 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 18 Sep 2015 18:51:44 -0700 Subject: [PATCH 136/192] Instancing work, second pass --- examples/cubePerfTest.js | 4 +- interface/src/Stars.cpp | 2 +- interface/src/Util.cpp | 28 +- interface/src/avatar/Avatar.cpp | 44 +- interface/src/avatar/Hand.cpp | 13 +- interface/src/avatar/Head.cpp | 7 +- interface/src/avatar/SkeletonModel.cpp | 22 +- interface/src/ui/overlays/Cube3DOverlay.cpp | 7 +- interface/src/ui/overlays/Sphere3DOverlay.cpp | 10 +- .../src/RenderableBoxEntityItem.cpp | 4 +- .../src/RenderableDebugableEntityItem.cpp | 9 +- .../src/RenderableSphereEntityItem.cpp | 17 +- .../src/RenderableZoneEntityItem.cpp | 12 +- libraries/fbx/src/FBXReader.cpp | 4 +- libraries/gpu/src/gpu/Batch.cpp | 8 +- libraries/gpu/src/gpu/Batch.h | 3 + libraries/gpu/src/gpu/Format.cpp | 21 + libraries/gpu/src/gpu/Format.h | 19 +- libraries/gpu/src/gpu/GLBackend.cpp | 9 +- libraries/gpu/src/gpu/GLBackendShared.h | 3 +- libraries/gpu/src/gpu/GLBackendTexture.cpp | 4 +- libraries/gpu/src/gpu/Resource.cpp | 5 - libraries/gpu/src/gpu/Resource.h | 5 + libraries/gpu/src/gpu/Stream.cpp | 44 + libraries/gpu/src/gpu/Stream.h | 30 +- .../src/DeferredLightingEffect.cpp | 95 +- .../render-utils/src/DeferredLightingEffect.h | 28 +- libraries/render-utils/src/Environment.cpp | 6 +- libraries/render-utils/src/GeometryCache.cpp | 983 +++++++++--------- libraries/render-utils/src/GeometryCache.h | 92 +- tests/gpu-test/src/main.cpp | 312 ++---- tests/gpu-test/src/{simple.slf => unlit.slf} | 1 - tests/gpu-test/src/{simple.slv => unlit.slv} | 11 +- 33 files changed, 960 insertions(+), 902 deletions(-) create mode 100644 libraries/gpu/src/gpu/Format.cpp rename tests/gpu-test/src/{simple.slf => unlit.slf} (94%) rename tests/gpu-test/src/{simple.slv => unlit.slv} (64%) diff --git a/examples/cubePerfTest.js b/examples/cubePerfTest.js index 699472edd9..f2f4d48b22 100644 --- a/examples/cubePerfTest.js +++ b/examples/cubePerfTest.js @@ -36,7 +36,7 @@ for (var x = 0; x < SIDE_SIZE; x++) { var position = Vec3.sum(MyAvatar.position, { x: x * 0.2, y: y * 0.2, z: z * 0.2}); var radius = Math.random() * 0.1; boxes.push(Entities.addEntity({ - type: cube ? "Box" : "Box", + type: cube ? "Box" : "Sphere", name: "PerfTest", position: position, dimensions: { x: radius, y: radius, z: radius }, @@ -52,7 +52,7 @@ for (var x = 0; x < SIDE_SIZE; x++) { function scriptEnding() { for (var i = 0; i < boxes.length; i++) { - //Entities.deleteEntity(boxes[i]); + Entities.deleteEntity(boxes[i]); } } Script.scriptEnding.connect(scriptEnding); diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index 119b9ed1a2..6bc160d5eb 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -204,7 +204,7 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { float msecs = (float)(usecTimestampNow() - start) / (float)USECS_PER_MSEC; float secs = msecs / (float)MSECS_PER_SECOND; batch._glUniform1f(_timeSlot, secs); - geometryCache->renderUnitCube(batch); + geometryCache->renderCube(batch); static const size_t VERTEX_STRIDE = sizeof(StarVertex); size_t offset = offsetof(StarVertex, position); diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index dad34e9243..d09dd41999 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -23,6 +23,7 @@ #include #include +#include #include "world.h" #include "Application.h" @@ -93,29 +94,28 @@ void renderWorldBox(gpu::Batch& batch) { geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), glm::vec3(HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY); - geometryCache->renderWireCube(batch, TREE_SCALE, GREY4); + auto deferredLighting = DependencyManager::get(); + + deferredLighting->renderWireCubeInstance(batch, Transform(), GREY4); // Draw meter markers along the 3 axis to help with measuring things const float MARKER_DISTANCE = 1.0f; const float MARKER_RADIUS = 0.05f; - geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, RED); + transform = Transform().setScale(MARKER_RADIUS); + deferredLighting->renderSolidSphereInstance(batch, transform, RED); - transform.setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, 0.0f)); - batch.setModelTransform(transform); - geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, RED); + transform = Transform().setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, 0.0f)).setScale(MARKER_RADIUS); + deferredLighting->renderSolidSphereInstance(batch, transform, RED); - transform.setTranslation(glm::vec3(0.0f, MARKER_DISTANCE, 0.0f)); - batch.setModelTransform(transform); - geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, GREEN); + transform = Transform().setTranslation(glm::vec3(0.0f, MARKER_DISTANCE, 0.0f)).setScale(MARKER_RADIUS); + deferredLighting->renderSolidSphereInstance(batch, transform, GREEN); - transform.setTranslation(glm::vec3(0.0f, 0.0f, MARKER_DISTANCE)); - batch.setModelTransform(transform); - geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, BLUE); + transform = Transform().setTranslation(glm::vec3(0.0f, 0.0f, MARKER_DISTANCE)).setScale(MARKER_RADIUS); + deferredLighting->renderSolidSphereInstance(batch, transform, BLUE); - transform.setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, MARKER_DISTANCE)); - batch.setModelTransform(transform); - geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, GREY); + transform = Transform().setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, MARKER_DISTANCE)).setScale(MARKER_RADIUS); + deferredLighting->renderSolidSphereInstance(batch, transform, GREY); } // Return a random vector of average length 1 diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 5ac830baf3..0a934e1ed3 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -448,15 +448,14 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { // If this is the avatar being looked at, render a little ball above their head if (_isLookAtTarget && Menu::getInstance()->isOptionChecked(MenuOption::RenderFocusIndicator)) { - const float INDICATOR_OFFSET = 0.22f; - const float INDICATOR_RADIUS = 0.03f; - const glm::vec4 LOOK_AT_INDICATOR_COLOR = { 0.8f, 0.0f, 0.0f, 0.75f }; + static const float INDICATOR_OFFSET = 0.22f; + static const float INDICATOR_RADIUS = 0.03f; + static const glm::vec4 LOOK_AT_INDICATOR_COLOR = { 0.8f, 0.0f, 0.0f, 0.75f }; glm::vec3 position = glm::vec3(_position.x, getDisplayNamePosition().y + INDICATOR_OFFSET, _position.z); Transform transform; transform.setTranslation(position); - batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, INDICATOR_RADIUS, - 15, 15, LOOK_AT_INDICATOR_COLOR); + transform.postScale(INDICATOR_RADIUS); + DependencyManager::get()->renderSolidSphereInstance(batch, transform, LOOK_AT_INDICATOR_COLOR); } // If the avatar is looking at me, indicate that they are @@ -473,27 +472,29 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { if (geometry && geometry->isLoaded()) { const float DEFAULT_EYE_DIAMETER = 0.048f; // Typical human eye const float RADIUS_INCREMENT = 0.005f; - Transform transform; + batch.setModelTransform(Transform()); glm::vec3 position = getHead()->getLeftEyePosition(); + Transform transform; transform.setTranslation(position); - batch.setModelTransform(transform); float eyeDiameter = geometry->getFBXGeometry().leftEyeSize; if (eyeDiameter == 0.0f) { eyeDiameter = DEFAULT_EYE_DIAMETER; } - DependencyManager::get()->renderSolidSphere(batch, - eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + + DependencyManager::get()->renderSolidSphereInstance(batch, + Transform(transform).postScale(eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT), + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); position = getHead()->getRightEyePosition(); transform.setTranslation(position); - batch.setModelTransform(transform); eyeDiameter = geometry->getFBXGeometry().rightEyeSize; if (eyeDiameter == 0.0f) { eyeDiameter = DEFAULT_EYE_DIAMETER; } - DependencyManager::get()->renderSolidSphere(batch, - eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + DependencyManager::get()->renderSolidSphereInstance(batch, + Transform(transform).postScale(eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT), + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); } } @@ -518,19 +519,16 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { if (renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE && (sphereRadius > MIN_SPHERE_SIZE) && (angle < MAX_SPHERE_ANGLE) && (angle > MIN_SPHERE_ANGLE)) { + batch.setModelTransform(Transform()); + + Transform transform; transform.setTranslation(_position); transform.setScale(height); - batch.setModelTransform(transform); - - if (_voiceSphereID == GeometryCache::UNKNOWN_ID) { - _voiceSphereID = DependencyManager::get()->allocateID(); - } - - DependencyManager::get()->bindSimpleProgram(batch); - DependencyManager::get()->renderSphere(batch, sphereRadius, 15, 15, - glm::vec4(SPHERE_COLOR[0], SPHERE_COLOR[1], SPHERE_COLOR[2], 1.0f - angle / MAX_SPHERE_ANGLE), true, - _voiceSphereID); + transform.postScale(sphereRadius); + DependencyManager::get()->renderSolidSphereInstance(batch, + transform, + glm::vec4(SPHERE_COLOR[0], SPHERE_COLOR[1], SPHERE_COLOR[2], 1.0f - angle / MAX_SPHERE_ANGLE)); } } } diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp index a0125ca736..a144661f8b 100644 --- a/interface/src/avatar/Hand.cpp +++ b/interface/src/avatar/Hand.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "Avatar.h" #include "AvatarManager.h" @@ -65,16 +66,16 @@ void Hand::renderHandTargets(RenderArgs* renderArgs, bool isMine) { Transform transform = Transform(); transform.setTranslation(position); transform.setRotation(palm.getRotation()); - batch.setModelTransform(transform); - DependencyManager::get()->renderSphere(batch, SPHERE_RADIUS, - NUM_FACETS, NUM_FACETS, grayColor); + transform.postScale(SPHERE_RADIUS); + DependencyManager::get()->renderSolidSphereInstance(batch, transform, grayColor); // draw a green sphere at the old "finger tip" + transform = Transform(); position = palm.getTipPosition(); transform.setTranslation(position); - batch.setModelTransform(transform); - DependencyManager::get()->renderSphere(batch, SPHERE_RADIUS, - NUM_FACETS, NUM_FACETS, greenColor, false); + transform.setRotation(palm.getRotation()); + transform.postScale(SPHERE_RADIUS); + DependencyManager::get()->renderSolidSphereInstance(batch, transform, greenColor); } } diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index a514eb4e8d..96c55dfa93 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -462,13 +462,10 @@ void Head::renderLookatTarget(RenderArgs* renderArgs, glm::vec3 lookatPosition) auto& batch = *renderArgs->_batch; auto transform = Transform{}; transform.setTranslation(lookatPosition); - batch.setModelTransform(transform); auto deferredLighting = DependencyManager::get(); - deferredLighting->bindSimpleProgram(batch); - - auto geometryCache = DependencyManager::get(); const float LOOK_AT_TARGET_RADIUS = 0.075f; + transform.postScale(LOOK_AT_TARGET_RADIUS); const glm::vec4 LOOK_AT_TARGET_COLOR = { 0.8f, 0.0f, 0.0f, 0.75f }; - geometryCache->renderSphere(batch, LOOK_AT_TARGET_RADIUS, 15, 15, LOOK_AT_TARGET_COLOR, true); + deferredLighting->renderSolidSphereInstance(batch, transform, LOOK_AT_TARGET_COLOR); } diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 27e61175eb..ff8cde3df8 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -639,27 +639,25 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha auto geometryCache = DependencyManager::get(); auto deferredLighting = DependencyManager::get(); - Transform transform; // = Transform(); - // draw a blue sphere at the capsule top point glm::vec3 topPoint = _translation + _boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * glm::vec3(0.0f, 1.0f, 0.0f); - transform.setTranslation(topPoint); - batch.setModelTransform(transform); - deferredLighting->bindSimpleProgram(batch); - geometryCache->renderSphere(batch, _boundingCapsuleRadius, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS, - glm::vec4(0.6f, 0.6f, 0.8f, alpha)); + + deferredLighting->renderSolidSphereInstance(batch, + Transform().setTranslation(topPoint).postScale(_boundingCapsuleRadius * 2.0), + glm::vec4(0.6f, 0.6f, 0.8f, alpha)); // draw a yellow sphere at the capsule bottom point glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, _boundingCapsuleHeight, 0.0f); glm::vec3 axis = topPoint - bottomPoint; - transform.setTranslation(bottomPoint); - batch.setModelTransform(transform); - deferredLighting->bindSimpleProgram(batch); - geometryCache->renderSphere(batch, _boundingCapsuleRadius, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS, - glm::vec4(0.8f, 0.8f, 0.6f, alpha)); + + deferredLighting->renderSolidSphereInstance(batch, + Transform().setTranslation(bottomPoint).postScale(_boundingCapsuleRadius * 2.0), + glm::vec4(0.8f, 0.8f, 0.6f, alpha)); // draw a green cylinder between the two points glm::vec3 origin(0.0f); + batch.setModelTransform(Transform().setTranslation(bottomPoint)); + deferredLighting->bindSimpleProgram(batch); Avatar::renderJointConnectingCone(batch, origin, axis, _boundingCapsuleRadius, _boundingCapsuleRadius, glm::vec4(0.6f, 0.8f, 0.6f, alpha)); } diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index a306c7c86d..3ad887ca65 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -61,8 +61,7 @@ void Cube3DOverlay::render(RenderArgs* args) { // } transform.setScale(dimensions); - batch->setModelTransform(transform); - DependencyManager::get()->renderSolidCube(*batch, 1.0f, cubeColor); + DependencyManager::get()->renderSolidCubeInstance(*batch, transform, cubeColor); } else { if (getIsDashedLine()) { @@ -98,9 +97,9 @@ void Cube3DOverlay::render(RenderArgs* args) { geometryCache->renderDashedLine(*batch, bottomRightFar, topRightFar, cubeColor); } else { + batch->setModelTransform(Transform()); transform.setScale(dimensions); - batch->setModelTransform(transform); - DependencyManager::get()->renderWireCube(*batch, 1.0f, cubeColor); + DependencyManager::get()->renderWireCubeInstance(*batch, transform, cubeColor); } } } diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 9712375209..3b503e87e8 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -36,10 +37,15 @@ void Sphere3DOverlay::render(RenderArgs* args) { auto batch = args->_batch; if (batch) { + batch->setModelTransform(Transform()); + Transform transform = _transform; transform.postScale(getDimensions()); - batch->setModelTransform(transform); - DependencyManager::get()->renderSphere(*batch, 1.0f, SLICES, SLICES, sphereColor, _isSolid); + if (_isSolid) { + DependencyManager::get()->renderSolidSphereInstance(*batch, transform, sphereColor); + } else { + DependencyManager::get()->renderWireSphereInstance(*batch, transform, sphereColor); + } } } diff --git a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp b/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp index b9ff69af52..187b25e75a 100644 --- a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp @@ -57,7 +57,9 @@ void RenderableBoxEntityItem::render(RenderArgs* args) { if (_procedural->ready()) { batch.setModelTransform(getTransformToCenter()); // we want to include the scale as well _procedural->prepare(batch, this->getDimensions()); - DependencyManager::get()->renderSolidCube(batch, 1.0f, _procedural->getColor(cubeColor)); + auto color = _procedural->getColor(cubeColor); + batch._glColor4f(color.r, color.g, color.b, color.a); + DependencyManager::get()->renderCube(batch); } else { DependencyManager::get()->renderSolidCubeInstance(batch, getTransformToCenter(), cubeColor); } diff --git a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp index 2770c4edc2..6986025133 100644 --- a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp @@ -23,8 +23,13 @@ void RenderableDebugableEntityItem::renderBoundingBox(EntityItem* entity, Render float puffedOut, glm::vec4& color) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; - batch.setModelTransform(entity->getTransformToCenter()); // we want to include the scale as well - DependencyManager::get()->renderWireCube(batch, 1.0f + puffedOut, color); + + auto xfm = entity->getTransformToCenter(); + if (puffedOut != 0.0) { + xfm.postScale(1.0 + puffedOut); + } + batch.setModelTransform(Transform()); // we want to include the scale as well + DependencyManager::get()->renderWireCubeInstance(batch, xfm, color); } void RenderableDebugableEntityItem::render(EntityItem* entity, RenderArgs* args) { diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp index 82257c67fb..63fbfff9cb 100644 --- a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp @@ -39,15 +39,7 @@ void RenderableSphereEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableSphereEntityItem::render"); Q_ASSERT(getType() == EntityTypes::Sphere); Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - batch.setModelTransform(getTransformToCenter()); // use a transform with scale, rotation, registration point and translation - // TODO: it would be cool to select different slices/stacks geometry based on the size of the sphere - // and the distance to the viewer. This would allow us to reduce the triangle count for smaller spheres - // that aren't close enough to see the tessellation and use larger triangle count for spheres that would - // expose that effect - static const int SLICES = 15, STACKS = 15; - if (!_procedural) { _procedural.reset(new Procedural(getUserData())); _procedural->_vertexSource = simple_vert; @@ -59,12 +51,17 @@ void RenderableSphereEntityItem::render(RenderArgs* args) { gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); } + gpu::Batch& batch = *args->_batch; glm::vec4 sphereColor(toGlm(getXColor()), getLocalRenderAlpha()); if (_procedural->ready()) { + batch.setModelTransform(getTransformToCenter()); // use a transform with scale, rotation, registration point and translation _procedural->prepare(batch, getDimensions()); - DependencyManager::get()->renderSphere(batch, 0.5f, SLICES, STACKS, _procedural->getColor(sphereColor)); + auto color = _procedural->getColor(sphereColor); + batch._glColor4f(color.r, color.g, color.b, color.a); + DependencyManager::get()->renderSphere(batch); } else { - DependencyManager::get()->renderSolidSphere(batch, 0.5f, SLICES, STACKS, sphereColor); + batch.setModelTransform(Transform()); + DependencyManager::get()->renderSolidSphereInstance(batch, getTransformToCenter(), sphereColor); } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 930a684617..90aff03f55 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -121,15 +121,15 @@ void RenderableZoneEntityItem::render(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; - batch.setModelTransform(getTransformToCenter()); - + batch.setModelTransform(Transform()); + + auto xfm = getTransformToCenter(); auto deferredLightingEffect = DependencyManager::get(); - if (getShapeType() == SHAPE_TYPE_SPHERE) { - const int SLICES = 15, STACKS = 15; - deferredLightingEffect->renderWireSphere(batch, 0.5f, SLICES, STACKS, DEFAULT_COLOR); + xfm.postScale(0.5); + deferredLightingEffect->renderWireSphereInstance(batch, xfm, DEFAULT_COLOR); } else { - deferredLightingEffect->renderWireCube(batch, 1.0f, DEFAULT_COLOR); + deferredLightingEffect->renderWireCubeInstance(batch, xfm, DEFAULT_COLOR); } break; } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 6f69e8befc..0864414e5e 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1436,12 +1436,12 @@ void buildModelMesh(ExtractedMesh& extracted, const QString& url) { if (clusterIndicesSize) { mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, model::BufferView(attribBuffer, clusterIndicesOffset, clusterIndicesSize, - gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW))); + gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW))); } if (clusterWeightsSize) { mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, model::BufferView(attribBuffer, clusterWeightsOffset, clusterWeightsSize, - gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW))); + gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW))); } diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index e5ec8525b6..e6e176be88 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -304,12 +304,16 @@ bool Batch::isSkyboxEnabled() const { return _enableSkybox; } -void Batch::setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function) { +void Batch::setupNamedCalls(const std::string& instanceName, size_t count, NamedBatchData::Function function) { NamedBatchData& instance = _namedData[instanceName]; - ++instance._count; + instance._count += count; instance._function = function; } +void Batch::setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function) { + setupNamedCalls(instanceName, 1, function); +} + BufferPointer Batch::getNamedBuffer(const std::string& instanceName, uint8_t index) { NamedBatchData& instance = _namedData[instanceName]; if (instance._buffers.size() <= index) { diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index c3bf6250c5..ec6fb26c34 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -62,8 +62,10 @@ public: Function _function; void process(Batch& batch) { + if (_function) { _function(batch, *this); } + } }; using NamedBatchDataMap = std::map; @@ -96,6 +98,7 @@ public: void drawIndexedInstanced(uint32 nbInstances, Primitive primitiveType, uint32 nbIndices, uint32 startIndex = 0, uint32 startInstance = 0); + void setupNamedCalls(const std::string& instanceName, size_t count, NamedBatchData::Function function); void setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function); BufferPointer getNamedBuffer(const std::string& instanceName, uint8_t index = 0); diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp new file mode 100644 index 0000000000..a66fc19458 --- /dev/null +++ b/libraries/gpu/src/gpu/Format.cpp @@ -0,0 +1,21 @@ +// +// Created by Bradley Austin Davis on 2015/09/20 +// Copyright 2013-2015 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 "Format.h" + +using namespace gpu; + +const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA }; +const Element Element::COLOR_RGBA{ VEC4, FLOAT, RGBA }; +const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; +const Element Element::VEC2F_XY{ VEC2, FLOAT, XY }; +const Element Element::VEC3F_XYZ{ VEC3, FLOAT, XYZ }; +const Element Element::VEC4F_XYZW{ VEC4, FLOAT, XYZW }; +const Element Element::INDEX_UINT16{ SCALAR, UINT16, INDEX }; +const Element Element::PART_DRAWCALL{ VEC4, UINT32, PART }; + diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index e16256574b..530db084a3 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -56,10 +56,8 @@ enum Type { INT8, UINT8, - NFLOAT, NINT32, NUINT32, - NHALF, NINT16, NUINT16, NINT8, @@ -68,6 +66,7 @@ enum Type { NUM_TYPES, BOOL = UINT8, + NORMALIZED_START = NINT32, }; // Array providing the size in bytes for a given scalar type static const int TYPE_SIZE[NUM_TYPES] = { @@ -79,10 +78,10 @@ static const int TYPE_SIZE[NUM_TYPES] = { 2, 1, 1, + + // normalized values 4, 4, - 4, - 2, 2, 2, 1, @@ -99,10 +98,9 @@ static const bool TYPE_IS_INTEGER[NUM_TYPES] = { true, true, - false, + // Normalized values true, true, - false, true, true, true, @@ -151,6 +149,7 @@ enum Semantic { RGB, RGBA, BGRA, + XY, XYZ, XYZW, QUAT, @@ -199,7 +198,7 @@ public: uint8 getLocationCount() const { return LOCATION_COUNT[(Dimension)_dimension]; } Type getType() const { return (Type)_type; } - bool isNormalized() const { return (getType() >= NFLOAT); } + bool isNormalized() const { return (getType() >= NORMALIZED_START); } bool isInteger() const { return TYPE_IS_INTEGER[getType()]; } uint32 getSize() const { return DIMENSION_COUNT[_dimension] * TYPE_SIZE[_type]; } @@ -215,10 +214,14 @@ public: } static const Element COLOR_RGBA_32; + static const Element COLOR_RGBA; + static const Element VEC2F_UV; + static const Element VEC2F_XY; static const Element VEC3F_XYZ; + static const Element VEC4F_XYZW; static const Element INDEX_UINT16; static const Element PART_DRAWCALL; - + protected: uint8 _semantic; uint8 _dimension : 4; diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index 43c01d0337..62508f273c 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -127,7 +127,12 @@ void GLBackend::renderPassTransfer(Batch& batch) { const size_t numCommands = batch.getCommands().size(); const Batch::Commands::value_type* command = batch.getCommands().data(); const Batch::CommandOffsets::value_type* offset = batch.getCommandOffsets().data(); - + + for (auto& cached : batch._buffers._items) { + if (cached._data) { + syncGPUObject(*cached._data); + } + } // Reset the transform buffers _transform._cameras.resize(0); _transform._cameraOffsets.clear(); @@ -330,7 +335,7 @@ void GLBackend::do_drawIndexedInstanced(Batch& batch, uint32 paramOffset) { uint32 startInstance = batch._params[paramOffset + 0]._uint; GLenum glType = _elementTypeToGLType[_input._indexBufferType]; - glDrawElementsInstanced(mode, numIndices, glType, nullptr, numInstances); + glDrawElementsInstanced(mode, numIndices, glType, reinterpret_cast(startIndex + _input._indexBufferOffset), numInstances); (void)CHECK_GL_ERROR(); } diff --git a/libraries/gpu/src/gpu/GLBackendShared.h b/libraries/gpu/src/gpu/GLBackendShared.h index 7ce54665be..21bd10a33a 100644 --- a/libraries/gpu/src/gpu/GLBackendShared.h +++ b/libraries/gpu/src/gpu/GLBackendShared.h @@ -34,10 +34,9 @@ static const GLenum _elementTypeToGLType[gpu::NUM_TYPES] = { GL_UNSIGNED_SHORT, GL_BYTE, GL_UNSIGNED_BYTE, - GL_FLOAT, + // Normalized values GL_INT, GL_UNSIGNED_INT, - GL_HALF_FLOAT, GL_SHORT, GL_UNSIGNED_SHORT, GL_BYTE, diff --git a/libraries/gpu/src/gpu/GLBackendTexture.cpp b/libraries/gpu/src/gpu/GLBackendTexture.cpp index d4e1db125d..dce5236868 100755 --- a/libraries/gpu/src/gpu/GLBackendTexture.cpp +++ b/libraries/gpu/src/gpu/GLBackendTexture.cpp @@ -156,7 +156,6 @@ public: texel.internalFormat = GL_DEPTH_COMPONENT32; break; } - case gpu::NFLOAT: case gpu::FLOAT: { texel.internalFormat = GL_DEPTH_COMPONENT32F; break; @@ -165,8 +164,7 @@ public: case gpu::INT16: case gpu::NUINT16: case gpu::NINT16: - case gpu::HALF: - case gpu::NHALF: { + case gpu::HALF: { texel.internalFormat = GL_DEPTH_COMPONENT16; break; } diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index 4cdbe8203a..c162b38b93 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -14,11 +14,6 @@ using namespace gpu; -const Element Element::COLOR_RGBA_32 = Element(VEC4, NUINT8, RGBA); -const Element Element::VEC3F_XYZ = Element(VEC3, FLOAT, XYZ); -const Element Element::INDEX_UINT16 = Element(SCALAR, UINT16, INDEX); -const Element Element::PART_DRAWCALL = Element(VEC4, UINT32, PART); - Resource::Size Resource::Sysmem::allocateMemory(Byte** dataAllocated, Size size) { if ( !dataAllocated ) { qWarning() << "Buffer::Sysmem::allocateMemory() : Must have a valid dataAllocated pointer."; diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index de5e4a7242..f330b0fd07 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -144,6 +144,11 @@ public: return append(sizeof(t), reinterpret_cast(&t)); } + template + Size append(const std::vector& t) { + return append(sizeof(T) * t.size(), reinterpret_cast(&t[0])); + } + // Access the sysmem object. const Sysmem& getSysmem() const { assert(_sysmem); return (*_sysmem); } Sysmem& editSysmem() { assert(_sysmem); return (*_sysmem); } diff --git a/libraries/gpu/src/gpu/Stream.cpp b/libraries/gpu/src/gpu/Stream.cpp index 634545b4dd..98d23ac266 100644 --- a/libraries/gpu/src/gpu/Stream.cpp +++ b/libraries/gpu/src/gpu/Stream.cpp @@ -15,6 +15,37 @@ using namespace gpu; +using ElementArray = std::array; + +const ElementArray& getDefaultElements() { + static ElementArray defaultElements{ + //POSITION = 0, + Element::VEC3F_XYZ, + //NORMAL = 1, + Element::VEC3F_XYZ, + //COLOR = 2, + Element::COLOR_RGBA_32, + //TEXCOORD0 = 3, + Element::VEC2F_UV, + //TANGENT = 4, + Element::VEC3F_XYZ, + //SKIN_CLUSTER_INDEX = 5, + Element::VEC4F_XYZW, + //SKIN_CLUSTER_WEIGHT = 6, + Element::VEC4F_XYZW, + //TEXCOORD1 = 7, + Element::VEC2F_UV, + //INSTANCE_SCALE = 8, + Element::VEC3F_XYZ, + //INSTANCE_TRANSLATE = 9, + Element::VEC3F_XYZ, + //INSTANCE_XFM = 10, + // FIXME make a matrix element + Element::VEC4F_XYZW + }; + return defaultElements; +} + void Stream::Format::evaluateCache() { _channels.clear(); _elementTotalSize = 0; @@ -34,6 +65,19 @@ bool Stream::Format::setAttribute(Slot slot, Slot channel, Element element, Offs return true; } +bool Stream::Format::setAttribute(Slot slot, Frequency frequency) { + _attributes[slot] = Attribute((InputSlot)slot, slot, getDefaultElements()[slot], 0, frequency); + evaluateCache(); + return true; +} + +bool Stream::Format::setAttribute(Slot slot, Slot channel, Frequency frequency) { + _attributes[slot] = Attribute((InputSlot)slot, channel, getDefaultElements()[slot], 0, frequency); + evaluateCache(); + return true; +} + + BufferStream::BufferStream() : _buffers(), _offsets(), diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h index c0ad1ebe46..420aa50f72 100644 --- a/libraries/gpu/src/gpu/Stream.h +++ b/libraries/gpu/src/gpu/Stream.h @@ -11,12 +11,14 @@ #ifndef hifi_gpu_Stream_h #define hifi_gpu_Stream_h +#include +#include +#include + #include #include "Resource.h" #include "Format.h" -#include -#include namespace gpu { @@ -55,6 +57,8 @@ public: // Every thing that is needed to detail a stream attribute and how to interpret it class Attribute { public: + Attribute() {} + Attribute(Slot slot, Slot channel, Element element, Offset offset = 0, Frequency frequency = PER_VERTEX) : _slot(slot), _channel(channel), @@ -62,21 +66,12 @@ public: _offset(offset), _frequency(frequency) {} - Attribute() : - _slot(POSITION), - _channel(0), - _element(), - _offset(0), - _frequency(PER_VERTEX) - {} - - Slot _slot; // Logical slot assigned to the attribute - Slot _channel; // index of the channel where to get the data from - Element _element; - - Offset _offset; - uint32 _frequency; + Slot _slot{ POSITION }; // Logical slot assigned to the attribute + Slot _channel{ POSITION }; // index of the channel where to get the data from + Element _element{ Element::VEC3F_XYZ }; + Offset _offset{ 0 }; + uint32 _frequency{ PER_VERTEX }; // Size of the uint32 getSize() const { return _element.getSize(); } @@ -113,6 +108,9 @@ public: uint32 getElementTotalSize() const { return _elementTotalSize; } bool setAttribute(Slot slot, Slot channel, Element element, Offset offset = 0, Frequency frequency = PER_VERTEX); + bool setAttribute(Slot slot, Frequency frequency = PER_VERTEX); + bool setAttribute(Slot slot, Slot channel, Frequency frequency = PER_VERTEX); + protected: AttributeMap _attributes; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 6c7310509a..1db26eae3b 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -187,15 +187,8 @@ gpu::PipelinePointer DeferredLightingEffect::bindSimpleProgram(gpu::Batch& batch return pipeline; } +void DeferredLightingEffect::renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { -void DeferredLightingEffect::renderSolidSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderSphere(batch, radius, slices, stacks, color); -} - -void DeferredLightingEffect::renderWireSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderSphere(batch, radius, slices, stacks, color, false); } uint32_t toCompactColor(const glm::vec4& color) { @@ -206,39 +199,89 @@ uint32_t toCompactColor(const glm::vec4& color) { return compactColor; } -void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { - static const std::string INSTANCE_NAME = __FUNCTION__; - static const size_t TRANSFORM_BUFFER = 0; - static const size_t COLOR_BUFFER = 1; +static const size_t INSTANCE_TRANSFORM_BUFFER = 0; +static const size_t INSTANCE_COLOR_BUFFER = 1; + +template +void renderInstances(const std::string& name, gpu::Batch& batch, const Transform& xfm, const glm::vec4& color, F f) { { - gpu::BufferPointer instanceTransformBuffer = batch.getNamedBuffer(INSTANCE_NAME, TRANSFORM_BUFFER); + gpu::BufferPointer instanceTransformBuffer = batch.getNamedBuffer(name, INSTANCE_TRANSFORM_BUFFER); glm::mat4 xfmMat4; instanceTransformBuffer->append(xfm.getMatrix(xfmMat4)); - gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(INSTANCE_NAME, COLOR_BUFFER); + gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(name, INSTANCE_COLOR_BUFFER); auto compactColor = toCompactColor(color); instanceColorBuffer->append(compactColor); } - batch.setupNamedCalls(INSTANCE_NAME, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { - auto pipeline = bindSimpleProgram(batch); + auto that = DependencyManager::get(); + batch.setupNamedCalls(name, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + auto pipeline = that->bindSimpleProgram(batch); auto location = pipeline->getProgram()->getUniforms().findLocation("Instanced"); batch._glUniform1i(location, 1); - DependencyManager::get()->renderSolidCubeInstances(batch, data._count, - data._buffers[TRANSFORM_BUFFER], data._buffers[COLOR_BUFFER]); + f(batch, data); batch._glUniform1i(location, 0); }); } -void DeferredLightingEffect::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderSolidCube(batch, size, color); +void DeferredLightingEffect::renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { + static const std::string INSTANCE_NAME = __FUNCTION__; + renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + DependencyManager::get()->renderSphereInstances(batch, data._count, + data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); + }); } -void DeferredLightingEffect::renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color) { - bindSimpleProgram(batch); - DependencyManager::get()->renderWireCube(batch, size, color); +static auto startTime = usecTimestampNow(); + +void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { + static const std::string INSTANCE_NAME = __FUNCTION__; + +#ifdef DEBUG_SHAPES + renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + + auto usecs = usecTimestampNow(); + usecs -= startTime; + auto msecs = usecs / USECS_PER_MSEC; + float seconds = msecs; + seconds /= MSECS_PER_SECOND; + float fractionalSeconds = seconds - floor(seconds); + int shapeIndex = (int)seconds; + + GeometryCache::Shape shapes[] = { + GeometryCache::Cube, + GeometryCache::Tetrahedron, + GeometryCache::Sphere, + GeometryCache::Icosahedron, + GeometryCache::Line, + }; + + shapeIndex %= 5; + GeometryCache::Shape shape = shapes[shapeIndex]; + + if (fractionalSeconds > 0.5f) { + DependencyManager::get()->renderShapeInstances(batch, shape, data._count, + data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); + } else { + DependencyManager::get()->renderWireShapeInstances(batch, shape, data._count, + data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); + } + }); +#else + renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + DependencyManager::get()->renderCubeInstances(batch, data._count, + data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); + }); +#endif +} + +void DeferredLightingEffect::renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { + static const std::string INSTANCE_NAME = __FUNCTION__; + renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + DependencyManager::get()->renderWireCubeInstances(batch, data._count, + data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); + }); } void DeferredLightingEffect::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, @@ -546,8 +589,10 @@ void DeferredLightingEffect::render(RenderArgs* args) { } else { Transform model; model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z)); + model.postScale(expandedRadius); batch.setModelTransform(model); - geometryCache->renderSphere(batch, expandedRadius, 32, 32, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); + batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + geometryCache->renderSphere(batch); } } } diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 83bb4c215f..9c4809a82e 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -40,24 +40,26 @@ public: gpu::PipelinePointer bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool culled = true, bool emmisive = false, bool depthBias = false); - /// Sets up the state necessary to render static untextured geometry with the simple program. - void bindInstanceProgram(gpu::Batch& batch, bool textured = false, bool culled = true, - bool emmisive = false, bool depthBias = false); + void renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color); + void renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) { + renderSolidSphereInstance(batch, xfm, glm::vec4(color, 1.0)); + } - //// Renders a solid sphere with the simple program. - void renderSolidSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color); + void renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color); + void renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) { + renderWireSphereInstance(batch, xfm, glm::vec4(color, 1.0)); + } - //// Renders a wireframe sphere with the simple program. - void renderWireSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color); - - //// Renders a solid cube using instancing. Transform should include scaling. void renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color); + void renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) { + renderSolidCubeInstance(batch, xfm, glm::vec4(color, 1.0)); + } - //// Renders a solid cube with the simple program. - void renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color); + void renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color); + void renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) { + renderWireCubeInstance(batch, xfm, glm::vec4(color, 1.0)); + } - //// Renders a wireframe cube with the simple program. - void renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color); //// Renders a quad with the simple program. void renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, const glm::vec4& color); diff --git a/libraries/render-utils/src/Environment.cpp b/libraries/render-utils/src/Environment.cpp index 605f67f957..bffac32b0c 100644 --- a/libraries/render-utils/src/Environment.cpp +++ b/libraries/render-utils/src/Environment.cpp @@ -197,6 +197,9 @@ bool Environment::findCapsulePenetration(const glm::vec3& start, const glm::vec3 } void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum, const EnvironmentData& data) { + // FIXME atmosphere rendering is broken in some way, + // should probably be replaced by a procedual skybox and put on the marketplace + return; glm::vec3 center = data.getAtmosphereCenter(); @@ -252,5 +255,6 @@ void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum, batch._glUniform1f(locations[G_LOCATION], -0.990f); batch._glUniform1f(locations[G2_LOCATION], -0.990f * -0.990f); - DependencyManager::get()->renderSphere(batch,1.0f, 100, 50, glm::vec4(1.0f, 0.0f, 0.0f, 0.5f)); //Draw a unit sphere + batch._glColor4f(1.0f, 0.0f, 0.0f, 0.5f); + DependencyManager::get()->renderSphere(batch); //Draw a unit sphere } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index ea05df84ef..53eb8a454b 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -33,11 +33,432 @@ const int GeometryCache::UNKNOWN_ID = -1; -GeometryCache::GeometryCache() : - _nextID(0) -{ +static const uint FLOATS_PER_VERTEX = 3; +static const uint VERTICES_PER_TRIANGLE = 3; +static const uint TRIANGLES_PER_QUAD = 2; +static const uint CUBE_FACES = 6; +static const uint CUBE_VERTICES_PER_FACE = 4; +static const uint CUBE_VERTICES = CUBE_FACES * CUBE_VERTICES_PER_FACE; +static const uint CUBE_VERTEX_POINTS = CUBE_VERTICES * FLOATS_PER_VERTEX; +static const uint CUBE_INDICES = CUBE_FACES * TRIANGLES_PER_QUAD * VERTICES_PER_TRIANGLE; +static const uint SPHERE_LATITUDES = 24; +static const uint SPHERE_MERIDIANS = SPHERE_LATITUDES * 2; +static const uint SPHERE_INDICES = SPHERE_MERIDIANS * (SPHERE_LATITUDES - 1) * TRIANGLES_PER_QUAD * VERTICES_PER_TRIANGLE; + +static const gpu::Element POSITION_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element NORMAL_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element COLOR_ELEMENT{ gpu::VEC4, gpu::NUINT8, gpu::RGBA }; +static const gpu::Element TRANSFORM_ELEMENT{ gpu::MAT4, gpu::FLOAT, gpu::XYZW }; + +static gpu::Stream::FormatPointer SOLID_STREAM_FORMAT; +static gpu::Stream::FormatPointer INSTANCED_SOLID_STREAM_FORMAT; + +static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec3) * 2; // vertices and normals +static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3); + + +void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices) { + vertexBuffer->append(vertices); + + _positionView = gpu::BufferView(vertexBuffer, 0, + vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, POSITION_ELEMENT); + _normalView = gpu::BufferView(vertexBuffer, SHAPE_NORMALS_OFFSET, + vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, NORMAL_ELEMENT); +} + +void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices) { + _indices = indexBuffer; + if (!indices.empty()) { + _indexOffset = indexBuffer->getSize(); + _indexCount = indices.size(); + indexBuffer->append(indices); + } + + if (!wireIndices.empty()) { + _wireIndexOffset = indexBuffer->getSize(); + _wireIndexCount = wireIndices.size(); + indexBuffer->append(wireIndices); + } +} + +void GeometryCache::ShapeData::setupBatch(gpu::Batch& batch) const { + batch.setInputBuffer(gpu::Stream::POSITION, _positionView); + batch.setInputBuffer(gpu::Stream::NORMAL, _normalView); +} + +void GeometryCache::ShapeData::draw(gpu::Batch& batch) const { + if (_indexCount) { + setupBatch(batch); + batch.setIndexBuffer(gpu::UINT16, _indices, _indexOffset); + batch.drawIndexed(gpu::TRIANGLES, _indexCount); + } +} + +void GeometryCache::ShapeData::drawWire(gpu::Batch& batch) const { + if (_wireIndexCount) { + setupBatch(batch); + batch.setIndexBuffer(gpu::UINT16, _indices, _wireIndexOffset); + batch.drawIndexed(gpu::LINES, _wireIndexCount); + } +} + +void GeometryCache::ShapeData::drawInstances(gpu::Batch& batch, size_t count) const { + if (_indexCount) { + setupBatch(batch); + batch.setIndexBuffer(gpu::UINT16, _indices, _indexOffset); + batch.drawIndexedInstanced(count, gpu::TRIANGLES, _indexCount); + } +} + +void GeometryCache::ShapeData::drawWireInstances(gpu::Batch& batch, size_t count) const { + if (_wireIndexCount) { + setupBatch(batch); + batch.setIndexBuffer(gpu::UINT16, _indices, _wireIndexOffset); + batch.drawIndexedInstanced(count, gpu::LINES, _wireIndexCount); + } +} + +const VVertex& icosahedronVertices() { + static const float phi = (1.0 + sqrt(5.0)) / 2.0; + static const float a = 0.5; + static const float b = 1.0 / (2.0 * phi); + + static const VVertex vertices{ // + vec3(0, b, -a), vec3(-b, a, 0), vec3(b, a, 0), // + vec3(0, b, a), vec3(b, a, 0), vec3(-b, a, 0), // + vec3(0, b, a), vec3(-a, 0, b), vec3(0, -b, a), // + vec3(0, b, a), vec3(0, -b, a), vec3(a, 0, b), // + vec3(0, b, -a), vec3(a, 0, -b), vec3(0, -b, -a),// + vec3(0, b, -a), vec3(0, -b, -a), vec3(-a, 0, -b), // + vec3(0, -b, a), vec3(-b, -a, 0), vec3(b, -a, 0), // + vec3(0, -b, -a), vec3(b, -a, 0), vec3(-b, -a, 0), // + vec3(-b, a, 0), vec3(-a, 0, -b), vec3(-a, 0, b), // + vec3(-b, -a, 0), vec3(-a, 0, b), vec3(-a, 0, -b), // + vec3(b, a, 0), vec3(a, 0, b), vec3(a, 0, -b), // + vec3(b, -a, 0), vec3(a, 0, -b), vec3(a, 0, b), // + vec3(0, b, a), vec3(-b, a, 0), vec3(-a, 0, b), // + vec3(0, b, a), vec3(a, 0, b), vec3(b, a, 0), // + vec3(0, b, -a), vec3(-a, 0, -b), vec3(-b, a, 0), // + vec3(0, b, -a), vec3(b, a, 0), vec3(a, 0, -b), // + vec3(0, -b, -a), vec3(-b, -a, 0), vec3(-a, 0, -b), // + vec3(0, -b, -a), vec3(a, 0, -b), vec3(b, -a, 0), // + vec3(0, -b, a), vec3(-a, 0, b), vec3(-b, -a, 0), // + vec3(0, -b, a), vec3(b, -a, 0), vec3(a, 0, b) + }; // + return vertices; +} + +const VVertex& tetrahedronVertices() { + static const float a = 1.0f / sqrt(2.0f); + static const auto A = vec3(0, 1, a); + static const auto B = vec3(0, -1, a); + static const auto C = vec3(1, 0, -a); + static const auto D = vec3(-1, 0, -a); + static const VVertex vertices{ + A, B, C, + D, B, A, + C, D, A, + C, B, D, + }; + return vertices; +} + +VVertex tesselate(const VVertex& startingTriangles, int count) { + VVertex triangles = startingTriangles; + if (0 != (triangles.size() % 3)) { + throw std::runtime_error("Bad number of vertices for tesselation"); + } + + for (size_t i = 0; i < triangles.size(); ++i) { + triangles[i] = glm::normalize(triangles[i]); + } + + VVertex newTriangles; + while (count) { + newTriangles.clear(); + newTriangles.reserve(triangles.size() * 4); + for (size_t i = 0; i < triangles.size(); i += 3) { + const vec3& a = triangles[i]; + const vec3& b = triangles[i + 1]; + const vec3& c = triangles[i + 2]; + vec3 ab = glm::normalize(a + b); + vec3 bc = glm::normalize(b + c); + vec3 ca = glm::normalize(c + a); + + newTriangles.push_back(a); + newTriangles.push_back(ab); + newTriangles.push_back(ca); + + newTriangles.push_back(b); + newTriangles.push_back(bc); + newTriangles.push_back(ab); + + newTriangles.push_back(c); + newTriangles.push_back(ca); + newTriangles.push_back(bc); + + newTriangles.push_back(ab); + newTriangles.push_back(bc); + newTriangles.push_back(ca); + } + triangles.swap(newTriangles); + --count; + } + return triangles; +} + +// FIXME solids need per-face vertices, but smooth shaded +// components do not. Find a way to support using draw elements +// or draw arrays as appropriate +// Maybe special case cone and cylinder since they combine flat +// and smooth shading +void GeometryCache::buildShapes() { + auto vertexBuffer = std::make_shared(); + auto indexBuffer = std::make_shared(); + uint16_t startingIndex = 0; + + // Cube + startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; + { + ShapeData& shapeData = _shapes[Cube]; + VVertex vertices; + // front + vertices.push_back(vec3(1, 1, 1)); + vertices.push_back(vec3(0, 0, 1)); + vertices.push_back(vec3(-1, 1, 1)); + vertices.push_back(vec3(0, 0, 1)); + vertices.push_back(vec3(-1, -1, 1)); + vertices.push_back(vec3(0, 0, 1)); + vertices.push_back(vec3(1, -1, 1)); + vertices.push_back(vec3(0, 0, 1)); + + // right + vertices.push_back(vec3(1, 1, 1)); + vertices.push_back(vec3(1, 0, 0)); + vertices.push_back(vec3(1, -1, 1)); + vertices.push_back(vec3(1, 0, 0)); + vertices.push_back(vec3(1, -1, -1)); + vertices.push_back(vec3(1, 0, 0)); + vertices.push_back(vec3(1, 1, -1)); + vertices.push_back(vec3(1, 0, 0)); + + // top + vertices.push_back(vec3(1, 1, 1)); + vertices.push_back(vec3(0, 1, 0)); + vertices.push_back(vec3(1, 1, -1)); + vertices.push_back(vec3(0, 1, 0)); + vertices.push_back(vec3(-1, 1, -1)); + vertices.push_back(vec3(0, 1, 0)); + vertices.push_back(vec3(-1, 1, 1)); + vertices.push_back(vec3(0, 1, 0)); + + // left + vertices.push_back(vec3(-1, 1, 1)); + vertices.push_back(vec3(-1, 0, 0)); + vertices.push_back(vec3(-1, 1, -1)); + vertices.push_back(vec3(-1, 0, 0)); + vertices.push_back(vec3(-1, -1, -1)); + vertices.push_back(vec3(-1, 0, 0)); + vertices.push_back(vec3(-1, -1, 1)); + vertices.push_back(vec3(-1, 0, 0)); + + // bottom + vertices.push_back(vec3(-1, -1, -1)); + vertices.push_back(vec3(0, -1, 0)); + vertices.push_back(vec3(1, -1, -1)); + vertices.push_back(vec3(0, -1, 0)); + vertices.push_back(vec3(1, -1, 1)); + vertices.push_back(vec3(0, -1, 0)); + vertices.push_back(vec3(-1, -1, 1)); + vertices.push_back(vec3(0, -1, 0)); + + // back + vertices.push_back(vec3(1, -1, -1)); + vertices.push_back(vec3(0, 0, -1)); + vertices.push_back(vec3(-1, -1, -1)); + vertices.push_back(vec3(0, 0, -1)); + vertices.push_back(vec3(-1, 1, -1)); + vertices.push_back(vec3(0, 0, -1)); + vertices.push_back(vec3(1, 1, -1)); + vertices.push_back(vec3(0, 0, -1)); + + for (size_t i = 0; i < vertices.size(); ++i) { + if (0 == i % 2) { + vertices[i] *= 0.5f; + } + } + shapeData.setupVertices(_shapeVertices, vertices); + + VIndex indices{ + 0, 1, 2, 2, 3, 0, // front + 4, 5, 6, 6, 7, 4, // right + 8, 9, 10, 10, 11, 8, // top + 12, 13, 14, 14, 15, 12, // left + 16, 17, 18, 18, 19, 16, // bottom + 20, 21, 22, 22, 23, 20 // back + }; + for (int i = 0; i < indices.size(); ++i) { + indices[i] += startingIndex; + } + + VIndex wireIndices{ + 0, 1, 1, 2, 2, 3, 3, 0, // front + 20, 21, 21, 22, 22, 23, 23, 20, // back + 0, 23, 1, 22, 2, 21, 3, 20 // sides + }; + for (int i = 0; i < wireIndices.size(); ++i) { + indices[i] += startingIndex; + } + + shapeData.setupIndices(_shapeIndices, indices, wireIndices); + } + + // Tetrahedron + startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; + { + ShapeData& shapeData = _shapes[Tetrahedron]; + size_t vertexCount = 4; + VVertex vertices; + { + VVertex originalVertices = tetrahedronVertices(); + vertexCount = originalVertices.size(); + vertices.reserve(originalVertices.size() * 2); + for (size_t i = 0; i < originalVertices.size(); i += 3) { + vec3 faceNormal; + for (size_t j = 0; j < 3; ++j) { + faceNormal += originalVertices[i + j]; + } + faceNormal = glm::normalize(faceNormal); + for (size_t j = 0; j < 3; ++j) { + vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f); + vertices.push_back(faceNormal); + } + } + } + shapeData.setupVertices(_shapeVertices, vertices); + + VIndex indices; + for (size_t i = 0; i < vertexCount; i += 3) { + for (size_t j = 0; j < 3; ++j) { + indices.push_back(i + j + startingIndex); + } + } + + VIndex wireIndices{ + 0, 1, 1, 2, 2, 0, + 0, 3, 1, 3, 2, 3, + }; + + for (int i = 0; i < wireIndices.size(); ++i) { + wireIndices[i] += startingIndex; + } + + shapeData.setupIndices(_shapeIndices, indices, wireIndices); + } + + // Sphere + // FIXME this uses way more vertices than required. Should find a way to calculate the indices + // using shared vertices for better vertex caching + startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; + { + ShapeData& shapeData = _shapes[Sphere]; + VVertex vertices; + VIndex indices; + { + VVertex originalVertices = tesselate(icosahedronVertices(), 3); + vertices.reserve(originalVertices.size() * 2); + for (size_t i = 0; i < originalVertices.size(); i += 3) { + for (int j = 0; j < 3; ++j) { + vertices.push_back(originalVertices[i + j] * 0.5f); + vertices.push_back(originalVertices[i + j]); + indices.push_back(i + j + startingIndex); + } + } + } + + shapeData.setupVertices(_shapeVertices, vertices); + // FIXME don't use solid indices for wire drawing. + shapeData.setupIndices(_shapeIndices, indices, indices); + } + + // Icosahedron + startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; + { + ShapeData& shapeData = _shapes[Icosahedron]; + + VVertex vertices; + VIndex indices; + { + const VVertex& originalVertices = icosahedronVertices(); + vertices.reserve(originalVertices.size() * 2); + for (size_t i = 0; i < originalVertices.size(); i += 3) { + vec3 faceNormal; + for (size_t j = 0; j < 3; ++j) { + faceNormal += originalVertices[i + j]; + } + faceNormal = glm::normalize(faceNormal); + for (int j = 0; j < 3; ++j) { + vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f); + vertices.push_back(faceNormal); + indices.push_back(i + j + startingIndex); + } + } + } + + shapeData.setupVertices(_shapeVertices, vertices); + // FIXME don't use solid indices for wire drawing. + shapeData.setupIndices(_shapeIndices, indices, indices); + } + + //Triangle, + //Quad, + //Circle, + //Octahetron, + //Dodecahedron, + //Torus, + //Cone, + //Cylinder, + // Line + startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; + { + ShapeData& shapeData = _shapes[Line]; + shapeData.setupVertices(_shapeVertices, VVertex{ + vec3(-0.5, 0, 0), vec3(-0.5, 0, 0), + vec3(0.5f, 0, 0), vec3(0.5f, 0, 0) + }); + VIndex wireIndices; + wireIndices.push_back(0 + startingIndex); + wireIndices.push_back(1 + startingIndex); + + shapeData.setupIndices(_shapeIndices, VIndex(), wireIndices); + } +} + +gpu::Stream::FormatPointer& getSolidStreamFormat() { + if (!SOLID_STREAM_FORMAT) { + SOLID_STREAM_FORMAT = std::make_shared(); // 1 for everyone + SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT); + SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT); + } + return SOLID_STREAM_FORMAT; +} + +gpu::Stream::FormatPointer& getInstancedSolidStreamFormat() { + if (!INSTANCED_SOLID_STREAM_FORMAT) { + INSTANCED_SOLID_STREAM_FORMAT = std::make_shared(); // 1 for everyone + INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT); + INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT); + INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::INSTANCE_XFM, gpu::Stream::INSTANCE_XFM, TRANSFORM_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + } + return INSTANCED_SOLID_STREAM_FORMAT; +} + + +GeometryCache::GeometryCache() { const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE); + buildShapes(); } GeometryCache::~GeometryCache() { @@ -56,255 +477,64 @@ QSharedPointer GeometryCache::createResource(const QUrl& url, const QS return QSharedPointer(); } -const int NUM_VERTICES_PER_TRIANGLE = 3; -const int NUM_TRIANGLES_PER_QUAD = 2; -const int NUM_VERTICES_PER_TRIANGULATED_QUAD = NUM_VERTICES_PER_TRIANGLE * NUM_TRIANGLES_PER_QUAD; -const int NUM_COORDS_PER_VERTEX = 3; - -void GeometryCache::renderSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color, bool solid, int id) { - bool registered = (id != UNKNOWN_ID); - - Vec2Pair radiusKey(glm::vec2(radius, slices), glm::vec2(stacks, 0)); - IntPair slicesStacksKey(slices, stacks); - Vec3Pair colorKey(glm::vec3(color.x, color.y, slices), glm::vec3(color.z, color.w, stacks)); - - int vertices = slices * (stacks - 1) + 2; - int indices = slices * (stacks - 1) * NUM_VERTICES_PER_TRIANGULATED_QUAD; - - if ((registered && (!_registeredSphereVertices.contains(id) || _lastRegisteredSphereVertices[id] != radiusKey)) - || (!registered && !_sphereVertices.contains(radiusKey))) { - - if (registered && _registeredSphereVertices.contains(id)) { - _registeredSphereVertices[id].reset(); - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderSphere()... RELEASING REGISTERED VERTICES BUFFER"; - #endif - } - - auto verticesBuffer = std::make_shared(); - if (registered) { - _registeredSphereVertices[id] = verticesBuffer; - _lastRegisteredSphereVertices[id] = radiusKey; - } else { - _sphereVertices[radiusKey] = verticesBuffer; - } - - GLfloat* vertexData = new GLfloat[vertices * NUM_COORDS_PER_VERTEX]; - GLfloat* vertex = vertexData; - - // south pole - *(vertex++) = 0.0f; - *(vertex++) = 0.0f; - *(vertex++) = -1.0f * radius; - - //add stacks vertices climbing up Y axis - for (int i = 1; i < stacks; i++) { - float phi = PI * (float)i / (float)(stacks) - PI_OVER_TWO; - float z = sinf(phi) * radius; - float stackRadius = cosf(phi) * radius; - - for (int j = 0; j < slices; j++) { - float theta = TWO_PI * (float)j / (float)slices; - - *(vertex++) = sinf(theta) * stackRadius; - *(vertex++) = cosf(theta) * stackRadius; - *(vertex++) = z; - } - } - - // north pole - *(vertex++) = 0.0f; - *(vertex++) = 0.0f; - *(vertex++) = 1.0f * radius; - - verticesBuffer->append(sizeof(GLfloat) * vertices * NUM_COORDS_PER_VERTEX, (gpu::Byte*) vertexData); - delete[] vertexData; - - #ifdef WANT_DEBUG - qCDebug(renderutils) << "GeometryCache::renderSphere()... --- CREATING VERTICES BUFFER"; - qCDebug(renderutils) << " radius:" << radius; - qCDebug(renderutils) << " slices:" << slices; - qCDebug(renderutils) << " stacks:" << stacks; - - qCDebug(renderutils) << " _sphereVertices.size():" << _sphereVertices.size(); - #endif - } - #ifdef WANT_DEBUG - else if (registered) { - qCDebug(renderutils) << "renderSphere()... REUSING PREVIOUSLY REGISTERED VERTICES BUFFER"; - } - #endif - - if ((registered && (!_registeredSphereIndices.contains(id) || _lastRegisteredSphereIndices[id] != slicesStacksKey)) - || (!registered && !_sphereIndices.contains(slicesStacksKey))) { - - if (registered && _registeredSphereIndices.contains(id)) { - _registeredSphereIndices[id].reset(); - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderSphere()... RELEASING REGISTERED INDICES BUFFER"; - #endif - } - - auto indicesBuffer = std::make_shared(); - if (registered) { - _registeredSphereIndices[id] = indicesBuffer; - _lastRegisteredSphereIndices[id] = slicesStacksKey; - } else { - _sphereIndices[slicesStacksKey] = indicesBuffer; - } - - GLushort* indexData = new GLushort[indices]; - GLushort* index = indexData; - - int indexCount = 0; - - // South cap - GLushort bottom = 0; - GLushort top = 1; - for (int i = 0; i < slices; i++) { - *(index++) = bottom; - *(index++) = top + i; - *(index++) = top + (i + 1) % slices; - - indexCount += 3; - } - - // (stacks - 2) ribbons - for (int i = 0; i < stacks - 2; i++) { - bottom = i * slices + 1; - top = bottom + slices; - for (int j = 0; j < slices; j++) { - int next = (j + 1) % slices; - - *(index++) = top + next; - *(index++) = bottom + j; - *(index++) = top + j; - - indexCount += 3; - - *(index++) = bottom + next; - *(index++) = bottom + j; - *(index++) = top + next; - - indexCount += 3; - - } - } - - // north cap - bottom = (stacks - 2) * slices + 1; - top = bottom + slices; - for (int i = 0; i < slices; i++) { - *(index++) = bottom + (i + 1) % slices; - *(index++) = bottom + i; - *(index++) = top; - - indexCount += 3; - - } - indicesBuffer->append(sizeof(GLushort) * indices, (gpu::Byte*) indexData); - delete[] indexData; - - #ifdef WANT_DEBUG - qCDebug(renderutils) << "GeometryCache::renderSphere()... --- CREATING INDICES BUFFER"; - qCDebug(renderutils) << " radius:" << radius; - qCDebug(renderutils) << " slices:" << slices; - qCDebug(renderutils) << " stacks:" << stacks; - qCDebug(renderutils) << "indexCount:" << indexCount; - qCDebug(renderutils) << " indices:" << indices; - qCDebug(renderutils) << " _sphereIndices.size():" << _sphereIndices.size(); - #endif - } - #ifdef WANT_DEBUG - else if (registered) { - qCDebug(renderutils) << "renderSphere()... REUSING PREVIOUSLY REGISTERED INDICES BUFFER"; - } - #endif - - if ((registered && (!_registeredSphereColors.contains(id) || _lastRegisteredSphereColors[id] != colorKey)) - || (!registered && !_sphereColors.contains(colorKey))) { - - if (registered && _registeredSphereColors.contains(id)) { - _registeredSphereColors[id].reset(); - #ifdef WANT_DEBUG - qCDebug(renderutils) << "renderSphere()... RELEASING REGISTERED COLORS BUFFER"; - #endif - } - - auto colorBuffer = std::make_shared(); - if (registered) { - _registeredSphereColors[id] = colorBuffer; - _lastRegisteredSphereColors[id] = colorKey; - } else { - _sphereColors[colorKey] = colorBuffer; - } - - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - - int* colorData = new int[vertices]; - int* colorDataAt = colorData; - - for(int v = 0; v < vertices; v++) { - *(colorDataAt++) = compactColor; - } - - colorBuffer->append(sizeof(int) * vertices, (gpu::Byte*) colorData); - delete[] colorData; - - #ifdef WANT_DEBUG - qCDebug(renderutils) << "GeometryCache::renderSphere()... --- CREATING COLORS BUFFER"; - qCDebug(renderutils) << " vertices:" << vertices; - qCDebug(renderutils) << " color:" << color; - qCDebug(renderutils) << " slices:" << slices; - qCDebug(renderutils) << " stacks:" << stacks; - qCDebug(renderutils) << " _sphereColors.size():" << _sphereColors.size(); - #endif - } - #ifdef WANT_DEBUG - else if (registered) { - qCDebug(renderutils) << "renderSphere()... REUSING PREVIOUSLY REGISTERED COLORS BUFFER"; - } - #endif - - gpu::BufferPointer verticesBuffer = registered ? _registeredSphereVertices[id] : _sphereVertices[radiusKey]; - gpu::BufferPointer indicesBuffer = registered ? _registeredSphereIndices[id] : _sphereIndices[slicesStacksKey]; - gpu::BufferPointer colorBuffer = registered ? _registeredSphereColors[id] : _sphereColors[colorKey]; - - const int VERTICES_SLOT = 0; - const int NORMALS_SLOT = 1; - const int COLOR_SLOT = 2; - static gpu::Stream::FormatPointer streamFormat; - static gpu::Element positionElement, normalElement, colorElement; - if (!streamFormat) { - streamFormat = std::make_shared(); // 1 for everyone - streamFormat->setAttribute(gpu::Stream::POSITION, VERTICES_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - streamFormat->setAttribute(gpu::Stream::NORMAL, NORMALS_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - streamFormat->setAttribute(gpu::Stream::COLOR, COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element; - normalElement = streamFormat->getAttributes().at(gpu::Stream::NORMAL)._element; - colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element; - } - - gpu::BufferView verticesView(verticesBuffer, positionElement); - gpu::BufferView normalsView(verticesBuffer, normalElement); - gpu::BufferView colorView(colorBuffer, colorElement); - - batch.setInputFormat(streamFormat); - batch.setInputBuffer(VERTICES_SLOT, verticesView); - batch.setInputBuffer(NORMALS_SLOT, normalsView); - batch.setInputBuffer(COLOR_SLOT, colorView); - batch.setIndexBuffer(gpu::UINT16, indicesBuffer, 0); - - if (solid) { - batch.drawIndexed(gpu::TRIANGLES, indices); - } else { - batch.drawIndexed(gpu::LINES, indices); - } +void setupBatchInstance(gpu::Batch& batch, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) { + gpu::BufferView colorView(colorBuffer, COLOR_ELEMENT); + batch.setInputBuffer(gpu::Stream::COLOR, colorView); + gpu::BufferView instanceXfmView(transformBuffer, 0, transformBuffer->getSize(), TRANSFORM_ELEMENT); + batch.setInputBuffer(gpu::Stream::INSTANCE_XFM, instanceXfmView); } +void GeometryCache::renderShape(gpu::Batch& batch, Shape shape) { + batch.setInputFormat(getSolidStreamFormat()); + _shapes[shape].draw(batch); +} + +void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape) { + batch.setInputFormat(getSolidStreamFormat()); + _shapes[shape].drawWire(batch); +} + +void GeometryCache::renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer) { + batch.setInputFormat(getInstancedSolidStreamFormat()); + setupBatchInstance(batch, transformBuffer, colorBuffer); + _shapes[shape].drawInstances(batch, count); +} + +void GeometryCache::renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer) { + batch.setInputFormat(getInstancedSolidStreamFormat()); + setupBatchInstance(batch, transformBuffer, colorBuffer); + _shapes[shape].drawWireInstances(batch, count); +} + +void GeometryCache::renderCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) { + renderShapeInstances(batch, Cube, count, transformBuffer, colorBuffer); +} + +void GeometryCache::renderWireCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) { + renderWireShapeInstances(batch, Cube, count, transformBuffer, colorBuffer); +} + +void GeometryCache::renderCube(gpu::Batch& batch) { + renderShape(batch, Cube); +} + +void GeometryCache::renderWireCube(gpu::Batch& batch) { + renderWireShape(batch, Cube); +} + +void GeometryCache::renderSphereInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) { + renderShapeInstances(batch, Sphere, count, transformBuffer, colorBuffer); +} + +void GeometryCache::renderSphere(gpu::Batch& batch) { + renderShape(batch, Sphere); +} + +void GeometryCache::renderWireSphere(gpu::Batch& batch) { + renderWireShape(batch, Sphere); +} + + void GeometryCache::renderGrid(gpu::Batch& batch, int xDivisions, int yDivisions, const glm::vec4& color) { IntPair key(xDivisions, yDivisions); Vec3Pair colorKey(glm::vec3(color.x, color.y, yDivisions), glm::vec3(color.z, color.y, xDivisions)); @@ -689,243 +919,6 @@ void GeometryCache::renderVertices(gpu::Batch& batch, gpu::Primitive primitiveTy } } -static const int FLOATS_PER_VERTEX = 3; -static const int VERTICES_PER_TRIANGLE = 3; - -static const int CUBE_NUMBER_OF_FACES = 6; -static const int CUBE_VERTICES_PER_FACE = 4; -static const int CUBE_TRIANGLES_PER_FACE = 2; -static const int CUBE_VERTICES = CUBE_NUMBER_OF_FACES * CUBE_VERTICES_PER_FACE; -static const int CUBE_VERTEX_POINTS = CUBE_VERTICES * FLOATS_PER_VERTEX; -static const int CUBE_INDICES = CUBE_NUMBER_OF_FACES * CUBE_TRIANGLES_PER_FACE * VERTICES_PER_TRIANGLE; - -static const gpu::Element CUBE_POSITION_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; -static const gpu::Element CUBE_NORMAL_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ }; -static const gpu::Element CUBE_COLOR_ELEMENT{ gpu::VEC4, gpu::NUINT8, gpu::RGBA }; -static const gpu::Element INSTANCE_XFM_ELEMENT{ gpu::MAT4, gpu::FLOAT, gpu::XYZW }; - -gpu::BufferPointer GeometryCache::getCubeVertices(float size) { - if (!_solidCubeVertices.contains(size)) { - auto verticesBuffer = std::make_shared(); - _solidCubeVertices[size] = verticesBuffer; - - GLfloat* vertexData = new GLfloat[CUBE_VERTEX_POINTS * 2]; // vertices and normals - GLfloat* vertex = vertexData; - float halfSize = size / 2.0f; - - static GLfloat cannonicalVertices[CUBE_VERTEX_POINTS] = - { 1, 1, 1, -1, 1, 1, -1,-1, 1, 1,-1, 1, // v0,v1,v2,v3 (front) - 1, 1, 1, 1,-1, 1, 1,-1,-1, 1, 1,-1, // v0,v3,v4,v5 (right) - 1, 1, 1, 1, 1,-1, -1, 1,-1, -1, 1, 1, // v0,v5,v6,v1 (top) - -1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1, 1, // v1,v6,v7,v2 (left) - -1,-1,-1, 1,-1,-1, 1,-1, 1, -1,-1, 1, // v7,v4,v3,v2 (bottom) - 1,-1,-1, -1,-1,-1, -1, 1,-1, 1, 1,-1 }; // v4,v7,v6,v5 (back) - - // normal array - static GLfloat cannonicalNormals[CUBE_VERTEX_POINTS] = - { 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // v0,v1,v2,v3 (front) - 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0,v3,v4,v5 (right) - 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // v0,v5,v6,v1 (top) - -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // v1,v6,v7,v2 (left) - 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, // v7,v4,v3,v2 (bottom) - 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1 }; // v4,v7,v6,v5 (back) - - - GLfloat* cannonicalVertex = &cannonicalVertices[0]; - GLfloat* cannonicalNormal = &cannonicalNormals[0]; - - for (int i = 0; i < CUBE_VERTICES; i++) { - // vertices - *(vertex++) = halfSize * *cannonicalVertex++; - *(vertex++) = halfSize * *cannonicalVertex++; - *(vertex++) = halfSize * *cannonicalVertex++; - - //normals - *(vertex++) = *cannonicalNormal++; - *(vertex++) = *cannonicalNormal++; - *(vertex++) = *cannonicalNormal++; - } - verticesBuffer->append(sizeof(GLfloat) * CUBE_VERTEX_POINTS * 2, (gpu::Byte*) vertexData); - } - - return _solidCubeVertices[size]; -} - -gpu::BufferPointer GeometryCache::getSolidCubeIndices() { - if (!_solidCubeIndexBuffer) { - static GLubyte cannonicalIndices[CUBE_INDICES] = { 0, 1, 2, 2, 3, 0, // front - 4, 5, 6, 6, 7, 4, // right - 8, 9,10, 10,11, 8, // top - 12,13,14, 14,15,12, // left - 16,17,18, 18,19,16, // bottom - 20,21,22, 22,23,20 }; // back - - auto indexBuffer = std::make_shared(); - _solidCubeIndexBuffer = indexBuffer; - - _solidCubeIndexBuffer->append(sizeof(cannonicalIndices), (gpu::Byte*) cannonicalIndices); - } - return _solidCubeIndexBuffer; -} - - -void GeometryCache::setupCubeVertices(gpu::Batch& batch, gpu::BufferPointer& verticesBuffer) { - static const int VERTEX_STRIDE = sizeof(GLfloat) * FLOATS_PER_VERTEX * 2; // vertices and normals - static const int NORMALS_OFFSET = sizeof(GLfloat) * FLOATS_PER_VERTEX; - - gpu::BufferView verticesView(verticesBuffer, 0, verticesBuffer->getSize(), VERTEX_STRIDE, CUBE_POSITION_ELEMENT); - gpu::BufferView normalsView(verticesBuffer, NORMALS_OFFSET, verticesBuffer->getSize(), VERTEX_STRIDE, CUBE_NORMAL_ELEMENT); - batch.setInputBuffer(gpu::Stream::POSITION, verticesView); - batch.setInputBuffer(gpu::Stream::NORMAL, normalsView); -} - -void GeometryCache::renderSolidCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) { - static gpu::Stream::FormatPointer streamFormat; - if (!streamFormat) { - streamFormat = std::make_shared(); // 1 for everyone - streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, CUBE_POSITION_ELEMENT, 0); - streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, CUBE_NORMAL_ELEMENT); - streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, CUBE_COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE); - streamFormat->setAttribute(gpu::Stream::INSTANCE_XFM, gpu::Stream::INSTANCE_XFM, INSTANCE_XFM_ELEMENT, 0, gpu::Stream::PER_INSTANCE); - } - batch.setInputFormat(streamFormat); - - gpu::BufferView colorView(colorBuffer, CUBE_COLOR_ELEMENT); - batch.setInputBuffer(gpu::Stream::COLOR, colorView); - - gpu::BufferView instanceXfmView(transformBuffer, 0, transformBuffer->getSize(), INSTANCE_XFM_ELEMENT); - batch.setInputBuffer(gpu::Stream::INSTANCE_XFM, instanceXfmView); - - gpu::BufferPointer verticesBuffer = getCubeVertices(1.0); - setupCubeVertices(batch, verticesBuffer); - batch.setIndexBuffer(gpu::UINT8, getSolidCubeIndices(), 0); - batch.drawIndexedInstanced(count, gpu::TRIANGLES, CUBE_INDICES); -} - - -void GeometryCache::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) { - Vec2Pair colorKey(glm::vec2(color.x, color.y), glm::vec2(color.z, color.y)); - if (!_solidCubeColors.contains(colorKey)) { - auto colorBuffer = std::make_shared(); - _solidCubeColors[colorKey] = colorBuffer; - - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - int colors[CUBE_VERTICES] = { - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor - }; - colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - } - gpu::BufferPointer colorBuffer = _solidCubeColors[colorKey]; - - static gpu::Stream::FormatPointer streamFormat; - if (!streamFormat) { - streamFormat = std::make_shared(); // 1 for everyone - streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, CUBE_POSITION_ELEMENT, 0); - streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, CUBE_NORMAL_ELEMENT); - streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, CUBE_COLOR_ELEMENT); - } - batch.setInputFormat(streamFormat); - - gpu::BufferView colorView(colorBuffer, CUBE_COLOR_ELEMENT); - batch.setInputBuffer(gpu::Stream::COLOR, colorView); - - gpu::BufferPointer verticesBuffer = getCubeVertices(size); - setupCubeVertices(batch, verticesBuffer); - - batch.setIndexBuffer(gpu::UINT8, getSolidCubeIndices(), 0); - batch.drawIndexed(gpu::TRIANGLES, CUBE_INDICES); -} - - -void GeometryCache::renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color) { - Vec2Pair colorKey(glm::vec2(color.x, color.y),glm::vec2(color.z, color.y)); - static const int WIRE_CUBE_VERTICES_PER_EDGE = 2; - static const int WIRE_CUBE_TOP_EDGES = 4; - static const int WIRE_CUBE_BOTTOM_EDGES = 4; - static const int WIRE_CUBE_SIDE_EDGES = 4; - static const int WIRE_CUBE_VERTICES = 8; - static const int WIRE_CUBE_INDICES = (WIRE_CUBE_TOP_EDGES + WIRE_CUBE_BOTTOM_EDGES + WIRE_CUBE_SIDE_EDGES) * WIRE_CUBE_VERTICES_PER_EDGE; - - if (!_cubeVerticies.contains(size)) { - auto verticesBuffer = std::make_shared(); - _cubeVerticies[size] = verticesBuffer; - - static const int WIRE_CUBE_VERTEX_POINTS = WIRE_CUBE_VERTICES * FLOATS_PER_VERTEX; - GLfloat* vertexData = new GLfloat[WIRE_CUBE_VERTEX_POINTS]; // only vertices, no normals because we're a wire cube - GLfloat* vertex = vertexData; - float halfSize = size / 2.0f; - - static GLfloat cannonicalVertices[] = - { 1, 1, 1, 1, 1,-1, -1, 1,-1, -1, 1, 1, // v0, v1, v2, v3 (top) - 1,-1, 1, 1,-1,-1, -1,-1,-1, -1,-1, 1 // v4, v5, v6, v7 (bottom) - }; - - for (int i = 0; i < WIRE_CUBE_VERTEX_POINTS; i++) { - vertex[i] = cannonicalVertices[i] * halfSize; - } - - verticesBuffer->append(sizeof(GLfloat) * WIRE_CUBE_VERTEX_POINTS, (gpu::Byte*) vertexData); // I'm skeptical that this is right - } - - if (!_wireCubeIndexBuffer) { - static GLubyte cannonicalIndices[WIRE_CUBE_INDICES] = { - 0, 1, 1, 2, 2, 3, 3, 0, // (top) - 4, 5, 5, 6, 6, 7, 7, 4, // (bottom) - 0, 4, 1, 5, 2, 6, 3, 7, // (side edges) - }; - - auto indexBuffer = std::make_shared(); - _wireCubeIndexBuffer = indexBuffer; - - _wireCubeIndexBuffer->append(sizeof(cannonicalIndices), (gpu::Byte*) cannonicalIndices); - } - - if (!_cubeColors.contains(colorKey)) { - auto colorBuffer = std::make_shared(); - _cubeColors[colorKey] = colorBuffer; - - const int NUM_COLOR_SCALARS_PER_CUBE = 8; - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - int colors[NUM_COLOR_SCALARS_PER_CUBE] = { compactColor, compactColor, compactColor, compactColor, - compactColor, compactColor, compactColor, compactColor }; - - colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - } - gpu::BufferPointer verticesBuffer = _cubeVerticies[size]; - gpu::BufferPointer colorBuffer = _cubeColors[colorKey]; - - const int VERTICES_SLOT = 0; - const int COLOR_SLOT = 1; - static gpu::Stream::FormatPointer streamFormat; - static gpu::Element positionElement, colorElement; - if (!streamFormat) { - streamFormat = std::make_shared(); // 1 for everyone - streamFormat->setAttribute(gpu::Stream::POSITION, VERTICES_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - streamFormat->setAttribute(gpu::Stream::COLOR, COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element; - colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element; - } - - gpu::BufferView verticesView(verticesBuffer, positionElement); - gpu::BufferView colorView(colorBuffer, colorElement); - - batch.setInputFormat(streamFormat); - batch.setInputBuffer(VERTICES_SLOT, verticesView); - batch.setInputBuffer(COLOR_SLOT, colorView); - batch.setIndexBuffer(gpu::UINT8, _wireCubeIndexBuffer, 0); - batch.drawIndexed(gpu::LINES, WIRE_CUBE_INDICES); -} void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id) { bool registered = (id != UNKNOWN_ID); @@ -1099,10 +1092,10 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } -void GeometryCache::renderUnitCube(gpu::Batch& batch) { - static const glm::vec4 color(1); - renderSolidCube(batch, 1, color); -} +//void GeometryCache::renderUnitCube(gpu::Batch& batch) { +// static const glm::vec4 color(1); +// renderSolidCube(batch, 1, color); +//} void GeometryCache::renderUnitQuad(gpu::Batch& batch, const glm::vec4& color, int id) { static const glm::vec2 topLeft(-1, 1); @@ -2043,8 +2036,8 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas // need lightmap texcoord UV but doesn't have uv#1 so just reuse the same channel networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum - 1, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); } - if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); - if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); + if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); } else { int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); @@ -2076,8 +2069,8 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); - if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); - if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)); + if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); } } diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 9ba2658a9c..d911629001 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -12,6 +12,8 @@ #ifndef hifi_GeometryCache_h #define hifi_GeometryCache_h +#include + #include #include @@ -119,37 +121,59 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) { seed); } +using VVertex = std::vector; +using VIndex = std::vector; + /// Stores cached geometry. class GeometryCache : public ResourceCache, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY public: + enum Shape { + Line, + Triangle, + Quad, + Circle, + Cube, + Sphere, + Tetrahedron, + Octahetron, + Dodecahedron, + Icosahedron, + Torus, + Cone, + Cylinder, + + NUM_SHAPES, + }; + int allocateID() { return _nextID++; } static const int UNKNOWN_ID; virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra); - gpu::BufferPointer getCubeVertices(float size); - void setupCubeVertices(gpu::Batch& batch, gpu::BufferPointer& verticesBuffer); + void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer); + void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer); + void renderShape(gpu::Batch& batch, Shape shape); + void renderWireShape(gpu::Batch& batch, Shape shape); - gpu::BufferPointer getSolidCubeIndices(); + void renderCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer); + void renderWireCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer); + void renderCube(gpu::Batch& batch); + void renderWireCube(gpu::Batch& batch); - void renderSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec3& color, bool solid = true, int id = UNKNOWN_ID) - { renderSphere(batch, radius, slices, stacks, glm::vec4(color, 1.0f), solid, id); } - - void renderSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color, bool solid = true, int id = UNKNOWN_ID); + void renderSphereInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer); + void renderWireSphereInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer); + void renderSphere(gpu::Batch& batch); + void renderWireSphere(gpu::Batch& batch); void renderGrid(gpu::Batch& batch, int xDivisions, int yDivisions, const glm::vec4& color); void renderGrid(gpu::Batch& batch, int x, int y, int width, int height, int rows, int cols, const glm::vec4& color, int id = UNKNOWN_ID); - void renderSolidCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer); - void renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color); - void renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color); void renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id = UNKNOWN_ID); - void renderUnitCube(gpu::Batch& batch); void renderUnitQuad(gpu::Batch& batch, const glm::vec4& color = glm::vec4(1), int id = UNKNOWN_ID); void renderQuad(gpu::Batch& batch, int x, int y, int width, int height, const glm::vec4& color, int id = UNKNOWN_ID) @@ -223,19 +247,41 @@ public: private: GeometryCache(); virtual ~GeometryCache(); + void buildShapes(); typedef QPair IntPair; typedef QPair VerticesIndices; + struct ShapeData { + size_t _indexOffset{ 0 }; + size_t _indexCount{ 0 }; + size_t _wireIndexOffset{ 0 }; + size_t _wireIndexCount{ 0 }; + + gpu::BufferView _positionView; + gpu::BufferView _normalView; + gpu::BufferPointer _indices; + + void setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices); + void setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices); + void setupBatch(gpu::Batch& batch) const; + void draw(gpu::Batch& batch) const; + void drawWire(gpu::Batch& batch) const; + void drawInstances(gpu::Batch& batch, size_t count) const; + void drawWireInstances(gpu::Batch& batch, size_t count) const; + }; + + using VShape = std::array; + + VShape _shapes; + + + gpu::PipelinePointer _standardDrawPipeline; gpu::PipelinePointer _standardDrawPipelineNoBlend; - QHash _cubeVerticies; - QHash _cubeColors; - gpu::BufferPointer _wireCubeIndexBuffer; - QHash _solidCubeVertices; - QHash _solidCubeColors; - gpu::BufferPointer _solidCubeIndexBuffer; + gpu::BufferPointer _shapeVertices{ std::make_shared() }; + gpu::BufferPointer _shapeIndices{ std::make_shared() }; class BatchItemDetails { public: @@ -257,7 +303,7 @@ private: QHash _coneVBOs; - int _nextID; + int _nextID{ 0 }; QHash _lastRegisteredQuad3DTexture; QHash _quad3DTextures; @@ -299,16 +345,6 @@ private: QHash _lastRegisteredAlternateGridBuffers; QHash _gridColors; - QHash _sphereVertices; - QHash _registeredSphereVertices; - QHash _lastRegisteredSphereVertices; - QHash _sphereIndices; - QHash _registeredSphereIndices; - QHash _lastRegisteredSphereIndices; - QHash _sphereColors; - QHash _registeredSphereColors; - QHash _lastRegisteredSphereColors; - QHash > _networkGeometry; }; diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index b27d10e312..ad9ed9bb4a 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -10,15 +10,22 @@ #include #include +#include #include +#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include #include #include @@ -26,17 +33,17 @@ #include #include -#include -#include -#include -#include -#include -#include +// Must come after GL headers +#include + +#include #include #include +#include +#include -#include "simple_frag.h" -#include "simple_vert.h" +#include "unlit_frag.h" +#include "unlit_vert.h" class RateCounter { std::vector times; @@ -74,173 +81,7 @@ public: } }; -#define MOVE_PARAM(name) decltype(name) && name - -struct BasicModel { - gpu::PipelinePointer pipeline; -// gpu::BufferPointer vertexBuffer; -// gpu::BufferPointer indexBuffer; -// gpu::BufferPointer normalBuffer; - - gpu::BufferView vertices; - gpu::BufferView normals; - gpu::BufferPointer indices; - - gpu::Stream::FormatPointer format; - - BasicModel (MOVE_PARAM(pipeline), MOVE_PARAM(vertices), MOVE_PARAM(normals), MOVE_PARAM(indices), MOVE_PARAM(format)) - : pipeline(pipeline), vertices(vertices), normals(normals), indices(indices), format(format) {} - -// BasicModel (gpu::PipelinePointer && pipeline, gpu::BufferPointer && buffer, gpu::Stream::FormatPointer && format) -// : pipeline(pipeline), buffer(buffer), format(format) {} -}; -typedef std::shared_ptr BasicModelPointer; -#undef MOVE_PARAM - -BasicModelPointer makeCube () { - // Axis-aligned cube, facing the user at +z - // coords == binary mapping of each index, with z inverted (front face faces camera, - // instead of away from the camera) - // - // -x,+y,-z ----------- +x,+y,-z - // ___--- | ___--- | - // -x,+y,+z --------- +x,+y,+z | - // | | | | - // | | | | - // | | | | - // | | | | - // | -x,-y,-z ------|---- +x,-y,-z - // | ___--- | ___---- - // -x,-y,+z --------- +x,-y,+z - // - float s = 1.0f; - const glm::vec3 raw_verts[8] = { - // x, y, z - { -s, -s, +s }, // 0b000 0x0 - { +s, -s, +s }, // 0b001 0x1 - { -s, +s, +s }, // 0b010 0x2 - { +s, +s, +s }, // 0b011 0x3 - { -s, -s, -s }, // 0b100 0x4 - { +s, -s, -s }, // 0b101 0x5 - { -s, +s, -s }, // 0b110 0x6 - { +s, +s, -s } // 0b111 0x7 - }; - const glm::vec3 raw_normals[6] = { - { 0.0f, 0.0f, +1.0f }, // x > 0: 1, 3, 5, 7 (N 0) - { 0.0f, 0.0f, -1.0f }, // x < 0: 0, 2, 4, 6 (N 1) - { 0.0f, +1.0f, 0.0f }, // y > 0: 2, 3, 6, 7 (N 2) - { 0.0f, -1.0f, 0.0f }, // y < 0: 0, 1, 4, 5 (N 3) - { +1.0f, 0.0f, 0.0f }, // z > 0: 0, 1, 2, 3 (N 4) - { -1.0f, 0.0f, 0.0f } // z < 0: 4, 5, 6, 7 (N 5) - }; - - const glm::vec3 cube_verts[24] = { - raw_verts[1], raw_verts[3], raw_verts[5], raw_verts[7], - raw_verts[0], raw_verts[2], raw_verts[4], raw_verts[6], - raw_verts[2], raw_verts[3], raw_verts[6], raw_verts[7], - raw_verts[0], raw_verts[1], raw_verts[4], raw_verts[5], - raw_verts[0], raw_verts[1], raw_verts[2], raw_verts[3], - raw_verts[4], raw_verts[5], raw_verts[6], raw_verts[7] - }; - const glm::vec3 cube_normals[24] = { - raw_normals[0], raw_normals[0], raw_normals[0], raw_normals[0], - raw_normals[1], raw_normals[1], raw_normals[1], raw_normals[1], - raw_normals[2], raw_normals[2], raw_normals[2], raw_normals[2], - raw_normals[3], raw_normals[3], raw_normals[3], raw_normals[3], - raw_normals[4], raw_normals[4], raw_normals[4], raw_normals[4], - raw_normals[5], raw_normals[5], raw_normals[5], raw_normals[5] - }; - - int16_t cube_indices_tris[36]; - for (int i = 0, k = 0; i < 36; k += 4) { - cube_indices_tris[i++] = k + 0; - cube_indices_tris[i++] = k + 3; - cube_indices_tris[i++] = k + 1; - cube_indices_tris[i++] = k + 0; - cube_indices_tris[i++] = k + 2; - cube_indices_tris[i++] = k + 3; - } - -// const int16_t cube_indices_tris[36] { -// 0, 3, 1, 0, 2, 3, -// }; - -// const glm::vec3 cube_normals[] = { -// { 0.0f, 0.0f, 1.0f }, -// { 0.0f, 0.0f, 1.0f }, -// { 0.0f, 0.0f, 1.0f }, -// { 0.0f, 0.0f, 1.0f }, -// { -1.0f, 0.0f, 0.0f }, -// { -1.0f, 0.0f, 0.0f }, -// { -1.0f, 0.0f, 0.0f }, -// { -1.0f, 0.0f, 0.0f }, -// }; -// const int16_t cube_indices[] = { -// 3, 1, 0, 2, 3, 0, -// 6, 2, 0, 4, 6, 0, -// }; - - gpu::Stream::FormatPointer format = std::make_shared(); - - assert(gpu::Stream::POSITION == 0 && gpu::Stream::NORMAL == 1); - const int BUFFER_SLOT = 0; - - format->setAttribute(gpu::Stream::POSITION, BUFFER_SLOT, gpu::Element::VEC3F_XYZ); - format->setAttribute(gpu::Stream::NORMAL, BUFFER_SLOT, gpu::Element::VEC3F_XYZ); - - auto vertexBuffer = std::make_shared(24 * sizeof(glm::vec3), (gpu::Byte*)cube_verts); - auto normalBuffer = std::make_shared(24 * sizeof(glm::vec3), (gpu::Byte*)cube_normals); - gpu::BufferPointer indexBuffer = std::make_shared(36 * sizeof(int16_t), (gpu::Byte*)cube_indices_tris); - - auto positionElement = format->getAttributes().at(gpu::Stream::POSITION)._element; - auto normalElement = format->getAttributes().at(gpu::Stream::NORMAL)._element; - - gpu::BufferView vertexView { vertexBuffer, positionElement }; - gpu::BufferView normalView { normalBuffer, normalElement }; - - // Create shaders - auto vs = gpu::ShaderPointer(gpu::Shader::createVertex({ simple_vert })); - auto fs = gpu::ShaderPointer(gpu::Shader::createPixel({ simple_frag })); - auto shader = gpu::ShaderPointer(gpu::Shader::createProgram(vs, fs)); - - gpu::Shader::BindingSet bindings; - bindings.insert({ "lightPosition", 1 }); - - if (!gpu::Shader::makeProgram(*shader, bindings)) { - printf("Could not compile shader\n"); - if (!vs) - printf("bad vertex shader\n"); - if (!fs) - printf("bad fragment shader\n"); - if (!shader) - printf("bad shader program\n"); - exit(-1); - } - - auto state = std::make_shared(); -// state->setAntialiasedLineEnable(true); - state->setMultisampleEnable(true); - state->setDepthTest({ true }); - auto pipeline = gpu::PipelinePointer(gpu::Pipeline::create(shader, state)); - - return std::make_shared( - std::move(pipeline), - std::move(vertexView), - std::move(normalView), - std::move(indexBuffer), - std::move(format) - ); -} -void renderCube(gpu::Batch & batch, const BasicModel & cube) { - - batch.setPipeline(cube.pipeline); - batch.setInputFormat(cube.format); - batch.setInputBuffer(gpu::Stream::POSITION, cube.vertices); - batch.setInputBuffer(gpu::Stream::NORMAL, cube.normals); - batch.setIndexBuffer(gpu::INT16, cube.indices, 0); -// batch.drawIndexed(gpu::TRIANGLES, 12); - batch.draw(gpu::TRIANGLES, 24); -} +uint32_t toCompactColor(const glm::vec4& color); gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings) { auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(vertexShaderSrc)); @@ -253,6 +94,14 @@ gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::st return shader; } +float getSeconds(quint64 start = 0) { + auto usecs = usecTimestampNow() - start; + auto msecs = usecs / USECS_PER_MSEC; + float seconds = (float)msecs / MSECS_PER_SECOND; + return seconds; +} + + // Creates an OpenGL window that renders a simple unlit scene using the gpu library and GeometryCache // Should eventually get refactored into something that supports multiple gpu backends. @@ -265,9 +114,9 @@ class QTestWindow : public QWindow { gpu::ContextPointer _context; gpu::PipelinePointer _pipeline; glm::mat4 _projectionMatrix; -// BasicModelPointer _cubeModel; RateCounter fps; QTime _time; + int _instanceLocation{ -1 }; protected: void renderText(); @@ -288,6 +137,7 @@ public: format.setVersion(4, 1); format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); format.setOption(QSurfaceFormat::DebugContext); + format.setSwapInterval(0); setFormat(format); @@ -301,23 +151,21 @@ public: gpu::Context::init(); _context = std::make_shared(); - auto shader = makeShader(simple_vert, simple_frag, gpu::Shader::BindingSet {}); + auto shader = makeShader(unlit_vert, unlit_frag, gpu::Shader::BindingSet{}); auto state = std::make_shared(); state->setMultisampleEnable(true); state->setDepthTest(gpu::State::DepthTest { true }); _pipeline = gpu::PipelinePointer(gpu::Pipeline::create(shader, state)); - + _instanceLocation = _pipeline->getProgram()->getUniforms().findLocation("Instanced"); // Clear screen gpu::Batch batch; batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 1.0, 0.0, 0.5, 1.0 }); _context->render(batch); -// _cubeModel = makeCube(); - DependencyManager::set(); + DependencyManager::set(); - setFramePosition(QPoint(-1000, 0)); resize(QSize(800, 600)); _time.start(); @@ -327,6 +175,8 @@ public: } void draw() { + static auto startTime = usecTimestampNow(); + if (!isVisible()) { return; } @@ -342,37 +192,81 @@ public: glm::vec3 unitscale { 1.0f }; glm::vec3 up { 0.0f, 1.0f, 0.0f }; - glm::vec3 cam_pos { 1.5f * sinf(t), 0.0f, 2.0f }; -// glm::vec3 camera_focus { 5.0f * cosf(t * 0.1f), 0.0f, 0.0f }; - glm::vec3 camera_focus { 0.0f, 0.0f, 0.0f }; - glm::quat cam_rotation; - // glm::quat cam_rotation = glm::quat_cast(glm::lookAt(cam_pos, camera_focus, up)); - // cam_rotation.w = -cam_rotation.w; - // printf("cam rotation: %f %f %f %f\n", cam_rotation.x, cam_rotation.y, cam_rotation.z, cam_rotation.w); - Transform cam_transform { cam_rotation, unitscale, cam_pos }; - - batch.setViewTransform(cam_transform); + glm::vec3 camera_position { 1.5f * sinf(t), 0.0f, 1.5f * cos(t) }; + + static const vec3 camera_focus(0); + static const vec3 camera_up(0, 1, 0); + glm::mat4 camera = glm::inverse(glm::lookAt(camera_position, camera_focus, up)); + batch.setViewTransform(camera); batch.setPipeline(_pipeline); - + batch.setModelTransform(Transform()); + auto geometryCache = DependencyManager::get(); // Render grid on xz plane (not the optimal way to do things, but w/e) // Note: GeometryCache::renderGrid will *not* work, as it is apparenly unaffected by batch rotations and renders xy only - batch.setModelTransform(Transform()); + static const std::string GRID_INSTANCE = "Grid"; + static auto compactColor1 = toCompactColor(vec4{ 0.35f, 0.25f, 0.15f, 1.0f }); + static auto compactColor2 = toCompactColor(vec4{ 0.15f, 0.25f, 0.35f, 1.0f }); + auto transformBuffer = batch.getNamedBuffer(GRID_INSTANCE, 0); + auto colorBuffer = batch.getNamedBuffer(GRID_INSTANCE, 1); for (int i = 0; i < 100; ++i) { - geometryCache->renderLine(batch, { -100.0f, -1.0f, -50.0f + float(i) }, { 100.0f, -1.0f, -50.0f + float(i) }, { 0.35f, 0.25f, 0.15f, 1.0f }); + { + glm::mat4 transform = glm::translate(mat4(), vec3(0, -1, -50 + i)); + transform = glm::scale(transform, vec3(100, 1, 1)); + transformBuffer->append(transform); + colorBuffer->append(compactColor1); + } + + { + glm::mat4 transform = glm::mat4_cast(quat(vec3(0, PI / 2.0f, 0))); + transform = glm::translate(transform, vec3(0, -1, -50 + i)); + transform = glm::scale(transform, vec3(100, 1, 1)); + transformBuffer->append(transform); + colorBuffer->append(compactColor2); + } } - for (int i = 0; i < 100; ++i) { - geometryCache->renderLine(batch, { -50.0f + float(i), -1.0f, -100.0f}, { -50.0f + float(i), -1.0f, 100.0f }, { 0.15f, 0.25f, 0.35f, 1.0f }); - } - + + batch.setupNamedCalls(GRID_INSTANCE, 200, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + batch.setViewTransform(camera); + batch.setModelTransform(Transform()); + batch.setPipeline(_pipeline); + auto& xfm = data._buffers[0]; + auto& color = data._buffers[1]; + batch._glUniform1i(_instanceLocation, 1); + geometryCache->renderWireShapeInstances(batch, GeometryCache::Line, data._count, xfm, color); + batch._glUniform1i(_instanceLocation, 0); + }); + + + // Render unlit cube + sphere - geometryCache->renderUnitCube(batch); - geometryCache->renderWireCube(batch, 1.0f, { 0.4f, 0.4f, 0.7f, 1.0f }); - - batch.setModelTransform(Transform().setTranslation({ 1.5f, -0.5f, -0.5f })); - geometryCache->renderSphere(batch, 0.5f, 50, 50, { 0.8f, 0.25f, 0.25f }); + + static GeometryCache::Shape SHAPE[] = { + GeometryCache::Cube, + GeometryCache::Sphere, + GeometryCache::Tetrahedron, + GeometryCache::Icosahedron, + }; + + static auto startUsecs = usecTimestampNow(); + float seconds = getSeconds(startUsecs); + seconds /= 4.0; + int shapeIndex = ((int)seconds) % 4; + bool wire = seconds - floor(seconds) > 0.5f; + batch.setModelTransform(Transform()); + batch._glColor4f(0.8f, 0.25f, 0.25f, 1.0f); + + if (wire) { + geometryCache->renderWireShape(batch, SHAPE[shapeIndex]); + } else { + geometryCache->renderShape(batch, SHAPE[shapeIndex]); + } + batch.setModelTransform(Transform().setScale(1.05f)); + batch._glColor4f(1, 1, 1, 1); + geometryCache->renderWireCube(batch); + _context->render(batch); _qGlContext->swapBuffers(this); diff --git a/tests/gpu-test/src/simple.slf b/tests/gpu-test/src/unlit.slf similarity index 94% rename from tests/gpu-test/src/simple.slf rename to tests/gpu-test/src/unlit.slf index 31d33a73e4..350190180a 100644 --- a/tests/gpu-test/src/simple.slf +++ b/tests/gpu-test/src/unlit.slf @@ -20,7 +20,6 @@ in vec3 _normal; in vec3 _color; void main(void) { - Material material = getMaterial(); packDeferredFragment( normalize(_normal.xyz), glowIntensity, diff --git a/tests/gpu-test/src/simple.slv b/tests/gpu-test/src/unlit.slv similarity index 64% rename from tests/gpu-test/src/simple.slv rename to tests/gpu-test/src/unlit.slv index 99f404eaec..3271d0ee90 100644 --- a/tests/gpu-test/src/simple.slv +++ b/tests/gpu-test/src/unlit.slv @@ -19,6 +19,7 @@ <$declareStandardTransform()$> // the interpolated normal +uniform bool Instanced = false; out vec3 _normal; out vec3 _color; @@ -31,6 +32,12 @@ void main(void) { // standard transform TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + if (Instanced) { + <$transformInstancedModelToClipPos(cam, obj, inPosition, gl_Position)$> + <$transformInstancedModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + } else { + <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> + <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> + } + _normal = vec3(0.0, 0.0, 1.0); } \ No newline at end of file From cee296e70c3b76cc363244a9d123d2e4e16b3ba2 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 21 Sep 2015 13:50:42 -0700 Subject: [PATCH 137/192] keep object from rotating during local grab --- examples/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 56b710c5dd..f57e79e974 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -305,7 +305,7 @@ function controller(hand, triggerAction) { this.activateEntity(this.grabbedEntity); - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, "position"); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]); var handRotation = this.getHandRotation(); var handPosition = this.getHandPosition(); From e12e4ece340a2f781a96e773f412118b09beff83 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 21 Sep 2015 13:51:08 -0700 Subject: [PATCH 138/192] Fixing lighting and atmosphere --- interface/src/avatar/SkeletonModel.cpp | 4 ++-- interface/src/ui/overlays/Sphere3DOverlay.cpp | 2 +- .../entities-renderer/src/RenderableSphereEntityItem.cpp | 8 +++++--- libraries/render-utils/src/DeferredLightingEffect.cpp | 1 - libraries/render-utils/src/Environment.cpp | 2 +- libraries/render-utils/src/GeometryCache.cpp | 6 +++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index ff8cde3df8..6b56e92d80 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -643,7 +643,7 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha glm::vec3 topPoint = _translation + _boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * glm::vec3(0.0f, 1.0f, 0.0f); deferredLighting->renderSolidSphereInstance(batch, - Transform().setTranslation(topPoint).postScale(_boundingCapsuleRadius * 2.0), + Transform().setTranslation(topPoint).postScale(_boundingCapsuleRadius), glm::vec4(0.6f, 0.6f, 0.8f, alpha)); // draw a yellow sphere at the capsule bottom point @@ -651,7 +651,7 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha glm::vec3 axis = topPoint - bottomPoint; deferredLighting->renderSolidSphereInstance(batch, - Transform().setTranslation(bottomPoint).postScale(_boundingCapsuleRadius * 2.0), + Transform().setTranslation(bottomPoint).postScale(_boundingCapsuleRadius), glm::vec4(0.8f, 0.8f, 0.6f, alpha)); // draw a green cylinder between the two points diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 3b503e87e8..c22748b214 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -40,7 +40,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { batch->setModelTransform(Transform()); Transform transform = _transform; - transform.postScale(getDimensions()); + transform.postScale(getDimensions() * 0.5f); if (_isSolid) { DependencyManager::get()->renderSolidSphereInstance(*batch, transform, sphereColor); } else { diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp index 63fbfff9cb..1ff8dcbbbd 100644 --- a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp @@ -53,15 +53,17 @@ void RenderableSphereEntityItem::render(RenderArgs* args) { gpu::Batch& batch = *args->_batch; glm::vec4 sphereColor(toGlm(getXColor()), getLocalRenderAlpha()); + Transform modelTransform = getTransformToCenter(); + modelTransform.postScale(0.5f); if (_procedural->ready()) { - batch.setModelTransform(getTransformToCenter()); // use a transform with scale, rotation, registration point and translation - _procedural->prepare(batch, getDimensions()); + batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation + _procedural->prepare(batch, getDimensions() / 2.0f); auto color = _procedural->getColor(sphereColor); batch._glColor4f(color.r, color.g, color.b, color.a); DependencyManager::get()->renderSphere(batch); } else { batch.setModelTransform(Transform()); - DependencyManager::get()->renderSolidSphereInstance(batch, getTransformToCenter(), sphereColor); + DependencyManager::get()->renderSolidSphereInstance(batch, modelTransform, sphereColor); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 1db26eae3b..59dd4e1d5a 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -589,7 +589,6 @@ void DeferredLightingEffect::render(RenderArgs* args) { } else { Transform model; model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z)); - model.postScale(expandedRadius); batch.setModelTransform(model); batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); geometryCache->renderSphere(batch); diff --git a/libraries/render-utils/src/Environment.cpp b/libraries/render-utils/src/Environment.cpp index bffac32b0c..8a4e0a55a6 100644 --- a/libraries/render-utils/src/Environment.cpp +++ b/libraries/render-utils/src/Environment.cpp @@ -199,7 +199,7 @@ bool Environment::findCapsulePenetration(const glm::vec3& start, const glm::vec3 void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum, const EnvironmentData& data) { // FIXME atmosphere rendering is broken in some way, // should probably be replaced by a procedual skybox and put on the marketplace - return; + //return; glm::vec3 center = data.getAtmosphereCenter(); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 53eb8a454b..55575022a5 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -330,7 +330,7 @@ void GeometryCache::buildShapes() { } faceNormal = glm::normalize(faceNormal); for (size_t j = 0; j < 3; ++j) { - vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f); + vertices.push_back(glm::normalize(originalVertices[i + j])); vertices.push_back(faceNormal); } } @@ -369,7 +369,7 @@ void GeometryCache::buildShapes() { vertices.reserve(originalVertices.size() * 2); for (size_t i = 0; i < originalVertices.size(); i += 3) { for (int j = 0; j < 3; ++j) { - vertices.push_back(originalVertices[i + j] * 0.5f); + vertices.push_back(originalVertices[i + j]); vertices.push_back(originalVertices[i + j]); indices.push_back(i + j + startingIndex); } @@ -398,7 +398,7 @@ void GeometryCache::buildShapes() { } faceNormal = glm::normalize(faceNormal); for (int j = 0; j < 3; ++j) { - vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f); + vertices.push_back(glm::normalize(originalVertices[i + j])); vertices.push_back(faceNormal); indices.push_back(i + j + startingIndex); } From 7ffde27224a3da36da45653bd78abbab1f902442 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Sep 2015 14:04:32 -0700 Subject: [PATCH 139/192] Fix HMD fullscreen mirror view --- interface/src/Application.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1b6be53e83..6540153c9b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1149,14 +1149,27 @@ void Application::paintGL() { } } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); - _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + - glm::vec3(0, _raiseMirror * _myAvatar->getScale(), 0) + - (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(_myAvatar->getHMDSensorMatrix()); + _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation() + * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)) * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(_myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * _myAvatar->getScale(), 0) + + (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))) * hmdOffset); + } else { + _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation() + * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); + _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * _myAvatar->getScale(), 0) + + (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); + } renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; } - // Update camera position + // Update camera position if (!isHMDMode()) { _myCamera.update(1.0f / _fps); } From 0c0af812c27f1a6ce36599a37a74f6ef3a8e23ff Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 21 Sep 2015 14:16:56 -0700 Subject: [PATCH 140/192] CR comments --- interface/src/ui/overlays/Sphere3DOverlay.cpp | 6 +++++- .../entities-renderer/src/RenderableSphereEntityItem.cpp | 9 +++++++-- libraries/render-utils/src/Environment.cpp | 4 ---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index c22748b214..0df09d25f6 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -18,6 +18,10 @@ QString const Sphere3DOverlay::TYPE = "sphere"; +// Sphere overlays should fit inside a cube of the specified dimensions, hence it needs to be a half unit sphere. +// However, the geometry cache renders a UNIT sphere, so we need to scale down. +static const float SPHERE_OVERLAY_SCALE = 0.5f; + Sphere3DOverlay::Sphere3DOverlay(const Sphere3DOverlay* Sphere3DOverlay) : Volume3DOverlay(Sphere3DOverlay) { @@ -40,7 +44,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { batch->setModelTransform(Transform()); Transform transform = _transform; - transform.postScale(getDimensions() * 0.5f); + transform.postScale(getDimensions() * SPHERE_OVERLAY_SCALE); if (_isSolid) { DependencyManager::get()->renderSolidSphereInstance(*batch, transform, sphereColor); } else { diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp index 1ff8dcbbbd..3cfc18046a 100644 --- a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp @@ -24,6 +24,11 @@ #include "../render-utils/simple_vert.h" #include "../render-utils/simple_frag.h" +// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 +// is a half unit sphere. However, the geometry cache renders a UNIT sphere, so we need to scale down. +static const float SPHERE_ENTITY_SCALE = 0.5f; + + EntityItemPointer RenderableSphereEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return std::make_shared(entityID, properties); } @@ -54,10 +59,10 @@ void RenderableSphereEntityItem::render(RenderArgs* args) { gpu::Batch& batch = *args->_batch; glm::vec4 sphereColor(toGlm(getXColor()), getLocalRenderAlpha()); Transform modelTransform = getTransformToCenter(); - modelTransform.postScale(0.5f); + modelTransform.postScale(SPHERE_ENTITY_SCALE); if (_procedural->ready()) { batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation - _procedural->prepare(batch, getDimensions() / 2.0f); + _procedural->prepare(batch, getDimensions()); auto color = _procedural->getColor(sphereColor); batch._glColor4f(color.r, color.g, color.b, color.a); DependencyManager::get()->renderSphere(batch); diff --git a/libraries/render-utils/src/Environment.cpp b/libraries/render-utils/src/Environment.cpp index 8a4e0a55a6..7fbd89acc1 100644 --- a/libraries/render-utils/src/Environment.cpp +++ b/libraries/render-utils/src/Environment.cpp @@ -197,10 +197,6 @@ bool Environment::findCapsulePenetration(const glm::vec3& start, const glm::vec3 } void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum, const EnvironmentData& data) { - // FIXME atmosphere rendering is broken in some way, - // should probably be replaced by a procedual skybox and put on the marketplace - //return; - glm::vec3 center = data.getAtmosphereCenter(); // transform the model transform to the center of our atmosphere From 46524632fe01615a99fa0c83498aabf3787140bc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Sep 2015 14:18:33 -0700 Subject: [PATCH 141/192] Fix eyeball directions in fullscreen HMD view --- interface/src/Application.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6540153c9b..ee98ce4c25 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2611,11 +2611,7 @@ void Application::updateMyAvatarLookAtPosition() { if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { // When I am in mirror mode, just look right at the camera (myself); don't switch gaze points because when physically // looking in a mirror one's eyes appear steady. - if (!isHMD) { - lookAtSpot = _myCamera.getPosition(); - } else { - lookAtSpot = _myCamera.getPosition() + transformPoint(_myAvatar->getSensorToWorldMatrix(), extractTranslation(getHMDSensorPose())); - } + lookAtSpot = _myCamera.getPosition(); } else if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { // Look at the point that the user is looking at. if (isHMD) { From fa9b0930d2a87909b9cb5ee20e44febf912f78c9 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 14:25:32 -0700 Subject: [PATCH 142/192] Bring back the procedural skybox --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 10 +++++----- libraries/model/src/model/Skybox.cpp | 2 +- libraries/model/src/model/Skybox.h | 5 ----- libraries/model/src/model/Stage.cpp | 1 - .../procedural/src/procedural/ProceduralSkybox.cpp | 6 ++++++ libraries/procedural/src/procedural/ProceduralSkybox.h | 3 +++ .../script-engine/src/SceneScriptingInterface.cpp | 9 +++++++-- libraries/script-engine/src/SceneScriptingInterface.h | 2 +- 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 02af1ee950..c57d1e5d23 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include "EntityTreeRenderer.h" @@ -294,17 +294,17 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrendOverrideEnvironmentData(); auto stage = scene->getSkyStage(); if (zone->getBackgroundMode() == BACKGROUND_MODE_SKYBOX) { - auto skybox = stage->getSkybox(); + auto skybox = std::dynamic_pointer_cast(stage->getSkybox()); skybox->setColor(zone->getSkyboxProperties().getColorVec3()); static QString userData; if (userData != zone->getUserData()) { userData = zone->getUserData(); - /* QSharedPointer procedural(new Procedural(userData)); + ProceduralPointer procedural(new Procedural(userData)); if (procedural->_enabled) { skybox->setProcedural(procedural); } else { - skybox->setProcedural(QSharedPointer()); - }*/ + skybox->setProcedural(ProceduralPointer()); + } } if (zone->getSkyboxProperties().getURL().isEmpty()) { skybox->setCubemap(gpu::TexturePointer()); diff --git a/libraries/model/src/model/Skybox.cpp b/libraries/model/src/model/Skybox.cpp index c931b78128..e27a0d25ce 100755 --- a/libraries/model/src/model/Skybox.cpp +++ b/libraries/model/src/model/Skybox.cpp @@ -48,7 +48,7 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky static gpu::BufferPointer theBuffer; static gpu::Stream::FormatPointer theFormat; - if (skybox._procedural || skybox.getCubemap()) { + if (skybox.getCubemap()) { if (!theBuffer) { const float CLIP = 1.0f; const glm::vec2 vertices[4] = { { -CLIP, -CLIP }, { CLIP, -CLIP }, { -CLIP, CLIP }, { CLIP, CLIP } }; diff --git a/libraries/model/src/model/Skybox.h b/libraries/model/src/model/Skybox.h index 68352e4309..fb20aadbac 100755 --- a/libraries/model/src/model/Skybox.h +++ b/libraries/model/src/model/Skybox.h @@ -16,8 +16,6 @@ #include "Light.h" class ViewFrustum; -struct Procedural; -typedef std::shared_ptr ProceduralPointer; namespace gpu { class Batch; } @@ -37,13 +35,10 @@ public: void setCubemap(const gpu::TexturePointer& cubemap); const gpu::TexturePointer& getCubemap() const { return _cubemap; } - void setProcedural(const ProceduralPointer& procedural); - static void render(gpu::Batch& batch, const ViewFrustum& frustum, const Skybox& skybox); protected: gpu::TexturePointer _cubemap; - ProceduralPointer _procedural; Color _color{1.0f, 1.0f, 1.0f}; }; typedef std::shared_ptr< Skybox > SkyboxPointer; diff --git a/libraries/model/src/model/Stage.cpp b/libraries/model/src/model/Stage.cpp index fa086d0d2b..386569dd08 100644 --- a/libraries/model/src/model/Stage.cpp +++ b/libraries/model/src/model/Stage.cpp @@ -204,7 +204,6 @@ SunSkyStage::SunSkyStage() : // Begining of march setYearTime(60.0f); - _skybox = std::make_shared(); _skybox->setColor(Color(1.0f, 0.0f, 0.0f)); } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index e237bd3d7d..0f1e0536c1 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -21,6 +21,12 @@ ProceduralSkybox::ProceduralSkybox() : model::Skybox() { } +ProceduralSkybox::ProceduralSkybox(const ProceduralSkybox& skybox) : + model::Skybox(skybox), + _procedural(skybox._procedural) { + +} + void ProceduralSkybox::setProcedural(const ProceduralPointer& procedural) { _procedural = procedural; if (_procedural) { diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 32d8200079..c7543dad93 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -17,9 +17,12 @@ #include "Procedural.h" +typedef std::shared_ptr ProceduralPointer; + class ProceduralSkybox: public model::Skybox { public: ProceduralSkybox(); + ProceduralSkybox(const ProceduralSkybox& skybox); ProceduralSkybox& operator= (const ProceduralSkybox& skybox); virtual ~ProceduralSkybox() {}; diff --git a/libraries/script-engine/src/SceneScriptingInterface.cpp b/libraries/script-engine/src/SceneScriptingInterface.cpp index debba309c5..a8d602e6aa 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.cpp +++ b/libraries/script-engine/src/SceneScriptingInterface.cpp @@ -9,12 +9,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "SceneScriptingInterface.h" + #include -#include "SceneScriptingInterface.h" -#include "SceneScriptingInterface.h" +#include +SceneScriptingInterface::SceneScriptingInterface() { + // Let's make sure the sunSkyStage is using a proceduralSKybox + _skyStage->setSkybox(model::SkyboxPointer(new ProceduralSkybox())); +} void SceneScriptingInterface::setStageOrientation(const glm::quat& orientation) { _skyStage->setOriginOrientation(orientation); diff --git a/libraries/script-engine/src/SceneScriptingInterface.h b/libraries/script-engine/src/SceneScriptingInterface.h index cb2f6b7b79..95919d6c0c 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.h +++ b/libraries/script-engine/src/SceneScriptingInterface.h @@ -117,7 +117,7 @@ signals: void shouldRenderAvatarsChanged(bool shouldRenderAvatars); void shouldRenderEntitiesChanged(bool shouldRenderEntities); protected: - SceneScriptingInterface() {}; + SceneScriptingInterface(); ~SceneScriptingInterface() {}; model::SunSkyStagePointer _skyStage = std::make_shared(); From 84cea1ffd4ae13cd03c8c29db3721495d39f5063 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 21 Sep 2015 14:33:32 -0700 Subject: [PATCH 143/192] More CR comments --- .../entities-renderer/src/RenderableZoneEntityItem.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 90aff03f55..a102c19512 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -19,6 +19,10 @@ #include #include +// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 +// is a half unit sphere. However, the geometry cache renders a UNIT sphere, so we need to scale down. +static const float SPHERE_ENTITY_SCALE = 0.5f; + EntityItemPointer RenderableZoneEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return std::make_shared(entityID, properties); } @@ -126,7 +130,7 @@ void RenderableZoneEntityItem::render(RenderArgs* args) { auto xfm = getTransformToCenter(); auto deferredLightingEffect = DependencyManager::get(); if (getShapeType() == SHAPE_TYPE_SPHERE) { - xfm.postScale(0.5); + xfm.postScale(SPHERE_ENTITY_SCALE); deferredLightingEffect->renderWireSphereInstance(batch, xfm, DEFAULT_COLOR); } else { deferredLightingEffect->renderWireCubeInstance(batch, xfm, DEFAULT_COLOR); From 4dd95be8c40e62638a5c332fe1008a26d24fcc41 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 21 Sep 2015 15:06:11 -0700 Subject: [PATCH 144/192] Forgot to actually persist. --- interface/src/avatar/MyAvatar.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index dfaf3c5239..4bbd0a4a0b 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -654,6 +654,7 @@ void MyAvatar::saveData() { settings.setValue("fullAvatarURL", _fullAvatarURLFromPreferences); settings.setValue("fullAvatarModelName", _fullAvatarModelName); + settings.setValue("animGraphURL", _animGraphUrl); settings.beginWriteArray("attachmentData"); for (int i = 0; i < _attachmentData.size(); i++) { @@ -791,6 +792,7 @@ void MyAvatar::loadData() { _targetScale = loadSetting(settings, "scale", 1.0f); setScale(_scale); + _animGraphUrl = settings.value("animGraphURL", "").toString(); _fullAvatarURLFromPreferences = settings.value("fullAvatarURL", AvatarData::defaultFullAvatarModelUrl()).toUrl(); _fullAvatarModelName = settings.value("fullAvatarModelName", DEFAULT_FULL_AVATAR_MODEL_NAME).toString(); From a485d3d6de4eb5b1dbb7cda4413597f631e884c0 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 15:21:48 -0700 Subject: [PATCH 145/192] Fixing the rendering of ProceduralSkybox --- interface/src/Application.cpp | 2 +- libraries/model/src/model/Skybox.h | 4 +++ .../src/procedural/ProceduralSkybox.cpp | 26 ++++++++++--------- .../src/procedural/ProceduralSkybox.h | 1 + 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 798b1ab9a2..36b56bfa07 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3507,7 +3507,7 @@ namespace render { skybox = skyStage->getSkybox(); if (skybox) { - model::Skybox::render(batch, *(Application::getInstance()->getDisplayViewFrustum()), *skybox); + skybox->render(batch, *(Application::getInstance()->getDisplayViewFrustum())); } } } diff --git a/libraries/model/src/model/Skybox.h b/libraries/model/src/model/Skybox.h index fb20aadbac..e9f95afa16 100755 --- a/libraries/model/src/model/Skybox.h +++ b/libraries/model/src/model/Skybox.h @@ -35,6 +35,10 @@ public: void setCubemap(const gpu::TexturePointer& cubemap); const gpu::TexturePointer& getCubemap() const { return _cubemap; } + virtual void render(gpu::Batch& batch, const ViewFrustum& frustum) const { + render(batch, frustum, (*this)); + } + static void render(gpu::Batch& batch, const ViewFrustum& frustum, const Skybox& skybox); protected: diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 0f1e0536c1..8d34f0e7e5 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -36,11 +36,19 @@ void ProceduralSkybox::setProcedural(const ProceduralPointer& procedural) { } } +void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& frustum) const { + ProceduralSkybox::render(batch, frustum, (*this)); +} + void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const ProceduralSkybox& skybox) { + if (!(skybox._procedural)) { + Skybox::render(batch, viewFrustum, skybox); + } + static gpu::BufferPointer theBuffer; static gpu::Stream::FormatPointer theFormat; - if (skybox._procedural || skybox.getCubemap()) { + if (skybox._procedural && skybox._procedural->_enabled && skybox._procedural->ready()) { if (!theBuffer) { const float CLIP = 1.0f; const glm::vec2 vertices[4] = { { -CLIP, -CLIP }, { CLIP, -CLIP }, { -CLIP, CLIP }, { CLIP, CLIP } }; @@ -60,18 +68,12 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setInputBuffer(gpu::Stream::POSITION, theBuffer, 0, 8); batch.setInputFormat(theFormat); - if (skybox._procedural && skybox._procedural->_enabled && skybox._procedural->ready()) { - if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { - batch.setResourceTexture(0, skybox.getCubemap()); - } - - skybox._procedural->prepare(batch, glm::vec3(1)); - batch.draw(gpu::TRIANGLE_STRIP, 4); + if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { + batch.setResourceTexture(0, skybox.getCubemap()); } - } else { - // skybox has no cubemap, just clear the color buffer - auto color = skybox.getColor(); - batch.clearFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(color, 0.0f), 0.0f, 0, true); + + skybox._procedural->prepare(batch, glm::vec3(1)); + batch.draw(gpu::TRIANGLE_STRIP, 4); } } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index c7543dad93..057a1ccc74 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -28,6 +28,7 @@ public: void setProcedural(const ProceduralPointer& procedural); + virtual void render(gpu::Batch& batch, const ViewFrustum& frustum) const; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); protected: From b3aeaba5f4b7e6d0951aa7513d3a753510f30ab4 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 21 Sep 2015 15:44:47 -0700 Subject: [PATCH 146/192] CR feedback --- .../src/RenderableDebugableEntityItem.cpp | 6 +- .../src/RenderableZoneEntityItem.cpp | 8 +- .../src/DeferredLightingEffect.cpp | 46 +-- libraries/render-utils/src/GeometryCache.cpp | 264 ++++++++++++------ libraries/render-utils/src/GeometryCache.h | 8 +- 5 files changed, 210 insertions(+), 122 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp index 6986025133..a13ed5b06d 100644 --- a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp @@ -24,12 +24,12 @@ void RenderableDebugableEntityItem::renderBoundingBox(EntityItem* entity, Render Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; - auto xfm = entity->getTransformToCenter(); + auto shapeTransform = entity->getTransformToCenter(); if (puffedOut != 0.0) { - xfm.postScale(1.0 + puffedOut); + shapeTransform.postScale(1.0 + puffedOut); } batch.setModelTransform(Transform()); // we want to include the scale as well - DependencyManager::get()->renderWireCubeInstance(batch, xfm, color); + DependencyManager::get()->renderWireCubeInstance(batch, shapeTransform, color); } void RenderableDebugableEntityItem::render(EntityItem* entity, RenderArgs* args) { diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index a102c19512..c8088b7406 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -127,13 +127,13 @@ void RenderableZoneEntityItem::render(RenderArgs* args) { gpu::Batch& batch = *args->_batch; batch.setModelTransform(Transform()); - auto xfm = getTransformToCenter(); + auto shapeTransform = getTransformToCenter(); auto deferredLightingEffect = DependencyManager::get(); if (getShapeType() == SHAPE_TYPE_SPHERE) { - xfm.postScale(SPHERE_ENTITY_SCALE); - deferredLightingEffect->renderWireSphereInstance(batch, xfm, DEFAULT_COLOR); + shapeTransform.postScale(SPHERE_ENTITY_SCALE); + deferredLightingEffect->renderWireSphereInstance(batch, shapeTransform, DEFAULT_COLOR); } else { - deferredLightingEffect->renderWireCubeInstance(batch, xfm, DEFAULT_COLOR); + deferredLightingEffect->renderWireCubeInstance(batch, shapeTransform, DEFAULT_COLOR); } break; } diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 59dd4e1d5a..690cc8c6bd 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -187,10 +187,6 @@ gpu::PipelinePointer DeferredLightingEffect::bindSimpleProgram(gpu::Batch& batch return pipeline; } -void DeferredLightingEffect::renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { - -} - uint32_t toCompactColor(const glm::vec4& color) { uint32_t compactColor = ((int(color.x * 255.0f) & 0xFF)) | ((int(color.y * 255.0f) & 0xFF) << 8) | @@ -203,11 +199,11 @@ static const size_t INSTANCE_TRANSFORM_BUFFER = 0; static const size_t INSTANCE_COLOR_BUFFER = 1; template -void renderInstances(const std::string& name, gpu::Batch& batch, const Transform& xfm, const glm::vec4& color, F f) { +void renderInstances(const std::string& name, gpu::Batch& batch, const Transform& transform, const glm::vec4& color, F f) { { gpu::BufferPointer instanceTransformBuffer = batch.getNamedBuffer(name, INSTANCE_TRANSFORM_BUFFER); - glm::mat4 xfmMat4; - instanceTransformBuffer->append(xfm.getMatrix(xfmMat4)); + glm::mat4 glmTransform; + instanceTransformBuffer->append(transform.getMatrix(glmTransform)); gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(name, INSTANCE_COLOR_BUFFER); auto compactColor = toCompactColor(color); @@ -225,21 +221,32 @@ void renderInstances(const std::string& name, gpu::Batch& batch, const Transform }); } -void DeferredLightingEffect::renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { +void DeferredLightingEffect::renderSolidSphereInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) { static const std::string INSTANCE_NAME = __FUNCTION__; - renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { - DependencyManager::get()->renderSphereInstances(batch, data._count, + renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + DependencyManager::get()->renderShapeInstances(batch, GeometryCache::Sphere, data._count, data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); }); } -static auto startTime = usecTimestampNow(); +void DeferredLightingEffect::renderWireSphereInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) { + static const std::string INSTANCE_NAME = __FUNCTION__; + renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + DependencyManager::get()->renderWireShapeInstances(batch, GeometryCache::Sphere, data._count, + data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); + }); +} -void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { +// Enable this in a debug build to cause 'box' entities to iterate through all the +// available shape types, both solid and wireframes +//#define DEBUG_SHAPES + +void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) { static const std::string INSTANCE_NAME = __FUNCTION__; #ifdef DEBUG_SHAPES - renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + static auto startTime = usecTimestampNow(); + renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { auto usecs = usecTimestampNow(); usecs -= startTime; @@ -249,7 +256,9 @@ void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Tr float fractionalSeconds = seconds - floor(seconds); int shapeIndex = (int)seconds; - GeometryCache::Shape shapes[] = { + // Every second we flip to the next shape. + static const int SHAPE_COUNT = 5; + GeometryCache::Shape shapes[SHAPE_COUNT] = { GeometryCache::Cube, GeometryCache::Tetrahedron, GeometryCache::Sphere, @@ -257,9 +266,10 @@ void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Tr GeometryCache::Line, }; - shapeIndex %= 5; + shapeIndex %= SHAPE_COUNT; GeometryCache::Shape shape = shapes[shapeIndex]; + // For the first half second for a given shape, show the wireframe, for the second half, show the solid. if (fractionalSeconds > 0.5f) { DependencyManager::get()->renderShapeInstances(batch, shape, data._count, data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); @@ -269,16 +279,16 @@ void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Tr } }); #else - renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { DependencyManager::get()->renderCubeInstances(batch, data._count, data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); }); #endif } -void DeferredLightingEffect::renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) { +void DeferredLightingEffect::renderWireCubeInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) { static const std::string INSTANCE_NAME = __FUNCTION__; - renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { DependencyManager::get()->renderWireCubeInstances(batch, data._count, data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]); }); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 55575022a5..319fc710b1 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -57,7 +57,7 @@ static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec3) * 2; // vertices and n static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3); -void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices) { +void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices) { vertexBuffer->append(vertices); _positionView = gpu::BufferView(vertexBuffer, 0, @@ -66,7 +66,7 @@ void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, c vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, NORMAL_ELEMENT); } -void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices) { +void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const IndexVector& indices, const IndexVector& wireIndices) { _indices = indexBuffer; if (!indices.empty()) { _indexOffset = indexBuffer->getSize(); @@ -118,12 +118,12 @@ void GeometryCache::ShapeData::drawWireInstances(gpu::Batch& batch, size_t count } } -const VVertex& icosahedronVertices() { +const VertexVector& icosahedronVertices() { static const float phi = (1.0 + sqrt(5.0)) / 2.0; static const float a = 0.5; static const float b = 1.0 / (2.0 * phi); - static const VVertex vertices{ // + static const VertexVector vertices{ // vec3(0, b, -a), vec3(-b, a, 0), vec3(b, a, 0), // vec3(0, b, a), vec3(b, a, 0), vec3(-b, a, 0), // vec3(0, b, a), vec3(-a, 0, b), vec3(0, -b, a), // @@ -148,13 +148,13 @@ const VVertex& icosahedronVertices() { return vertices; } -const VVertex& tetrahedronVertices() { +const VertexVector& tetrahedronVertices() { static const float a = 1.0f / sqrt(2.0f); static const auto A = vec3(0, 1, a); static const auto B = vec3(0, -1, a); static const auto C = vec3(1, 0, -a); static const auto D = vec3(-1, 0, -a); - static const VVertex vertices{ + static const VertexVector vertices{ A, B, C, D, B, A, C, D, A, @@ -163,8 +163,13 @@ const VVertex& tetrahedronVertices() { return vertices; } -VVertex tesselate(const VVertex& startingTriangles, int count) { - VVertex triangles = startingTriangles; +static const size_t TESSELTATION_MULTIPLIER = 4; +static const size_t ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT = 3; +static const size_t VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER = 2; + + +VertexVector tesselate(const VertexVector& startingTriangles, int count) { + VertexVector triangles = startingTriangles; if (0 != (triangles.size() % 3)) { throw std::runtime_error("Bad number of vertices for tesselation"); } @@ -173,11 +178,13 @@ VVertex tesselate(const VVertex& startingTriangles, int count) { triangles[i] = glm::normalize(triangles[i]); } - VVertex newTriangles; + VertexVector newTriangles; while (count) { newTriangles.clear(); - newTriangles.reserve(triangles.size() * 4); - for (size_t i = 0; i < triangles.size(); i += 3) { + // Tesselation takes one triangle and makes it into 4 triangles + // See https://en.wikipedia.org/wiki/Space-filling_tree#/media/File:Space_Filling_Tree_Tri_iter_1_2_3.png + newTriangles.reserve(triangles.size() * TESSELTATION_MULTIPLIER); + for (size_t i = 0; i < triangles.size(); i += VERTICES_PER_TRIANGLE) { const vec3& a = triangles[i]; const vec3& b = triangles[i + 1]; const vec3& c = triangles[i + 2]; @@ -221,7 +228,7 @@ void GeometryCache::buildShapes() { startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; { ShapeData& shapeData = _shapes[Cube]; - VVertex vertices; + VertexVector vertices; // front vertices.push_back(vec3(1, 1, 1)); vertices.push_back(vec3(0, 0, 1)); @@ -282,14 +289,21 @@ void GeometryCache::buildShapes() { vertices.push_back(vec3(1, 1, -1)); vertices.push_back(vec3(0, 0, -1)); + static const size_t VERTEX_FORMAT_SIZE = 2; + static const size_t VERTEX_OFFSET = 0; + static const size_t NORMAL_OFFSET = 1; + for (size_t i = 0; i < vertices.size(); ++i) { - if (0 == i % 2) { - vertices[i] *= 0.5f; + auto vertexIndex = i; + // Make a unit cube by having the vertices (at index N) + // while leaving the normals (at index N + 1) alone + if (VERTEX_OFFSET == vertexIndex % VERTEX_FORMAT_SIZE) { + vertices[vertexIndex] *= 0.5f; } } shapeData.setupVertices(_shapeVertices, vertices); - VIndex indices{ + IndexVector indices{ 0, 1, 2, 2, 3, 0, // front 4, 5, 6, 6, 7, 4, // right 8, 9, 10, 10, 11, 8, // top @@ -297,11 +311,11 @@ void GeometryCache::buildShapes() { 16, 17, 18, 18, 19, 16, // bottom 20, 21, 22, 22, 23, 20 // back }; - for (int i = 0; i < indices.size(); ++i) { - indices[i] += startingIndex; + for (auto& index : indices) { + index += startingIndex; } - VIndex wireIndices{ + IndexVector wireIndices{ 0, 1, 1, 2, 2, 3, 3, 0, // front 20, 21, 21, 22, 22, 23, 23, 20, // back 0, 23, 1, 22, 2, 21, 3, 20 // sides @@ -318,33 +332,41 @@ void GeometryCache::buildShapes() { { ShapeData& shapeData = _shapes[Tetrahedron]; size_t vertexCount = 4; - VVertex vertices; + VertexVector vertices; { - VVertex originalVertices = tetrahedronVertices(); + VertexVector originalVertices = tetrahedronVertices(); vertexCount = originalVertices.size(); - vertices.reserve(originalVertices.size() * 2); - for (size_t i = 0; i < originalVertices.size(); i += 3) { + vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); + for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) { + auto triangleStartIndex = i; vec3 faceNormal; - for (size_t j = 0; j < 3; ++j) { - faceNormal += originalVertices[i + j]; + for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { + auto triangleVertexIndex = j; + auto vertexIndex = triangleStartIndex + triangleVertexIndex; + faceNormal += originalVertices[vertexIndex]; } faceNormal = glm::normalize(faceNormal); - for (size_t j = 0; j < 3; ++j) { - vertices.push_back(glm::normalize(originalVertices[i + j])); + for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { + auto triangleVertexIndex = j; + auto vertexIndex = triangleStartIndex + triangleVertexIndex; + vertices.push_back(glm::normalize(originalVertices[vertexIndex])); vertices.push_back(faceNormal); } } } shapeData.setupVertices(_shapeVertices, vertices); - VIndex indices; - for (size_t i = 0; i < vertexCount; i += 3) { - for (size_t j = 0; j < 3; ++j) { - indices.push_back(i + j + startingIndex); + IndexVector indices; + for (size_t i = 0; i < vertexCount; i += VERTICES_PER_TRIANGLE) { + auto triangleStartIndex = i; + for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) { + auto triangleVertexIndex = j; + auto vertexIndex = triangleStartIndex + triangleVertexIndex; + indices.push_back(vertexIndex + startingIndex); } } - VIndex wireIndices{ + IndexVector wireIndices{ 0, 1, 1, 2, 2, 0, 0, 3, 1, 3, 2, 3, }; @@ -362,16 +384,21 @@ void GeometryCache::buildShapes() { startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; { ShapeData& shapeData = _shapes[Sphere]; - VVertex vertices; - VIndex indices; + VertexVector vertices; + IndexVector indices; { - VVertex originalVertices = tesselate(icosahedronVertices(), 3); - vertices.reserve(originalVertices.size() * 2); - for (size_t i = 0; i < originalVertices.size(); i += 3) { - for (int j = 0; j < 3; ++j) { - vertices.push_back(originalVertices[i + j]); - vertices.push_back(originalVertices[i + j]); - indices.push_back(i + j + startingIndex); + VertexVector originalVertices = tesselate(icosahedronVertices(), ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT); + vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); + for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) { + auto triangleStartIndex = i; + for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { + auto triangleVertexIndex = j; + auto vertexIndex = triangleStartIndex + triangleVertexIndex; + const auto& vertex = originalVertices[i + j]; + // Spheres use the same values for vertices and normals + vertices.push_back(vertex); + vertices.push_back(vertex); + indices.push_back(vertexIndex + startingIndex); } } } @@ -386,21 +413,26 @@ void GeometryCache::buildShapes() { { ShapeData& shapeData = _shapes[Icosahedron]; - VVertex vertices; - VIndex indices; + VertexVector vertices; + IndexVector indices; { - const VVertex& originalVertices = icosahedronVertices(); - vertices.reserve(originalVertices.size() * 2); + const VertexVector& originalVertices = icosahedronVertices(); + vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER); for (size_t i = 0; i < originalVertices.size(); i += 3) { + auto triangleStartIndex = i; vec3 faceNormal; - for (size_t j = 0; j < 3; ++j) { - faceNormal += originalVertices[i + j]; + for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { + auto triangleVertexIndex = j; + auto vertexIndex = triangleStartIndex + triangleVertexIndex; + faceNormal += originalVertices[vertexIndex]; } faceNormal = glm::normalize(faceNormal); - for (int j = 0; j < 3; ++j) { - vertices.push_back(glm::normalize(originalVertices[i + j])); + for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) { + auto triangleVertexIndex = j; + auto vertexIndex = triangleStartIndex + triangleVertexIndex; + vertices.push_back(glm::normalize(originalVertices[vertexIndex])); vertices.push_back(faceNormal); - indices.push_back(i + j + startingIndex); + indices.push_back(vertexIndex + startingIndex); } } } @@ -410,6 +442,24 @@ void GeometryCache::buildShapes() { shapeData.setupIndices(_shapeIndices, indices, indices); } + // Line + startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; + { + ShapeData& shapeData = _shapes[Line]; + shapeData.setupVertices(_shapeVertices, VertexVector{ + vec3(-0.5, 0, 0), vec3(-0.5f, 0, 0), + vec3(0.5f, 0, 0), vec3(0.5f, 0, 0) + }); + IndexVector wireIndices; + // Only two indices + wireIndices.push_back(0 + startingIndex); + wireIndices.push_back(1 + startingIndex); + + shapeData.setupIndices(_shapeIndices, IndexVector(), wireIndices); + } + + // Not implememented yet: + //Triangle, //Quad, //Circle, @@ -418,20 +468,6 @@ void GeometryCache::buildShapes() { //Torus, //Cone, //Cylinder, - // Line - startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE; - { - ShapeData& shapeData = _shapes[Line]; - shapeData.setupVertices(_shapeVertices, VVertex{ - vec3(-0.5, 0, 0), vec3(-0.5, 0, 0), - vec3(0.5f, 0, 0), vec3(0.5f, 0, 0) - }); - VIndex wireIndices; - wireIndices.push_back(0 + startingIndex); - wireIndices.push_back(1 + startingIndex); - - shapeData.setupIndices(_shapeIndices, VIndex(), wireIndices); - } } gpu::Stream::FormatPointer& getSolidStreamFormat() { @@ -1092,11 +1128,6 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } -//void GeometryCache::renderUnitCube(gpu::Batch& batch) { -// static const glm::vec4 color(1); -// renderSolidCube(batch, 1, color); -//} - void GeometryCache::renderUnitQuad(gpu::Batch& batch, const glm::vec4& color, int id) { static const glm::vec2 topLeft(-1, 1); static const glm::vec2 bottomRight(1, -1); @@ -2015,29 +2046,54 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas // otherwise, at least the cluster indices/weights can be static networkMesh->_vertexStream = std::make_shared(); networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3)); - if (mesh.normals.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, normalsOffset, sizeof(glm::vec3)); - if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, tangentsOffset, sizeof(glm::vec3)); - if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); - if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); - if (mesh.texCoords1.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoords1Offset, sizeof(glm::vec2)); - if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); - if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); - + if (mesh.normals.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, normalsOffset, sizeof(glm::vec3)); + } + if (mesh.tangents.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, tangentsOffset, sizeof(glm::vec3)); + } + if (mesh.colors.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); + } + if (mesh.texCoords.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); + } + if (mesh.texCoords1.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoords1Offset, sizeof(glm::vec2)); + } + if (mesh.clusterIndices.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); + } + if (mesh.clusterWeights.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); + } int channelNum = 0; networkMesh->_vertexFormat = std::make_shared(); networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); - if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + if (mesh.normals.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + } + if (mesh.tangents.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + } + if (mesh.colors.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); + } + if (mesh.texCoords.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + } if (mesh.texCoords1.size()) { networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); } else if (checkForTexcoordLightmap && mesh.texCoords.size()) { // need lightmap texcoord UV but doesn't have uv#1 so just reuse the same channel networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum - 1, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); } - if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); - if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + if (mesh.clusterIndices.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + } + if (mesh.clusterWeights.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + } } else { int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); @@ -2056,21 +2112,43 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas mesh.clusterWeights.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterWeights.constData()); networkMesh->_vertexStream = std::make_shared(); - if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3)); - if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); - if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); - if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); - if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); + if (mesh.tangents.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3)); + } + if (mesh.colors.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3)); + } + if (mesh.texCoords.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2)); + } + if (mesh.clusterIndices.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4)); + } + if (mesh.clusterWeights.size()) { + networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4)); + } int channelNum = 0; networkMesh->_vertexFormat = std::make_shared(); networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); - if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); - if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); - if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); - if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + if (mesh.normals.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + } + if (mesh.tangents.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + } + if (mesh.colors.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB)); + } + if (mesh.texCoords.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + } + if (mesh.clusterIndices.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + } + if (mesh.clusterWeights.size()) { + networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)); + } } } diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index d911629001..df57b5cfb4 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -121,8 +121,8 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) { seed); } -using VVertex = std::vector; -using VIndex = std::vector; +using VertexVector = std::vector; +using IndexVector = std::vector; /// Stores cached geometry. class GeometryCache : public ResourceCache, public Dependency { @@ -262,8 +262,8 @@ private: gpu::BufferView _normalView; gpu::BufferPointer _indices; - void setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices); - void setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices); + void setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices); + void setupIndices(gpu::BufferPointer& indexBuffer, const IndexVector& indices, const IndexVector& wireIndices); void setupBatch(gpu::Batch& batch) const; void draw(gpu::Batch& batch) const; void drawWire(gpu::Batch& batch) const; From fd67a2b86fd86f1f8a9f055d192eed6d4253741e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 16:10:08 -0700 Subject: [PATCH 147/192] Add on/off by pressure; add point bulb to end of flashlight --- examples/toys/flashlight/createFlashlight.js | 33 +-- examples/toys/flashlight/flashlight.js | 240 +++++++++++-------- 2 files changed, 153 insertions(+), 120 deletions(-) diff --git a/examples/toys/flashlight/createFlashlight.js b/examples/toys/flashlight/createFlashlight.js index bf03de547b..ad089e001a 100644 --- a/examples/toys/flashlight/createFlashlight.js +++ b/examples/toys/flashlight/createFlashlight.js @@ -15,30 +15,33 @@ Script.include("https://hifi-public.s3.amazonaws.com/scripts/utilities.js"); -var scriptURL = "https://hifi-public.s3.amazonaws.com/scripts/toys/flashlight/flashlight.js?"+randInt(0,1000); +var scriptURL = Script.resolvePath('flashlight.js?12322'); var modelURL = "https://hifi-public.s3.amazonaws.com/models/props/flashlight.fbx"; - -var center = Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0, y: 0.5, z: 0}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); +var center = Vec3.sum(Vec3.sum(MyAvatar.position, { + x: 0, + y: 0.5, + z: 0 +}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); var flashlight = Entities.addEntity({ - type: "Model", - modelURL: modelURL, - position: center, - dimensions: { - x: 0.04, - y: 0.15, - z: 0.04 - }, - collisionsWillMove: true, - shapeType: 'box', - script: scriptURL + type: "Model", + modelURL: modelURL, + position: center, + dimensions: { + x: 0.04, + y: 0.15, + z: 0.04 + }, + collisionsWillMove: true, + shapeType: 'box', + script: scriptURL }); function cleanup() { - Entities.deleteEntity(flashlight); + Entities.deleteEntity(flashlight); } diff --git a/examples/toys/flashlight/flashlight.js b/examples/toys/flashlight/flashlight.js index 9aa64a4435..7f2bd210da 100644 --- a/examples/toys/flashlight/flashlight.js +++ b/examples/toys/flashlight/flashlight.js @@ -4,6 +4,7 @@ // Script Type: Entity // // Created by Sam Gateau on 9/9/15. +// Additions by James B. Pollack @imgntn on 9/21/2015 // Copyright 2015 High Fidelity, Inc. // // This is a toy script that can be added to the Flashlight model entity: @@ -13,15 +14,9 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// TODO: update to use new grab signals, which will include handedness. -// BONUS: dim the light with pressure instead of binary on/off (function() { - function debugPrint(message) { - //print(message); - } - Script.include("../../libraries/utils.js"); var _this; @@ -30,11 +25,11 @@ // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) Flashlight = function() { _this = this; - _this._hasSpotlight = false; - _this._spotlight = null; }; - var DISABLE_LIGHT_THRESHOLD = 0.5; + //if the trigger value goes below this while held, the flashlight will turn off. if it goes above, it will + var DISABLE_LIGHT_THRESHOLD = 0.7; + // These constants define the Spotlight position and orientation relative to the model var MODEL_LIGHT_POSITION = { x: 0, @@ -47,101 +42,131 @@ z: 0 }); - // Evaluate the world light entity position and orientation from the model ones + var GLOW_LIGHT_POSITION = { + x: 0, + y: -0.1, + z: 0 + } + + // Evaluate the world light entity positions and orientations from the model ones function evalLightWorldTransform(modelPos, modelRot) { + return { p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)), q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION) }; }; + function glowLightWorldTransform(modelPos, modelRot) { + return { + p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, GLOW_LIGHT_POSITION)), + q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION) + }; + }; + + Flashlight.prototype = { lightOn: false, + hand: null, + whichHand: null, + hasSpotlight: false, + spotlight: null, + setRightHand: function() { + this.hand = 'RIGHT'; + }, + setLeftHand: function() { + this.hand = 'LEFT'; + }, + startNearGrab: function() { + if (!_this.hasSpotlight) { + //this light casts the beam + this.spotlight = Entities.addEntity({ + type: "Light", + isSpotlight: true, + dimensions: { + x: 2, + y: 2, + z: 20 + }, + color: { + red: 255, + green: 255, + blue: 255 + }, + intensity: 2, + exponent: 0.3, + cutoff: 20 + }); - // update() will be called regulary, because we've hooked the update signal in our preload() function - // we will check out userData for the grabData. In the case of the hydraGrab script, it will tell us - // if we're currently being grabbed and if the person grabbing us is the current interfaces avatar. - // we will watch this for state changes and print out if we're being grabbed or released when it changes. - update: function() { - var GRAB_USER_DATA_KEY = "grabKey"; + //this light creates the effect of a bulb at the end of the flashlight + this.glowLight = Entities.addEntity({ + type: "Light", + dimensions: { + x: 0.25, + y: 0.25, + z: 0.25 + }, + isSpotlight: false, + color: { + red: 255, + green: 255, + blue: 255 + }, + exponent: 0, + cutoff: 90, // in degrees + }); - // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID - var entityID = _this.entityID; + this.hasSpotlight = true; - // we want to assume that if there is no grab data, then we are not being grabbed - var defaultGrabData = { - activated: false, - avatarId: null - }; - - // this handy function getEntityCustomData() is available in utils.js and it will return just the specific section - // of user data we asked for. If it's not available it returns our default data. - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, defaultGrabData); - - - // if the grabData says we're being grabbed, and the owner ID is our session, then we are being grabbed by this interface - if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { - - // remember we're being grabbed so we can detect being released - _this.beingGrabbed = true; - - var modelProperties = Entities.getEntityProperties(entityID); - var lightTransform = evalLightWorldTransform(modelProperties.position, modelProperties.rotation); - - // Create the spot light driven by this model if we don;t have one yet - // Or make sure to keep it's position in sync - if (!_this._hasSpotlight) { - - _this._spotlight = Entities.addEntity({ - type: "Light", - position: lightTransform.p, - rotation: lightTransform.q, - isSpotlight: true, - dimensions: { - x: 2, - y: 2, - z: 20 - }, - color: { - red: 255, - green: 255, - blue: 255 - }, - intensity: 2, - exponent: 0.3, - cutoff: 20 - }); - _this._hasSpotlight = true; - - - debugPrint("Flashlight:: creating a spotlight"); - } else { - // Updating the spotlight - Entities.editEntity(_this._spotlight, { - position: lightTransform.p, - rotation: lightTransform.q - }); - _this.changeLightWithTriggerPressure(); - debugPrint("Flashlight:: updating the spotlight"); - } - - debugPrint("I'm being grabbed..."); - - } else if (_this.beingGrabbed) { - - if (_this._hasSpotlight) { - Entities.deleteEntity(_this._spotlight); - debugPrint("Destroying flashlight spotlight..."); - } - _this._hasSpotlight = false; - _this._spotlight = null; - - // if we are not being grabbed, and we previously were, then we were just released, remember that - // and print out a message - _this.beingGrabbed = false; - debugPrint("I'm was released..."); } + + }, + setWhichHand: function() { + this.whichHand = this.hand; + }, + continueNearGrab: function() { + if (this.whichHand === null) { + //only set the active hand once -- if we always read the current hand, our 'holding' hand will get overwritten + this.setWhichHand(); + } else { + this.updateLightPositions(); + this.changeLightWithTriggerPressure(this.whichHand); + } + }, + releaseGrab: function() { + //delete the lights and reset state + if (this.hasSpotlight) { + Entities.deleteEntity(this.spotlight); + Entities.deleteEntity(this.glowLight); + this.hasSpotlight = false; + this.glowLight = null; + this.spotlight = null; + this.whichHand = null; + + } + }, + updateLightPositions: function() { + var modelProperties = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); + + //move the two lights along the vectors we set above + var lightTransform = evalLightWorldTransform(modelProperties.position, modelProperties.rotation); + var glowLightTransform = glowLightWorldTransform(modelProperties.position, modelProperties.rotation); + + + //move them with the entity model + Entities.editEntity(this.spotlight, { + position: lightTransform.p, + rotation: lightTransform.q, + }) + + + Entities.editEntity(this.glowLight, { + position: glowLightTransform.p, + rotation: glowLightTransform.q, + }) + + }, changeLightWithTriggerPressure: function(flashLightHand) { @@ -156,33 +181,34 @@ } else if (this.triggerValue >= DISABLE_LIGHT_THRESHOLD && this.lightOn === false) { this.turnLightOn(); } - - return triggerValue + return }, turnLightOff: function() { - Entities.editEntity(_this._spotlight, { + print('turn light off') + Entities.editEntity(this.spotlight, { + intensity: 0 + }); + Entities.editEntity(this.glowLight, { intensity: 0 }); this.lightOn = false }, turnLightOn: function() { - Entities.editEntity(_this._spotlight, { + print('turn light on') + Entities.editEntity(this.glowLight, { + intensity: 2 + }); + Entities.editEntity(this.spotlight, { intensity: 2 }); this.lightOn = true }, - // preload() will be called when the entity has become visible (or known) to the interface // it gives us a chance to set our local JavaScript object up. In this case it means: // * remembering our entityID, so we can access it in cases where we're called without an entityID // * connecting to the update signal so we can check our grabbed state preload: function(entityID) { - _this.entityID = entityID; - - var modelProperties = Entities.getEntityProperties(entityID); - - - + this.entityID = entityID; Script.update.connect(this.update); }, @@ -191,11 +217,15 @@ // to the update signal unload: function(entityID) { - if (_this._hasSpotlight) { - Entities.deleteEntity(_this._spotlight); + if (this.hasSpotlight) { + Entities.deleteEntity(this.spotlight); + Entities.deleteEntity(this.glowLight); + this.hasSpotlight = false; + this.glowLight = null; + this.spotlight = null; + this.whichHand = null; } - _this._hasSpotlight = false; - _this._spotlight = null; + Script.update.disconnect(this.update); }, From dfe17e5708b87db2dfef3923c2ecb5710d8d0007 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 16:12:17 -0700 Subject: [PATCH 148/192] Fix issues from review --- libraries/fbx/src/FBXReader.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index b4c15ac2a8..50d27ea283 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1061,13 +1061,13 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QString parentID = getID(connection.properties, 2); ooChildToParent.insert(childID, parentID); if (!hifiGlobalNodeID.isEmpty() && (parentID == hifiGlobalNodeID)) { - std::map< QString, FBXLight >::iterator lit = lights.find(childID); - if (lit != lights.end()) { - _lightmapLevel = (*lit).second.intensity; + std::map< QString, FBXLight >::iterator lightIt = lights.find(childID); + if (lightIt != lights.end()) { + _lightmapLevel = (*lightIt).second.intensity; if (_lightmapLevel <= 0.0f) { _loadLightmaps = false; } - _lightmapOffset = glm::clamp((*lit).second.color.x, 0.f, 1.f); + _lightmapOffset = glm::clamp((*lightIt).second.color.x, 0.f, 1.f); } } } @@ -1141,8 +1141,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { - std::map< QString, FBXLight >::iterator l = lights.begin(); - _lightmapLevel = (*l).second.intensity; + auto light = lights.begin(); + _lightmapLevel = (*light).second.intensity; } } @@ -1365,7 +1365,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) { - // extracted.mesh.parts[j].diffuseTexture = texture; + // TODO: DO something here that replaces this legacy code + // Maybe create a material just for this part with the correct textures? + // extracted.mesh.parts[j].diffuseTexture = texture; } } textureIndex++; From d79c1bec953e0b1f930b7adf158c86b0a895141d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 16:13:24 -0700 Subject: [PATCH 149/192] remove query string --- examples/toys/flashlight/createFlashlight.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/toys/flashlight/createFlashlight.js b/examples/toys/flashlight/createFlashlight.js index ad089e001a..f0d31b6934 100644 --- a/examples/toys/flashlight/createFlashlight.js +++ b/examples/toys/flashlight/createFlashlight.js @@ -15,7 +15,7 @@ Script.include("https://hifi-public.s3.amazonaws.com/scripts/utilities.js"); -var scriptURL = Script.resolvePath('flashlight.js?12322'); +var scriptURL = Script.resolvePath('flashlight.js'); var modelURL = "https://hifi-public.s3.amazonaws.com/models/props/flashlight.fbx"; From 0401672c82e099a04112c5766d505c21d0bd9da0 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 21 Sep 2015 16:24:55 -0700 Subject: [PATCH 150/192] Fixing lighting again --- libraries/render-utils/src/DeferredLightingEffect.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 690cc8c6bd..b120bb8a57 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -599,7 +599,7 @@ void DeferredLightingEffect::render(RenderArgs* args) { } else { Transform model; model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z)); - batch.setModelTransform(model); + batch.setModelTransform(model.postScale(expandedRadius)); batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); geometryCache->renderSphere(batch); } From cd6ad306acde014a5262c1ef862031df8587f65a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 16:45:23 -0700 Subject: [PATCH 151/192] start transitioning to new grab methods --- examples/toys/bubblewand/createWand.js | 5 ++-- examples/toys/bubblewand/wand.js | 34 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 76681a50d7..9938acb63f 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -30,7 +30,7 @@ var wand = Entities.addEntity({ position: center, gravity: { x: 0, - y: -9.8, + y: 0, z: 0, }, dimensions: { @@ -45,7 +45,8 @@ var wand = Entities.addEntity({ }); function cleanup() { - Entities.deleteEntity(wand); +// the line below this is commented out to make the wand that you create persistent. + Entities.deleteEntity(wand); } diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 064d045f35..ae9a43eefc 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -45,6 +45,7 @@ } BubbleWand.prototype = { + timePassed:null, currentBubble: null, preload: function(entityID) { this.entityID = entityID; @@ -55,6 +56,7 @@ Script.update.disconnect(this.update); }, update: function(deltaTime) { + this.timePassed=deltaTime; var defaultGrabData = { activated: false, avatarId: null @@ -210,7 +212,37 @@ }); - } + }, + startNearGrab: function() { + print('START NEAR GRAB') + if (_this.currentBubble === null) { + _this.createBubbleAtTipOfWand(); + } + }, + continueNearGrab: function() { + + if (this.timePassed === null) { + this.timePassed = Date.now(); + } else { + var newTime = = Date.now() - this.timePassed; + // this.timePassed = newTime; + } + print('CONTINUE NEAR GRAB::' + this.timePassed); + + + }, + releaseGrab: function() { + //delete the lights and reset state + if (this.hasSpotlight) { + Entities.deleteEntity(this.spotlight); + Entities.deleteEntity(this.glowLight); + this.hasSpotlight = false; + this.glowLight = null; + this.spotlight = null; + this.whichHand = null; + + } + }, } From 21f53f1bbd1d48f9a58579861470e84bd741f9f9 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 16:57:55 -0700 Subject: [PATCH 152/192] debugging the lighting issue on the craps table --- libraries/render-utils/src/Model.cpp | 23 +++++++++++++---------- libraries/render-utils/src/Model.h | 1 + 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index c87be06dba..cae4167567 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -190,6 +190,7 @@ void Model::RenderPipelineLib::initLocations(gpu::ShaderPointer& program, Model: locations.emissiveParams = program->getUniforms().findLocation("emissiveParams"); locations.glowIntensity = program->getUniforms().findLocation("glowIntensity"); locations.normalFittingMapUnit = program->getTextures().findLocation("normalFittingMap"); + locations.normalTextureUnit = program->getTextures().findLocation("normalMap"); locations.specularTextureUnit = program->getTextures().findLocation("specularMap"); locations.emissiveTextureUnit = program->getTextures().findLocation("emissiveMap"); locations.materialBufferUnit = program->getBuffers().findLocation("materialBuffer"); @@ -1496,10 +1497,14 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape const FBXGeometry& geometry = _geometry->getFBXGeometry(); const std::vector>& networkMeshes = _geometry->getMeshes(); - auto drawMaterial = _geometry->getShapeMaterial(shapeID); - if (!drawMaterial) { + auto networkMaterial = _geometry->getShapeMaterial(shapeID); + if (!networkMaterial) { return; }; + auto material = networkMaterial->_material; + if (!material) { + return; + } // TODO: Not yet // auto drawMesh = _geometry->getShapeMesh(shapeID); @@ -1516,12 +1521,12 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape const FBXMesh& mesh = geometry.meshes.at(meshIndex); const MeshState& state = _meshStates.at(meshIndex); - auto drawMaterialKey = drawMaterial->_material->getKey(); + auto drawMaterialKey = material->getKey(); bool translucentMesh = drawMaterialKey.isTransparent() || drawMaterialKey.isTransparentMap(); - bool hasTangents = !mesh.tangents.isEmpty(); - bool hasSpecular = !drawMaterial->specularTextureName.isEmpty(); //mesh.hasSpecularTexture(); - bool hasLightmap = !drawMaterial->emissiveTextureName.isEmpty(); //mesh.hasEmissiveTexture(); + bool hasTangents = drawMaterialKey.isNormalMap() && !mesh.tangents.isEmpty(); + bool hasSpecular = drawMaterialKey.isGlossMap(); // !drawMaterial->specularTextureName.isEmpty(); //mesh.hasSpecularTexture(); + bool hasLightmap = drawMaterialKey.isLightmapMap(); // !drawMaterial->emissiveTextureName.isEmpty(); //mesh.hasEmissiveTexture(); bool isSkinned = state.clusterMatrices.size() > 1; bool wireframe = isWireframe(); @@ -1624,8 +1629,6 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape const FBXMeshPart& part = mesh.parts.at(partIndex); - //model::MaterialPointer material = part._material; - auto material = drawMaterial->_material; #ifdef WANT_DEBUG if (material == nullptr) { @@ -1633,7 +1636,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape } #endif - if (material != nullptr) { + { // apply material properties if (mode != RenderArgs::SHADOW_RENDER_MODE) { @@ -1667,7 +1670,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape } // Normal map - if (materialKey.isNormalMap()) { + if ((locations->normalTextureUnit >= 0) && hasTangents) { auto normalMap = textureMaps[model::MaterialKey::NORMAL_MAP]; if (normalMap && normalMap->isDefined()) { batch.setResourceTexture(NORMAL_MAP_SLOT, normalMap->getTextureView()); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index eaa7089d12..acc419a81f 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -339,6 +339,7 @@ private: int tangent; int alphaThreshold; int texcoordMatrices; + int normalTextureUnit; int specularTextureUnit; int emissiveTextureUnit; int emissiveParams; From 81358b7f14c04bd695046e786abcee25e27bb5fb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 17:01:31 -0700 Subject: [PATCH 153/192] wand changes --- examples/toys/bubblewand/wand.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index ae9a43eefc..1541985315 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -40,12 +40,26 @@ var _this; + function interval() { + var lastTime = new Date().getTime() / 1000; + + return function getInterval() { + var newTime = new Date().getTime() / 1000; + var delta = newTime - lastTime; + lastTime = newTime; + return delta; + }; + } + + var checkInterval = interval(); + + var BubbleWand = function() { _this = this; } BubbleWand.prototype = { - timePassed:null, + timePassed: null, currentBubble: null, preload: function(entityID) { this.entityID = entityID; @@ -56,7 +70,7 @@ Script.update.disconnect(this.update); }, update: function(deltaTime) { - this.timePassed=deltaTime; + this.timePassed = deltaTime; var defaultGrabData = { activated: false, avatarId: null @@ -220,7 +234,7 @@ } }, continueNearGrab: function() { - + print('time passed:::' + checkInterval()); if (this.timePassed === null) { this.timePassed = Date.now(); } else { From e6776ef5ebe41cbe121ce672faa7f9197ba0d83c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 21 Sep 2015 17:29:39 -0700 Subject: [PATCH 154/192] split AnimIK::evaluate() into sub-functions also IK targets now in model-frame instead of root-frame --- .../animation/src/AnimInverseKinematics.cpp | 306 +++++++++--------- .../animation/src/AnimInverseKinematics.h | 10 +- libraries/animation/src/AnimNode.h | 17 + libraries/animation/src/Rig.cpp | 18 +- 4 files changed, 186 insertions(+), 165 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 6af58e89a1..1ccf984815 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -82,22 +82,8 @@ static int findRootJointInSkeleton(AnimSkeleton::ConstPointer skeleton, int inde return rootIndex; } -struct IKTarget { - AnimPose pose; - int index; - int rootIndex; -}; - -//virtual -const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) { - - // NOTE: we assume that _relativePoses are up to date (e.g. loadPoses() was just called) - if (_relativePoses.empty()) { - return _relativePoses; - } - - // build a list of targets from _targetVarVec - std::vector targets; +void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::vector& targets) { + // build a list of valid targets from _targetVarVec and animVars bool removeUnfoundJoints = false; for (auto& targetVar : _targetVarVec) { if (targetVar.jointIndex == -1) { @@ -141,154 +127,160 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar } } } +} - if (targets.empty()) { - // no IK targets but still need to enforce constraints - std::map::iterator constraintItr = _constraints.begin(); - while (constraintItr != _constraints.end()) { - int index = constraintItr->first; - glm::quat rotation = _relativePoses[index].rot; - constraintItr->second->apply(rotation); - _relativePoses[index].rot = rotation; - ++constraintItr; - } - } else { - // clear the accumulators before we start the IK solver - for (auto& accumulatorPair: _accumulators) { - accumulatorPair.second.clear(); - } +void AnimInverseKinematics::solveWithCyclicCoordinateDescent(std::vector& targets) { + // compute absolute poses that correspond to relative target poses + AnimPoseVec absolutePoses; + computeAbsolutePoses(absolutePoses); - // compute absolute poses that correspond to relative target poses - AnimPoseVec absolutePoses; - computeAbsolutePoses(absolutePoses); + // clear the accumulators before we start the IK solver + for (auto& accumulatorPair: _accumulators) { + accumulatorPair.second.clear(); + } - float largestError = 0.0f; - const float ACCEPTABLE_RELATIVE_ERROR = 1.0e-3f; - int numLoops = 0; - const int MAX_IK_LOOPS = 16; - const quint64 MAX_IK_TIME = 10 * USECS_PER_MSEC; - quint64 expiry = usecTimestampNow() + MAX_IK_TIME; - do { - largestError = 0.0f; - int lowestMovedIndex = _relativePoses.size(); - for (auto& target: targets) { - int tipIndex = target.index; - AnimPose targetPose = target.pose; - int rootIndex = target.rootIndex; - if (rootIndex != -1) { - // transform targetPose into skeleton's absolute frame - AnimPose& rootPose = _relativePoses[rootIndex]; - targetPose.trans = rootPose.trans + rootPose.rot * targetPose.trans; - targetPose.rot = rootPose.rot * targetPose.rot; - } - - glm::vec3 tip = absolutePoses[tipIndex].trans; - float error = glm::length(targetPose.trans - tip); - - // descend toward root, pivoting each joint to get tip closer to target - int pivotIndex = _skeleton->getParentIndex(tipIndex); - while (pivotIndex != -1 && error > ACCEPTABLE_RELATIVE_ERROR) { - // compute the two lines that should be aligned - glm::vec3 jointPosition = absolutePoses[pivotIndex].trans; - glm::vec3 leverArm = tip - jointPosition; - glm::vec3 targetLine = targetPose.trans - jointPosition; - - // compute the axis of the rotation that would align them - glm::vec3 axis = glm::cross(leverArm, targetLine); - float axisLength = glm::length(axis); - if (axisLength > EPSILON) { - // compute deltaRotation for alignment (brings tip closer to target) - axis /= axisLength; - float angle = acosf(glm::dot(leverArm, targetLine) / (glm::length(leverArm) * glm::length(targetLine))); - - // NOTE: even when axisLength is not zero (e.g. lever-arm and pivot-arm are not quite aligned) it is - // still possible for the angle to be zero so we also check that to avoid unnecessary calculations. - if (angle > EPSILON) { - // reduce angle by half: slows convergence but adds stability to IK solution - angle = 0.5f * angle; - glm::quat deltaRotation = glm::angleAxis(angle, axis); - - int parentIndex = _skeleton->getParentIndex(pivotIndex); - if (parentIndex == -1) { - // TODO? apply constraints to root? - // TODO? harvest the root's transform as movement of entire skeleton? - } else { - // compute joint's new parent-relative rotation - // Q' = dQ * Q and Q = Qp * q --> q' = Qp^ * dQ * Q - glm::quat newRot = glm::normalize(glm::inverse( - absolutePoses[parentIndex].rot) * - deltaRotation * - absolutePoses[pivotIndex].rot); - RotationConstraint* constraint = getConstraint(pivotIndex); - if (constraint) { - bool constrained = constraint->apply(newRot); - if (constrained) { - // the constraint will modify the movement of the tip so we have to compute the modified - // model-frame deltaRotation - // Q' = Qp^ * dQ * Q --> dQ = Qp * Q' * Q^ - deltaRotation = absolutePoses[parentIndex].rot * - newRot * - glm::inverse(absolutePoses[pivotIndex].rot); - } - } - // store the rotation change in the accumulator - _accumulators[pivotIndex].add(newRot); - } - // this joint has been changed so we check to see if it has the lowest index - if (pivotIndex < lowestMovedIndex) { - lowestMovedIndex = pivotIndex; - } - - // keep track of tip's new position as we descend towards root - tip = jointPosition + deltaRotation * leverArm; - error = glm::length(targetPose.trans - tip); - } - } - pivotIndex = _skeleton->getParentIndex(pivotIndex); - } - if (largestError < error) { - largestError = error; - } - } - ++numLoops; - - // harvest accumulated rotations and apply the average - for (auto& accumulatorPair: _accumulators) { - RotationAccumulator& accumulator = accumulatorPair.second; - if (accumulator.size() > 0) { - _relativePoses[accumulatorPair.first].rot = accumulator.getAverage(); - accumulator.clear(); - } - } - - // only update the absolutePoses that need it: those between lowestMovedIndex and _maxTargetIndex - for (int i = lowestMovedIndex; i <= _maxTargetIndex; ++i) { - int parentIndex = _skeleton->getParentIndex(i); - if (parentIndex != -1) { - absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; - } - } - } while (largestError > ACCEPTABLE_RELATIVE_ERROR && numLoops < MAX_IK_LOOPS && usecTimestampNow() < expiry); - - // finally set the relative rotation of each tip to agree with absolute target rotation + float largestError = 0.0f; + const float ACCEPTABLE_RELATIVE_ERROR = 1.0e-3f; + int numLoops = 0; + const int MAX_IK_LOOPS = 16; + const quint64 MAX_IK_TIME = 10 * USECS_PER_MSEC; + quint64 expiry = usecTimestampNow() + MAX_IK_TIME; + do { + largestError = 0.0f; + int lowestMovedIndex = _relativePoses.size(); for (auto& target: targets) { int tipIndex = target.index; - int parentIndex = _skeleton->getParentIndex(tipIndex); - if (parentIndex != -1) { - AnimPose targetPose = target.pose; - // compute tip's new parent-relative rotation - // Q = Qp * q --> q' = Qp^ * Q - glm::quat newRelativeRotation = glm::inverse(absolutePoses[parentIndex].rot) * targetPose.rot; - RotationConstraint* constraint = getConstraint(tipIndex); - if (constraint) { - constraint->apply(newRelativeRotation); - // TODO: ATM the final rotation target just fails but we need to provide - // feedback to the IK system so that it can adjust the bones up the skeleton - // to help this rotation target get met. + AnimPose targetPose = target.pose; + + glm::vec3 tip = absolutePoses[tipIndex].trans; + float error = glm::length(targetPose.trans - tip); + + // descend toward root, pivoting each joint to get tip closer to target + int pivotIndex = _skeleton->getParentIndex(tipIndex); + while (pivotIndex != -1 && error > ACCEPTABLE_RELATIVE_ERROR) { + // compute the two lines that should be aligned + glm::vec3 jointPosition = absolutePoses[pivotIndex].trans; + glm::vec3 leverArm = tip - jointPosition; + glm::vec3 targetLine = targetPose.trans - jointPosition; + + // compute the axis of the rotation that would align them + glm::vec3 axis = glm::cross(leverArm, targetLine); + float axisLength = glm::length(axis); + if (axisLength > EPSILON) { + // compute deltaRotation for alignment (brings tip closer to target) + axis /= axisLength; + float angle = acosf(glm::dot(leverArm, targetLine) / (glm::length(leverArm) * glm::length(targetLine))); + + // NOTE: even when axisLength is not zero (e.g. lever-arm and pivot-arm are not quite aligned) it is + // still possible for the angle to be zero so we also check that to avoid unnecessary calculations. + if (angle > EPSILON) { + // reduce angle by half: slows convergence but adds stability to IK solution + angle = 0.5f * angle; + glm::quat deltaRotation = glm::angleAxis(angle, axis); + + int parentIndex = _skeleton->getParentIndex(pivotIndex); + if (parentIndex == -1) { + // TODO? apply constraints to root? + // TODO? harvest the root's transform as movement of entire skeleton? + } else { + // compute joint's new parent-relative rotation + // Q' = dQ * Q and Q = Qp * q --> q' = Qp^ * dQ * Q + glm::quat newRot = glm::normalize(glm::inverse( + absolutePoses[parentIndex].rot) * + deltaRotation * + absolutePoses[pivotIndex].rot); + RotationConstraint* constraint = getConstraint(pivotIndex); + if (constraint) { + bool constrained = constraint->apply(newRot); + if (constrained) { + // the constraint will modify the movement of the tip so we have to compute the modified + // model-frame deltaRotation + // Q' = Qp^ * dQ * Q --> dQ = Qp * Q' * Q^ + deltaRotation = absolutePoses[parentIndex].rot * + newRot * + glm::inverse(absolutePoses[pivotIndex].rot); + } + } + // store the rotation change in the accumulator + _accumulators[pivotIndex].add(newRot); + } + // this joint has been changed so we check to see if it has the lowest index + if (pivotIndex < lowestMovedIndex) { + lowestMovedIndex = pivotIndex; + } + + // keep track of tip's new position as we descend towards root + tip = jointPosition + deltaRotation * leverArm; + error = glm::length(targetPose.trans - tip); + } } - _relativePoses[tipIndex].rot = newRelativeRotation; - absolutePoses[tipIndex].rot = targetPose.rot; + pivotIndex = _skeleton->getParentIndex(pivotIndex); } + if (largestError < error) { + largestError = error; + } + } + ++numLoops; + + // harvest accumulated rotations and apply the average + for (auto& accumulatorPair: _accumulators) { + RotationAccumulator& accumulator = accumulatorPair.second; + if (accumulator.size() > 0) { + _relativePoses[accumulatorPair.first].rot = accumulator.getAverage(); + accumulator.clear(); + } + } + + // only update the absolutePoses that need it: those between lowestMovedIndex and _maxTargetIndex + for (int i = lowestMovedIndex; i <= _maxTargetIndex; ++i) { + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex != -1) { + absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; + } + } + } while (largestError > ACCEPTABLE_RELATIVE_ERROR && numLoops < MAX_IK_LOOPS && usecTimestampNow() < expiry); + + // finally set the relative rotation of each tip to agree with absolute target rotation + for (auto& target: targets) { + int tipIndex = target.index; + int parentIndex = _skeleton->getParentIndex(tipIndex); + if (parentIndex != -1) { + AnimPose targetPose = target.pose; + // compute tip's new parent-relative rotation + // Q = Qp * q --> q' = Qp^ * Q + glm::quat newRelativeRotation = glm::inverse(absolutePoses[parentIndex].rot) * targetPose.rot; + RotationConstraint* constraint = getConstraint(tipIndex); + if (constraint) { + constraint->apply(newRelativeRotation); + // TODO: ATM the final rotation target just fails but we need to provide + // feedback to the IK system so that it can adjust the bones up the skeleton + // to help this rotation target get met. + } + _relativePoses[tipIndex].rot = newRelativeRotation; + absolutePoses[tipIndex].rot = targetPose.rot; + } + } +} + +//virtual +const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) { + if (!_relativePoses.empty()) { + // build a list of targets from _targetVarVec + std::vector targets; + computeTargets(animVars, targets); + + if (targets.empty()) { + // no IK targets but still need to enforce constraints + std::map::iterator constraintItr = _constraints.begin(); + while (constraintItr != _constraints.end()) { + int index = constraintItr->first; + glm::quat rotation = _relativePoses[index].rot; + constraintItr->second->apply(rotation); + _relativePoses[index].rot = rotation; + ++constraintItr; + } + } else { + solveWithCyclicCoordinateDescent(targets); } } return _relativePoses; diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index c4bda1be89..f2073c01b8 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -37,6 +37,14 @@ public: virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; protected: + struct IKTarget { + AnimPose pose; + int index; + int rootIndex; + }; + + void computeTargets(const AnimVariantMap& animVars, std::vector& targets); + void solveWithCyclicCoordinateDescent(std::vector& targets); virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton); // for AnimDebugDraw rendering @@ -64,7 +72,7 @@ protected: }; std::map _constraints; - std::map _accumulators; + std::map _accumulators; // class-member to exploit temporal coherency std::vector _targetVarVec; AnimPoseVec _defaultRelativePoses; // poses of the relaxed state AnimPoseVec _relativePoses; // current relative poses diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h index 9325ef3835..b6f9987f33 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -87,6 +87,23 @@ public: return evaluate(animVars, dt, triggersOut); } + const AnimPose getRootPose(int jointIndex) const { + AnimPose pose = AnimPose::identity; + if (_skeleton && jointIndex != -1) { + const AnimPoseVec& poses = getPosesInternal(); + int numJoints = (int)(poses.size()); + if (jointIndex < numJoints) { + int parentIndex = _skeleton->getParentIndex(jointIndex); + while (parentIndex != -1 && parentIndex < numJoints) { + jointIndex = parentIndex; + parentIndex = _skeleton->getParentIndex(jointIndex); + } + pose = poses[jointIndex]; + } + } + return pose; + } + protected: void setCurrentFrame(float frame) { diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b0ffd081c2..29d0bc011b 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -986,7 +986,7 @@ void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, floa void Rig::updateNeckJoint(int index, const HeadParameters& params) { if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { - if (_enableAnimGraph && _animSkeleton) { + if (_enableAnimGraph && _animSkeleton && _animNode) { // the params.localHeadOrientation is composed incorrectly, so re-compose it correctly from pitch, yaw and roll. glm::quat realLocalHeadOrientation = (glm::angleAxis(glm::radians(-params.localHeadRoll), Z_AXIS) * glm::angleAxis(glm::radians(params.localHeadYaw), Y_AXIS) * @@ -995,7 +995,9 @@ void Rig::updateNeckJoint(int index, const HeadParameters& params) { // There's a theory that when not in hmd, we should _animVars.unset("headPosition"). // However, until that works well, let's always request head be positioned where requested by hmd, camera, or default. - _animVars.set("headPosition", params.localHeadPosition); + int headIndex = _animSkeleton->nameToJointIndex("Head"); + AnimPose rootPose = _animNode->getRootPose(headIndex); + _animVars.set("headPosition", rootPose.trans + rootPose.rot * params.localHeadPosition); } else if (!_enableAnimGraph) { auto& state = _jointStates[index]; @@ -1044,20 +1046,22 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm void Rig::updateFromHandParameters(const HandParameters& params, float dt) { - if (_enableAnimGraph && _animSkeleton) { + if (_enableAnimGraph && _animSkeleton && _animNode) { // TODO: figure out how to obtain the yFlip from where it is actually stored glm::quat yFlipHACK = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + int leftHandIndex = _animSkeleton->nameToJointIndex("LeftHand"); + AnimPose rootPose = _animNode->getRootPose(leftHandIndex); if (params.isLeftEnabled) { - _animVars.set("leftHandPosition", yFlipHACK * params.leftPosition); - _animVars.set("leftHandRotation", yFlipHACK * params.leftOrientation); + _animVars.set("leftHandPosition", rootPose.trans + rootPose.rot * yFlipHACK * params.leftPosition); + _animVars.set("leftHandRotation", rootPose.rot * yFlipHACK * params.leftOrientation); } else { _animVars.unset("leftHandPosition"); _animVars.unset("leftHandRotation"); } if (params.isRightEnabled) { - _animVars.set("rightHandPosition", yFlipHACK * params.rightPosition); - _animVars.set("rightHandRotation", yFlipHACK * params.rightOrientation); + _animVars.set("rightHandPosition", rootPose.trans + rootPose.rot * yFlipHACK * params.rightPosition); + _animVars.set("rightHandRotation", rootPose.rot * yFlipHACK * params.rightOrientation); } else { _animVars.unset("rightHandPosition"); _animVars.unset("rightHandRotation"); From f8d743aff0700f81d7f2ace92b8718e1e5e64978 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 17:38:49 -0700 Subject: [PATCH 155/192] Update to use new grab methods, only specifically request properties that we are actually using --- examples/toys/bubblewand/createWand.js | 46 +++++------ examples/toys/bubblewand/wand.js | 104 ++++++------------------- 2 files changed, 47 insertions(+), 103 deletions(-) diff --git a/examples/toys/bubblewand/createWand.js b/examples/toys/bubblewand/createWand.js index 9938acb63f..632037ff7c 100644 --- a/examples/toys/bubblewand/createWand.js +++ b/examples/toys/bubblewand/createWand.js @@ -18,35 +18,35 @@ var WAND_SCRIPT_URL = Script.resolvePath("wand.js"); //create the wand in front of the avatar var center = Vec3.sum(Vec3.sum(MyAvatar.position, { - x: 0, - y: 0.5, - z: 0 + x: 0, + y: 0.5, + z: 0 }), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); var wand = Entities.addEntity({ - name: 'Bubble Wand', - type: "Model", - modelURL: WAND_MODEL, - position: center, - gravity: { - x: 0, - y: 0, - z: 0, - }, - dimensions: { - x: 0.05, - y: 0.25, - z: 0.05 - }, - //must be enabled to be grabbable in the physics engine - collisionsWillMove: true, - compoundShapeURL: WAND_COLLISION_SHAPE, - script: WAND_SCRIPT_URL + name: 'Bubble Wand', + type: "Model", + modelURL: WAND_MODEL, + position: center, + gravity: { + x: 0, + y: 0, + z: 0, + }, + dimensions: { + x: 0.05, + y: 0.25, + z: 0.05 + }, + //must be enabled to be grabbable in the physics engine + collisionsWillMove: true, + compoundShapeURL: WAND_COLLISION_SHAPE, + script: WAND_SCRIPT_URL }); function cleanup() { -// the line below this is commented out to make the wand that you create persistent. - Entities.deleteEntity(wand); + // the line below this is commented out to make the wand that you create persistent. + // Entities.deleteEntity(wand); } diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index 1541985315..f8e484bbde 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -36,10 +36,6 @@ var WAND_TIP_OFFSET = 0.095; var VELOCITY_THRESHOLD = 0.5; - var GRAB_USER_DATA_KEY = "grabKey"; - - var _this; - function interval() { var lastTime = new Date().getTime() / 1000; @@ -53,6 +49,7 @@ var checkInterval = interval(); + var _this; var BubbleWand = function() { _this = this; @@ -63,73 +60,27 @@ currentBubble: null, preload: function(entityID) { this.entityID = entityID; - this.properties = Entities.getEntityProperties(this.entityID); Script.update.connect(this.update); }, - unload: function(entityID) { + unload: function() { Script.update.disconnect(this.update); - }, - update: function(deltaTime) { - this.timePassed = deltaTime; - var defaultGrabData = { - activated: false, - avatarId: null - }; - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, _this.entityID, defaultGrabData); - - if (grabData.activated && grabData.avatarId === MyAvatar.sessionUUID) { - // remember we're being grabbed so we can detect being released - _this.beingGrabbed = true; - - //the first time we want to make a bubble - if (_this.currentBubble === null) { - _this.createBubbleAtTipOfWand(); - } - - var properties = Entities.getEntityProperties(_this.entityID); - var dt = deltaTime; - _this.growBubbleWithWandVelocity(properties, dt); - - var wandTipPosition = _this.getWandTipPosition(properties); - - //update the bubble to stay with the wand tip - Entities.editEntity(_this.currentBubble, { - position: wandTipPosition, - - }); - - } else if (_this.beingGrabbed) { - // if we are not being grabbed, and we previously were, then we were just released, remember that - _this.beingGrabbed = false; - - //remove the current bubble when the wand is released - Entities.deleteEntity(_this.currentBubble); - _this.currentBubble = null - return - } - - }, getWandTipPosition: function(properties) { - //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position - var upVector = Quat.getUp(properties.rotation); var frontVector = Quat.getFront(properties.rotation); var upOffset = Vec3.multiply(upVector, WAND_TIP_OFFSET); var wandTipPosition = Vec3.sum(properties.position, upOffset); - this.wandTipPosition = wandTipPosition; return wandTipPosition }, addCollisionsToBubbleAfterCreation: function(bubble) { - + //if the bubble collide immediately, we get weird effects. so we add collisions after release Entities.editEntity(bubble, { collisionsWillMove: true }) - }, randomizeBubbleGravity: function() { - + //change up the gravity a little bit for variation in floating effects var randomNumber = randInt(0, 3); var gravity = { x: 0, @@ -139,9 +90,11 @@ return gravity }, growBubbleWithWandVelocity: function(properties, deltaTime) { + //get the wand and tip position for calculations var wandPosition = properties.position; var wandTipPosition = this.getWandTipPosition(properties) + // velocity = distance / time var distance = Vec3.subtract(wandPosition, this.lastPosition); var velocity = Vec3.multiply(distance, 1 / deltaTime); @@ -152,7 +105,7 @@ this.lastPosition = wandPosition; //actually grow the bubble - var dimensions = Entities.getEntityProperties(this.currentBubble).dimensions; + var dimensions = Entities.getEntityProperties(this.currentBubble, "dimensions").dimensions; if (velocityStrength > VELOCITY_THRESHOLD) { //add some variation in bubble sizes @@ -165,6 +118,7 @@ //bubbles pop after existing for a bit -- so set a random lifetime var lifetime = randInt(BUBBLE_LIFETIME_MIN, BUBBLE_LIFETIME_MAX); + //edit the bubble properties at release Entities.editEntity(this.currentBubble, { velocity: velocity, lifetime: lifetime, @@ -178,8 +132,7 @@ this.createBubbleAtTipOfWand(); return } else { - - // if the bubble is not yet full size, make the current bubble bigger + //grow small bubbles dimensions.x += GROWTH_FACTOR * velocityStrength; dimensions.y += GROWTH_FACTOR * velocityStrength; dimensions.z += GROWTH_FACTOR * velocityStrength; @@ -203,11 +156,10 @@ createBubbleAtTipOfWand: function() { //create a new bubble at the tip of the wand - var properties = Entities.getEntityProperties(this.entityID); + var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); var wandPosition = properties.position; wandTipPosition = this.getWandTipPosition(properties); - this.wandTipPosition = wandTipPosition; //store the position of the tip for use in velocity calculations this.lastPosition = wandPosition; @@ -225,37 +177,29 @@ shapeType: "sphere" }); - }, startNearGrab: function() { - print('START NEAR GRAB') - if (_this.currentBubble === null) { - _this.createBubbleAtTipOfWand(); + if (this.currentBubble === null) { + this.createBubbleAtTipOfWand(); } }, continueNearGrab: function() { - print('time passed:::' + checkInterval()); - if (this.timePassed === null) { - this.timePassed = Date.now(); - } else { - var newTime = = Date.now() - this.timePassed; - // this.timePassed = newTime; - } - print('CONTINUE NEAR GRAB::' + this.timePassed); + var deltaTime = checkInterval() + var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); + this.growBubbleWithWandVelocity(properties, deltaTime); + var wandTipPosition = this.getWandTipPosition(properties); + + //update the bubble to stay with the wand tip + Entities.editEntity(this.currentBubble, { + position: wandTipPosition, + }); }, releaseGrab: function() { - //delete the lights and reset state - if (this.hasSpotlight) { - Entities.deleteEntity(this.spotlight); - Entities.deleteEntity(this.glowLight); - this.hasSpotlight = false; - this.glowLight = null; - this.spotlight = null; - this.whichHand = null; - - } + //delete the current buble and reset state when the wand is released + Entities.deleteEntity(this.currentBubble); + this.currentBubble = null }, } From bf22d5a942d5f71c0a745a5d8ae29d2a8e036787 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 21 Sep 2015 17:43:05 -0700 Subject: [PATCH 156/192] dont delete wand at cleanup from createWand.js --- examples/toys/flashlight/createFlashlight.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/toys/flashlight/createFlashlight.js b/examples/toys/flashlight/createFlashlight.js index f0d31b6934..65cafbcdaa 100644 --- a/examples/toys/flashlight/createFlashlight.js +++ b/examples/toys/flashlight/createFlashlight.js @@ -41,7 +41,8 @@ var flashlight = Entities.addEntity({ function cleanup() { - Entities.deleteEntity(flashlight); + //commenting out the line below makes this persistent. to delete at cleanup, uncomment + //Entities.deleteEntity(flashlight); } From 3869887610e10e69ecfb7d2368448905f9b4776b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 21 Sep 2015 17:53:59 -0700 Subject: [PATCH 157/192] splitting AnimNode implementation into two files --- libraries/animation/src/AnimNode.cpp | 54 ++++++++++++++++++++++++++++ libraries/animation/src/AnimNode.h | 48 ++++--------------------- 2 files changed, 61 insertions(+), 41 deletions(-) create mode 100644 libraries/animation/src/AnimNode.cpp diff --git a/libraries/animation/src/AnimNode.cpp b/libraries/animation/src/AnimNode.cpp new file mode 100644 index 0000000000..02d0e1b283 --- /dev/null +++ b/libraries/animation/src/AnimNode.cpp @@ -0,0 +1,54 @@ +// +// AnimNode.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimNode.h" + +void AnimNode::removeChild(AnimNode::Pointer child) { + auto iter = std::find(_children.begin(), _children.end(), child); + if (iter != _children.end()) { + _children.erase(iter); + } +} + +AnimNode::Pointer AnimNode::getChild(int i) const { + assert(i >= 0 && i < (int)_children.size()); + return _children[i]; +} + +void AnimNode::setSkeleton(const AnimSkeleton::Pointer skeleton) { + setSkeletonInternal(skeleton); + for (auto&& child : _children) { + child->setSkeleton(skeleton); + } +} + +const AnimPose AnimNode::getRootPose(int jointIndex) const { + AnimPose pose = AnimPose::identity; + if (_skeleton && jointIndex != -1) { + const AnimPoseVec& poses = getPosesInternal(); + int numJoints = (int)(poses.size()); + if (jointIndex < numJoints) { + int parentIndex = _skeleton->getParentIndex(jointIndex); + while (parentIndex != -1 && parentIndex < numJoints) { + jointIndex = parentIndex; + parentIndex = _skeleton->getParentIndex(jointIndex); + } + pose = poses[jointIndex]; + } + } + return pose; +} + +void AnimNode::setCurrentFrame(float frame) { + setCurrentFrameInternal(frame); + for (auto&& child : _children) { + child->setCurrentFrameInternal(frame); + } +} diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h index b6f9987f33..d5da552a0f 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -60,25 +60,13 @@ public: // hierarchy accessors void addChild(Pointer child) { _children.push_back(child); } - void removeChild(Pointer child) { - auto iter = std::find(_children.begin(), _children.end(), child); - if (iter != _children.end()) { - _children.erase(iter); - } - } - Pointer getChild(int i) const { - assert(i >= 0 && i < (int)_children.size()); - return _children[i]; - } + void removeChild(Pointer child); + + Pointer getChild(int i) const; int getChildCount() const { return (int)_children.size(); } // pair this AnimNode graph with a skeleton. - void setSkeleton(const AnimSkeleton::Pointer skeleton) { - setSkeletonInternal(skeleton); - for (auto&& child : _children) { - child->setSkeleton(skeleton); - } - } + void setSkeleton(const AnimSkeleton::Pointer skeleton); AnimSkeleton::ConstPointer getSkeleton() const { return _skeleton; } @@ -87,36 +75,14 @@ public: return evaluate(animVars, dt, triggersOut); } - const AnimPose getRootPose(int jointIndex) const { - AnimPose pose = AnimPose::identity; - if (_skeleton && jointIndex != -1) { - const AnimPoseVec& poses = getPosesInternal(); - int numJoints = (int)(poses.size()); - if (jointIndex < numJoints) { - int parentIndex = _skeleton->getParentIndex(jointIndex); - while (parentIndex != -1 && parentIndex < numJoints) { - jointIndex = parentIndex; - parentIndex = _skeleton->getParentIndex(jointIndex); - } - pose = poses[jointIndex]; - } - } - return pose; - } + const AnimPose getRootPose(int jointIndex) const; protected: - void setCurrentFrame(float frame) { - setCurrentFrameInternal(frame); - for (auto&& child : _children) { - child->setCurrentFrameInternal(frame); - } - } + void setCurrentFrame(float frame); virtual void setCurrentFrameInternal(float frame) {} - virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { - _skeleton = skeleton; - } + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { _skeleton = skeleton; } // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const = 0; From 91b9940fe9a175329e1561062ff2a254092a5bde Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 18:09:25 -0700 Subject: [PATCH 158/192] Merging and a fix for the diffuse map floating around --- libraries/render-utils/src/Model.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 918d9005ae..7221a00267 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1632,6 +1632,8 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape } else { batch.setResourceTexture(DIFFUSE_MAP_SLOT, textureCache->getGrayTexture()); } + } else { + batch.setResourceTexture(DIFFUSE_MAP_SLOT, textureCache->getGrayTexture()); } // Normal map @@ -1644,6 +1646,8 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape } else { batch.setResourceTexture(NORMAL_MAP_SLOT, textureCache->getBlueTexture()); } + } else { + batch.setResourceTexture(NORMAL_MAP_SLOT, nullptr); } // TODO: For now gloss map is used as the "specular map in the shading, we ll need to fix that @@ -1656,6 +1660,8 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape } else { batch.setResourceTexture(SPECULAR_MAP_SLOT, textureCache->getBlackTexture()); } + } else { + batch.setResourceTexture(SPECULAR_MAP_SLOT, nullptr); } // TODO: For now lightmaop is piped into the emissive map unit, we need to fix that and support for real emissive too @@ -1675,6 +1681,8 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape else { batch.setResourceTexture(LIGHTMAP_MAP_SLOT, textureCache->getGrayTexture()); } + } else { + batch.setResourceTexture(LIGHTMAP_MAP_SLOT, nullptr); } // Texcoord transforms ? From a23a90bf5f99dbf4b1d7c2e4ed108b97b667e4a8 Mon Sep 17 00:00:00 2001 From: BOB LONG Date: Mon, 21 Sep 2015 19:11:13 -0700 Subject: [PATCH 159/192] Code simplification Simplify the code a bit as suggested: 1) Use unsigned int instead of signed int, so we can avoid checking the negative case 2) Merge two lines into a single line so we can inline the implementation Correct the js sample file header. testing done: - Build locally - Pass -1 as index from js and the code still can correctly handle the input. --- examples/faceBlendCoefficients.js | 4 ++-- libraries/render-utils/src/Model.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/faceBlendCoefficients.js b/examples/faceBlendCoefficients.js index 56fcadca9d..6756be548f 100644 --- a/examples/faceBlendCoefficients.js +++ b/examples/faceBlendCoefficients.js @@ -1,10 +1,10 @@ // -// coefficients.js +// faceBlendCoefficients.js // // version 2.0 // // Created by Bob Long, 9/14/2015 -// A simple panel that can display the blending coefficients of Avatar's face model. +// A simple panel that can select and display the blending coefficient of the Avatar's face model. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index d1aa2901c8..e8f158be08 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -192,9 +192,9 @@ public: void setCauterizeBoneSet(const std::unordered_set& boneSet) { _cauterizeBoneSet = boneSet; } int getBlendshapeCoefficientsNum() const { return _blendshapeCoefficients.size(); } - float getBlendshapeCoefficient(int index) const { - return index >= _blendshapeCoefficients.size() || index < 0 ? - 0.0f : _blendshapeCoefficients.at(index); } + float getBlendshapeCoefficient(unsigned int index) const { + return index >= _blendshapeCoefficients.size() ? 0.0f : _blendshapeCoefficients.at(index); + } protected: From 0f85c280983f0353b859e44a839ca15eb6380b05 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Sep 2015 21:08:13 -0700 Subject: [PATCH 160/192] Fix HMD 3rd person view --- interface/src/Application.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ee98ce4c25..0d55c08860 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1136,18 +1136,23 @@ void Application::paintGL() { } } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { if (isHMDMode()) { - _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation()); + glm::quat hmdRotation = extractRotation(_myAvatar->getHMDSensorMatrix()); + _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation() * hmdRotation); + // Ignore MenuOption::CenterPlayerInView in HMD view + glm::vec3 hmdOffset = extractTranslation(_myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + + _myAvatar->getOrientation() + * (_myAvatar->getScale() * _myAvatar->getBoomLength() * glm::vec3(0.0f, 0.0f, 1.0f) + hmdOffset)); } else { _myCamera.setRotation(_myAvatar->getHead()->getOrientation()); + if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + + _myCamera.getRotation() * glm::vec3(0.0f, 0.0f, 1.0f) * _myAvatar->getBoomLength() * _myAvatar->getScale()); + } else { + _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + + _myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, 1.0f) * _myAvatar->getBoomLength() * _myAvatar->getScale()); + } } - if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + - _myCamera.getRotation() * glm::vec3(0.0f, 0.0f, 1.0f) * _myAvatar->getBoomLength() * _myAvatar->getScale()); - } else { - _myCamera.setPosition(_myAvatar->getDefaultEyePosition() + - _myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, 1.0f) * _myAvatar->getBoomLength() * _myAvatar->getScale()); - } - } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { if (isHMDMode()) { glm::quat hmdRotation = extractRotation(_myAvatar->getHMDSensorMatrix()); From 30dc07b62122139a04e08fd18462cd4847cb6c46 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 21 Sep 2015 21:22:36 -0700 Subject: [PATCH 161/192] Code tidying --- interface/src/Application.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0d55c08860..3d69cdd17c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1120,19 +1120,17 @@ void Application::paintGL() { // The render mode is default or mirror if the camera is in mirror mode, assigned further below renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - if (!getActiveDisplayPlugin()->isHmd()) { - _myCamera.setPosition(_myAvatar->getDefaultEyePosition()); - _myCamera.setRotation(_myAvatar->getHead()->getCameraOrientation()); - } else { + if (isHMDMode()) { mat4 camMat = _myAvatar->getSensorToWorldMatrix() * _myAvatar->getHMDSensorMatrix(); _myCamera.setPosition(extractTranslation(camMat)); _myCamera.setRotation(glm::quat_cast(camMat)); + } else { + _myCamera.setPosition(_myAvatar->getDefaultEyePosition()); + _myCamera.setRotation(_myAvatar->getHead()->getCameraOrientation()); } } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { if (isHMDMode()) { @@ -1147,10 +1145,12 @@ void Application::paintGL() { _myCamera.setRotation(_myAvatar->getHead()->getOrientation()); if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { _myCamera.setPosition(_myAvatar->getDefaultEyePosition() - + _myCamera.getRotation() * glm::vec3(0.0f, 0.0f, 1.0f) * _myAvatar->getBoomLength() * _myAvatar->getScale()); + + _myCamera.getRotation() + * (_myAvatar->getScale() * _myAvatar->getBoomLength() * glm::vec3(0.0f, 0.0f, 1.0f))); } else { _myCamera.setPosition(_myAvatar->getDefaultEyePosition() - + _myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, 1.0f) * _myAvatar->getBoomLength() * _myAvatar->getScale()); + + _myAvatar->getOrientation() + * (_myAvatar->getScale() * _myAvatar->getBoomLength() * glm::vec3(0.0f, 0.0f, 1.0f))); } } } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { From 4513b64b00eeff294d83be23700000618357ee0b Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 22:42:24 -0700 Subject: [PATCH 162/192] fixing review comments --- interface/src/ModelPackager.cpp | 2 +- libraries/fbx/src/FBXReader.cpp | 10 +-- libraries/gpu/src/gpu/Shader.cpp | 99 ++++++++++---------------- libraries/gpu/src/gpu/Shader.h | 21 ------ libraries/model/src/model/Material.slh | 31 ++++---- libraries/render-utils/src/model.slv | 32 --------- 6 files changed, 60 insertions(+), 135 deletions(-) diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 5e045d4ca8..35be11f2bb 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -340,7 +340,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename void ModelPackager::listTextures() { _textures.clear(); - foreach (FBXMaterial mat, _geometry->materials) { + foreach (const FBXMaterial mat, _geometry->materials) { if (!mat.diffuseTexture.filename.isEmpty() && mat.diffuseTexture.content.isEmpty() && !_textures.contains(mat.diffuseTexture.filename)) { _textures << mat.diffuseTexture.filename; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 91d2739887..d1798df397 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -246,19 +246,19 @@ public: glm::mat4 transformLink; }; -void appendModelIDs(const QString& parentID, const QMultiHash& _connectionChildMap, +void appendModelIDs(const QString& parentID, const QMultiHash& connectionChildMap, QHash& models, QSet& remainingModels, QVector& modelIDs) { if (remainingModels.contains(parentID)) { modelIDs.append(parentID); remainingModels.remove(parentID); } int parentIndex = modelIDs.size() - 1; - foreach (const QString& childID, _connectionChildMap.values(parentID)) { + foreach (const QString& childID, connectionChildMap.values(parentID)) { if (remainingModels.contains(childID)) { FBXModel& model = models[childID]; if (model.parentIndex == -1) { model.parentIndex = parentIndex; - appendModelIDs(childID, _connectionChildMap, models, remainingModels, modelIDs); + appendModelIDs(childID, connectionChildMap, models, remainingModels, modelIDs); } } } @@ -403,11 +403,11 @@ void addBlendshapes(const ExtractedBlendshape& extracted, const QList& _connectionParentMap, +QString getTopModelID(const QMultiHash& connectionParentMap, const QHash& models, const QString& modelID) { QString topID = modelID; forever { - foreach (const QString& parentID, _connectionParentMap.values(topID)) { + foreach (const QString& parentID, connectionParentMap.values(topID)) { if (models.contains(parentID)) { topID = parentID; goto outerContinue; diff --git a/libraries/gpu/src/gpu/Shader.cpp b/libraries/gpu/src/gpu/Shader.cpp index 223a78ed93..60bcbdaed0 100755 --- a/libraries/gpu/src/gpu/Shader.cpp +++ b/libraries/gpu/src/gpu/Shader.cpp @@ -1,40 +1,40 @@ -// -// Shader.cpp -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 2/27/2015. -// Copyright 2014 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 "Shader.h" -#include -#include - -#include "Context.h" - -using namespace gpu; - -Shader::Shader(Type type, const Source& source): - _source(source), - _type(type) -{ -} - -Shader::Shader(Type type, Pointer& vertex, Pointer& pixel): - _type(type) -{ - _shaders.resize(2); - _shaders[VERTEX] = vertex; - _shaders[PIXEL] = pixel; -} - - -Shader::~Shader() -{ -} +// +// Shader.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 2/27/2015. +// Copyright 2014 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 "Shader.h" +#include +#include + +#include "Context.h" + +using namespace gpu; + +Shader::Shader(Type type, const Source& source): + _source(source), + _type(type) +{ +} + +Shader::Shader(Type type, Pointer& vertex, Pointer& pixel): + _type(type) +{ + _shaders.resize(2); + _shaders[VERTEX] = vertex; + _shaders[PIXEL] = pixel; +} + + +Shader::~Shader() +{ +} Shader* Shader::createVertex(const Source& source) { Shader* shader = new Shader(VERTEX, source); @@ -71,28 +71,3 @@ bool Shader::makeProgram(Shader& shader, const Shader::BindingSet& bindings) { } return false; } - - -// ShaderSource -ShaderSource::ShaderSource() { -} - -ShaderSource::~ShaderSource() { -} - -void ShaderSource::reset(const QUrl& url) { - _shaderUrl = url; - _gpuShader.reset(); -} - -void ShaderSource::resetShader(gpu::Shader* shader) { - _gpuShader.reset(shader); -} - -bool ShaderSource::isDefined() const { - if (_gpuShader) { - return true; - } else { - return false; - } -} diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index d3c3992e6d..55812c6166 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -189,27 +189,6 @@ protected: typedef Shader::Pointer ShaderPointer; typedef std::vector< ShaderPointer > Shaders; -// ShaderSource is the bridge between a URL or a a way to produce the final gpu::Shader that will be used to render it. -class ShaderSource { -public: - ShaderSource(); - ~ShaderSource(); - - const QUrl& getUrl() const { return _shaderUrl; } - const gpu::ShaderPointer getGPUShader() const { return _gpuShader; } - - void reset(const QUrl& url); - - void resetShader(gpu::Shader* texture); - - bool isDefined() const; - -protected: - gpu::ShaderPointer _gpuShader; - QUrl _shaderUrl; -}; -typedef std::shared_ptr< ShaderSource > ShaderSourcePointer; - }; diff --git a/libraries/model/src/model/Material.slh b/libraries/model/src/model/Material.slh index 2f75c2b41e..35aa96042c 100644 --- a/libraries/model/src/model/Material.slh +++ b/libraries/model/src/model/Material.slh @@ -25,6 +25,8 @@ uniform materialBuffer { Material getMaterial() { return _mat; } + + T) { - return pow((cs * A + B), G); - } else { - return cs * C; + // C = 1 / 12.92 = 0.0773993808 + // G = 2.4 + const float T = 0.04045; + const float A = 0.947867; + const float B = 0.052132; + const float C = 0.077399; + const float G = 2.4; + + if (cs > T) { + return pow((cs * A + B), G); + } else { + return cs * C; } } -vec3 SRGBToLinear(vec3 srgb) { +vec3 SRGBToLinear(vec3 srgb) { return vec3(componentSRGBToLinear(srgb.x),componentSRGBToLinear(srgb.y),componentSRGBToLinear(srgb.z)); } vec3 getMaterialDiffuse(Material m) { return (gl_FragCoord.x < 800 ? SRGBToLinear(m._diffuse.rgb) : m._diffuse.rgb); } -*/ +*/!> + float getMaterialOpacity(Material m) { return m._diffuse.a; } vec3 getMaterialDiffuse(Material m) { return m._diffuse.rgb; } vec3 getMaterialSpecular(Material m) { return m._specular.rgb; } diff --git a/libraries/render-utils/src/model.slv b/libraries/render-utils/src/model.slv index 75237b52b7..6d56833de1 100755 --- a/libraries/render-utils/src/model.slv +++ b/libraries/render-utils/src/model.slv @@ -17,36 +17,6 @@ <$declareStandardTransform()$> - -/* -float componentSRGBToLinear(float cs) { - // sRGB to linear conversion - // { cs / 12.92, cs <= 0.04045 - // cl = { - // { ((cs + 0.055)/1.055)^2.4, cs > 0.04045 - // constants: - // T = 0.04045 - // A = 1 / 1.055 = 0.94786729857 - // B = 0.055 * A = 0.05213270142 - // C = 1 / 12.92 = 0.0773993808 - // G = 2.4 - const float T = 0.04045; - const float A = 0.947867; - const float B = 0.052132; - const float C = 0.077399; - const float G = 2.4; - - if (cs > T) { - return pow((cs * A + B), G); - } else { - return cs * C; - } -} - -vec3 SRGBToLinear(vec3 srgb) { - return vec3(componentSRGBToLinear(srgb.x),componentSRGBToLinear(srgb.y),componentSRGBToLinear(srgb.z)); -} -*/ const int MAX_TEXCOORDS = 2; uniform mat4 texcoordMatrices[MAX_TEXCOORDS]; @@ -60,8 +30,6 @@ void main(void) { // pass along the diffuse color _color = inColor.xyz; - // _color = SRGBToLinear(inColor.xyz); - // and the texture coordinates _texCoord0 = (texcoordMatrices[0] * vec4(inTexCoord0.st, 0.0, 1.0)).st; From 80e558163926cbb2ae7075cce9fffc67a35c4d28 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 21 Sep 2015 22:51:14 -0700 Subject: [PATCH 163/192] fixing review comments --- libraries/render-utils/src/Model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 7221a00267..7cf965951b 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1588,7 +1588,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, int shape } // guard against partially loaded meshes - if (/*partIndex >= (int)networkMesh._parts.size() ||*/ partIndex >= mesh.parts.size()) { + if (partIndex >= mesh.parts.size()) { return; } From b9db495ebf78e85ada8f1b5faf1fa954fe6ecfbb Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Tue, 22 Sep 2015 09:38:06 -0700 Subject: [PATCH 164/192] Modified handControllerGrab script to trigger entity touch events for non-physical entities --- examples/controllers/handControllerGrab.js | 100 +++++++++++++++------ 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index f57e79e974..0fa32baf53 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -29,9 +29,21 @@ var TRIGGER_ON_VALUE = 0.2; var DISTANCE_HOLDING_RADIUS_FACTOR = 5; // multiplied by distance between hand and object var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did -var NO_INTERSECT_COLOR = {red: 10, green: 10, blue: 255}; // line color when pick misses -var INTERSECT_COLOR = {red: 250, green: 10, blue: 10}; // line color when pick hits -var LINE_ENTITY_DIMENSIONS = {x: 1000, y: 1000, z: 1000}; +var NO_INTERSECT_COLOR = { + red: 10, + green: 10, + blue: 255 +}; // line color when pick misses +var INTERSECT_COLOR = { + red: 250, + green: 10, + blue: 10 +}; // line color when pick hits +var LINE_ENTITY_DIMENSIONS = { + x: 1000, + y: 1000, + z: 1000 +}; var LINE_LENGTH = 500; @@ -54,7 +66,11 @@ var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things var RIGHT_HAND = 1; var LEFT_HAND = 0; -var ZERO_VEC = {x: 0, y: 0, z: 0}; +var ZERO_VEC = { + x: 0, + y: 0, + z: 0 +}; var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}"; var MSEC_PER_SEC = 1000.0; @@ -68,7 +84,9 @@ var STATE_DISTANCE_HOLDING = 1; var STATE_CONTINUE_DISTANCE_HOLDING = 2; var STATE_NEAR_GRABBING = 3; var STATE_CONTINUE_NEAR_GRABBING = 4; -var STATE_RELEASE = 5; +var STATE_NEAR_TOUCHING = 5; +var STATE_CONTINUE_NEAR_TOUCHING = 6; +var STATE_RELEASE = 7; var GRAB_USER_DATA_KEY = "grabKey"; @@ -93,7 +111,7 @@ function controller(hand, triggerAction) { this.triggerValue = 0; // rolling average of trigger value this.update = function() { - switch(this.state) { + switch (this.state) { case STATE_SEARCHING: this.search(); break; @@ -109,6 +127,12 @@ function controller(hand, triggerAction) { case STATE_CONTINUE_NEAR_GRABBING: this.continueNearGrabbing(); break; + case STATE_NEAR_TOUCHING: + this.nearTouching(); + break; + case STATE_CONTINUE_NEAR_TOUCHING: + this.continueNearTouching(); + break; case STATE_RELEASE: this.release(); break; @@ -125,14 +149,14 @@ function controller(hand, triggerAction) { dimensions: LINE_ENTITY_DIMENSIONS, visible: true, position: closePoint, - linePoints: [ ZERO_VEC, farPoint ], + linePoints: [ZERO_VEC, farPoint], color: color, lifetime: LIFETIME }); } else { Entities.editEntity(this.pointer, { position: closePoint, - linePoints: [ ZERO_VEC, farPoint ], + linePoints: [ZERO_VEC, farPoint], color: color, lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME }); @@ -171,7 +195,10 @@ function controller(hand, triggerAction) { // the trigger is being pressed, do a ray test var handPosition = this.getHandPosition(); - var pickRay = {origin: handPosition, direction: Quat.getUp(this.getHandRotation())}; + var pickRay = { + origin: handPosition, + direction: Quat.getUp(this.getHandRotation()) + }; var intersection = Entities.findRayIntersection(pickRay, true); if (intersection.intersects && intersection.properties.collisionsWillMove === 1 && @@ -189,24 +216,25 @@ function controller(hand, triggerAction) { this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } } else { - // forward ray test failed, try sphere test. + // forward ray test failed, try sphere test. var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS); var minDistance = GRAB_RADIUS; var grabbedEntity = null; for (var i = 0; i < nearbyEntities.length; i++) { var props = Entities.getEntityProperties(nearbyEntities[i]); var distance = Vec3.distance(props.position, handPosition); - if (distance < minDistance && props.name !== "pointer" && - props.collisionsWillMove === 1 && - props.locked === 0) { + if (distance < minDistance && props.name !== "pointer") { this.grabbedEntity = nearbyEntities[i]; minDistance = distance; } } if (this.grabbedEntity === null) { this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); - } else { + } else if (props.locked === 0 && props.collisionsWillMove === 1) { this.state = STATE_NEAR_GRABBING; + } else if (props.collisionsWillMove === 0) { + // We have grabbed a non-physical object, so we want to trigger a touch event as opposed to a grab event + this.state = STATE_NEAR_TOUCHING; } } } @@ -215,7 +243,7 @@ function controller(hand, triggerAction) { this.distanceHolding = function() { var handControllerPosition = Controller.getSpatialControlPosition(this.palm); var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position","rotation"]); + var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]); // add the action and initialize some variables this.currentObjectPosition = grabbedProperties.position; @@ -263,8 +291,8 @@ function controller(hand, triggerAction) { // the action was set up on a previous call. update the targets. var radius = Math.max(Vec3.distance(this.currentObjectPosition, - handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, - DISTANCE_HOLDING_RADIUS_FACTOR); + handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR, + DISTANCE_HOLDING_RADIUS_FACTOR); var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition); this.handPreviousPosition = handControllerPosition; @@ -281,16 +309,18 @@ function controller(hand, triggerAction) { // this doubles hand rotation var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, - DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - Quat.inverse(this.handPreviousRotation)); + DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + Quat.inverse(this.handPreviousRotation)); this.handPreviousRotation = handRotation; this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation); Entities.callEntityMethod(this.grabbedEntity, "continueDistantGrab"); Entities.updateAction(this.grabbedEntity, this.actionID, { - targetPosition: this.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, - targetRotation: this.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME + targetPosition: this.currentObjectPosition, + linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME, + targetRotation: this.currentObjectRotation, + angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME }); } @@ -339,6 +369,23 @@ function controller(hand, triggerAction) { this.currentObjectTime = Date.now(); } + this.nearTouching = function() { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + Entities.callEntityMethod(this.grabbedEntity, "startNearTouch") + this.state = STATE_CONTINUE_NEAR_TOUCHING; + } + + this.continueNearTouching = function() { + if (!this.triggerSmoothedSqueezed()) { + this.state = STATE_RELEASE; + return; + } + Entities.callEntityMethod(this.grabbedEntity, "continueNearTouch"); + } + this.continueNearGrabbing = function() { if (!this.triggerSmoothedSqueezed()) { @@ -367,9 +414,8 @@ function controller(hand, triggerAction) { // value would otherwise give the held object time to slow down. if (this.triggerSqueezed()) { this.grabbedVelocity = - Vec3.sum(Vec3.multiply(this.grabbedVelocity, - (1.0 - NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)), - Vec3.multiply(grabbedVelocity, NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)); + Vec3.sum(Vec3.multiply(this.grabbedVelocity, (1.0 - NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)), + Vec3.multiply(grabbedVelocity, NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)); } if (useMultiplier) { @@ -389,7 +435,9 @@ function controller(hand, triggerAction) { // the action will tend to quickly bring an object's velocity to zero. now that // the action is gone, set the objects velocity to something the holder might expect. - Entities.editEntity(this.grabbedEntity, {velocity: this.grabbedVelocity}); + Entities.editEntity(this.grabbedEntity, { + velocity: this.grabbedVelocity + }); this.deactivateEntity(this.grabbedEntity); this.grabbedVelocity = ZERO_VEC; @@ -438,4 +486,4 @@ function cleanup() { Script.scriptEnding.connect(cleanup); -Script.update.connect(update) +Script.update.connect(update) \ No newline at end of file From f38bb77d0a9fc2bb46d717e0e86f7cf8e069c86b Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 22 Sep 2015 18:48:46 +0200 Subject: [PATCH 165/192] less calls to QHash --- interface/src/Application.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.h b/interface/src/Application.h index 30f89d8421..2a5138638e 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -292,7 +292,7 @@ public: NodeToJurisdictionMap& getEntityServerJurisdictions() { return _entityServerJurisdictions; } QStringList getRunningScripts() { return _scriptEnginesHash.keys(); } - ScriptEngine* getScriptEngine(const QString& scriptHash) { return _scriptEnginesHash.contains(scriptHash) ? _scriptEnginesHash[scriptHash] : NULL; } + ScriptEngine* getScriptEngine(const QString& scriptHash) { return _scriptEnginesHash.value(scriptHash, NULL); } bool isLookingAtMyAvatar(AvatarSharedPointer avatar); From 8f1dde69cc1e98d98fc6ef237a09268f8e4cea5f Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 22 Sep 2015 10:10:29 -0700 Subject: [PATCH 166/192] Always keep targets, even when both position and rotation are unset. (Get from underpose.) Filtering these was necessary before when the underpose coordinate was wrong, but now that we have that working, there shouldn't be any need to filter. --- .../animation/src/AnimInverseKinematics.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 1ccf984815..57459abacb 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -99,15 +99,13 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: } } else { // TODO: get this done without a double-lookup of each var in animVars - if (animVars.hasKey(targetVar.positionVar) || animVars.hasKey(targetVar.rotationVar)) { - IKTarget target; - AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, _relativePoses); - target.pose.trans = animVars.lookup(targetVar.positionVar, defaultPose.trans); - target.pose.rot = animVars.lookup(targetVar.rotationVar, defaultPose.rot); - target.rootIndex = targetVar.rootIndex; - target.index = targetVar.jointIndex; - targets.push_back(target); - } + IKTarget target; + AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, _relativePoses); + target.pose.trans = animVars.lookup(targetVar.positionVar, defaultPose.trans); + target.pose.rot = animVars.lookup(targetVar.rotationVar, defaultPose.rot); + target.rootIndex = targetVar.rootIndex; + target.index = targetVar.jointIndex; + targets.push_back(target); } } From 7e52d38870d296ead5bf9b3053cfbcaf6686c0fa Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 22 Sep 2015 10:12:59 -0700 Subject: [PATCH 167/192] Don't include the root rot, because it seems that this is already accounted for in the head params. Restore the hmd conditional on setting head position. This had been removed when failing to pin it cause lean. I believe that lean was being caused by coordinate system issues that are now addressed by the above and Andrew's big cleanup. --- libraries/animation/src/Rig.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 29d0bc011b..a74560114a 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -993,11 +993,13 @@ void Rig::updateNeckJoint(int index, const HeadParameters& params) { glm::angleAxis(glm::radians(-params.localHeadPitch), X_AXIS)); _animVars.set("headRotation", realLocalHeadOrientation); - // There's a theory that when not in hmd, we should _animVars.unset("headPosition"). - // However, until that works well, let's always request head be positioned where requested by hmd, camera, or default. - int headIndex = _animSkeleton->nameToJointIndex("Head"); - AnimPose rootPose = _animNode->getRootPose(headIndex); - _animVars.set("headPosition", rootPose.trans + rootPose.rot * params.localHeadPosition); + if (params.isInHMD) { + int headIndex = _animSkeleton->nameToJointIndex("Head"); + AnimPose rootPose = _animNode->getRootPose(headIndex); + _animVars.set("headPosition", rootPose.trans + params.localHeadPosition); // rootPose.rot is handled in params?d + } else { + _animVars.unset("headPosition"); + } } else if (!_enableAnimGraph) { auto& state = _jointStates[index]; From 37efb80945280bdddd73493158d25a6686ab2e7e Mon Sep 17 00:00:00 2001 From: Eric Levin Date: Tue, 22 Sep 2015 10:19:01 -0700 Subject: [PATCH 168/192] Update BUILD_WIN.md --- BUILD_WIN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD_WIN.md b/BUILD_WIN.md index b6ec31b713..d291cef4f2 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -71,7 +71,7 @@ Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll To prevent these problems, install OpenSSL yourself. Download the following binary packages [from this website](http://slproweb.com/products/Win32OpenSSL.html): * Visual C++ 2008 Redistributables -* Win32 OpenSSL v1.0.1m +* Win32 OpenSSL v1.0.1p Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. From fed5e8e573320ce27dee28c96dd7e5d4f06dbd4e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Sep 2015 11:35:34 -0700 Subject: [PATCH 169/192] Don't run Script Editor scripts from the cache --- interface/src/ui/ScriptEditorWidget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp index 6d66e04bc6..fa829d4ace 100644 --- a/interface/src/ui/ScriptEditorWidget.cpp +++ b/interface/src/ui/ScriptEditorWidget.cpp @@ -97,7 +97,8 @@ bool ScriptEditorWidget::setRunning(bool run) { if (run) { const QString& scriptURLString = QUrl(_currentScript).toString(); - _scriptEngine = Application::getInstance()->loadScript(scriptURLString, true, true); + // Reload script so that an out of date copy is not retrieved from the cache + _scriptEngine = Application::getInstance()->loadScript(scriptURLString, true, true, false, true); connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); connect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError); connect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint); From 5bb908f081f9f41a01160205cfd8458eaacfc908 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 22 Sep 2015 12:34:47 -0700 Subject: [PATCH 170/192] remove dead code and fix warning --- libraries/model/src/model/Material.cpp | 12 ------------ libraries/model/src/model/Material.h | 2 -- 2 files changed, 14 deletions(-) diff --git a/libraries/model/src/model/Material.cpp b/libraries/model/src/model/Material.cpp index cf91836254..c2ff828af3 100755 --- a/libraries/model/src/model/Material.cpp +++ b/libraries/model/src/model/Material.cpp @@ -15,18 +15,6 @@ using namespace model; using namespace gpu; -float componentSRGBToLinear(float cs) { - if (cs > 0.04045) { - return pow(((cs + 0.055)/1.055), 2.4); - } else { - return cs / 12.92; - } -} - -glm::vec3 convertSRGBToLinear(const glm::vec3& srgb) { - return glm::vec3(componentSRGBToLinear(srgb.x), componentSRGBToLinear(srgb.y), componentSRGBToLinear(srgb.z)); -} - Material::Material() : _key(0), _schemaBuffer(), diff --git a/libraries/model/src/model/Material.h b/libraries/model/src/model/Material.h index 0e7388f722..b0725d9908 100755 --- a/libraries/model/src/model/Material.h +++ b/libraries/model/src/model/Material.h @@ -20,8 +20,6 @@ namespace model { -static glm::vec3 convertSRGBToLinear(const glm::vec3& srgb); - class TextureMap; typedef std::shared_ptr< TextureMap > TextureMapPointer; From 0ad238a5082a978e463d2b25acb28215a3f0e1d0 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Tue, 22 Sep 2015 12:39:48 -0700 Subject: [PATCH 171/192] Enlarge flashlight --- examples/toys/flashlight/createFlashlight.js | 8 +++---- examples/toys/flashlight/flashlight.js | 24 ++++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/examples/toys/flashlight/createFlashlight.js b/examples/toys/flashlight/createFlashlight.js index 65cafbcdaa..38907efa75 100644 --- a/examples/toys/flashlight/createFlashlight.js +++ b/examples/toys/flashlight/createFlashlight.js @@ -15,7 +15,7 @@ Script.include("https://hifi-public.s3.amazonaws.com/scripts/utilities.js"); -var scriptURL = Script.resolvePath('flashlight.js'); +var scriptURL = Script.resolvePath('flashlight.js?123123'); var modelURL = "https://hifi-public.s3.amazonaws.com/models/props/flashlight.fbx"; @@ -30,9 +30,9 @@ var flashlight = Entities.addEntity({ modelURL: modelURL, position: center, dimensions: { - x: 0.04, - y: 0.15, - z: 0.04 + x: 0.08, + y: 0.30, + z: 0.08 }, collisionsWillMove: true, shapeType: 'box', diff --git a/examples/toys/flashlight/flashlight.js b/examples/toys/flashlight/flashlight.js index 7f2bd210da..f5929dcfc8 100644 --- a/examples/toys/flashlight/flashlight.js +++ b/examples/toys/flashlight/flashlight.js @@ -33,7 +33,7 @@ // These constants define the Spotlight position and orientation relative to the model var MODEL_LIGHT_POSITION = { x: 0, - y: 0, + y: -0.3, z: 0 }; var MODEL_LIGHT_ROTATION = Quat.angleAxis(-90, { @@ -117,6 +117,21 @@ cutoff: 90, // in degrees }); + this.debugBox = Entities.addEntity({ + type: 'Box', + color: { + red: 255, + blue: 0, + green: 0 + }, + dimensions: { + x: 0.25, + y: 0.25, + z: 0.25 + }, + ignoreForCollisions:true + }) + this.hasSpotlight = true; } @@ -166,6 +181,10 @@ rotation: glowLightTransform.q, }) + // Entities.editEntity(this.debugBox, { + // position: lightTransform.p, + // rotation: lightTransform.q, + // }) }, changeLightWithTriggerPressure: function(flashLightHand) { @@ -184,7 +203,6 @@ return }, turnLightOff: function() { - print('turn light off') Entities.editEntity(this.spotlight, { intensity: 0 }); @@ -194,7 +212,6 @@ this.lightOn = false }, turnLightOn: function() { - print('turn light on') Entities.editEntity(this.glowLight, { intensity: 2 }); @@ -227,7 +244,6 @@ } - Script.update.disconnect(this.update); }, }; From e5c42c76a23e2242e01cf5117d3ffc14256a5b89 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Tue, 22 Sep 2015 12:41:00 -0700 Subject: [PATCH 172/192] Remove update loop refs --- examples/toys/flashlight/flashlight.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/toys/flashlight/flashlight.js b/examples/toys/flashlight/flashlight.js index f5929dcfc8..0465266b1c 100644 --- a/examples/toys/flashlight/flashlight.js +++ b/examples/toys/flashlight/flashlight.js @@ -223,15 +223,12 @@ // preload() will be called when the entity has become visible (or known) to the interface // it gives us a chance to set our local JavaScript object up. In this case it means: // * remembering our entityID, so we can access it in cases where we're called without an entityID - // * connecting to the update signal so we can check our grabbed state preload: function(entityID) { this.entityID = entityID; - Script.update.connect(this.update); }, // unload() will be called when our entity is no longer available. It may be because we were deleted, - // or because we've left the domain or quit the application. In all cases we want to unhook our connection - // to the update signal + // or because we've left the domain or quit the application. unload: function(entityID) { if (this.hasSpotlight) { From 561b4ca8e121cd8f09130944555248595031baa7 Mon Sep 17 00:00:00 2001 From: James Pollack Date: Tue, 22 Sep 2015 12:52:24 -0700 Subject: [PATCH 173/192] Remove update loop refs, remove unload method --- examples/toys/bubblewand/wand.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/toys/bubblewand/wand.js b/examples/toys/bubblewand/wand.js index f8e484bbde..b71932b500 100644 --- a/examples/toys/bubblewand/wand.js +++ b/examples/toys/bubblewand/wand.js @@ -60,10 +60,6 @@ currentBubble: null, preload: function(entityID) { this.entityID = entityID; - Script.update.connect(this.update); - }, - unload: function() { - Script.update.disconnect(this.update); }, getWandTipPosition: function(properties) { //the tip of the wand is going to be in a different place than the center, so we move in space relative to the model to find that position @@ -179,12 +175,14 @@ }, startNearGrab: function() { + //create a bubble to grow at the start of the grab if (this.currentBubble === null) { this.createBubbleAtTipOfWand(); } }, continueNearGrab: function() { var deltaTime = checkInterval() + //only get the properties that we need var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); this.growBubbleWithWandVelocity(properties, deltaTime); From 42a2125336a7edfba03b9e662f1e175ef85fd7ff Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Tue, 22 Sep 2015 13:29:54 -0700 Subject: [PATCH 174/192] Only asking for needed props --- examples/controllers/handControllerGrab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 0fa32baf53..af1c97da68 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -216,12 +216,12 @@ function controller(hand, triggerAction) { this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } } else { - // forward ray test failed, try sphere test. + // forward ray test failed, try sphere test. var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS); var minDistance = GRAB_RADIUS; var grabbedEntity = null; for (var i = 0; i < nearbyEntities.length; i++) { - var props = Entities.getEntityProperties(nearbyEntities[i]); + var props = Entities.getEntityProperties(nearbyEntities[i], ["position", "name", "collisionsWillMove", "locked"]); var distance = Vec3.distance(props.position, handPosition); if (distance < minDistance && props.name !== "pointer") { this.grabbedEntity = nearbyEntities[i]; From eda12fd3af56cd91632904b71aae509a1bd6092a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 22 Sep 2015 16:15:54 -0700 Subject: [PATCH 175/192] Don't look straight ahead in fullscreen mirror --- interface/src/Application.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1deb45cf90..c359275d8a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2614,20 +2614,19 @@ void Application::updateMyAvatarLookAtPosition() { bool isLookingAtSomeone = false; bool isHMD = _avatarUpdate->isHMDMode(); glm::vec3 lookAtSpot; - if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - // When I am in mirror mode, just look right at the camera (myself); don't switch gaze points because when physically - // looking in a mirror one's eyes appear steady. - lookAtSpot = _myCamera.getPosition(); - } else if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { + if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { // Look at the point that the user is looking at. + glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); + if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + lookAtPosition.x = -lookAtPosition.x; + } if (isHMD) { glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); glm::quat hmdRotation = glm::quat_cast(headPose); - lookAtSpot = _myCamera.getPosition() + - _myAvatar->getOrientation() * (hmdRotation * eyeTracker->getLookAtPosition()); + lookAtSpot = _myCamera.getPosition() + _myAvatar->getOrientation() * (hmdRotation * lookAtPosition); } else { - lookAtSpot = _myAvatar->getHead()->getEyePosition() + - (_myAvatar->getHead()->getFinalOrientationInWorldFrame() * eyeTracker->getLookAtPosition()); + lookAtSpot = _myAvatar->getHead()->getEyePosition() + + (_myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); } } else { AvatarSharedPointer lookingAt = _myAvatar->getLookAtTargetAvatar().lock(); From efda1216195a41c4596126fe011ee7f44c60e0d3 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Tue, 22 Sep 2015 16:37:54 -0700 Subject: [PATCH 176/192] fix a bunch of warnings on windows --- examples/edit.js | 2 +- examples/html/entityProperties.html | 219 +++++++++--------- .../src/model-networking/ModelCache.cpp | 4 +- .../src/model-networking/ModelCache.h | 2 +- .../networking/src/udt/ConnectionStats.cpp | 12 +- libraries/render-utils/src/GeometryCache.cpp | 2 +- libraries/render-utils/src/Model.h | 4 +- 7 files changed, 123 insertions(+), 122 deletions(-) diff --git a/examples/edit.js b/examples/edit.js index d28d51df9d..0d1164685a 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -1320,7 +1320,7 @@ PropertiesTool = function(opts) { if (data.action == "moveSelectionToGrid") { if (selectionManager.hasSelection()) { selectionManager.saveProperties(); - var dY = grid.getOrigin().y - (selectionManager.worldPosition.y - selectionManager.worldDimensions.y / 2), + var dY = grid.getOrigin().y - (selectionManager.worldPosition.y - selectionManager.worldDimensions.y / 2); var diff = { x: 0, y: dY, z: 0 }; for (var i = 0; i < selectionManager.selections.length; i++) { var properties = selectionManager.savedProperties[selectionManager.selections[i]]; diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index ad489afddf..268a2fb7f2 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -1,5 +1,6 @@ + Properties @@ -961,7 +962,7 @@
-
@@ -970,7 +971,7 @@
Name
- +
@@ -1003,13 +1004,13 @@
Href
- +
Description
- +
@@ -1021,9 +1022,9 @@
Position
-
X
-
Y
-
Z
+
X
+
Y
+
Z
@@ -1034,26 +1035,26 @@
Registration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Dimensions
-
X
-
Y
-
Z
+
X
+
Y
+
Z
- % + %
- +
@@ -1061,9 +1062,9 @@
Voxel Volume Size
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Surface Extractor
@@ -1078,26 +1079,26 @@
X-axis Texture URL
- +
Y-axis Texture URL
- +
Z-axis Texture URL
- +
Rotation
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
@@ -1109,66 +1110,66 @@
Linear Velocity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Linear Damping
- +
Angular Velocity
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Angular Damping
- +
Restitution
- +
Friction
- +
Gravity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Acceleration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Density
- +
@@ -1176,9 +1177,9 @@
Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1190,38 +1191,38 @@
Ignore For Collisions - +
Collisions Will Move - +
Collision Sound URL
- +
Lifetime
- +
Script URL - - + +
- +
@@ -1233,14 +1234,14 @@
Model URL
- +
Shape Type
- @@ -1251,13 +1252,13 @@
Compound Shape URL
- +
Animation URL
- +
@@ -1269,13 +1270,13 @@
Animation FPS
- +
Animation Frame
- +
@@ -1305,7 +1306,7 @@
Source URL
- +
@@ -1317,45 +1318,45 @@
Max Particles
- +
Particle Life Span
- +
Particle Emission Rate
- +
Particle Emission Direction
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Particle Emission Strength
- +
Particle Local Gravity
- +
Particle Radius
- +
@@ -1367,31 +1368,31 @@
Text Content
- +
Line Height
- +
Text Color
-
R
-
G
-
B
+
R
+
G
+
B
Background Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1410,27 +1411,27 @@
Color
-
R
-
G
-
B
+
R
+
G
+
B
Intensity
- +
Spot Light Exponent
- +
Spot Light Cutoff (degrees)
- +
@@ -1450,48 +1451,48 @@
Key Light Color
-
R
-
G
-
B
+
R
+
G
+
B
Key Light Intensity
- +
Key Light Ambient Intensity
- +
Key Light Direction
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Stage Latitude
- +
Stage Longitude
- +
Stage Altitude
- +
@@ -1505,20 +1506,20 @@
Stage Day
- +
Stage Hour
- +
Background Mode
- @@ -1535,15 +1536,15 @@
Skybox Color
-
R
-
G
-
B
+
R
+
G
+
B
Skybox URL
- +
@@ -1555,9 +1556,9 @@
Atmosphere Center
-
X
-
Y
-
Z
+
X
+
Y
+
Z
@@ -1566,33 +1567,33 @@
Atmosphere Inner Radius
- +
Atmosphere Outer Radius
- +
Atmosphere Mie Scattering
- +
Atmosphere Rayleigh Scattering
- +
Atmosphere Scattering Wavelenghts
-
X
-
Y
-
Z
+
X
+
Y
+
Z