diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 8aa8f48e24..53e2ef95b8 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -2211,8 +2211,6 @@ { "title": "Test updateTextureData", "playgroundId": "#EVX1DH#80", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "testUpdateTextureData.png" }, { diff --git a/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js b/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js index 1621956e72..19ef31d671 100644 --- a/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js +++ b/Apps/UnitTests/JavaScript/dist/tests.shaderCompilation.comprehensiveGLSL.js @@ -780,7 +780,26 @@ var vertexSource = "\n// \u2500\u2500 Precision qualifiers \u2500\u2500\u2500\u2 // --------------------------------------------------------------------------- // Comprehensive WebGL2 GLSL ES 3.00 fragment shader // --------------------------------------------------------------------------- -var fragmentSource = "\n// \u2500\u2500 Precision qualifiers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\nprecision highp samplerCube;\nprecision highp sampler2DArray;\nprecision highp sampler2DShadow;\nprecision highp samplerCubeShadow;\nprecision highp sampler2DArrayShadow;\nprecision highp isampler2D;\nprecision highp isampler3D;\nprecision highp isamplerCube;\nprecision highp isampler2DArray;\nprecision highp usampler2D;\nprecision highp usampler3D;\nprecision highp usamplerCube;\nprecision highp usampler2DArray;\n\n// \u2500\u2500 Struct (must match vertex) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nstruct LightInfo {\n vec3 position;\n vec3 color;\n float intensity;\n};\n\nstruct Material {\n vec4 diffuse;\n vec4 specular;\n float shininess;\n LightInfo mainLight;\n};\n\n// \u2500\u2500 Uniform blocks (std140) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(std140) uniform TransformBlock {\n mat4 uModel;\n mat4 uViewProj;\n mat4 uNormalMatrix;\n};\n\nlayout(std140) uniform SceneBlock {\n vec4 uAmbientColor;\n float uTime;\n int uFrameCount;\n uint uFlags;\n};\n\n// \u2500\u2500 All sampler types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform sampler2D uSampler2D;\nuniform sampler3D uSampler3D;\nuniform samplerCube uSamplerCube;\nuniform sampler2DArray uSampler2DArray;\nuniform sampler2DShadow uSampler2DShadow;\nuniform samplerCubeShadow uSamplerCubeShadow;\nuniform sampler2DArrayShadow uSampler2DArrayShadow;\nuniform isampler2D uISampler2D;\nuniform isampler3D uISampler3D;\nuniform isamplerCube uISamplerCube;\nuniform isampler2DArray uISampler2DArray;\nuniform usampler2D uUSampler2D;\nuniform usampler3D uUSampler3D;\nuniform usamplerCube uUSamplerCube;\nuniform usampler2DArray uUSampler2DArray;\n\n// \u2500\u2500 Plain uniforms \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform Material uMaterial;\nuniform float uCustomFloat;\n\n// \u2500\u2500 Fragment inputs (from vertex shader) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nin vec3 vWorldPos;\nin vec3 vNormal;\nin vec2 vUV;\nflat in int vVertexID;\nflat in uint vFlagsOut;\nsmooth in vec4 vColor;\ncentroid in vec2 vCentroidUV;\nin vec4 vSplatColor;\n\n// \u2500\u2500 Fragment output (MRT-capable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(location = 0) out vec4 fragColor;\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst float PI = 3.14159265359;\nconst float EPSILON = 1e-6;\n\n// \u2500\u2500 Helper: Blinn-Phong \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 blinnPhong(vec3 N, vec3 L, vec3 V, vec3 lightColor, float shininess) {\n float NdotL = max(dot(N, L), 0.0);\n vec3 H = normalize(L + V);\n float NdotH = max(dot(N, H), 0.0);\n float specPower = pow(NdotH, shininess);\n return lightColor * (NdotL + specPower);\n}\n\n// \u2500\u2500 Helper: Fresnel-Schlick \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 fresnelSchlick(float cosTheta, vec3 F0) {\n return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);\n}\n\n// \u2500\u2500 Helper: normal mapping simulation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 perturbNormal(vec3 N, vec3 texNormal) {\n // unpack from [0,1] to [-1,1]\n vec3 mapped = texNormal * 2.0 - 1.0;\n return normalize(N + mapped * 0.5);\n}\n\nvoid main() {\n // \u2500\u2500 Fragment built-in variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 fragCoord = gl_FragCoord;\n bool frontFacing = gl_FrontFacing;\n float fragDepth = gl_FragCoord.z;\n\n // \u2500\u2500 Derivative functions (fragment only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec2 dxUV = dFdx(vUV);\n vec2 dyUV = dFdy(vUV);\n vec2 fwUV = fwidth(vUV);\n \n // \u2500\u2500 Texture sampling: texture() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2d = texture(uSampler2D, vUV);\n vec4 tex3d = texture(uSampler3D, vec3(vUV, 0.0));\n vec4 texCube = texture(uSamplerCube, vNormal);\n vec4 texArr = texture(uSampler2DArray, vec3(vUV, 0.0));\n\n // \u2500\u2500 Texture sampling: textureLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dLod = textureLod(uSampler2D, vUV, 1.0);\n\n // \u2500\u2500 Texture sampling: textureOffset() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dOff = textureOffset(uSampler2D, vUV, ivec2(1, 0));\n\n // \u2500\u2500 Texture sampling: textureGrad() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dGrad = textureGrad(uSampler2D, vUV, dxUV, dyUV);\n /* textureProj not used in Babylon\n // \u2500\u2500 Texture sampling: textureProj() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProj = textureProj(uSampler2D, vec3(vUV, 1.0));\n vec4 tex2dProj4 = textureProj(uSampler2D, vec4(vUV, 0.0, 1.0));\n\n // \u2500\u2500 Texture sampling: textureProjLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProjL = textureProjLod(uSampler2D, vec3(vUV, 1.0), 0.0);\n */\n // \u2500\u2500 Texture sampling: texelFetch() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 texCoord = ivec2(gl_FragCoord.xy);\n vec4 fetched = texelFetch(uSampler2D, texCoord, 0);\n \n // \u2500\u2500 Texture sampling: textureSize() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 size2d = textureSize(uSampler2D, 0);\n ivec3 size3d = textureSize(uSampler3D, 0);\n \n ivec2 sizeCube = textureSize(uSamplerCube, 0);\n /* No 2D array support\n ivec3 sizeArr = textureSize(uSampler2DArray, 0);\n\n // \u2500\u2500 Shadow sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float shadow2d = texture(uSampler2DShadow, vec3(vUV, 0.5));\n float shadowCube = texture(uSamplerCubeShadow, vec4(vNormal, 0.5));\n float shadowArr = texture(uSampler2DArrayShadow, vec4(vUV, 0.0, 0.5));\n\n // \u2500\u2500 Integer sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec4 iTex2d = texture(uISampler2D, vUV);\n ivec4 iTex3d = texture(uISampler3D, vec3(vUV, 0.0));\n ivec4 iTexCube = texture(uISamplerCube, vNormal);\n ivec4 iTexArr = texture(uISampler2DArray, vec3(vUV, 0.0));\n uvec4 uTex2d = texture(uUSampler2D, vUV);\n uvec4 uTex3d = texture(uUSampler3D, vec3(vUV, 0.0));\n uvec4 uTexCube = texture(uUSamplerCube, vNormal);\n uvec4 uTexArr = texture(uUSampler2DArray, vec3(vUV, 0.0));\n */\n // \u2500\u2500 trunc / round (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float truncF = trunc(1.7);\n float roundF = round(1.5);\n float roundEven_ = roundEven(2.5);\n\n // \u2500\u2500 modf (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float integralPart;\n float fractionalPart = modf(3.75, integralPart);\n\n // \u2500\u2500 min/max/clamp on vec types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 minV = min(vNormal, vec3(0.5));\n vec3 maxV = max(vNormal, vec3(0.0));\n vec3 clampV = clamp(vNormal, vec3(0.0), vec3(1.0));\n \n // \u2500\u2500 mix with bvec selector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 mixBvec = mix(vec3(0.0), vec3(1.0), bvec3(true, false, true));\n\n // \u2500\u2500 Control flow: if / else with discard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (tex2d.a < 0.01) {\n discard;\n }\n\n // \u2500\u2500 Control flow: for loop with early exit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 lightAccum = vec3(0.0);\n for (int j = 0; j < 4; j++) {\n LightInfo lt;\n lt.position = vec3(float(j) * 3.0, 5.0, 0.0);\n lt.color = vec3(1.0, 0.9, 0.8);\n lt.intensity = 1.0 / (1.0 + float(j));\n\n vec3 L = normalize(lt.position - vWorldPos);\n vec3 V = normalize(-vWorldPos);\n lightAccum += blinnPhong(normalize(vNormal), L, V, lt.color, uMaterial.shininess) * lt.intensity;\n\n if (length(lightAccum) > 3.0) break;\n }\n \n // \u2500\u2500 Control flow: while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float decay = 1.0;\n int wc = 0;\n while (decay > 0.01 && wc < 10) {\n decay *= 0.7;\n wc++;\n }\n\n // \u2500\u2500 Control flow: do-while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float buildUp = 0.0;\n int dc = 0;\n do {\n buildUp += 0.1;\n dc++;\n } while (buildUp < 0.5 && dc < 10);\n \n // \u2500\u2500 Control flow: switch / case \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 modeColor = vec3(0.0);\n switch (vVertexID % 4) {\n case 0: modeColor = vec3(1.0, 0.0, 0.0); break;\n case 1: modeColor = vec3(0.0, 1.0, 0.0); break;\n case 2: modeColor = vec3(0.0, 0.0, 1.0); break;\n default: modeColor = vec3(1.0); break;\n }\n\n // \u2500\u2500 Bitwise on uint flags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n bool flag0 = (vFlagsOut & 1u) != 0u;\n bool flag1 = (vFlagsOut & 2u) != 0u;\n uint shifted = vFlagsOut << 1u;\n uint masked = vFlagsOut & 0xF0u;\n \n // \u2500\u2500 Front-facing conditional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 normal = frontFacing ? normalize(vNormal) : -normalize(vNormal);\n\n // \u2500\u2500 Fresnel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 viewDir = normalize(-vWorldPos);\n vec3 fresnel = fresnelSchlick(max(dot(normal, viewDir), 0.0), vec3(0.04));\n\n // \u2500\u2500 Normal perturbation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 perturbedN = perturbNormal(normal, tex2d.rgb);\n \n // \u2500\u2500 Final composition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 materialDiffuse = uMaterial.diffuse;\n vec4 materialSpecular = uMaterial.specular;\n vec3 albedo = tex2d.rgb * materialDiffuse.rgb * vColor.rgb;\n // using uAmbientColor.rgb crashes SpvBuilder because it assumes it's a single float\n // this case doesn't seem to be found anywhere in Babylon shader so won't fix\n vec3 ambient = /*uAmbientColor.rgb * */albedo;\n \n vec3 lit = ambient + lightAccum * albedo;\n vec3 specular = fresnel * materialSpecular.rgb;\n \n vec4 finalColor = vec4(lit + specular + modeColor * 0.01, tex2d.a * materialDiffuse.a);\n \n // \u2500\u2500 Tone mapping (Reinhard) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = finalColor.rgb / (finalColor.rgb + vec3(1.0));\n\n // \u2500\u2500 Gamma correction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = pow(finalColor.rgb, vec3(1.0 / 2.2));\n\n // \u2500\u2500 Write fragment depth (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n gl_FragDepth = fragDepth;\n\n // \u2500\u2500 Output (blend in splat color contribution) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n fragColor = finalColor + vSplatColor * 0.001;\n}\n"; +var fragmentSource = "\n// \u2500\u2500 Precision qualifiers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\nprecision highp samplerCube;\nprecision highp sampler2DArray;\nprecision highp sampler2DShadow;\nprecision highp samplerCubeShadow;\nprecision highp sampler2DArrayShadow;\nprecision highp isampler2D;\nprecision highp isampler3D;\nprecision highp isamplerCube;\nprecision highp isampler2DArray;\nprecision highp usampler2D;\nprecision highp usampler3D;\nprecision highp usamplerCube;\nprecision highp usampler2DArray;\n\n// \u2500\u2500 Struct (must match vertex) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nstruct LightInfo {\n vec3 position;\n vec3 color;\n float intensity;\n};\n\nstruct Material {\n vec4 diffuse;\n vec4 specular;\n float shininess;\n LightInfo mainLight;\n};\n\n// \u2500\u2500 Uniform blocks (std140) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(std140) uniform TransformBlock {\n mat4 uModel;\n mat4 uViewProj;\n mat4 uNormalMatrix;\n};\n\nlayout(std140) uniform SceneBlock {\n vec4 uAmbientColor;\n float uTime;\n int uFrameCount;\n uint uFlags;\n};\n\n// \u2500\u2500 All sampler types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform sampler2D uSampler2D;\nuniform sampler3D uSampler3D;\nuniform samplerCube uSamplerCube;\nuniform sampler2DArray uSampler2DArray;\nuniform sampler2DShadow uSampler2DShadow;\nuniform samplerCubeShadow uSamplerCubeShadow;\nuniform sampler2DArrayShadow uSampler2DArrayShadow;\nuniform isampler2D uISampler2D;\nuniform isampler3D uISampler3D;\nuniform isamplerCube uISamplerCube;\nuniform isampler2DArray uISampler2DArray;\nuniform usampler2D uUSampler2D;\nuniform usampler3D uUSampler3D;\nuniform usamplerCube uUSamplerCube;\nuniform usampler2DArray uUSampler2DArray;\n\n// \u2500\u2500 Plain uniforms \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nuniform Material uMaterial;\nuniform float uCustomFloat;\n\n// \u2500\u2500 Fragment inputs (from vertex shader) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nin vec3 vWorldPos;\nin vec3 vNormal;\nin vec2 vUV;\nflat in int vVertexID;\nflat in uint vFlagsOut;\nsmooth in vec4 vColor;\ncentroid in vec2 vCentroidUV;\nin vec4 vSplatColor;\n\n// \u2500\u2500 Fragment output (MRT-capable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlayout(location = 0) out vec4 fragColor;\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst float PI = 3.14159265359;\nconst float EPSILON = 1e-6;\n\n// \u2500\u2500 Helper: Blinn-Phong \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 blinnPhong(vec3 N, vec3 L, vec3 V, vec3 lightColor, float shininess) {\n float NdotL = max(dot(N, L), 0.0);\n vec3 H = normalize(L + V);\n float NdotH = max(dot(N, H), 0.0);\n float specPower = pow(NdotH, shininess);\n return lightColor * (NdotL + specPower);\n}\n\n// \u2500\u2500 Helper: Fresnel-Schlick \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 fresnelSchlick(float cosTheta, vec3 F0) {\n return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);\n}\n\n// \u2500\u2500 Helper: normal mapping simulation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvec3 perturbNormal(vec3 N, vec3 texNormal) {\n // unpack from [0,1] to [-1,1]\n vec3 mapped = texNormal * 2.0 - 1.0;\n return normalize(N + mapped * 0.5);\n}\n\nvoid main() {\n // \u2500\u2500 Fragment built-in variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 fragCoord = gl_FragCoord;\n bool frontFacing = gl_FrontFacing;\n float fragDepth = gl_FragCoord.z;\n\n // \u2500\u2500 Derivative functions (fragment only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec2 dxUV = dFdx(vUV);\n vec2 dyUV = dFdy(vUV);\n vec2 fwUV = fwidth(vUV);\n \n // \u2500\u2500 Texture sampling: texture() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2d = texture(uSampler2D, vUV);\n vec4 tex3d = texture(uSampler3D, vec3(vUV, 0.0));\n vec4 texCube = texture(uSamplerCube, vNormal);\n vec4 texArr = texture(uSampler2DArray, vec3(vUV, 0.0));\n\n // \u2500\u2500 Texture sampling: textureLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dLod = textureLod(uSampler2D, vUV, 1.0);\n\n // \u2500\u2500 Texture sampling: textureOffset() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dOff = textureOffset(uSampler2D, vUV, ivec2(1, 0));\n\n // \u2500\u2500 Texture sampling: textureGrad() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dGrad = textureGrad(uSampler2D, vUV, dxUV, dyUV);\n /* textureProj not used in Babylon\n // \u2500\u2500 Texture sampling: textureProj() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProj = textureProj(uSampler2D, vec3(vUV, 1.0));\n vec4 tex2dProj4 = textureProj(uSampler2D, vec4(vUV, 0.0, 1.0));\n\n // \u2500\u2500 Texture sampling: textureProjLod() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 tex2dProjL = textureProjLod(uSampler2D, vec3(vUV, 1.0), 0.0);\n */\n // \u2500\u2500 Texture sampling: texelFetch() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 texCoord = ivec2(gl_FragCoord.xy);\n vec4 fetched = texelFetch(uSampler2D, texCoord, 0);\n\n // texelFetch coordinates are flipped vertically by the shader compiler, which has to clone\n // the coordinate expression to reference it twice. Cover the operand shapes that appear in\n // Babylon shaders beyond a plain symbol: a constructor, a binary expression, a nested\n // constructor over a float expression, a nested constructor over integer binaries, built-in\n // calls, and a non-constant lod. Integer multiply, divide, modulo and bitwise operators are\n // deliberately avoided: Babylon Native builds glslang and SPIRV-Cross in their WEBMIN\n // configurations, which do not support them, so they would fail for reasons that have nothing\n // to do with texel coordinates.\n int fetchIndex = int(gl_FragCoord.x) + int(gl_FragCoord.y);\n int fetchLod = fetchIndex - fetchIndex;\n ivec2 fetchSize = textureSize(uSampler2D, 0);\n vec4 fetchedCtor = texelFetch(uSampler2D, ivec2(gl_FragCoord.xy), 0);\n vec4 fetchedOffset = texelFetch(uSampler2D, texCoord + ivec2(1, 1), 0);\n vec4 fetchedScaled = texelFetch(uSampler2D, ivec2(vUV * vec2(fetchSize)), 0);\n vec4 fetchedNested = texelFetch(uSampler2D, ivec2(fetchSize.x - texCoord.x, fetchSize.y - texCoord.y), 0);\n vec4 fetchedCall = texelFetch(uSampler2D, ivec2(abs(texCoord.x), abs(texCoord.y)), 0);\n vec4 fetchedLod = texelFetch(uSampler2D, texCoord.yx, fetchLod);\n vec4 fetchedComplex = fetchedCtor + fetchedOffset + fetchedScaled + fetchedNested + fetchedCall + fetchedLod;\n \n // \u2500\u2500 Texture sampling: textureSize() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec2 size2d = textureSize(uSampler2D, 0);\n ivec3 size3d = textureSize(uSampler3D, 0);\n \n ivec2 sizeCube = textureSize(uSamplerCube, 0);\n /* No 2D array support\n ivec3 sizeArr = textureSize(uSampler2DArray, 0);\n\n // \u2500\u2500 Shadow sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float shadow2d = texture(uSampler2DShadow, vec3(vUV, 0.5));\n float shadowCube = texture(uSamplerCubeShadow, vec4(vNormal, 0.5));\n float shadowArr = texture(uSampler2DArrayShadow, vec4(vUV, 0.0, 0.5));\n\n // \u2500\u2500 Integer sampler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n ivec4 iTex2d = texture(uISampler2D, vUV);\n ivec4 iTex3d = texture(uISampler3D, vec3(vUV, 0.0));\n ivec4 iTexCube = texture(uISamplerCube, vNormal);\n ivec4 iTexArr = texture(uISampler2DArray, vec3(vUV, 0.0));\n uvec4 uTex2d = texture(uUSampler2D, vUV);\n uvec4 uTex3d = texture(uUSampler3D, vec3(vUV, 0.0));\n uvec4 uTexCube = texture(uUSamplerCube, vNormal);\n uvec4 uTexArr = texture(uUSampler2DArray, vec3(vUV, 0.0));\n */\n // \u2500\u2500 trunc / round (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float truncF = trunc(1.7);\n float roundF = round(1.5);\n float roundEven_ = roundEven(2.5);\n\n // \u2500\u2500 modf (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float integralPart;\n float fractionalPart = modf(3.75, integralPart);\n\n // \u2500\u2500 min/max/clamp on vec types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 minV = min(vNormal, vec3(0.5));\n vec3 maxV = max(vNormal, vec3(0.0));\n vec3 clampV = clamp(vNormal, vec3(0.0), vec3(1.0));\n \n // \u2500\u2500 mix with bvec selector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 mixBvec = mix(vec3(0.0), vec3(1.0), bvec3(true, false, true));\n\n // \u2500\u2500 Control flow: if / else with discard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (tex2d.a < 0.01) {\n discard;\n }\n\n // \u2500\u2500 Control flow: for loop with early exit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 lightAccum = vec3(0.0);\n for (int j = 0; j < 4; j++) {\n LightInfo lt;\n lt.position = vec3(float(j) * 3.0, 5.0, 0.0);\n lt.color = vec3(1.0, 0.9, 0.8);\n lt.intensity = 1.0 / (1.0 + float(j));\n\n vec3 L = normalize(lt.position - vWorldPos);\n vec3 V = normalize(-vWorldPos);\n lightAccum += blinnPhong(normalize(vNormal), L, V, lt.color, uMaterial.shininess) * lt.intensity;\n\n if (length(lightAccum) > 3.0) break;\n }\n \n // \u2500\u2500 Control flow: while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float decay = 1.0;\n int wc = 0;\n while (decay > 0.01 && wc < 10) {\n decay *= 0.7;\n wc++;\n }\n\n // \u2500\u2500 Control flow: do-while \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n float buildUp = 0.0;\n int dc = 0;\n do {\n buildUp += 0.1;\n dc++;\n } while (buildUp < 0.5 && dc < 10);\n \n // \u2500\u2500 Control flow: switch / case \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 modeColor = vec3(0.0);\n switch (vVertexID % 4) {\n case 0: modeColor = vec3(1.0, 0.0, 0.0); break;\n case 1: modeColor = vec3(0.0, 1.0, 0.0); break;\n case 2: modeColor = vec3(0.0, 0.0, 1.0); break;\n default: modeColor = vec3(1.0); break;\n }\n\n // \u2500\u2500 Bitwise on uint flags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n bool flag0 = (vFlagsOut & 1u) != 0u;\n bool flag1 = (vFlagsOut & 2u) != 0u;\n uint shifted = vFlagsOut << 1u;\n uint masked = vFlagsOut & 0xF0u;\n \n // \u2500\u2500 Front-facing conditional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 normal = frontFacing ? normalize(vNormal) : -normalize(vNormal);\n\n // \u2500\u2500 Fresnel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 viewDir = normalize(-vWorldPos);\n vec3 fresnel = fresnelSchlick(max(dot(normal, viewDir), 0.0), vec3(0.04));\n\n // \u2500\u2500 Normal perturbation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec3 perturbedN = perturbNormal(normal, tex2d.rgb);\n \n // \u2500\u2500 Final composition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n vec4 materialDiffuse = uMaterial.diffuse;\n vec4 materialSpecular = uMaterial.specular;\n vec3 albedo = tex2d.rgb * materialDiffuse.rgb * vColor.rgb;\n // using uAmbientColor.rgb crashes SpvBuilder because it assumes it's a single float\n // this case doesn't seem to be found anywhere in Babylon shader so won't fix\n vec3 ambient = /*uAmbientColor.rgb * */albedo;\n \n vec3 lit = ambient + lightAccum * albedo;\n vec3 specular = fresnel * materialSpecular.rgb;\n \n vec4 finalColor = vec4(lit + specular + modeColor * 0.01, tex2d.a * materialDiffuse.a);\n \n // \u2500\u2500 Tone mapping (Reinhard) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = finalColor.rgb / (finalColor.rgb + vec3(1.0));\n\n // \u2500\u2500 Gamma correction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n finalColor.rgb = pow(finalColor.rgb, vec3(1.0 / 2.2));\n\n // \u2500\u2500 Write fragment depth (ES 3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n gl_FragDepth = fragDepth;\n\n // \u2500\u2500 Output (blend in splat color contribution) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n fragColor = finalColor + vSplatColor * 0.001 + fetchedComplex * 0.0001;\n}\n"; + + + + + + + + + + + + + + + + + + + diff --git a/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts b/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts index bcb62581ae..cb1fca550c 100644 --- a/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts +++ b/Apps/UnitTests/JavaScript/src/tests.shaderCompilation.comprehensiveGLSL.ts @@ -826,6 +826,25 @@ void main() { // ── Texture sampling: texelFetch() ────────────────────────────── ivec2 texCoord = ivec2(gl_FragCoord.xy); vec4 fetched = texelFetch(uSampler2D, texCoord, 0); + + // texelFetch coordinates are flipped vertically by the shader compiler, which has to clone + // the coordinate expression to reference it twice. Cover the operand shapes that appear in + // Babylon shaders beyond a plain symbol: a constructor, a binary expression, a nested + // constructor over a float expression, a nested constructor over integer binaries, built-in + // calls, and a non-constant lod. Integer multiply, divide, modulo and bitwise operators are + // deliberately avoided: Babylon Native builds glslang and SPIRV-Cross in their WEBMIN + // configurations, which do not support them, so they would fail for reasons that have nothing + // to do with texel coordinates. + int fetchIndex = int(gl_FragCoord.x) + int(gl_FragCoord.y); + int fetchLod = fetchIndex - fetchIndex; + ivec2 fetchSize = textureSize(uSampler2D, 0); + vec4 fetchedCtor = texelFetch(uSampler2D, ivec2(gl_FragCoord.xy), 0); + vec4 fetchedOffset = texelFetch(uSampler2D, texCoord + ivec2(1, 1), 0); + vec4 fetchedScaled = texelFetch(uSampler2D, ivec2(vUV * vec2(fetchSize)), 0); + vec4 fetchedNested = texelFetch(uSampler2D, ivec2(fetchSize.x - texCoord.x, fetchSize.y - texCoord.y), 0); + vec4 fetchedCall = texelFetch(uSampler2D, ivec2(abs(texCoord.x), abs(texCoord.y)), 0); + vec4 fetchedLod = texelFetch(uSampler2D, texCoord.yx, fetchLod); + vec4 fetchedComplex = fetchedCtor + fetchedOffset + fetchedScaled + fetchedNested + fetchedCall + fetchedLod; // ── Texture sampling: textureSize() ───────────────────────────── ivec2 size2d = textureSize(uSampler2D, 0); @@ -951,7 +970,7 @@ void main() { gl_FragDepth = fragDepth; // ── Output (blend in splat color contribution) ──────────────────── - fragColor = finalColor + vSplatColor * 0.001; + fragColor = finalColor + vSplatColor * 0.001 + fetchedComplex * 0.0001; } `; diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h index 8f4e2477c4..60d3bd69db 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/Texture.h @@ -22,6 +22,9 @@ namespace Babylon::Graphics void Create2D(uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags); void Update2D(uint16_t layer, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch = UINT16_MAX); + void Create3D(uint16_t width, uint16_t height, uint16_t depth, bool hasMips, bgfx::TextureFormat::Enum format, uint64_t flags); + void Update3D(uint8_t mip, uint16_t x, uint16_t y, uint16_t z, uint16_t width, uint16_t height, uint16_t depth, const bgfx::Memory* mem); + void CreateCube(uint16_t size, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags); void UpdateCube(uint16_t layer, uint8_t side, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch = UINT16_MAX); @@ -31,7 +34,10 @@ namespace Babylon::Graphics uint16_t Width() const; uint16_t Height() const; bool HasMips() const; + bool IsCube() const; + bool Is3D() const; uint16_t NumLayers() const; + uint16_t Depth() const; bgfx::TextureFormat::Enum Format() const; uint64_t Flags() const; uint32_t SamplerFlags() const; @@ -50,12 +56,21 @@ namespace Babylon::Graphics void BlitViewId(bgfx::ViewId viewId) { m_blitViewId = viewId; } private: + // Resets every piece of shape metadata to its default. Each Create*/Attach calls this + // before assigning the subset that applies to it, so a field a given path does not set + // (m_depth is only meaningful for Create3D) cannot survive from the previous, differently + // shaped texture this object described. + void ResetMetadata(); + bgfx::TextureHandle m_handle{bgfx::kInvalidHandle}; bool m_ownsHandle{false}; uint16_t m_width{0}; uint16_t m_height{0}; bool m_hasMips{false}; + bool m_isCube{false}; + bool m_is3D{false}; uint16_t m_numLayers{0}; + uint16_t m_depth{0}; bgfx::TextureFormat::Enum m_format{bgfx::TextureFormat::Enum::Unknown}; uint64_t m_flags{BGFX_TEXTURE_NONE}; uint32_t m_samplerFlags{BGFX_SAMPLER_NONE}; diff --git a/Core/Graphics/Source/Texture.cpp b/Core/Graphics/Source/Texture.cpp index a99006e7ea..bdd986273f 100644 --- a/Core/Graphics/Source/Texture.cpp +++ b/Core/Graphics/Source/Texture.cpp @@ -43,9 +43,23 @@ namespace Babylon::Graphics return bgfx::isValid(m_handle); } + void Texture::ResetMetadata() + { + m_width = 0; + m_height = 0; + m_depth = 0; + m_hasMips = false; + m_isCube = false; + m_is3D = false; + m_numLayers = 0; + m_format = bgfx::TextureFormat::Enum::Unknown; + m_flags = BGFX_TEXTURE_NONE; + } + void Texture::Create2D(uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); + ResetMetadata(); // make sure render targets are filled with 0 : https://registry.khronos.org/webgl/specs/latest/1.0/#TEXIMAGE2D const auto* mem = (flags & BGFX_TEXTURE_RT) ? GetZeroImageMemory(width, height, hasMips, numLayers, format) : nullptr; @@ -71,9 +85,37 @@ namespace Babylon::Graphics bgfx::updateTexture2D(m_handle, layer, mip, x, y, width, height, mem, pitch); } + void Texture::Create3D(uint16_t width, uint16_t height, uint16_t depth, bool hasMips, bgfx::TextureFormat::Enum format, uint64_t flags) + { + Dispose(); + ResetMetadata(); + + m_handle = bgfx::createTexture3D(width, height, depth, hasMips, format, flags); + if (!bgfx::isValid(m_handle)) + { + throw std::runtime_error{"Failed to create 3D texture"}; + } + + m_ownsHandle = true; + m_width = width; + m_height = height; + m_depth = depth; + m_hasMips = hasMips; + m_numLayers = 1; + m_format = format; + m_flags = flags; + m_is3D = true; + } + + void Texture::Update3D(uint8_t mip, uint16_t x, uint16_t y, uint16_t z, uint16_t width, uint16_t height, uint16_t depth, const bgfx::Memory* mem) + { + bgfx::updateTexture3D(m_handle, mip, x, y, z, width, height, depth, mem); + } + void Texture::CreateCube(uint16_t size, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); + ResetMetadata(); m_handle = bgfx::createTextureCube(size, hasMips, numLayers, format, flags); m_ownsHandle = true; @@ -83,6 +125,7 @@ namespace Babylon::Graphics m_numLayers = numLayers; m_format = format; m_flags = flags; + m_isCube = true; } void Texture::UpdateCube(uint16_t layer, uint8_t side, uint8_t mip, uint16_t x, uint16_t y, uint16_t width, uint16_t height, const bgfx::Memory* mem, uint16_t pitch) @@ -93,6 +136,7 @@ namespace Babylon::Graphics void Texture::Attach(bgfx::TextureHandle handle, bool ownsHandle, uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, uint64_t flags) { Dispose(); + ResetMetadata(); assert(bgfx::isValid(handle)); m_handle = handle; @@ -125,11 +169,26 @@ namespace Babylon::Graphics return m_hasMips; } + bool Texture::IsCube() const + { + return m_isCube; + } + + bool Texture::Is3D() const + { + return m_is3D; + } + uint16_t Texture::NumLayers() const { return m_numLayers; } + uint16_t Texture::Depth() const + { + return m_depth; + } + bgfx::TextureFormat::Enum Texture::Format() const { return m_format; diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 5a3b977c26..270fcae0c3 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #ifdef BABYLON_NATIVE_NATIVEENGINE_TEST_HOOKS #include @@ -288,17 +289,34 @@ namespace Babylon if (generateMips) { - if (image->m_format == bimg::TextureFormat::RGB8) - { - bimg::ImageContainer* oldImage{image}; - image = bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA8, *image, false); - bimg::imageFree(oldImage); - } - else if (image->m_format == bimg::TextureFormat::RG16F || image->m_format == bimg::TextureFormat::RGBA16F || image->m_format == bimg::TextureFormat::RGBA16 || image->m_format == bimg::TextureFormat::R16) + // bimg::imageGenerateMips only supports RGBA8 and RGBA32F source data; for any + // other format it returns NULL. Convert first so mip generation (and the + // subsequent GPU upload) succeeds. High-precision / float formats are promoted to + // RGBA32F to preserve range; everything else - including single/dual-channel, + // RGB8/BGRA8 and block-compressed formats such as BC1/DXT1 - is decoded to RGBA8. + // Without this, e.g. a BC1 texture with generateMips produced a NULL image that the + // caller dereferenced, crashing the process. + if (image->m_format != bimg::TextureFormat::RGBA8 && + image->m_format != bimg::TextureFormat::RGBA32F) { + const bimg::TextureFormat::Enum dstFormat = + (image->m_format == bimg::TextureFormat::R16 || + image->m_format == bimg::TextureFormat::R16F || + image->m_format == bimg::TextureFormat::RG16F || + image->m_format == bimg::TextureFormat::RG32F || + image->m_format == bimg::TextureFormat::RGBA16 || + image->m_format == bimg::TextureFormat::RGBA16F) + ? bimg::TextureFormat::RGBA32F + : bimg::TextureFormat::RGBA8; + bimg::ImageContainer* oldImage{image}; - image = bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *image, false); + image = bimg::imageConvert(&allocator, dstFormat, *image, false); bimg::imageFree(oldImage); + + if (image == nullptr) + { + return nullptr; + } } bimg::ImageContainer* oldImage{image}; @@ -306,7 +324,6 @@ namespace Babylon bimg::imageFree(oldImage); } - assert(image != nullptr); return image; } @@ -419,6 +436,241 @@ namespace Babylon } } } + // Parse a single self-contained cubemap container (e.g. .dds / .ktx / + // .ktx2) that already holds all six faces and their mip chain. bimg + // decodes these natively, so there is no need to split into six images + // on the JS side. Unlike ParseImage (which targets single-face 2D images + // and asserts !m_cubeMap), this keeps the container as-is. + bimg::ImageContainer* ParseCubeImage(bx::AllocatorI& allocator, gsl::span data) + { + bx::ErrorIgnore parseError; + bimg::ImageContainer* image{bimg::imageParse(&allocator, data.data(), static_cast(data.size()), bimg::TextureFormat::Count, &parseError)}; + if (image == nullptr) + { + throw std::runtime_error{"Failed to parse cube image."}; + } + + if (!image->m_cubeMap) + { + bimg::imageFree(image); + throw std::runtime_error{"Image is not a cubemap."}; + } + + return image; + } + + // Port of Babylon.js CubeMapToSphericalPolynomialTools.ConvertCubeMapToSphericalPolynomial. + // Prefiltered .dds environments need diffuse-IBL spherical harmonics, which Babylon's WebGL + // path computes on the CPU from the top-mip faces. The native engine cannot read cube faces + // back from the GPU (_readTexturePixels throws for cube faces), so we compute the harmonics + // here from the bimg-decoded top mip. Returns the 9x3 polynomial coefficients in + // SphericalPolynomial.FromArray order: x, y, z, xx, yy, zz, yz, zx, xy. + std::array ComputeCubeSphericalPolynomial(bx::AllocatorI& allocator, bimg::ImageContainer* image) + { + std::array result{}; + + bimg::ImageContainer* f32{bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *image, false)}; + if (f32 == nullptr) + { + return result; + } + + const uint32_t size{f32->m_width}; + constexpr double pi{3.14159265358979323846}; + + // Face orientations matching Babylon's _FileFaces, indexed by bimg cube side order + // (+X, -X, +Y, -Y, +Z, -Z): worldAxisForNormal, worldAxisForFileX, worldAxisForFileY. + struct FaceAxes + { + double n[3]; + double fx[3]; + double fy[3]; + }; + static const FaceAxes faces[6] = { + {{1, 0, 0}, {0, 0, -1}, {0, -1, 0}}, // +X right + {{-1, 0, 0}, {0, 0, 1}, {0, -1, 0}}, // -X left + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, // +Y up + {{0, -1, 0}, {1, 0, 0}, {0, 0, -1}}, // -Y down + {{0, 0, 1}, {1, 0, 0}, {0, -1, 0}}, // +Z front + {{0, 0, -1}, {-1, 0, 0}, {0, -1, 0}}, // -Z back + }; + + const double shConst[9] = { + std::sqrt(1.0 / (4.0 * pi)), + -std::sqrt(3.0 / (4.0 * pi)), + std::sqrt(3.0 / (4.0 * pi)), + -std::sqrt(3.0 / (4.0 * pi)), + std::sqrt(15.0 / (4.0 * pi)), + -std::sqrt(15.0 / (4.0 * pi)), + std::sqrt(5.0 / (16.0 * pi)), + -std::sqrt(15.0 / (4.0 * pi)), + std::sqrt(15.0 / (16.0 * pi)), + }; + const double cosKernel[9] = {pi, 2.0 * pi / 3.0, 2.0 * pi / 3.0, 2.0 * pi / 3.0, pi / 4.0, pi / 4.0, pi / 4.0, pi / 4.0, pi / 4.0}; + + const auto areaElement = [](double x, double y) { return std::atan2(x * y, std::sqrt(x * x + y * y + 1.0)); }; + + double sh[9][3] = {}; + double totalSolidAngle{0.0}; + + const double du{2.0 / static_cast(size)}; + const double halfTexel{0.5 * du}; + const double minUV{halfTexel - 1.0}; + const double maxHdri{4096.0}; + + for (uint16_t side = 0; side < 6; ++side) + { + bimg::ImageMip mip{}; + if (!bimg::imageGetRawData(*f32, side, 0, f32->m_data, f32->m_size, mip)) + { + continue; + } + + const float* data{reinterpret_cast(mip.m_data)}; + const FaceAxes& f{faces[side]}; + + double v{minUV}; + for (uint32_t y = 0; y < size; ++y) + { + double u{minUV}; + for (uint32_t x = 0; x < size; ++x) + { + double dir[3] = { + f.fx[0] * u + f.fy[0] * v + f.n[0], + f.fx[1] * u + f.fy[1] * v + f.n[1], + f.fx[2] * u + f.fy[2] * v + f.n[2], + }; + const double len{std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2])}; + dir[0] /= len; + dir[1] /= len; + dir[2] /= len; + + const double deltaSolidAngle{ + areaElement(u - halfTexel, v - halfTexel) - + areaElement(u - halfTexel, v + halfTexel) - + areaElement(u + halfTexel, v - halfTexel) + + areaElement(u + halfTexel, v + halfTexel)}; + + const size_t idx{(static_cast(y) * size + x) * 4}; + double rgb[3] = {data[idx + 0], data[idx + 1], data[idx + 2]}; + for (int c = 0; c < 3; ++c) + { + if (std::isnan(rgb[c])) + { + rgb[c] = 0.0; + } + rgb[c] = rgb[c] < 0.0 ? 0.0 : (rgb[c] > maxHdri ? maxHdri : rgb[c]); + } + + const double trig[9] = { + 1.0, + dir[1], + dir[2], + dir[0], + dir[0] * dir[1], + dir[1] * dir[2], + 3.0 * dir[2] * dir[2] - 1.0, + dir[0] * dir[2], + dir[0] * dir[0] - dir[1] * dir[1], + }; + for (int lm = 0; lm < 9; ++lm) + { + const double basis{shConst[lm] * trig[lm] * deltaSolidAngle}; + sh[lm][0] += rgb[0] * basis; + sh[lm][1] += rgb[1] * basis; + sh[lm][2] += rgb[2] * basis; + } + totalSolidAngle += deltaSolidAngle; + u += du; + } + v += du; + } + } + + bimg::imageFree(f32); + + if (totalSolidAngle <= 0.0) + { + return result; + } + + // scaleInPlace(correction) + convertIncidentRadianceToIrradiance + convertIrradianceToLambertianRadiance. + const double correction{(4.0 * pi) / totalSolidAngle}; + for (int lm = 0; lm < 9; ++lm) + { + const double scale{correction * cosKernel[lm] / pi}; + sh[lm][0] *= scale; + sh[lm][1] *= scale; + sh[lm][2] *= scale; + } + + // SphericalPolynomial.FromHarmonics (updateFromHarmonics then *1/pi). + for (int c = 0; c < 3; ++c) + { + const double l00{sh[0][c]}, l1_1{sh[1][c]}, l10{sh[2][c]}, l11{sh[3][c]}; + const double l2_2{sh[4][c]}, l2_1{sh[5][c]}, l20{sh[6][c]}, l21{sh[7][c]}, l22{sh[8][c]}; + const double invPi{1.0 / pi}; + result[0 * 3 + c] = static_cast(-1.02333 * l11 * invPi); // x + result[1 * 3 + c] = static_cast(-1.02333 * l1_1 * invPi); // y + result[2 * 3 + c] = static_cast(1.02333 * l10 * invPi); // z + result[3 * 3 + c] = static_cast((0.886277 * l00 - 0.247708 * l20 + 0.429043 * l22) * invPi); // xx + result[4 * 3 + c] = static_cast((0.886277 * l00 - 0.247708 * l20 - 0.429043 * l22) * invPi); // yy + result[5 * 3 + c] = static_cast((0.886277 * l00 + 0.495417 * l20) * invPi); // zz + result[6 * 3 + c] = static_cast(-0.858086 * l2_1 * invPi); // yz + result[7 * 3 + c] = static_cast(-0.858086 * l21 * invPi); // zx + result[8 * 3 + c] = static_cast(0.858086 * l2_2 * invPi); // xy + } + + return result; + } + + void LoadCubeTextureFromContainer(Graphics::Texture* texture, bimg::ImageContainer* image, bool srgb) + { + assert(image->m_cubeMap); + assert(image->m_width == image->m_height); + const uint32_t size{image->m_width}; + + if (texture->IsValid()) + { + if (texture->Width() != size || texture->Height() != size) + { + bimg::imageFree(image); + throw std::runtime_error{"Cannot update texture from image of different size"}; + } + } + else + { + const bool hasMips{image->m_numMips > 1}; + const bgfx::TextureFormat::Enum format{Cast(image->m_format)}; + const uint64_t flags{srgb ? BGFX_TEXTURE_SRGB : BGFX_TEXTURE_NONE}; + texture->CreateCube(static_cast(size), hasMips, 1, format, flags); + } + + // Every (side, mip) view points into the single container's backing + // store, so the allocation is released exactly once, after bgfx has + // consumed the final upload. + const uint8_t numMips{static_cast(image->m_numMips)}; + for (uint8_t side = 0; side < 6; ++side) + { + for (uint8_t mip = 0; mip < numMips; ++mip) + { + bimg::ImageMip imageMip{}; + if (bimg::imageGetRawData(*image, side, mip, image->m_data, image->m_size, imageMip)) + { + bgfx::ReleaseFn releaseFn{}; + if (side == 5 && mip == numMips - 1) + { + releaseFn = [](void*, void* userData) { + bimg::imageFree(static_cast(userData)); + }; + } + + const bgfx::Memory* mem{bgfx::makeRef(imageMip.m_data, imageMip.m_size, releaseFn, image)}; + texture->UpdateCube(0, side, mip, 0, 0, static_cast(imageMip.m_width), static_cast(imageMip.m_height), mem); + } + } + } + } #endif // BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES auto RenderTargetSamplesToBgfxMsaaFlag(uint32_t renderTargetSamples) @@ -718,7 +970,10 @@ namespace Babylon InstanceMethod("initializeTexture", &NativeEngine::InitializeTexture), InstanceMethod("loadTexture", &NativeEngine::LoadTexture), InstanceMethod("loadRawTexture", &NativeEngine::LoadRawTexture), + InstanceMethod("updateTextureData", &NativeEngine::UpdateTextureData), InstanceMethod("loadRawTexture2DArray", &NativeEngine::LoadRawTexture2DArray), + InstanceMethod("loadRawTexture3D", &NativeEngine::LoadRawTexture3D), + InstanceMethod("updateTextureDirectly", &NativeEngine::UpdateTextureDirectly), InstanceMethod("loadCubeTexture", &NativeEngine::LoadCubeTexture), InstanceMethod("loadCubeTextureWithMips", &NativeEngine::LoadCubeTextureWithMips), InstanceMethod("getTextureWidth", &NativeEngine::GetTextureWidth), @@ -1392,6 +1647,10 @@ namespace Babylon arcana::trace_region loadRegion{"NativeEngine::LoadTexture"}; bimg::ImageContainer* image{ParseImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan)}; image = PrepareImage(Graphics::DeviceContext::GetDefaultAllocator(), image, invertY, srgb, generateMips); + if (image == nullptr) + { + throw std::runtime_error{"Failed to prepare image for texture (unsupported format or image conversion failure)."}; + } LoadTextureFromImage(texture, image, srgb); }) .then(m_runtimeScheduler, *m_cancellationSource, [dataRef{Napi::Persistent(data)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected result) { @@ -1454,6 +1713,105 @@ namespace Babylon #endif } + void NativeEngine::UpdateTextureData(const Napi::CallbackInfo& info) + { + const auto texture{info[0].As>().Get()}; + const auto data{info[1].As()}; + const auto x{static_cast(info[2].As().Uint32Value())}; + const auto y{static_cast(info[3].As().Uint32Value())}; + const auto width{static_cast(info[4].As().Uint32Value())}; + const auto height{static_cast(info[5].As().Uint32Value())}; + const uint16_t layer{info.Length() > 6 && !info[6].IsUndefined() ? static_cast(info[6].As().Uint32Value()) : static_cast(0)}; + const uint8_t mip{info.Length() > 7 && !info[7].IsUndefined() ? static_cast(info[7].As().Uint32Value()) : static_cast(0)}; + const bool invertY{info.Length() > 8 && !info[8].IsUndefined() ? info[8].As().Value() : false}; + + if (texture == nullptr || !texture->IsValid()) + { + throw Napi::Error::New(info.Env(), "updateTextureData called on an invalid texture"); + } + + // Validate the (JS-controlled) update rectangle against the mip-level extents before handing it to + // bgfx, so an out-of-range origin/size can't drive an out-of-bounds read of the source buffer below. + uint32_t mipWidth{static_cast(texture->Width()) >> mip}; + uint32_t mipHeight{static_cast(texture->Height()) >> mip}; + if (mipWidth == 0) + { + mipWidth = 1; + } + if (mipHeight == 0) + { + mipHeight = 1; + } + const uint16_t numLayers{texture->NumLayers() > 0 ? texture->NumLayers() : static_cast(1)}; + // A cube texture is addressed by 6 faces per array layer (bgfx side index 0-5). The JS side passes the + // face in the same "layer" argument used for 2D-array slices, so the valid range is 6*numLayers. + const uint16_t maxLayers{texture->IsCube() ? static_cast(6 * numLayers) : numLayers}; + if (width == 0 || height == 0 || + static_cast(x) + width > mipWidth || + static_cast(y) + height > mipHeight || + layer >= maxLayers) + { + throw Napi::Error::New(info.Env(), "updateTextureData region is out of bounds"); + } + + // Size of the source rectangle in the texture's own format. bgfx is always linked (bimg is not, in + // builds without image loading), so size the upload with bgfx::calcTextureSize rather than bimg. + bgfx::TextureInfo textureInfo; + bgfx::calcTextureSize(textureInfo, width, height, 1, false, false, 1, texture->Format()); + const uint32_t requiredSize{textureInfo.storageSize}; + if (requiredSize == 0 || data.ByteLength() < requiredSize) + { + throw Napi::Error::New(info.Env(), "updateTextureData data size does not match width, height, and texture format"); + } + + // Block-compressed formats are rejected outright. Their payload is a grid of 4x4 (or larger) + // blocks, so the row-reversal below would be wrong twice over: the stride it derives is + // requiredSize / height, which is a fraction of a block row rather than a whole one (an 8-byte + // BC1 block at 4x4 yields 2), and even with the right stride, mirroring block rows cannot flip a + // texture vertically without re-encoding the texel rows packed inside each block. bgfx also + // requires block-aligned x/y/width/height for these formats. The base upload path + // (loadTexture -> PrepareImage) already handles compressed data, so this is not a capability loss. + // In bgfx's TextureFormat enum every block-compressed format sorts before Unknown, which lets + // this be checked without bimg. + if (texture->Format() < bgfx::TextureFormat::Unknown) + { + throw Napi::Error::New(info.Env(), "updateTextureData does not support block-compressed texture formats"); + } + + const auto bytes{static_cast(data.ArrayBuffer().Data()) + data.ByteOffset()}; + + // Match the vertical orientation the base upload applies (PrepareImage flips the whole image when + // originBottomLeft ? invertY : !invertY). To land a sub-rectangle at the same place, flip it to the + // mirrored Y origin and reverse its rows so row 0 of the source lines up with the flipped base data. + const bool flip{bgfx::getCaps()->originBottomLeft ? invertY : !invertY}; + const uint16_t targetY{flip ? static_cast(mipHeight - y - height) : y}; + const bgfx::Memory* mem{bgfx::alloc(requiredSize)}; + if (flip) + { + const uint32_t rowBytes{requiredSize / height}; + for (uint16_t row = 0; row < height; ++row) + { + std::memcpy(mem->data + static_cast(row) * rowBytes, bytes + static_cast(height - 1 - row) * rowBytes, rowBytes); + } + } + else + { + std::memcpy(mem->data, bytes, requiredSize); + } + if (texture->IsCube()) + { + // bgfx addresses a cube texture by (array layer, side 0-5), but the JS side packs both into the + // single "layer" argument it also uses for 2D-array slices (6 consecutive faces per array layer, + // which is what the bounds check above allows). Decompose it, otherwise a cube-array face index + // above 5 would be forwarded as an out-of-range side and every update would land on array layer 0. + texture->UpdateCube(static_cast(layer / 6), static_cast(layer % 6), mip, x, targetY, width, height, mem); + } + else + { + texture->Update2D(layer, mip, x, targetY, width, height, mem); + } + } + void NativeEngine::LoadRawTexture2DArray(const Napi::CallbackInfo& info) { #ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES @@ -1546,6 +1904,213 @@ namespace Babylon #endif } + void NativeEngine::LoadRawTexture3D(const Napi::CallbackInfo& info) + { +#ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES + throw Napi::Error::New(info.Env(), "Image loading is disabled in this build (BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES=OFF)."); +#else + const auto texture{info[0].As>().Get()}; + const auto data = info[1].As(); + const auto rawWidth{info[2].As().Uint32Value()}; + const auto rawHeight{info[3].As().Uint32Value()}; + const auto rawDepth{info[4].As().Uint32Value()}; + const auto format{static_cast(info[5].As().Uint32Value())}; + const auto generateMips = info[6].As().Value(); + const auto invertY = info[7].As().Value(); + + if (generateMips) + { + throw Napi::Error::New(Env(), "Texture 3D currently do not support mipmaps."); + } + + if (invertY) + { + throw Napi::Error::New(Env(), "Texture 3D currently do not support invert Y."); + } + + // width/height/depth originate from JS. Validate the raw 32-bit values against the GPU limits + // before narrowing to uint16_t, otherwise an out-of-range value (e.g. 70000) would wrap into an + // in-range uint16_t and slip past this check, driving an oversized allocation or an out-of-bounds + // read in the renderer. + const auto maxTextureSize = bgfx::getCaps()->limits.maxTextureSize; + if (rawWidth == 0 || rawHeight == 0 || rawDepth == 0 || + rawWidth > maxTextureSize || + rawHeight > maxTextureSize || + rawDepth > maxTextureSize) + { + throw Napi::Error::New(Env(), "Invalid width, height, or depth for the 3D texture."); + } + + const auto width{static_cast(rawWidth)}; + const auto height{static_cast(rawHeight)}; + const auto depth{static_cast(rawDepth)}; + + uint64_t flags{BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE}; + texture->Create3D(width, height, depth, false, Cast(format), flags); + + if (!data.IsNull()) + { + // imageGetSize returns the full size in 64-bit; compare against the 64-bit byte length so a + // crafted width/height/depth cannot wrap the expected size and slip an undersized buffer past + // this check. A 3D texture is a single volume (numLayers = 1) whose depth is the 3rd argument. + const uint64_t expectedSize{bimg::imageGetSize(nullptr, width, height, depth, false, false, 1, format)}; + if (expectedSize == 0 || static_cast(data.ByteLength()) != expectedSize) + { + throw Napi::Error::New(Env(), "The data size does not match width, height, depth and format"); + } + + uint8_t* dataPtr = static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(); + const size_t dataSize = data.ByteLength(); + + // bgfx::Memory uses a 32-bit size; reject a payload that would be truncated by the bgfx::copy + // cast below. + if (dataSize > UINT32_MAX) + { + throw Napi::Error::New(Env(), "The 3D texture volume size is too large."); + } + + // This is required since BGFX must manage the memory backing the update. + const bgfx::Memory* dataCopy = bgfx::copy(dataPtr, static_cast(dataSize)); + texture->Update3D(0, 0, 0, 0, width, height, depth, dataCopy); + } +#endif + } + + // Implements the shared JS texture-loader sink (Babylon's _uploadDataToTextureDirectly / + // _uploadCompressedDataToTextureDirectly), letting DDS/KTX/KTX2/Basis/IES/HDR/EXR/TGA load + // through the same loaders WebGL/WebGPU use. The loaders upload one (face, mip) at a time, + // WebGL texImage2D-style; bgfx instead needs the whole texture allocated before any update, + // so the texture is created lazily on the first upload. + void NativeEngine::UpdateTextureDirectly(const Napi::CallbackInfo& info) + { +#ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES + throw Napi::Error::New(info.Env(), "Image loading is disabled in this build (BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES=OFF)."); +#else + const auto texture{info[0].As>().Get()}; + const auto data{info[1].As()}; + const auto faceIndexValue{info[2].As().Uint32Value()}; + const auto lodValue{info[3].As().Uint32Value()}; + const auto baseWidth{info[4].As().Uint32Value()}; + const auto baseHeight{info[5].As().Uint32Value()}; + const auto mipWidth{info[6].As().Uint32Value()}; + const auto mipHeight{info[7].As().Uint32Value()}; + const auto formatValue{info[8].As().Uint32Value()}; + const auto isCube{info[9].As().Value()}; + const auto hasMips{info[10].As().Value()}; + const auto invertY{info[11].As().Value()}; + + // Validate the JS-provided dimensions against GPU limits before narrowing to uint16_t, + // so an out-of-range value can't wrap into an in-range one and drive an OOB read/upload. + const auto maxTextureSize = bgfx::getCaps()->limits.maxTextureSize; + if (baseWidth == 0 || baseHeight == 0 || mipWidth == 0 || mipHeight == 0 || + baseWidth > maxTextureSize || baseHeight > maxTextureSize || + mipWidth > maxTextureSize || mipHeight > maxTextureSize) + { + throw Napi::Error::New(Env(), "Invalid base or mip dimensions for the texture."); + } + + // The format is a raw JS enum value; validate it before narrowing so an out-of-range value + // can't index past bimg/bgfx's format tables in Create*/imageGetSize (OOB read). + if (formatValue >= bimg::TextureFormat::Count) + { + throw Napi::Error::New(Env(), "Invalid texture format."); + } + const auto format{static_cast(formatValue)}; + + // Cube textures must be square and address one of the six faces; 2D textures have a single + // face. Validate the JS-provided face index before narrowing it to uint8_t. + if (isCube) + { + if (baseWidth != baseHeight || mipWidth != mipHeight) + { + throw Napi::Error::New(Env(), "Cube texture dimensions must be square."); + } + if (faceIndexValue >= 6) + { + throw Napi::Error::New(Env(), "Cube face index must be in the range [0, 5]."); + } + } + else if (faceIndexValue != 0) + { + throw Napi::Error::New(Env(), "Face index must be 0 for a 2D texture."); + } + + // The lod must address a real mip level: level 0 only when the texture has no mipmaps, + // otherwise within the mip chain implied by the base dimensions. + uint32_t numMips{1}; + if (hasMips) + { + uint32_t levelDim{baseWidth > baseHeight ? baseWidth : baseHeight}; + while (levelDim > 1) + { + levelDim >>= 1; + ++numMips; + } + } + if (lodValue >= numMips) + { + throw Napi::Error::New(Env(), "Lod exceeds the texture's mip level count."); + } + + // The mip dimensions must match the level implied by (base >> lod); otherwise bgfx would + // update a region that doesn't match the allocated mip level (asserts / UB on some backends). + const uint32_t shiftedWidth{baseWidth >> lodValue}; + const uint32_t shiftedHeight{baseHeight >> lodValue}; + const uint32_t expectedMipWidth{shiftedWidth > 1 ? shiftedWidth : 1}; + const uint32_t expectedMipHeight{shiftedHeight > 1 ? shiftedHeight : 1}; + if (mipWidth != expectedMipWidth || mipHeight != expectedMipHeight) + { + throw Napi::Error::New(Env(), "Mip dimensions do not match the base dimensions and lod."); + } + + if (!texture->IsValid()) + { + if (isCube) + { + texture->CreateCube(static_cast(baseWidth), hasMips, 1, Cast(format), BGFX_TEXTURE_NONE); + } + else + { + texture->Create2D(static_cast(baseWidth), static_cast(baseHeight), hasMips, 1, Cast(format), BGFX_TEXTURE_NONE); + } + } + + const uint64_t expectedSize{bimg::imageGetSize(nullptr, static_cast(mipWidth), static_cast(mipHeight), 1, false, false, 1, format)}; + if (expectedSize == 0 || static_cast(data.ByteLength()) != expectedSize) + { + throw Napi::Error::New(Env(), "The data size does not match mip dimensions and format."); + } + + // bgfx::copy takes a 32-bit size; reject anything that would truncate. + if (expectedSize > 0xFFFFFFFFull) + { + throw Napi::Error::New(Env(), "Texture upload size exceeds the maximum supported size."); + } + + const auto bytes{static_cast(data.ArrayBuffer().Data()) + data.ByteOffset()}; + // bgfx must own the upload buffer (released asynchronously after the GPU consumes it). + const bgfx::Memory* mem{bgfx::copy(bytes, static_cast(data.ByteLength()))}; + + // Match the existing loader flip conventions: cube faces flip only on origin-bottom-left + // (OpenGL), like LoadCubeTextureFromImages; 2D follows the raw/loadTexture convention. + // Compressed block data cannot be row-flipped. + const bool flip{isCube ? bgfx::getCaps()->originBottomLeft : (bgfx::getCaps()->originBottomLeft ? invertY : !invertY)}; + if (flip && !bimg::isCompressed(format)) + { + FlipImage({mem->data, mem->size}, static_cast(mipHeight)); + } + + if (isCube) + { + texture->UpdateCube(0, static_cast(faceIndexValue), static_cast(lodValue), 0, 0, static_cast(mipWidth), static_cast(mipHeight), mem); + } + else + { + texture->Update2D(0, static_cast(lodValue), 0, 0, static_cast(mipWidth), static_cast(mipHeight), mem); + } +#endif + } + void NativeEngine::LoadCubeTexture(const Napi::CallbackInfo& info) { #ifndef BABYLON_NATIVE_PLUGIN_NATIVEENGINE_LOAD_IMAGES @@ -1559,6 +2124,44 @@ namespace Babylon const auto onSuccess{info[5].As()}; const auto onError{info[6].As()}; + // A single buffer means a self-contained cubemap container (.dds / .ktx / + // .ktx2) that already holds all six faces and their mip chain; hand it to + // bimg directly instead of expecting six pre-split face images. + if (data.Length() == 1) + { + const auto typedArray{data[0u].As()}; + const auto dataSpan{gsl::make_span(static_cast(typedArray.ArrayBuffer().Data()) + typedArray.ByteOffset(), typedArray.ByteLength())}; + auto dataRef{Napi::Persistent(typedArray)}; + arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, [dataSpan]() { + return ParseCubeImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan); + }) + .then(arcana::inline_scheduler, *m_cancellationSource, [texture, srgb, cancellationSource{m_cancellationSource}](bimg::ImageContainer* image) { + // Compute the spherical harmonics from the decoded top mip before the upload + // hands the container's memory to bgfx. + auto sphericalPolynomial = ComputeCubeSphericalPolynomial(Graphics::DeviceContext::GetDefaultAllocator(), image); + LoadCubeTextureFromContainer(texture, image, srgb); + return sphericalPolynomial; + }) + .then(m_runtimeScheduler, *m_cancellationSource, [dataRef{std::move(dataRef)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected, std::exception_ptr> result) { + if (result.has_error()) + { + onErrorRef.Call({}); + } + else + { + const auto& sphericalPolynomial{result.value()}; + auto array{Napi::Float32Array::New(onSuccessRef.Env(), sphericalPolynomial.size())}; + float* dst{array.Data()}; + for (size_t i = 0; i < sphericalPolynomial.size(); ++i) + { + dst[i] = sphericalPolynomial[i]; + } + onSuccessRef.Call({array}); + } + }); + return; + } + std::array, 6> dataRefs; std::array, 6> tasks; for (uint32_t face = 0; face < data.Length(); face++) @@ -1759,6 +2362,10 @@ namespace Babylon auto buffer{info[6].As()}; uint32_t bufferOffset{info[7].As().Uint32Value()}; uint32_t bufferLength{info[8].As().Uint32Value()}; + // Optional cube-map face index (0-5). -1 (or absent) means a plain 2D read. + const int32_t faceIndex{(info.Length() > 9 && info[9].IsNumber()) ? info[9].As().Int32Value() : -1}; + const bool isCubeFace{faceIndex >= 0}; + const uint16_t srcZ{isCubeFace ? static_cast(faceIndex) : static_cast(0)}; const auto deferred{Napi::Promise::Deferred::New(env)}; @@ -1781,12 +2388,23 @@ namespace Babylon buffer = Napi::ArrayBuffer::New(env, bufferLength); } + // The face/layer index is JS-controlled and is forwarded to encoder->blit as srcZ, so validate it + // before it can drive an out-of-bounds read inside bgfx. Babylon.js passes this argument for both + // cube maps (face 0-5, six consecutive faces per array layer) and 2D arrays (slice index, which can + // legitimately exceed 5), and -1 for a plain 2D read -- so the bound is the texture's srcZ extent, + // not a flat 0-5. + const uint16_t numLayers{texture->NumLayers() > 0 ? texture->NumLayers() : static_cast(1)}; + const uint32_t maxSrcZ{texture->IsCube() ? 6u * numLayers : numLayers}; + if (isCubeFace && static_cast(faceIndex) >= maxSrcZ) + { + deferred.Reject(Napi::Error::New(env, "readTexture face/layer index is out of range for this texture.").Value()); + } // Make sure the buffer is big enough for the offset + length. Both // bufferOffset and bufferLength are JS-supplied uint32_t, so widen the // addition to 64-bit: computing it in 32-bit can wrap around (e.g. offset // 0xF0000000 + length 0x20000000), letting an out-of-range offset pass this // gate and overflow the ArrayBuffer backing store in the memcpy below. - if (buffer.ByteLength() < static_cast(bufferOffset) + bufferLength) + else if (buffer.ByteLength() < static_cast(bufferOffset) + bufferLength) { deferred.Reject(Napi::Error::New(env, "Provided buffer is too small for the specified offset and length.").Value()); } @@ -1805,13 +2423,15 @@ namespace Babylon bgfx::TextureHandle sourceTextureHandle{texture->Handle()}; auto tempTexture = std::make_shared(false); - // If the image needs to be cropped or the texture lacks the READ_BACK flag, blit to a temp texture. - if (x != 0 || y != 0 || width != (texture->Width() >> mipLevel) || height != (texture->Height() >> mipLevel) || (texture->Flags() & BGFX_TEXTURE_READ_BACK) == 0) + // If the image needs to be cropped, the texture lacks the READ_BACK flag, or we are reading a + // specific cube-map face, blit to a temp 2D texture. bgfx::readTexture cannot address an + // individual cube face, so a cube-face read always goes through the blit (srcZ = face index). + if (isCubeFace || x != 0 || y != 0 || width != (texture->Width() >> mipLevel) || height != (texture->Height() >> mipLevel) || (texture->Flags() & BGFX_TEXTURE_READ_BACK) == 0) { const bgfx::TextureHandle blitTextureHandle{bgfx::createTexture2D(width, height, /*hasMips*/ false, /*numLayers*/ 1, sourceTextureFormat, BGFX_TEXTURE_BLIT_DST | BGFX_TEXTURE_READ_BACK)}; bgfx::Encoder* encoder = GetEncoder(); - encoder->blit(static_cast(bgfx::getCaps()->limits.maxViews - 1), blitTextureHandle, /*dstMip*/ 0, /*dstX*/ 0, /*dstY*/ 0, /*dstZ*/ 0, sourceTextureHandle, mipLevel, x, y, /*srcZ*/ 0, width, height, /*depth*/ 0); + encoder->blit(static_cast(bgfx::getCaps()->limits.maxViews - 1), blitTextureHandle, /*dstMip*/ 0, /*dstX*/ 0, /*dstY*/ 0, /*dstZ*/ 0, sourceTextureHandle, mipLevel, x, y, srcZ, width, height, /*depth*/ 0); sourceTextureHandle = blitTextureHandle; *tempTexture = true; diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index e3ce8af04b..fcae56fe1c 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -105,7 +105,10 @@ namespace Babylon void LoadTexture(const Napi::CallbackInfo& info); void CopyTexture(NativeDataStream::Reader& data); void LoadRawTexture(const Napi::CallbackInfo& info); + void UpdateTextureData(const Napi::CallbackInfo& info); void LoadRawTexture2DArray(const Napi::CallbackInfo& info); + void LoadRawTexture3D(const Napi::CallbackInfo& info); + void UpdateTextureDirectly(const Napi::CallbackInfo& info); void LoadCubeTexture(const Napi::CallbackInfo& info); void LoadCubeTextureWithMips(const Napi::CallbackInfo& info); Napi::Value GetTextureWidth(const Napi::CallbackInfo& info); diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp index 425dc359b6..e2222792a4 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp @@ -41,27 +41,16 @@ namespace Babylon::ShaderCompilerCommon std::string ProcessSamplerFlip(std::string_view source) { - static const std::string shaderNameDefineStr = "#define SHADER_NAME"; - const auto shaderNameDefine = source.find(shaderNameDefineStr); - if (shaderNameDefine == std::string::npos) - { - throw std::runtime_error{"ProcessSamplerFlip: Could not find shader name define."}; - } - - // The vertical (V) flip applied to texture()/textureLod() sample coordinates is performed by - // the FlipSamplerCoordinates AST traverser, not by a preprocessor macro. A 2-argument - // function-like macro (`#define texture(x,y) texture(x, flip(y))`) cannot match the - // 3-argument bias form `texture(sampler, uv, bias)` emitted by some Babylon.js shaders (e.g. - // GreasedLine), and glslang's preprocessor has no variadic-macro support, so those shaders - // failed to compile. texelFetch keeps its macro because it takes integer texel coordinates, - // which the float-coordinate AST flip does not handle. - static const auto textureSamplerFunctions = R"( - #define texelFetch(tex, uv, lod) texelFetch((tex), ivec2((uv).x, textureSize((tex), (lod)).y - 1 - (uv).y), (lod)) - #define SHADER_NAME)"; - - std::string result{source}; - result.replace(shaderNameDefine, shaderNameDefineStr.length(), textureSamplerFunctions); - return result; + // The vertical (V) flip for both float sample coordinates (texture()/textureLod()) and + // integer texel coordinates (texelFetch()) is now performed by the FlipSamplerCoordinates + // AST traverser, not by a preprocessor macro. The macro form + // #define texelFetch(tex, uv, lod) texelFetch((tex), ivec2(...), (lod)) + // forced every coordinate through ivec2(...), so it could not compile against sampler3D / + // sampler2DArray ('no matching overloaded function'). The AST traverser knows the sampler + // dimensionality and only flips 2-component coordinates, leaving 3D/array fetches intact. + // This function is retained as an identity passthrough so the backend call sites don't need + // to change. + return std::string{source}; } void AppendUniformBuffer(std::vector& bytes, const NonSamplerUniformsInfo& uniformBuffer, bool isFragment) diff --git a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp index 3289d86ce4..2c2192c5f1 100644 --- a/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp +++ b/Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp @@ -1760,13 +1760,42 @@ namespace Babylon::ShaderCompilerTraversers } } } + else if (visit == EvPostVisit && node->getOp() == EOpTextureFetch) + { + // texelFetch(sampler, ivec coord, lod). The vertical flip that used to be applied + // by a preprocessor macro in ProcessSamplerFlip is done here instead so that the + // sampler dimensionality is known: only 2-component integer coordinates + // (sampler2D-style) are flipped. sampler3D / sampler2DArray coordinates (ivec3) + // are left untouched — the old macro forced every coordinate through ivec2(...), + // which failed to compile against sampler3D ('no matching overloaded function'). + auto& sequence = node->getSequence(); + if (sequence.size() >= 3) + { + auto* sampler = sequence[0]->getAsTyped(); + auto* coordinate = sequence[1]->getAsTyped(); + auto* lod = sequence[2]->getAsTyped(); + if (sampler != nullptr && coordinate != nullptr && lod != nullptr && + coordinate->getType().getBasicType() == EbtInt && + !coordinate->getType().isArray() && + coordinate->getType().getVectorSize() == 2) + { + sequence[1] = FlipVerticalTexelCoordinate(coordinate, sampler, lod); + } + } + } return true; } private: + // Post-visit is enabled so that texelFetch coordinates can be rewritten after their + // children have been traversed. The rewrite copies the coordinate subtree, and doing + // that on the way down would leave the copy unvisited while the original still got + // flipped, so a nested texture() call inside the coordinate would be flipped in one + // reference but not the other. FlipSamplerCoordinatesTraverser(TIntermediate* intermediate) - : m_intermediate{intermediate} + : TIntermTraverser{true, false, true} + , m_intermediate{intermediate} { } @@ -1790,6 +1819,125 @@ namespace Babylon::ShaderCompilerTraversers return m_intermediate->addBinaryMath(EOpAdd, scaled, offset, loc); } + // Builds `ivec2(coordinate.x, textureSize(sampler, lod).y - 1 - coordinate.y)`, the + // integer-texel-coordinate equivalent of FlipVerticalCoordinate. This is exactly the + // expression the former ProcessSamplerFlip texelFetch macro expanded to, including its + // double evaluation of the coordinate operand. + // + // The obvious vector form `coordinate * ivec2(1, -1) + ivec2(0, size.y - 1)` must NOT + // be used: it emits SPIR-V OpIMul, which SPIRV-Cross omits entirely when built with + // SPIRV_CROSS_WEBMIN (the configuration Babylon Native ships). The multiply then + // silently produces no HLSL/MSL expression and the whole shader fails to cross-compile + // with "Cannot resolve expression type". Integer subtract and vector construction are + // both retained by that build, so express the flip with those only. + TIntermTyped* FlipVerticalTexelCoordinate(TIntermTyped* coordinate, TIntermTyped* sampler, TIntermTyped* lod) + { + const TSourceLoc& loc{coordinate->getLoc()}; + + // Every operand referenced more than once below has to be an independent subtree. + // Reusing a node pointer would give it two parents in the AST, which later traversers + // (sampler splitting, SPIR-V generation) do not expect. The sampler and lod are each + // referenced twice because textureSize repeats them, and the coordinate is referenced + // twice because the flip reads both .x and .y, so all three need a copy. The original + // node is kept for one reference and the clone used for the other, leaving every node + // with exactly one parent. + TIntermTyped* samplerClone{CloneExpression(sampler)}; + TIntermTyped* lodClone{CloneExpression(lod)}; + TIntermTyped* coordinateClone{CloneExpression(coordinate)}; + + TType ivec2Type{EbtInt, EvqTemporary, 2}; + TType intType{EbtInt, EvqTemporary, 1}; + + // textureSize(sampler, lod) -> ivec2 + TIntermAggregate* sizeArgs{m_intermediate->makeAggregate(samplerClone, loc)}; + sizeArgs = m_intermediate->growAggregate(sizeArgs, lodClone, loc); + TIntermTyped* size{m_intermediate->addBuiltInFunctionCall(loc, EOpTextureQuerySize, false, sizeArgs, ivec2Type)}; + + // textureSize(sampler, lod).y - 1 + TIntermTyped* sizeY{m_intermediate->addIndex(EOpIndexDirect, size, m_intermediate->addConstantUnion(1, loc), loc)}; + sizeY->setType(intType); + TIntermTyped* maxY{m_intermediate->addBinaryMath(EOpSub, sizeY, m_intermediate->addConstantUnion(1, loc), loc)}; + + // coordinate.x and coordinate.y. The clone supplies the second reference so that + // neither subtree ends up with two parents. + TIntermTyped* coordinateX{m_intermediate->addIndex(EOpIndexDirect, coordinate, m_intermediate->addConstantUnion(0, loc), loc)}; + coordinateX->setType(intType); + TIntermTyped* coordinateY{m_intermediate->addIndex(EOpIndexDirect, coordinateClone, m_intermediate->addConstantUnion(1, loc), loc)}; + coordinateY->setType(intType); + + // ivec2(coordinate.x, (textureSize(sampler, lod).y - 1) - coordinate.y) + TIntermTyped* flippedY{m_intermediate->addBinaryMath(EOpSub, maxY, coordinateY, loc)}; + TIntermAggregate* flipped{m_intermediate->makeAggregate(coordinateX, loc)}; + flipped = m_intermediate->growAggregate(flipped, flippedY, loc); + return m_intermediate->setAggregateOperator(flipped, EOpConstructIVec2, ivec2Type, loc); + } + + // Produces an independent copy of an expression subtree so it can be referenced from a + // second call site without giving any original node two parents in the AST. + // + // The clone is structural: each node's operator, type and source location are copied + // verbatim rather than rebuilt through TIntermediate::add*, so no constant folding, + // type promotion or precision inference can make the copy diverge from the original. + // + // Anything not reachable in a texel coordinate expression (ternaries, array methods) + // is reported rather than silently skipped -- returning the coordinate unflipped would + // sample with an un-flipped Y and produce a wrong image with no diagnostic at all. + TIntermTyped* CloneExpression(TIntermTyped* node) + { + if (node == nullptr) + { + throw std::runtime_error{"FlipSamplerCoordinates: missing operand in texelFetch."}; + } + + if (TIntermSymbol* symbol = node->getAsSymbolNode()) + { + return m_intermediate->addSymbol(*symbol); + } + + if (TIntermConstantUnion* constant = node->getAsConstantUnion()) + { + return m_intermediate->addConstantUnion(constant->getConstArray(), constant->getType(), node->getLoc()); + } + + if (TIntermBinary* binary = node->getAsBinaryNode()) + { + auto* clone = new TIntermBinary{binary->getOp()}; + clone->setLeft(CloneExpression(binary->getLeft())); + clone->setRight(CloneExpression(binary->getRight())); + clone->setType(binary->getType()); + clone->setLoc(binary->getLoc()); + return clone; + } + + if (TIntermUnary* unary = node->getAsUnaryNode()) + { + auto* clone = new TIntermUnary{unary->getOp()}; + clone->setOperand(CloneExpression(unary->getOperand())); + clone->setType(unary->getType()); + clone->setLoc(unary->getLoc()); + return clone; + } + + if (TIntermAggregate* aggregate = node->getAsAggregate()) + { + auto* clone = new TIntermAggregate{aggregate->getOp()}; + for (TIntermNode* child : aggregate->getSequence()) + { + clone->getSequence().push_back(CloneExpression(child == nullptr ? nullptr : child->getAsTyped())); + } + clone->setType(aggregate->getType()); + clone->setLoc(aggregate->getLoc()); + clone->setName(aggregate->getName()); + if (aggregate->isUserDefined()) + { + clone->setUserDefined(); + } + return clone; + } + + throw std::runtime_error{"FlipSamplerCoordinates: unsupported expression in a texelFetch operand; cannot flip the texel coordinate."}; + } + TIntermediate* m_intermediate{}; }; }