diff --git a/examples/custompass_ssr.html b/examples/custompass_ssr.html index 6562a16..aee7cbf 100644 --- a/examples/custompass_ssr.html +++ b/examples/custompass_ssr.html @@ -127,8 +127,6 @@ const shadowMapPass = new t3d.ShadowMapPass(); const ssrPass = new t3d.ShaderPostPass(SSRShader); - ssrPass.uniforms.maxRayDistance = 10; - ssrPass.uniforms.pixelStrideZCutoff = 50; const tempRenderTarget = new t3d.RenderTarget2D(width, height); tempRenderTarget.texture.minFilter = t3d.TEXTURE_FILTER.LINEAR; @@ -158,6 +156,7 @@ ssrPass.uniforms["gBufferTexture2"] = gBuffer.getDepthTexture(); ssrPass.uniforms["viewportSize"][0] = width; ssrPass.uniforms["viewportSize"][1] = height; + ssrPass.uniforms["nearZ"] = 1; const blurPass = new t3d.ShaderPostPass(BlurShader); blurPass.uniforms["projection"] = projection.elements; @@ -178,7 +177,6 @@ const blendPass = new t3d.ShaderPostPass(BlendShader); blendPass.uniforms["tDiffuse1"] = tempRenderTarget.texture; blendPass.uniforms["tDiffuse2"] = tempRenderTarget3.texture; - blendPass.uniforms["opacity2"] = 1; const gui = new GUI(); gui.add(material, 'roughness', 0, 1, 0.01); diff --git a/examples/jsm/shaders/SSRShader.js b/examples/jsm/shaders/SSRShader.js index ddae1d1..ffe8409 100644 --- a/examples/jsm/shaders/SSRShader.js +++ b/examples/jsm/shaders/SSRShader.js @@ -3,119 +3,108 @@ * https://github.com/pissang/claygl/blob/master/example/shader/ssr.glsl * http://casual-effects.blogspot.jp/2014/08/screen-space-ray-tracing.html */ - - - -var SSRShader = { - +const SSRShader = { + name: 'effect_ssr', defines: { - + MAX_ITERATION: 50, + MAX_BINARY_SEARCH_ITERATION: 5 }, - uniforms: { - 'colorTex': null, - 'gBufferTexture1': null, - 'gBufferTexture2': null, + colorTex: null, + gBufferTexture1: null, + gBufferTexture2: null, - 'projection': new Float32Array(16), - 'projectionInv': new Float32Array(16), - 'viewInverseTranspose': new Float32Array(16), + projection: new Float32Array(16), + projectionInv: new Float32Array(16), + viewInverseTranspose: new Float32Array(16), - 'maxRayDistance': 4, + pixelStride: 8, + maxRayDistance: 200, - 'pixelStride': 16, - 'pixelStrideZCutoff': 10, + enablePixelStrideZCutoff: 1.0, + pixelStrideZCutoff: 50, - 'screenEdgeFadeStart': 0.9, + screenEdgeFadeStart: 0.9, - 'eyeFadeStart': 0.4, - 'eyeFadeEnd': 0.8, + eyeFadeStart: 0.99, + eyeFadeEnd: 1, - 'minGlossiness': 0.2, - 'zThicknessThreshold': 0.1, - 'jitterOffset': 0, - - 'nearZ': 0, - 'viewportSize': [512, 512], - - 'maxMipmapLevel': 5, + minGlossiness: 0.2, + nearZ: 0.1, + zThicknessThreshold: 0.1, + jitterOffset: 0, + viewportSize: [512, 512] }, + vertexShader: ` + attribute vec3 a_Position; + attribute vec2 a_Uv; - vertexShader: [ - "attribute vec3 a_Position;", - "attribute vec2 a_Uv;", - - "uniform mat4 u_ProjectionView;", - "uniform mat4 u_Model;", - - "varying vec2 v_Uv;", - - "void main() {", - - " v_Uv = a_Uv;", - " gl_Position = u_ProjectionView * u_Model * vec4( a_Position, 1.0 );", + uniform mat4 u_ProjectionView; + uniform mat4 u_Model; - "}" - ].join("\n"), + varying vec2 v_Uv; - fragmentShader: [ - "#define MAX_ITERATION 20;", - "#define MAX_BINARY_SEARCH_ITERATION 5;", + void main() { + v_Uv = a_Uv; + gl_Position = u_ProjectionView * u_Model * vec4(a_Position, 1.0); + } + `, + fragmentShader: ` + varying vec2 v_Uv; - "uniform sampler2D colorTex;", - "uniform sampler2D gBufferTexture1;", - "uniform sampler2D gBufferTexture2;", + uniform sampler2D colorTex; + uniform sampler2D gBufferTexture1; + uniform sampler2D gBufferTexture2; - "uniform mat4 projection;", - "uniform mat4 projectionInv;", - "uniform mat4 viewInverseTranspose;", + uniform mat4 projection; + uniform mat4 projectionInv; + uniform mat4 viewInverseTranspose; + uniform float maxRayDistance; - "uniform float maxRayDistance;", - - 'uniform float pixelStride;', + uniform float pixelStride; // ray origin Z at this distance will have a pixel stride of 1.0 - 'uniform float pixelStrideZCutoff;', + uniform float pixelStrideZCutoff; // distance to screen edge that ray hits will start to fade (0.0 -> 1.0) - 'uniform float screenEdgeFadeStart;', + uniform float screenEdgeFadeStart; // ray direction's Z that ray hits will start to fade (0.0 -> 1.0) - 'uniform float eyeFadeStart;', + uniform float eyeFadeStart; // ray direction's Z that ray hits will be cut (0.0 -> 1.0) - 'uniform float eyeFadeEnd;', + uniform float eyeFadeEnd; // Object larger than minGlossiness will have ssr effect - 'uniform float minGlossiness;', - 'uniform float zThicknessThreshold;', - 'uniform float jitterOffset;', + uniform float minGlossiness; + uniform float zThicknessThreshold; + uniform float jitterOffset; - 'uniform float nearZ;', - 'uniform vec2 viewportSize;', + uniform float nearZ; + uniform vec2 viewportSize; - 'uniform float maxMipmapLevel;', + uniform float maxMipmapLevel; - "varying vec2 v_Uv;", + uniform float enablePixelStrideZCutoff; - 'float fetchDepth(sampler2D depthTexture, vec2 uv) {', - ' vec4 depthTexel = texture2D(depthTexture, uv);', - ' return depthTexel.r * 2.0 - 1.0;', - '}', + float fetchDepth(sampler2D depthTexture, vec2 uv) { + vec4 depthTexel = texture2D(depthTexture, uv); + return depthTexel.r * 2.0 - 1.0; + } - 'float linearDepth(float depth) {', - ' return projection[3][2] / (depth * projection[2][3] - projection[2][2]);', - '}', + float linearDepth(float depth) { + return projection[3][2] / (depth * projection[2][3] - projection[2][2]); + } - 'bool rayIntersectDepth(float rayZNear, float rayZFar, vec2 hitPixel) {', - // Swap if bigger - ' if (rayZFar > rayZNear) {', - ' float t = rayZFar; rayZFar = rayZNear; rayZNear = t;', - ' }', - ' float cameraZ = linearDepth(fetchDepth(gBufferTexture2, hitPixel));', - // float cameraBackZ = linearDepth(fetchDepth(backDepthTex, hitPixel)); - // Cross z - ' return rayZFar <= cameraZ && rayZNear >= cameraZ - zThicknessThreshold;', - '}', + bool rayIntersectDepth(float rayZNear, float rayZFar, vec2 hitPixel) { + // Swap if bigger + if (rayZFar > rayZNear) { + float t = rayZFar; rayZFar = rayZNear; rayZNear = t; + } + float cameraZ = linearDepth(fetchDepth(gBufferTexture2, hitPixel)); + // float cameraBackZ = linearDepth(fetchDepth(backDepthTex, hitPixel)); + // Cross z + return rayZFar <= cameraZ && rayZNear >= cameraZ - zThicknessThreshold; + } // Trace a ray in screenspace from rayOrigin (in camera space) pointing in rayDir (in camera space) // @@ -127,217 +116,211 @@ var SSRShader = { // // Based on Morgan McGuire & Mike Mara's GLSL implementation: // http://casual-effects.blogspot.com/2014/08/screen-space-ray-tracing.html - 'bool traceScreenSpaceRay(vec3 rayOrigin, vec3 rayDir, float jitter, out vec2 hitPixel, out vec3 hitPoint, out float iterationCount) {', - // Clip to the near plane - ' float rayLength = ((rayOrigin.z + rayDir.z * maxRayDistance) > -nearZ) ? (-nearZ - rayOrigin.z) / rayDir.z : maxRayDistance;', - - ' vec3 rayEnd = rayOrigin + rayDir * rayLength;', - - // Project into homogeneous clip space - ' vec4 H0 = projection * vec4(rayOrigin, 1.0);', - ' vec4 H1 = projection * vec4(rayEnd, 1.0);', - - ' float k0 = 1.0 / H0.w, k1 = 1.0 / H1.w;', - - // The interpolated homogeneous version of the camera space points - ' vec3 Q0 = rayOrigin * k0, Q1 = rayEnd * k1;', - - // Screen space endpoints - // PENDING viewportSize ? - ' vec2 P0 = (H0.xy * k0 * 0.5 + 0.5) * viewportSize;', - ' vec2 P1 = (H1.xy * k1 * 0.5 + 0.5) * viewportSize;', - - // If the line is degenerate, make it cover at least one pixel to avoid handling - // zero-pixel extent as a special case later - ' P1 += dot(P1 - P0, P1 - P0) < 0.0001 ? 0.01 : 0.0;', - ' vec2 delta = P1 - P0;', - - // Permute so that the primary iteration is in x to collapse - // all quadrant-specific DDA case later - ' bool permute = false;', - ' if (abs(delta.x) < abs(delta.y)) {', - // More vertical line - ' permute = true;', - ' delta = delta.yx;', - ' P0 = P0.yx;', - ' P1 = P1.yx;', - ' }', - ' float stepDir = sign(delta.x);', - ' float invdx = stepDir / delta.x;', - - // Track the derivatives of Q and K - ' vec3 dQ = (Q1 - Q0) * invdx;', - ' float dk = (k1 - k0) * invdx;', - - ' vec2 dP = vec2(stepDir, delta.y * invdx);', - - // Calculate pixel stride based on distance of ray origin from camera. - // Since perspective means distant objects will be smaller in screen space - // we can use this to have higher quality reflections for far away objects - // while still using a large pixel stride for near objects (and increase performance) - // this also helps mitigate artifacts on distant reflections when we use a large - // pixel stride. - ' float strideScaler = 1.0 - min(1.0, -rayOrigin.z / pixelStrideZCutoff);', - ' float pixStride = 1.0 + strideScaler * pixelStride;', - - // Scale derivatives by the desired pixel stride and the offset the starting values by the jitter fraction - ' dP *= pixStride; dQ *= pixStride; dk *= pixStride;', - - // Track ray step and derivatives in a vec4 to parallelize - ' vec4 pqk = vec4(P0, Q0.z, k0);', - ' vec4 dPQK = vec4(dP, dQ.z, dk);', - - ' pqk += dPQK * jitter;', - ' float rayZFar = (dPQK.z * 0.5 + pqk.z) / (dPQK.w * 0.5 + pqk.w);', - ' float rayZNear;', - - ' bool intersect = false;', - - ' vec2 texelSize = 1.0 / viewportSize;', - - ' iterationCount = 0.0;', - - ' for (int i = 0; i < 20; i++) {', - ' pqk += dPQK;', - - ' rayZNear = rayZFar;', - ' rayZFar = (dPQK.z * 0.5 + pqk.z) / (dPQK.w * 0.5 + pqk.w);', - - ' hitPixel = permute ? pqk.yx : pqk.xy;', - ' hitPixel *= texelSize;', - - ' intersect = rayIntersectDepth(rayZNear, rayZFar, hitPixel);', - - ' iterationCount += 1.0;', - - // PENDING Right on all platforms? - ' if (intersect) {', - ' break;', - ' }', - ' }', - - // Binary search refinement - // FIXME If intersect in first iteration binary search may easily lead to the pixel of reflect object it self - ' if (pixStride > 1.0 && intersect && iterationCount > 1.0) {', - // Roll back - ' pqk -= dPQK;', - ' dPQK /= pixStride;', - - ' float originalStride = pixStride * 0.5;', - ' float stride = originalStride;', - - ' rayZNear = pqk.z / pqk.w;', - ' rayZFar = rayZNear;', - - ' for (int j = 0; j < 5; j++) {', - ' pqk += dPQK * stride;', - ' rayZNear = rayZFar;', - ' rayZFar = (dPQK.z * -0.5 + pqk.z) / (dPQK.w * -0.5 + pqk.w);', - ' hitPixel = permute ? pqk.yx : pqk.xy;', - ' hitPixel *= texelSize;', - - ' originalStride *= 0.5;', - ' stride = rayIntersectDepth(rayZNear, rayZFar, hitPixel) ? -originalStride : originalStride;', - ' }', - ' }', - - ' Q0.xy += dQ.xy * iterationCount;', - ' Q0.z = pqk.z;', - ' hitPoint = Q0 / pqk.w;', - - ' return intersect;', - '}', - - 'float calculateAlpha(float iterationCount, float reflectivity, vec2 hitPixel, vec3 hitPoint, float dist, vec3 rayDir) {', - ' float alpha = clamp(reflectivity, 0.0, 1.0);', - // Fade ray hits that approach the maximum iterations - ' alpha *= 1.0 - (iterationCount / float(20));', - // Fade ray hits that approach the screen edge - ' vec2 hitPixelNDC = hitPixel * 2.0 - 1.0;', - ' float maxDimension = min(1.0, max(abs(hitPixelNDC.x), abs(hitPixelNDC.y)));', - ' alpha *= 1.0 - max(0.0, maxDimension - screenEdgeFadeStart) / (1.0 - screenEdgeFadeStart);', - - // Fade ray hits base on how much they face the camera - ' float _eyeFadeStart = eyeFadeStart;', - ' float _eyeFadeEnd = eyeFadeEnd;', - ' if (_eyeFadeStart > _eyeFadeEnd) {', - ' float tmp = _eyeFadeEnd;', - ' _eyeFadeEnd = _eyeFadeStart;', - ' _eyeFadeStart = tmp;', - ' }', - - ' float eyeDir = clamp(rayDir.z, _eyeFadeStart, _eyeFadeEnd);', - ' alpha *= 1.0 - (eyeDir - _eyeFadeStart) / (_eyeFadeEnd - _eyeFadeStart);', - - // Fade ray hits based on distance from ray origin - ' alpha *= 1.0 - clamp(dist / maxRayDistance, 0.0, 1.0);', - - ' return alpha;', - '}', - - 'void main() {', - ' vec4 normalAndGloss = texture2D(gBufferTexture1, v_Uv);', - - // Is empty - ' if (dot(normalAndGloss.rgb, vec3(1.0)) == 0.0) {', - ' discard;', - ' }', - - ' float g = normalAndGloss.a;', - ' if (g <= minGlossiness) {', - ' discard;', - ' }', - - ' float reflectivity = (g - minGlossiness) / (1.0 - minGlossiness);', - - ' vec3 N = normalAndGloss.rgb * 2.0 - 1.0;', - ' N = normalize((viewInverseTranspose * vec4(N, 0.0)).xyz);', - - // Position in view - ' vec4 projectedPos = vec4(v_Uv * 2.0 - 1.0, fetchDepth(gBufferTexture2, v_Uv), 1.0);', - ' vec4 pos = projectionInv * projectedPos;', - ' vec3 rayOrigin = pos.xyz / pos.w;', - - ' vec3 rayDir = normalize(reflect(normalize(rayOrigin), N));', - ' vec2 hitPixel;', - ' vec3 hitPoint;', - ' float iterationCount;', - - // Get jitter - ' vec2 uv2 = v_Uv * viewportSize;', - ' float jitter = fract((uv2.x + uv2.y) * 0.25);', - - ' bool intersect = traceScreenSpaceRay(rayOrigin, rayDir, jitter, hitPixel, hitPoint, iterationCount);', - - ' float dist = distance(rayOrigin, hitPoint);', - - ' float alpha = calculateAlpha(iterationCount, reflectivity, hitPixel, hitPoint, dist, rayDir) * float(intersect);', - - ' vec3 hitNormal = texture2D(gBufferTexture1, hitPixel).rgb * 2.0 - 1.0;', - ' hitNormal = normalize((viewInverseTranspose * vec4(hitNormal, 0.0)).xyz);', - - // Ignore the pixel not face the ray - // TODO fadeout ? - // PENDING Can be configured? - ' if (dot(hitNormal, rayDir) >= 0.0) {', - ' discard;', - ' }', - - // vec4 color = decodeHDR(texture2DLodEXT(colorTex, hitPixel, clamp(dist / maxRayDistance, 0.0, 1.0) * maxMipmapLevel)); - - ' if (!intersect) {', - ' discard;', - ' }', - - ' vec4 color = texture2D(colorTex, hitPixel);', - ' gl_FragColor = vec4(color.rgb * alpha, color.a);', - - // gl_FragColor = vec4(vec3(iterationCount / 2.0), 1.0); - - '}' - - ].join("\n") - + bool traceScreenSpaceRay(vec3 rayOrigin, vec3 rayDir, float jitter, out vec2 hitPixel, out vec3 hitPoint, out float iterationCount) { + // Clip to the near plane + float rayLength = ((rayOrigin.z + rayDir.z * maxRayDistance) > -nearZ) ? (-nearZ - rayOrigin.z) / rayDir.z : maxRayDistance; + + vec3 rayEnd = rayOrigin + rayDir * rayLength; + + // Project into homogeneous clip space + vec4 H0 = projection * vec4(rayOrigin, 1.0); + vec4 H1 = projection * vec4(rayEnd, 1.0); + + float k0 = 1.0 / H0.w, k1 = 1.0 / H1.w; + + // The interpolated homogeneous version of the camera space points + vec3 Q0 = rayOrigin * k0, Q1 = rayEnd * k1; + + // Screen space endpoints + // PENDING viewportSize ? + vec2 P0 = (H0.xy * k0 * 0.5 + 0.5) * viewportSize; + vec2 P1 = (H1.xy * k1 * 0.5 + 0.5) * viewportSize; + + // If the line is degenerate, make it cover at least one pixel to avoid handling + // zero-pixel extent as a special case later + P1 += dot(P1 - P0, P1 - P0) < 0.0001 ? 0.01 : 0.0; + vec2 delta = P1 - P0; + + // Permute so that the primary iteration is in x to collapse + // all quadrant-specific DDA case later + bool permute = false; + if (abs(delta.x) < abs(delta.y)) { + // More vertical line + permute = true; + delta = delta.yx; + P0 = P0.yx; + P1 = P1.yx; + } + float stepDir = sign(delta.x); + float invdx = stepDir / delta.x; + + // Track the derivatives of Q and K + vec3 dQ = (Q1 - Q0) * invdx; + float dk = (k1 - k0) * invdx; + + vec2 dP = vec2(stepDir, delta.y * invdx); + + // Calculate pixel stride based on distance of ray origin from camera. + // Since perspective means distant objects will be smaller in screen space + // we can use this to have higher quality reflections for far away objects + // while still using a large pixel stride for near objects (and increase performance) + // this also helps mitigate artifacts on distant reflections when we use a large + // pixel stride. + float strideScaler = 1.0 - min(1.0, -rayOrigin.z / pixelStrideZCutoff); + float pixStride = mix(pixelStride, 1.0 + strideScaler * pixelStride, enablePixelStrideZCutoff); + + // Scale derivatives by the desired pixel stride and the offset the starting values by the jitter fraction + dP *= pixStride; dQ *= pixStride; dk *= pixStride; + + // Track ray step and derivatives in a vec4 to parallelize + vec4 pqk = vec4(P0, Q0.z, k0); + vec4 dPQK = vec4(dP, dQ.z, dk); + + pqk += dPQK * jitter; + float rayZFar = (dPQK.z * 0.5 + pqk.z) / (dPQK.w * 0.5 + pqk.w); + float rayZNear; + + bool intersect = false; + + vec2 texelSize = 1.0 / viewportSize; + + iterationCount = 0.0; + float end = P1.x * stepDir; + for (int i = 0; i < MAX_ITERATION; i++) { + pqk += dPQK; + if ((pqk.x * stepDir) >= end) { + break; + } + + rayZNear = rayZFar; + rayZFar = (dPQK.z * 0.5 + pqk.z) / (dPQK.w * 0.5 + pqk.w); + + hitPixel = permute ? pqk.yx : pqk.xy; + hitPixel *= texelSize; + + intersect = rayIntersectDepth(rayZNear, rayZFar, hitPixel); + + iterationCount += 1.0; + + // PENDING Right on all platforms? + if (intersect) { + break; + } + } + + // Binary search refinement + // FIXME If intersect in first iteration binary search may easily lead to the pixel of reflect object it self + if (pixStride > 1.0 && intersect && iterationCount > 1.0) { + // Roll back + pqk -= dPQK; + dPQK /= pixStride; + + float originalStride = pixStride * 0.5; + float stride = originalStride; + + rayZNear = pqk.z / pqk.w; + rayZFar = rayZNear; + + for (int j = 0; j < MAX_BINARY_SEARCH_ITERATION; j++) { + pqk += dPQK * stride; + rayZNear = rayZFar; + rayZFar = (dPQK.z * -0.5 + pqk.z) / (dPQK.w * -0.5 + pqk.w); + hitPixel = permute ? pqk.yx : pqk.xy; + hitPixel *= texelSize; + + originalStride *= 0.5; + stride = rayIntersectDepth(rayZNear, rayZFar, hitPixel) ? -originalStride : originalStride; + } + } + + Q0.xy += dQ.xy * iterationCount; + Q0.z = pqk.z; + hitPoint = Q0 / pqk.w; + + return intersect; + } + + float calculateAlpha(float iterationCount, float reflectivity, vec2 hitPixel, vec3 hitPoint, float dist, vec3 rayDir) { + float alpha = clamp(reflectivity, 0.0, 1.0); + + // Fade ray hits that approach the maximum iterations + alpha *= 1.0 - (iterationCount / float(MAX_ITERATION)); + + // Fade ray hits that approach the screen edge + vec2 hitPixelNDC = hitPixel * 2.0 - 1.0; + float maxDimension = min(1.0, max(abs(hitPixelNDC.x), abs(hitPixelNDC.y))); + alpha *= 1.0 - max(0.0, maxDimension - screenEdgeFadeStart) / (1.0 - screenEdgeFadeStart); + + // Fade ray hits base on how much they face the camera + float _eyeFadeStart = eyeFadeStart; + float _eyeFadeEnd = eyeFadeEnd; + if (_eyeFadeStart > _eyeFadeEnd) { + float tmp = _eyeFadeEnd; + _eyeFadeEnd = _eyeFadeStart; + _eyeFadeStart = tmp; + } + float eyeDir = clamp(rayDir.z, _eyeFadeStart, _eyeFadeEnd); + alpha *= 1.0 - (eyeDir - _eyeFadeStart) / (_eyeFadeEnd - _eyeFadeStart); + + // Fade ray hits based on distance from ray origin + alpha *= 1.0 - clamp(dist / maxRayDistance, 0.0, 1.0); + + return alpha; + } + void main() { + vec4 normalAndGloss = texture2D(gBufferTexture1, v_Uv); + + if (dot(normalAndGloss.rgb, vec3(1.0)) == 0.0) { + discard; + } + + float g = normalAndGloss.a; + if (g <= minGlossiness) { + discard; + } + + float reflectivity = g; + + vec3 N = normalAndGloss.rgb * 2.0 - 1.0; + N = normalize((viewInverseTranspose * vec4(N, 0.0)).xyz); + + // Position in view + vec4 projectedPos = vec4(v_Uv * 2.0 - 1.0, fetchDepth(gBufferTexture2, v_Uv), 1.0); + vec4 pos = projectionInv * projectedPos; + vec3 rayOrigin = pos.xyz / pos.w; + + vec3 rayDir = normalize(reflect(normalize(rayOrigin), N)); + vec2 hitPixel; + vec3 hitPoint; + float iterationCount; + + // Get jitter + vec2 uv2 = v_Uv * viewportSize; + float jitter = fract((uv2.x + uv2.y) * 0.25); + + bool intersect = traceScreenSpaceRay(rayOrigin, rayDir, jitter, hitPixel, hitPoint, iterationCount); + // Is empty + if (!intersect) { + discard; + } + float dist = distance(rayOrigin, hitPoint); + + float alpha = calculateAlpha(iterationCount, reflectivity, hitPixel, hitPoint, dist, rayDir) * float(intersect); + + vec3 hitNormal = texture2D(gBufferTexture1, hitPixel).rgb * 2.0 - 1.0; + hitNormal = normalize((viewInverseTranspose * vec4(hitNormal, 0.0)).xyz); + + // Ignore the pixel not face the ray + // TODO fadeout ? + // PENDING Can be configured? + if (dot(hitNormal, rayDir) >= 0.0) { + discard; + } + + vec4 color = texture2D(colorTex, hitPixel); + gl_FragColor = vec4(color.rgb * alpha, color.a); + } + ` }; export { SSRShader }; \ No newline at end of file