Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/UntoldEngine/Renderer/RenderPasses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1415,7 +1415,7 @@ public enum RenderPasses {
let projectionMatrix = renderInfo.perspectiveSpace

// Render each cascade into its own depth array slice.
for cascadeIdx in 0 ..< csmCascadeCount {
for cascadeIdx in 0 ..< shadowSystem.cascadeCount {
guard cascadeIdx < renderInfo.csmRenderPassDescriptors.count else { break }
let descriptor = renderInfo.csmRenderPassDescriptors[cascadeIdx]
descriptor.depthAttachment.loadAction = .clear
Expand Down Expand Up @@ -1528,7 +1528,7 @@ public enum RenderPasses {
batchUniforms.cameraPosition = shadingCameraPosition(cameraComponent)
batchUniforms.projectionMatrix = renderInfo.perspectiveSpace

for cascadeIdx in 0 ..< csmCascadeCount {
for cascadeIdx in 0 ..< shadowSystem.cascadeCount {
guard cascadeIdx < renderInfo.csmRenderPassDescriptors.count else { break }

// Batched geometry adds into the existing cascade depth slice (already written by shadowExecution).
Expand Down
49 changes: 32 additions & 17 deletions Sources/UntoldEngine/Shaders/LightShader.metal
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,12 @@ EnvironmentLight computeIBLParts(texture2d<float> irradianceTexture,
//compute ibl ambient contribution
// The angle to the surface, whichever side of it is seen (see specularIBL).
float NoV = min(abs(dot(normalMap.xyz, viewVector)), 1.0);

float3 irradiance=diffuseIBL(normalMap.xyz, irradianceTexture, float3(0.0,1.0,0.0),degreesToRadians(iblRotationAngle));
// The rotation of the environment, in radians, once; the lookup table's two values at
// this angle and roughness, once, for the reflection and the share it takes from diffuse.
float rotation = degreesToRadians(iblRotationAngle);
float2 brdf = brdfIntegration(NoV, roughness, iblBRDFTexture);

float3 irradiance=diffuseIBL(normalMap.xyz, irradianceTexture, float3(0.0,1.0,0.0), rotation);

float3 diffuse = irradiance*inBaseColor.rgb;

Expand All @@ -315,15 +319,16 @@ EnvironmentLight computeIBLParts(texture2d<float> irradianceTexture,
// grows when the surface is seen at a slant.
float3 f0 = mix(0.04, inBaseColor.rgb, metallic);

float3 specular=specularIBL(f0, roughness, normalMap.xyz, viewVector, specularTexture, iblBRDFTexture,float3(0.0,1.0,0.0),degreesToRadians(iblRotationAngle));
float3 specular = blurredEnvironment(reflect(-viewVector, normalMap.xyz), roughness, specularTexture, float3(0.0,1.0,0.0), rotation)
* environmentReflectance(f0, brdf);

// Every surface reflects the environment, a non-metal too: that is the sheen of
// polished plastic, glass or lacquer. What it reflects it does not scatter, so the
// diffuse light gives up that share, and a metal scatters none at all. This is the
// single-bounce share only: the multi-bounce energy compensation in
// environmentReflectance exists to correct the specular term's own approximation,
// not to measure how much the surface diverts away from diffuse scattering.
float3 reflected = singleBounceReflectance(f0, roughness, NoV, iblBRDFTexture);
float3 reflected = singleBounceReflectance(f0, brdf);

EnvironmentLight light;
light.diff = max(1.0 - reflected, 0.0) * (1.0 - metallic) * diffuse;
Expand Down Expand Up @@ -636,8 +641,11 @@ fragment float4 fragmentLightShader(VertexCompositeOutput vertexOut [[stage_in]]
color.diff = brdf.diff * (half3)lights.color * (half)lights.intensity;
color.spec = brdf.spec*lights.color*lights.intensity;

// Compute shadow using cascaded shadow maps
float shadow = computeCSMShadow(csmShadowArray, csmUniforms, verticesInWorldSpace.xyz, surfaceNormal, lightRayDirection);
// Compute shadow using cascaded shadow maps, only with a sun (see modelShader.metal).
float shadow = 1.0;
if (lights.intensity != 0.0) {
shadow = computeCSMShadow(csmShadowArray, csmUniforms, verticesInWorldSpace.xyz, surfaceNormal, lightRayDirection);
}

// shadows affect directional light for now
color.diff = color.diff*(half)shadow;
Expand Down Expand Up @@ -753,15 +761,18 @@ float3 computeDirectLighting(
constant PointShadowUniforms &pointShadowUniforms,
constant SpotShadowUniforms &spotShadowUniforms
) {
float3 lightRayDirection = normalize(lights.direction);

LightContribution brdf = computeBRDF(lightRayDirection, viewVector, normal, albedo, float3(1.0), roughness, metallic);

LightContribution color;
color.diff = brdf.diff * (half3)lights.color * (half)lights.intensity;
color.spec = brdf.spec * lights.color * lights.intensity;
color.diff *= (half)sunShadow;
color.spec *= sunShadow;
// A sun at no intensity adds nothing, so its BRDF is not evaluated: the branch is on a
// per-frame constant, uniform across the draw. Any other intensity takes the old path,
// in the old order of products.
if (lights.intensity != 0.0) {
float3 lightRayDirection = normalize(lights.direction);
LightContribution brdf = computeBRDF(lightRayDirection, viewVector, normal, albedo, float3(1.0), roughness, metallic);
color.diff = brdf.diff * (half3)lights.color * (half)lights.intensity;
color.spec = brdf.spec * lights.color * lights.intensity;
color.diff *= (half)sunShadow;
color.spec *= sunShadow;
}

LightContribution pointColor;
uint lightCount = min(plBlock.count.x, MAX_POINT_LIGHTS);
Expand Down Expand Up @@ -881,9 +892,13 @@ static inline float4 lightTBDRSample(
indirectLighting *= ambientOcclusion * iblParam.ambientIntensity;
}

float shadow = indirectFromGBuffer
? float(gbEmissive.a)
: computeCSMShadow(csmShadowArray, csmUniforms, verticesInWorldSpace.xyz, surfaceNormal, lightRayDirection);
// The sun's shadow: the one the geometry wrote, else the cascades, only with a sun.
float shadow = 1.0;
if (indirectFromGBuffer) {
shadow = float(gbEmissive.a);
} else if (lights.intensity != 0.0) {
shadow = computeCSMShadow(csmShadowArray, csmUniforms, verticesInWorldSpace.xyz, surfaceNormal, lightRayDirection);
}
float3 direct = computeDirectLighting(
verticesInWorldSpace, viewVector, surfaceNormal, albedo.rgb, roughness, metallic, shadow,
lights, plBlock, slBlock, alBlock, ltcMagTexture, ltcMatTexture, pointShadowCube, spotShadowMap,
Expand Down
3 changes: 3 additions & 0 deletions Sources/UntoldEngine/Shaders/ShadersUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,11 @@ float4 specularImportanceMap(float2 texCoords, texture2d<float> environmentTextu

float4 BRDFIntegrationMap(float roughness, float NoV);

float2 brdfIntegration(float NoV, float roughness, texture2d<float> brdfMap);
float3 environmentReflectance(float3 F0, float2 brdf);
float3 environmentReflectance(float3 F0, float roughness, float NoV, texture2d<float> brdfMap);

float3 singleBounceReflectance(float3 F0, float2 brdf);
float3 singleBounceReflectance(float3 F0, float roughness, float NoV, texture2d<float> brdfMap);

float3 blurredEnvironment(float3 direction, float roughness, texture2d<float> specularMap, float3 rotationAxis, float rotationAngle);
Expand Down
61 changes: 39 additions & 22 deletions Sources/UntoldEngine/Shaders/ShadersUtils.metal
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ float2 equirectUVFromCubeDirection(float3 v){
const float2 biases {0.5f,0.5f};

//assume +z is forward. for -x forward, use atan2(v.z,v.x)
float2 uv=float2(atan2(-v.x, v.z),asin(-v.y))*scales+biases;
// The latitude's asin is clamped: a reflection of two unit vectors can round a hair past
// 1 at the poles, and asin of that is NaN.
float2 uv=float2(atan2(-v.x, v.z),asin(clamp(-v.y, -1.0, 1.0)))*scales+biases;

return uv;
}
Expand Down Expand Up @@ -498,11 +500,10 @@ float4 BRDFIntegrationMap(float roughness, float NoV){

}

// The share of the light around it that a surface reflects in a single bounce off its
// microfacets: F0 head on (4 % for a non-metal, its own color for a metal), more when it
// is seen at a slant. This is the share diffuse scattering gives up to specular, before
// any later-bounce energy compensation.
float3 singleBounceReflectance(float3 F0, float roughness, float NoV, texture2d<float> brdfMap) {
// The lookup table's two values for a surface seen at an angle (NoV) with a roughness: the
// scale and the bias the reflectance at that slant puts on F0. Read once per pixel and
// handed to the two reflectances below.
float2 brdfIntegration(float NoV, float roughness, texture2d<float> brdfMap) {

// The lookup table is read right at its edges (a surface seen head on, a roughness
// of 1), so it clamps: wrapping there reads the far edge and leaves a white spot.
Expand All @@ -511,36 +512,43 @@ float3 singleBounceReflectance(float3 F0, float roughness, float NoV, texture2d<
mip_filter::none,
address::clamp_to_edge);

float4 brdfIntegration=brdfMap.sample(s,float2(NoV,roughness));
return brdfMap.sample(s, float2(NoV, roughness)).xy;
}

// The share of the light around it that a surface reflects in a single bounce off its
// microfacets: F0 head on (4 % for a non-metal, its own color for a metal), more when it
// is seen at a slant. This is the share diffuse scattering gives up to specular, before
// any later-bounce energy compensation. `brdf` is the lookup table's pair for the surface.
float3 singleBounceReflectance(float3 F0, float2 brdf) {
return F0 * brdf.x + brdf.y;
}

return F0 * float(brdfIntegration.x) + float(brdfIntegration.y);
float3 singleBounceReflectance(float3 F0, float roughness, float NoV, texture2d<float> brdfMap) {
return singleBounceReflectance(F0, brdfIntegration(NoV, roughness, brdfMap));
}

// The share of the light around it that a surface reflects: F0 head on (4 % for a
// non-metal, its own color for a metal), more when it is seen at a slant.
float3 environmentReflectance(float3 F0, float roughness, float NoV, texture2d<float> brdfMap) {
float3 environmentReflectance(float3 F0, float2 brdf) {

constexpr sampler s(coord::normalized,
filter::linear,
mip_filter::none,
address::clamp_to_edge);

float4 brdfIntegration=brdfMap.sample(s,float2(NoV,roughness));

float3 reflectedInOneBounce = F0 * float(brdfIntegration.x) + float(brdfIntegration.y);
float3 reflectedInOneBounce = F0 * brdf.x + brdf.y;

// A rough surface bounces part of the light between its own bumps before it lets it
// go. The table counts a single bounce and takes the rest for lost: up to two thirds
// on the roughest metal, which then comes out too dark. The sum of its two values is
// what a white mirror of this roughness reflects in one bounce; what is missing from
// 1 left on a later bounce, tinted again by the surface each time (Kulla and Conty's
// approximation, in the form Filament uses). An empty table stays an empty result.
float whiteInOneBounce = float(brdfIntegration.x) + float(brdfIntegration.y);
float whiteInOneBounce = brdf.x + brdf.y;
float3 laterBounces = 1.0 + F0 * (1.0 / max(whiteInOneBounce, 0.05) - 1.0);

return reflectedInOneBounce * laterBounces;
}

float3 environmentReflectance(float3 F0, float roughness, float NoV, texture2d<float> brdfMap) {
return environmentReflectance(F0, brdfIntegration(NoV, roughness, brdfMap));
}

// The environment in a direction, as blurred as a roughness makes it.
float3 blurredEnvironment(float3 direction, float roughness, texture2d<float> specularMap, float3 rotationAxis, float rotationAngle) {

Expand All @@ -556,8 +564,14 @@ float3 blurredEnvironment(float3 direction, float roughness, texture2d<float> sp

int mipCount=6;

//Rotate the direction
float3 rotatedDirection=normalize(rotateDirection(direction, rotationAxis, rotationAngle));
// Rotate the direction, unless the environment is not rotated (the headset's room probe
// never is): the angle is a per-frame constant, so the branch is uniform. Either way the
// direction comes out unit length, which the equirectangular lookup's asin needs: the
// simulator's light pass hands in a G-buffer normal as stored.
float3 rotatedDirection = normalize(direction);
if (rotationAngle != 0.0) {
rotatedDirection = rotateDirection(direction, rotationAxis, rotationAngle);
}

float2 uv = equirectUVFromCubeDirection(rotatedDirection);
float mipLevel = roughness * float(mipCount - 1);
Expand Down Expand Up @@ -586,8 +600,11 @@ float3 diffuseIBL(float3 normal, texture2d<float> irradianceMap, float3 rotation
mip_filter::linear,
address::repeat);

//Rotate the normal
float3 rotatedNormal=rotateDirection(normal, rotationAxis, rotationAngle);
// Rotate the normal, unless the environment is not rotated (see blurredEnvironment).
float3 rotatedNormal = normalize(normal);
if (rotationAngle != 0.0) {
rotatedNormal = rotateDirection(normal, rotationAxis, rotationAngle);
}
float2 uv = equirectUVFromCubeDirection(rotatedNormal);
return float3(irradianceMap.sample(s,uv).rgb);
}
Expand Down
53 changes: 30 additions & 23 deletions Sources/UntoldEngine/Shaders/TAAShader.metal
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,21 @@ static inline float4 taaResolveColor(
const float2 texel = 1.0 / max(taa.physicalSize, float2(1.0));
const float4 current = colorTexture.sample(pointSampler, uv);

// This pixel's position in space, from its depth, and where it was on the previous
// frame's screen and in the history texture. Computed for every pixel, before any pixel
// leaves: the local rates of the two maps are read below from the derivatives of these
// across the 2×2 quad, which need every lane of the quad to have computed them.
// A texel at the clear depth has no position: nothing opaque was drawn there — the
// background, or a transparent quad over it — and reprojecting it from the far plane
// would fetch the wrong history and ghost or wipe it under a moving camera; such a pixel
// keeps its own frame, and in passthrough most of the frame is such pixels.
// keeps its own frame, and in passthrough most of the frame is such pixels. It leaves
// here, before the rate-map decodes and the reprojection: nothing below reads the
// neighbouring lanes (the maps' local rates come from a second decode a texel over, not
// from derivatives across the quad), so a lane that leaves early is safe.
const float depth = depthTexture.sample(pointSampler, uv);
const bool cleared = (taa.reverseZ != 0u) ? (depth <= 1e-6) : (depth >= 1.0 - 1e-6);
if (taa.historyValid == 0u || cleared) {
return current;
}

// This pixel's position in space, from its depth, and where it was on the previous
// frame's screen and in the history texture.
const float2 physical = in.position.xy;
const float2 screen = taaScreenFromPhysical(physical, taa.rateMapSizes, rateMap);
const float2 screenSize = taa.rateMapSizes.x > 0.0 ? taa.rateMapSizes.xy : taa.physicalSize;
Expand All @@ -162,24 +166,7 @@ static inline float4 taaResolveColor(
const float2 prevScreen = float2((prevNdc.x * 0.5 + 0.5) * prevScreenSize.x, (1.0 - (prevNdc.y * 0.5 + 0.5)) * prevScreenSize.y);
const float2 prevPhysical = taaPhysicalFromScreen(prevScreen, taa.prevRateMapSizes, prevRateMap);

// Under foveation the history may be coarser here than this frame: the spot was in the
// periphery of the previous map (one texel spanning several screen pixels) and the gaze
// has moved onto it. Trusting such a history at full weight shows the sharp frame with a
// blurry shadow that fades over twenty frames; its weight is cut by how much coarser it
// was, measured from the two maps' local rates (texels per screen pixel), per axis: a map
// is coarser in one axis than the other wherever its rows and columns differ, so the
// ratio is taken on the axis where the history is coarsest relative to this frame. The
// rates are the derivatives of the mappings across the quad — this frame's screen
// position per texel, and the history's texel per screen pixel — instead of a second
// decode of each map a pixel over.
const float2 screenStep = float2(abs(dfdx(screen.x)), abs(dfdy(screen.y)));
const float2 currentRate = 1.0 / max(screenStep, float2(1e-3));
const float2 prevScreenStep = float2(abs(dfdx(prevScreen.x)), abs(dfdy(prevScreen.y)));
const float2 prevPhysicalStep = float2(abs(dfdx(prevPhysical.x)), abs(dfdy(prevPhysical.y)));
const float2 prevRate = prevPhysicalStep / max(prevScreenStep, float2(1e-4));
const float2 ratio = prevRate / currentRate;

if (taa.historyValid == 0u || cleared || !worldValid || !prevInFront) {
if (!worldValid || !prevInFront) {
return current;
}
const float2 historyUV = prevPhysical * texel;
Expand Down Expand Up @@ -208,6 +195,26 @@ static inline float4 taaResolveColor(

float weight = taa.historyWeight;
if (taa.rateMapSizes.x > 0.0 || taa.prevRateMapSizes.x > 0.0) {
// Under foveation the history may be coarser here than this frame: the spot was in
// the periphery of the previous map (one texel spanning several screen pixels) and
// the gaze has moved onto it. Trusting such a history at full weight shows the sharp
// frame with a blurry shadow that fades over twenty frames; its weight is cut by how
// much coarser it was, measured from the two maps' local rates (texels per screen
// pixel), per axis: a map is coarser in one axis than the other wherever its rows and
// columns differ, so the ratio is taken on the axis where the history is coarsest
// relative to this frame. Both rates are read as texels per screen pixel over the
// same screen interval, one pixel on from this pixel's screen position (or back,
// at either screen's far edge), each from its own map a second time: the maps' rows
// and columns are separable, so one decode gives both axes. The same interval for
// both keeps the ratio exactly 1 wherever the two maps agree, zone boundaries
// included. Not from derivatives across the quad: the pixels that left above would
// leave those undefined. Where the maps differ, the band before a boundary weighs a
// little differently than the quad's span did; it is at most a pixel wide.
const float2 delta = select(float2(1.0), float2(-1.0),
screen + 1.0 >= screenSize || prevScreen + 1.0 >= prevScreenSize);
const float2 currentRate = abs(taaPhysicalFromScreen(screen + delta, taa.rateMapSizes, rateMap) - physical);
const float2 prevRate = abs(taaPhysicalFromScreen(prevScreen + delta, taa.prevRateMapSizes, prevRateMap) - prevPhysical);
const float2 ratio = prevRate / max(currentRate, float2(1e-4));
weight *= saturate(min(ratio.x, ratio.y));
}

Expand Down
10 changes: 7 additions & 3 deletions Sources/UntoldEngine/Shaders/TransparencyShader.metal
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,13 @@ fragment TransparencyOutput fragmentTransparencyShader(
totalLight.diff = brdf.diff * (half3)lights.color * (half)lights.intensity;
totalLight.spec = brdf.spec * lights.color * lights.intensity;

float shadow = computeCSMShadow(csmShadowArray, csmUniforms, verticesInWorldSpace.xyz, normal, lightDirection);
totalLight.diff *= (half)shadow;
totalLight.spec *= shadow;
// The sun's shadow only with a sun (see modelShader.metal's emissiveWithIndirect).
float shadow = 1.0;
if (lights.intensity != 0.0) {
shadow = computeCSMShadow(csmShadowArray, csmUniforms, verticesInWorldSpace.xyz, normal, lightDirection);
totalLight.diff *= (half)shadow;
totalLight.spec *= shadow;
}

uint pointLightCount = min(plBlock.count.x, MAX_POINT_LIGHTS);
for (uint i = 0; i < pointLightCount; ++i) {
Expand Down
Loading