static const float LUT_SIZE = 33.0; static const float LUT_LAST_INDEX = 32.0; float3 sampleLutCell(float3 index) { float r = floor(index.r + 0.5); float g = floor(index.g + 0.5); float b = floor(index.b + 0.5); // The 33^3 cube is packed as blue slices laid horizontally, with red across // each slice and green down the atlas. float atlasWidth = LUT_SIZE * LUT_SIZE; float2 lutUv; lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth; lutUv.y = (g + 0.5) / LUT_SIZE; return lutTexture.Sample(lutUv).rgb; } float3 applyLut33(float3 color) { float3 lutCoord = saturate(color) * LUT_LAST_INDEX; float3 baseIndex = floor(lutCoord); float3 nextIndex = min(baseIndex + 1.0, LUT_LAST_INDEX); float3 blend = lutCoord - baseIndex; float3 c000 = sampleLutCell(float3(baseIndex.r, baseIndex.g, baseIndex.b)); float3 c100 = sampleLutCell(float3(nextIndex.r, baseIndex.g, baseIndex.b)); float3 c010 = sampleLutCell(float3(baseIndex.r, nextIndex.g, baseIndex.b)); float3 c110 = sampleLutCell(float3(nextIndex.r, nextIndex.g, baseIndex.b)); float3 c001 = sampleLutCell(float3(baseIndex.r, baseIndex.g, nextIndex.b)); float3 c101 = sampleLutCell(float3(nextIndex.r, baseIndex.g, nextIndex.b)); float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b)); float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b)); // Tetrahedral interpolation chooses one of six paths through the cube. // This avoids the muddy diagonals that simple trilinear LUT sampling can // introduce for strong grades. if (blend.r > blend.g) { if (blend.g > blend.b) return c000 + blend.r * (c100 - c000) + blend.g * (c110 - c100) + blend.b * (c111 - c110); if (blend.r > blend.b) return c000 + blend.r * (c100 - c000) + blend.b * (c101 - c100) + blend.g * (c111 - c101); return c000 + blend.b * (c001 - c000) + blend.r * (c101 - c001) + blend.g * (c111 - c101); } if (blend.b > blend.g) return c000 + blend.b * (c001 - c000) + blend.g * (c011 - c001) + blend.r * (c111 - c011); if (blend.b > blend.r) return c000 + blend.g * (c010 - c000) + blend.b * (c011 - c010) + blend.r * (c111 - c011); return c000 + blend.g * (c010 - c000) + blend.r * (c110 - c010) + blend.b * (c111 - c110); } float hash12(float2 value) { float3 p = frac(float3(value.xyx) * 0.1031); p += dot(p, p.yzx + 33.33); return frac((p.x + p.y) * p.z); } float3 outputDither(float2 pixel) { // Subtract paired hashes to center the dither around zero, then scale to // roughly one 8-bit code value. float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0)); float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0)); float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0)); return float3(r, g, b) / 255.0; } float4 shadeVideo(ShaderContext context) { float4 source = context.sourceColor; float3 inputColor = source.rgb * pow(2.0, preExposure); if (clampInput) inputColor = saturate(inputColor); float3 lutColor = applyLut33(inputColor); float3 graded = lerp(inputColor, lutColor, lutStrength); graded = (graded - 0.5) * postContrast + 0.5; graded += outputDither(context.uv * context.outputResolution) * ditherAmount; return float4(saturate(graded), source.a); }