From 6fb43d32e5ffc093567c4688138d20e760f234ed Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sat, 1 Aug 2026 23:38:47 +0200 Subject: [PATCH 1/4] Rebuild Photon transport and temporal stability --- include/atlas/core/default_shaders.h | 327 ++++++++++++------ include/photon/illuminate.h | 19 +- photon/path_tracing.cpp | 187 ++++++++-- shaders/metal/path_tracing/path.metal | 264 ++++++++++---- shaders/metal/path_tracing/path_denoise.metal | 14 +- 5 files changed, 612 insertions(+), 199 deletions(-) diff --git a/include/atlas/core/default_shaders.h b/include/atlas/core/default_shaders.h index 64156c5c..a66e07fc 100644 --- a/include/atlas/core/default_shaders.h +++ b/include/atlas/core/default_shaders.h @@ -6716,6 +6716,20 @@ struct AreaLight { float twoSided; }; +struct EmissiveTriangle { + float4 p0; + float4 p1; + float4 p2; + float4 normal; + packed_float3 emission; + float area; + float cdf; + float selectionPdf; + float2 _pad; +}; + +static_assert(sizeof(EmissiveTriangle) == 96); + struct SceneData { uint numDirectionalLights; uint numPointLights; @@ -6736,6 +6750,9 @@ struct SceneData { uint pixelStride; float3 ambientColor; uint environmentEnabled; + uint accumulationFrameLimit; + float fireflyClamp; + uint numEmissiveTriangles; }; static_assert(sizeof(SceneData) == 144); @@ -6744,6 +6761,8 @@ static_assert(__builtin_offsetof(SceneData, atmosphereSunIntensity) == 64); static_assert(__builtin_offsetof(SceneData, atmosphereSunColor) == 80); static_assert(__builtin_offsetof(SceneData, pixelStride) == 96); static_assert(__builtin_offsetof(SceneData, ambientColor) == 112); +static_assert(__builtin_offsetof(SceneData, accumulationFrameLimit) == 132); +static_assert(__builtin_offsetof(SceneData, numEmissiveTriangles) == 140); float pow5(float x) { float x2 = x * x; @@ -6886,15 +6905,15 @@ float2 encodeNormal(float3 normal) { return encoded; } -constexpr sampler materialTexSampler(coord::normalized, address::repeat, +constexpr sampler materialTexSampler()", +R"(coord::normalized, address::repeat, filter::linear, mip_filter::linear); #define PT_MATERIAL_TEXTURE_PARAMS \ texture2d materialTexture0, texture2d materialTexture1, \ texture2d materialTexture2, texture2d materialTexture3, \ texture2d materialTexture4, texture2d materialTexture5, \ - texture2d materialTexture6, text)", -R"(ure2d materialTexture7, \ + texture2d materialTexture6, texture2d materialTexture7, \ texture2d materialTexture8, texture2d materialTexture9, \ texture2d materialTexture10, \ texture2d materialTexture11, \ @@ -6992,14 +7011,14 @@ R"(ure2d materialTexture7, \ texture2d materialTexture36 [[texture(48)]], \ texture2d materialTexture37 [[texture(49)]], \ texture2d materialTexture38 [[texture(50)]], \ - texture2d materialTexture39 [[texture(51)]], \ + texture2d)", +R"( materialTexture39 [[texture(51)]], \ texture2d materialTexture40 [[texture(52)]], \ texture2d materialTexture41 [[texture(53)]], \ texture2d materialTexture42 [[texture(54)]], \ texture2d materialTexture43 [[texture(55)]], \ texture2d materialTexture44 [[texture(56)]], \ - textu)", -R"(re2d materialTexture45 [[texture(57)]], \ + texture2d materialTexture45 [[texture(57)]], \ texture2d materialTexture46 [[texture(58)]], \ texture2d materialTexture47 [[texture(59)]] @@ -7187,7 +7206,8 @@ void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, roughness *= clamp(roughnessValue, 0.0, 1.0); } if (mat.aoTextureIndex >= 0 && uint(mat.aoTextureIndex) < textureCount) { - ao *= clamp(sampleMaterialTexture(mat.aoTextureIndex, uv, + ao *= clamp(sampleM)", +R"(aterialTexture(mat.aoTextureIndex, uv, PT_MATERIAL_TEXTURE_ARGS) .x, 0.0, 1.0); @@ -7200,8 +7220,7 @@ void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, } float3 resolveShadingNormal(Material mat, float2 uv, float3 localN, - float3 localT, float3 localB, Instance)", -R"(Data inst, + float3 localT, float3 localB, InstanceData inst, uint textureCount, PT_MATERIAL_TEXTURE_PARAMS) { float3x3 normalMatrix = float3x3(inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); @@ -7291,11 +7310,7 @@ float3 traceShadowVisibility(intersector isect, float3 p0 = float3(vertices[i0].position); float3 p1 = float3(vertices[i1].position); float3 p2 = float3(vertices[i2].position); - InstanceData inst = instanceData[objectIndex]; - float3x3 normalMatrix = float3x3( - inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); - float3 hitNormal = normalizeOr( - normalMatrix * cross(p1 - p0, p2 - p0), -L); + float3 hitNormal = normalizeOr(cross(p1 - p0, p2 - p0), -L); hitNormal = dot(hitNormal, L) < 0.0 ? hitNormal : -hitNormal; float ior = max(material.ior, 1.0); float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); @@ -7366,11 +7381,22 @@ float3 F_Schlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow5(c); } +float3 materialF0(float3 albedo, float metallic, float reflectivity, + float ior) { + float dielectricF0 = pow((max(ior, 1.0001) - 1.0) / + (max(ior, 1.0001) + 1.0), + 2.0); + float dielectricScale = mix(0.5, 1.5, clamp(reflectivity, 0.0, 1.0)); + return mix(float3(clamp(dielectricF0 * dielectricScale, 0.0, 0.16)), + albedo, clamp(metallic, 0.0, 1.0)); +} + float G_Smith(float NdotV, float NdotL, float roughness) { float r = roughness + 1.0; float k = (r * r) / 8.0; float gV = NdotV / (NdotV * (1.0 - k) + k); - float gL = NdotL / (NdotL * (1.0 - k) + k); + )", +R"(float gL = NdotL / (NdotL * (1.0 - k) + k); return gV * gL; } @@ -7389,8 +7415,7 @@ float disneyDiffuseFactor(float NdotV, float NdotL, float LdotH, float fd90 = 0.5 + 2.0 * LdotH * LdotH * roughness; float lightScatter = 1.0 + (fd90 - 1.0) * pow5(1.0 - NdotL); float viewScatter = 1.0 + (fd90 - 1.0) * pow5(1.0 - NdotV); - return lightSc)", -R"(atter * viewScatter; + return lightScatter * viewScatter; } // GGX importance-sampled microfacet half-vector (in local TBN space, Z=up) @@ -7441,20 +7466,13 @@ float3 evalPBR(float3 albedo, float metallic, float roughness, float VdotH = max(dot(V, H), 0.0); float clampedRoughness = clamp(roughness, 0.045, 1.0); - float dielectricF0 = pow((max(ior, 1.0) - 1.0) / - (max(ior, 1.0) + 1.0), - 2.0); - float3 baseF0 = mix(float3(dielectricF0), albedo, - clamp(metallic, 0.0, 1.0)); - float3 reflectedColor = mix(float3(1.0), albedo, metallic); - float3 F0 = mix(baseF0, reflectedColor, clamp(reflectivity, 0.0, 1.0)); + float3 F0 = materialF0(albedo, metallic, reflectivity, ior); float3 F = F_Schlick(VdotH, F0); float D = D_GGX(NdotH, clampedRoughness); float G = G_Smith(NdotV, NdotL, clampedRoughness); float3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4); float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)) * - (1.0 - clamp(reflectivity, 0.0, 1.0)) * (1.0 - clamp(transmittance, 0.0, 1.0)); float diffuseFactor = disneyDiffuseFactor(NdotV, NdotL, max(dot(L, H), 0.0), clampedRoughness); @@ -7495,13 +7513,73 @@ float3 evalTransmission(float3 albedo, float3 N, float3 V, float3 L, transmissionLobe; } +float3 evalEmissiveTriangleLighting( + intersector isect, + primitive_acceleration_structure sceneAS, float3 P, float3 N, float3 Ng, + float3 V, float3 albedo, float metallic, float roughness, + float reflectivity, float ior, float transmittance, thread uint &rng, + constant SceneData &sceneData, + constant EmissiveTriangle *emissiveTriangles, + constant Material *materials, constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, constant VertexData *vertices, + constant uint *indices, constant InstanceData *instanceData, + PT_MATERIAL_TEXTURE_PARAMS) { + if (sceneData.numEmissiveTriangles == 0) { + return float3(0.0); + } + float selector = rand(rng); + uint first = 0; + uint last = sceneData.numEmissiveTriangles - 1; + while (first < last) { + uint middle = first + (last - first) / 2; + if (selector <= emissiveTriangles[middle].cdf) { + last = middle; + } else { + first = middle + 1; + } + } + EmissiveTriangle light = emissiveTriangles[first]; + float sqrtU = sqrt(rand(rng)); + float barycentricV = rand(rng); + float b0 = 1.0 - sqrtU; + float b1 = sqrtU * (1.0 - barycentricV); + float b2 = sqrtU * barycentricV; + float3 lightPosition = light.p0.xyz * b0 + light.p1.xyz * b1 + + light.p2.xyz * b2; + float3 toLight = lightPosition - P; + float distanceSquared = dot(toLight, toLight); + if (distanceSquared <= 1e-8) { + return float3(0.0); + } + float distanceToLight = sqrt(distanceSquared); + float3 L = toLight / distanceToLight; + float surfaceCosine = dot(N, L); + float lightCosine = abs(dot(light.normal.xyz, -L)); + if (surfaceCosine <= 0.0 || dot(Ng, L) <= 0.0 || + lightCosine <= 1e-5 || light.area <= 1e-8 || + light.selectionPdf <= 1e-8) { + return float3(0.0); + } + float solidAnglePdf = light.selectionPdf * distanceSquared / + max(lightCosine * light.area, 1e-8); + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, distanceToLight, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, indices, + instanceData, sceneData, PT_MATERIAL_TEXTURE_ARGS); + return evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, light.emission, + 1.0 / max(solidAnglePdf, 1e-8)) * + visibility; +} + // --------------------------------------------------------------------------- // Direct lighting with full PBR (replaces old evalDirectLighting) // --------------------------------------------------------------------------- float3 evalDirectLightingPBR(intersector isect, primitive_acceleration_structure sceneAS, float3 P, - float3 N, float3 Ng, float3 V, float3 albedo, + float3 N, fl)", +R"(oat3 Ng, float3 V, float3 albedo, float metallic, float roughness, float reflectivity, float ior, float transmittance, float sssStrength, float sssThickness, @@ -7511,6 +7589,7 @@ float3 evalDirectLightingPBR(intersector isect, constant PointLight *pointLights, constant SpotLight *spotLights, constant AreaLight *areaLights, + constant EmissiveTriangle *emissiveTriangles, constant Material *materials, constant uint *primitiveObjects, constant uint *blasPrimitiveOffsets, @@ -7551,8 +7630,7 @@ float3 evalDirectLightingPBR(intersector isect, transmittance, N, V, L, pointLights[i].color, intensity); float3 s = - evalSubsurface(albedo, N)", -R"(, V, L, pointLights[i].color, intensity, + evalSubsurface(albedo, N, V, L, pointLights[i].color, intensity, roughness, sssStrength, sssThickness); float3 visibility = traceShadowVisibility( isect, sceneAS, P, Ng, L, dist, rng, materials, primitiveObjects, @@ -7623,6 +7701,12 @@ R"(, V, L, pointLights[i].color, intensity, lighting += (c + s * (1.0 - transmittance)) * visibility; } + lighting += evalEmissiveTriangleLighting( + isect, sceneAS, P, N, Ng, V, albedo, metallic, roughness, + reflectivity, ior, transmittance, rng, sceneData, emissiveTriangles, + materials, primitiveObjects, blasPrimitiveOffsets, vertices, indices, + instanceData, PT_MATERIAL_TEXTURE_ARGS); + return lighting; } @@ -7643,10 +7727,12 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, constant PointLight *pointLights, constant SpotLight *spotLights, constant AreaLight *areaLights, + constant EmissiveTriangle *emissiveTriangles, PT_MATERIAL_TEXTURE_PARAMS, texturecube skybox, thread float3 &primaryAlbedo, thread float3 &primaryNormal, - thread float3 &primaryPosition, + thread f)", +R"(loat3 &primaryPosition, thread float &primaryDepth, thread float &primaryRoughness, thread float &primaryHitDistance, @@ -7714,8 +7800,7 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, float3x3 normalMatrix = float3x3( inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); geometricNormal = normalizeOr( - normalMatrix *)", -R"( cross(p1 - p0, p2 - p0), + cross(p1 - p0, p2 - p0), normalizeOr(normalMatrix * localN, float3(0.0, 1.0, 0.0))); float alpha = resolveMaterialOpacity( @@ -7779,17 +7864,21 @@ R"( cross(p1 - p0, p2 - p0), primaryObjectId = surfaceObjectIndex; } - float reflectivity = clamp(mat.reflectivity, 0.0, 1.0) * - (1.0 - transmittance); + float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); float sssStrength = 0.0; float sssThickness = mix(0.25, 1.75, ao); float3 direct = evalDirectLightingPBR( isect, sceneAS, P, N, Ng, V, albedo, metallic, roughness, reflectivity, ior, transmittance, sssStrength, sssThickness, rng, - dirLight, sceneData, pointLights, spotLights, areaLights, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, indices, - instanceData, PT_MATERIAL_TEXTURE_ARGS); - radiance += throughput * (direct + emissive); + dirLight, sceneData, pointLights, spotLights, areaLights, + emissiveTriangles, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, + PT_MATERIAL_TEXTURE_ARGS); + radiance += throughput * direct; + if (depth == 0 || previousEventWasDelta || + sceneData.numEmissiveTriangles == 0) { + radiance += throughput * emissive; + } if (depth == 0 && sceneData.ambientIntensity > 0.0) { float aoVisibility = mix(0.2, 1.0, ao); @@ -7807,22 +7896,18 @@ R"( cross(p1 - p0, p2 - p0), } float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); - float3 baseF0 = mix(float3(dielectricF0), albedo, metallic); - float3 reflectedColor = mix(float3(1.0), albedo, metallic); - float3 F0 = mix(baseF0, reflectedColor, reflectivity); + float3 F0 = materialF0(albedo, metallic, mat.reflectivity, ior); float NdotV = max(dot(N, V), 1e-4); - float dielectricFresnel = - F_Schlick(NdotV, float3(dielectricF0)).x; - float specProb = metallic * mix(0.35, 0.9, 1.0 - roughness) + - (1.0 - metallic) * dielectricFresnel; + float3 viewFresnel = F_Schlick(NdotV, F0); + float fresnelProbability = clamp(luminance(viewFresnel), 0.001, 0.999); + float specProb = fresnelProbability; float transmitProb = transmittance * (1.0 - metallic) * - (1.0 - dielectricFresnel); - float diffuseProb = (1.0 - metallic) * (1.0 - transmittance); - specProb = mix(specProb, 1.0, reflectivity); - transmitProb *= 1.0 - reflectivity; - diffuseProb *= 1.0 - reflectivity; + (1.0 - fresnelProbability); + float diffuseProb = (1.0 - metallic) * (1.0 - transmittance) * + (1.0 - fresnelProbability); float eta = frontFace ? 1.0 / ior : ior; - float3 idealRefractedDirection = refract(-V, N, eta); + float3 idealRefractedDi)", +R"(rection = refract(-V, N, eta); bool totalInternalReflection = dot(idealRefractedDirection, idealRefractedDirection) < 1e-8; if (totalInternalReflection) { @@ -7858,7 +7943,7 @@ R"( cross(p1 - p0, p2 - p0), float VdotH = max(dot(V, H), 1e-5); float3 F = F_Schlick(VdotH, F0); float3 kD = (1.0 - F) * (1.0 - metallic) * - (1.0 - transmittance) * (1.0 - reflectivity); + (1.0 - transmittance); float diffuseFactor = disneyDiffuseFactor( NdotV, NdotEnvironment, max(dot(environmentDirection, H), 0.0), roughness); @@ -7883,8 +7968,7 @@ R"( cross(p1 - p0, p2 - p0), float3 environmentRadiance = skyColor( environmentDirection, 0.0, skybox, sceneData); radiance += throughput * reflectionBsdf * - )", -R"( environmentRadiance * visibility * NdotEnvironment * + environmentRadiance * visibility * NdotEnvironment * misWeight / max(environmentPdf, 1e-6); } } @@ -7964,7 +8048,8 @@ R"( environmentRadiance * visibility * NdotEnvironment * float NdotL = max(dot(N, nextDirection), 0.0); float3 H = normalizeOr(V + nextDirection, N); float3 F = F_Schlick(max(dot(V, H), 0.0), F0); - float3 kD = (1.0 - F) * (1.0 - metallic); + float3 kD = (1.0 - F) * (1.0 - metallic) * + (1.0 - transmittance); float diffuseFactor = disneyDiffuseFactor( NdotV, NdotL, max(dot(nextDirection, H), 0.0), roughness); float3 diffuseBsdf = kD * albedo * diffuseFactor / M_PI_F; @@ -7992,7 +8077,8 @@ R"( environmentRadiance * visibility * NdotEnvironment * if (depth >= 2) { float survival = clamp(max(throughput.x, max(throughput.y, throughput.z)), - 0.05, 0.95); + )", +R"( 0.05, 0.95); if (rand(rng) > survival) { break; } @@ -8013,13 +8099,15 @@ R"( environmentRadiance * visibility * NdotEnvironment * } kernel void main0(texture2d outTex [[texture(0)]], - texture2d historyTex [[texture(1)]], + texture2d historyTex [[texture(1)]], texture2d brightTex [[texture(2)]], texture2d albedoRoughnessTex [[texture(3)]], texture2d normalDepthTex [[texture(4)]], texture2d motionObjectTex [[texture(5)]], - texture2d momentsHitTex [[texture(6)]], - texture2d historyGuideTex [[texture(7)]], + texture2d momentsHitTex [[texture(6)]], + texture2d historyGuideTex [[texture(7)]], + texture2d historyOutTex [[texture(8)]], + texture2d historyGuideOutTex [[texture(9)]], primitive_acceleration_structure sceneAS [[buffer(0)]], constant CameraUniforms &cam [[buffer(1)]], constant Material *materials [[buffer(2)]], @@ -8032,6 +8120,7 @@ kernel void main0(texture2d outTex [[texture(0)]], constant PointLight *pointLights [[buffer(9)]], constant SpotLight *spotLights [[buffer(10)]], constant AreaLight *areaLights [[buffer(11)]], + constant EmissiveTriangle *emissiveTriangles [[buffer(14)]], PT_MATERIAL_TEXTURE_BINDINGS, constant uint *blasPrimitiveOffsets [[buffer(13)]], texturecube skybox [[texture(60)]], @@ -8062,10 +8151,11 @@ kernel void main0(texture2d outTex [[texture(0)]], uint spp = max(sceneData.raysPerPixel, 1u); for (uint s = 0; s < spp; ++s) { uint cameraRng = seedBase(gid, w, sceneData.frameIndex, - s + 0x9E3779B9u);)", -R"( - float2 pixelJitter = - float2(rand(cameraRng), rand(cameraRng)) - 0.5; + s + 0x9E3779B9u); + float2 pixelJitter = s == 0 + ? float2(0.0) + : float2(rand(cameraRng), rand(cameraRng)) - + 0.5; float2 sampleUv = (float2(gid) + 0.5 + pixelJitter) / float2(w, h); float2 sampleNdc = sampleUv * 2.0 - 1.0; sampleNdc.y = -sampleNdc.y; @@ -8091,13 +8181,14 @@ R"( gid, s, w, isect, sceneAS, primaryRay, materials, primitiveObjects, blasPrimitiveOffsets, vertices, indices, instanceData, dirLight, sceneData, pointLights, spotLights, areaLights, - PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, sampleNormal, - samplePosition, sampleDepth, sampleRoughness, sampleHitDistance, - sampleObjectId); + emissiveTriangles, PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, + sampleNormal, samplePosition, sampleDepth, sampleRoughness, + sampleHitDistance, sampleObjectId); if (!all(isfinite(sample))) { sample = float3(0.0); } - color += clampLuminance(max(sample, float3(0.0)), 12.0); + color += clampLuminance(max(sample, float3(0.0)), + max(sceneData.fireflyClamp, 1.0)); if (s == 0) { primaryAlbedo = sampleAlbedo; primaryNormal = sampleNormal; @@ -8114,36 +8205,67 @@ R"( color = float3(0.0); } - int frameIndex = int(sceneData.frameIndex); - - float4 prevColor = historyTex.read(gid); - float4 previousGuide = historyGuideTex.read(gid); float objectIdValue = primaryObjectId == 0xFFFFFFFFu ? -1.0 : float(primaryObjectId); float2 encodedNormal = encodeNormal(primaryNormal); float4 currentGuide = float4(encodedNormal, primaryDepth, objectIdValue); - bool historyValid = frameIndex > 0 && + float4 previousClip = cam.prevViewProj * float4(primaryPosition, 1.0); + float2 previousNdc = previousClip.xy / max(abs(previousClip.w), 0.0001); + float2 previousUv = float2(previousNdc.x * 0.5 + 0.5, + 0.5 - previousNdc.y * 0.5); + bool previousUvValid = previousClip.w > 0.0 && + all(previousUv >= float2(0.0)) && + all(previousUv <= float2(1.0)); + uint2 previousPixel = gid; + if (primaryObjectId != 0xFFFFFFFFu && previousUvValid) { + previousPixel = uint2(clamp(previousUv * float2(w, h), float2(0.0), + float2(w - 1, h - 1))); + } + float4 prevColor = historyTex.read(previousPixel); + float4 previousGuide = historyGuideTex.read(previousPixel); + float4 previousMoments = momentsHitTex.read(gid); + bool historyValid = sceneData.frameIndex > 0 && prevColor.w > 0.0 && abs(previousGuide.z - primaryDepth) < - max(0.05, primaryDepth * 0.02) && - distance(previousGuide.xy, encodedNormal) < 0.08 && + max(0.02, primaryDepth * 0.01) && + distance(previousGuide.xy, encodedNormal) < 0.04 && abs(previousGuide.w - objectIdValue) < 0.5; - if (frameIndex == 0) - prevColor = float4(0, 0, 0, 1); - float sampleLuminanceLimit = - historyValid ? max(4.0, luminance(prevColor.xyz) * 2.0 + 0.5) : 12.0; + if (!historyValid) { + prevColor = float4(0.0); + previousMoments = float4(0.0); + } + float previousMean = historyValid ? previousMoments.x : 0.0; + float previousVariance = + historyValid + ? max(previousMoments.y - previousMean * previousMean, 0.0) + : 0.0; + float sampleLuminanceLimit = max(sceneData.fireflyClamp, 1.0); + if (historyValid && prevColor.w >= 4.0) { + float statisticalLimit = previousMean + + max(0.5, 6.0 * sqrt(previousVariance)); + sampleLuminanceLimit = + min(sampleLuminanceLimit, max(4.0, statisticalLimit)); + } color = clampLuminance(color, sampleLuminanceLimit); - if (!historyValid) - prevColor = float4(color, 1.0); - - float historyLength = historyValid ? min(float(frameIndex), 255.0) : 0.0; - float3 lower = min(prevColor.xyz, color) - float3(0.35); - float3 upper = max(prevColor.xyz, color) + float3(0.35); - float3 clippedHistory = clamp(prevColor.xyz, lower, upper); - float3 accum = mix(color, clippedHistory, - historyLength / (historyLength + 1.0)); - accum = clampLuminance(accum, 24.0); + float historyLimit = max(float(sceneData.accumulationFrameLimit), 1.0); + float previousWeight = + historyValid ? min(prevColor.w, max(historyLimit - 1.0, 0.0)) : 0.0; + float newHistoryLength = min(previousWeight + 1.0, historyLimit); + float accumulationDenominator = max(previousWeight + 1.0, 1.0); + float3 accum = + (prevColor.xyz * previousWeight + color) / accumulationDenominator; + float moment = luminance(color); + float accumulatedMoment = +)", +R"( (previousMoments.x * previousWeight + moment) / + accumulationDenominator; + float accumulatedMomentSquared = + (previousMoments.y * previousWeight + moment * moment) / + accumulationDenominator; + float variance = max(accumulatedMomentSquared - + accumulatedMoment * accumulatedMoment, + 0.0); constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; @@ -8155,11 +8277,7 @@ R"( float contribution = max(brightness - bloomThreshold, soft) / max(brightness, 0.00001); float3 brightColor = accum * contribution; - float4 previousClip = cam.prevViewProj * float4(primaryPosition, 1.0); - float2 previousUv = previousClip.xy / max(abs(previousClip.w), 0.0001); - previousUv = previousUv * 0.5 + 0.5; - float2 motion = uv - previousUv; - float moment = luminance(color); + float2 motion = previousUvValid ? uv - previousUv : float2(0.0); for (uint y = 0; y < pixelStride; ++y) { for (uint x = 0; x < pixelStride; ++x) { @@ -8167,14 +8285,15 @@ R"( if (pixel.x >= w || pixel.y >= h) { continue; } - historyTex.write(float4(accum, 1.0), pixel); - historyGuideTex.write(currentGuide, pixel); + historyOutTex.write(float4(accum, newHistoryLength), pixel); + historyGuideOutTex.write(currentGuide, pixel); albedoRoughnessTex.write(float4(primaryAlbedo, primaryRoughness), pixel); normalDepthTex.write(float4(primaryNormal, primaryDepth), pixel); motionObjectTex.write(float4(motion, objectIdValue, 1.0), pixel); - momentsHitTex.write(float4(moment, moment * moment, - primaryRoughness, primaryHitDistance), + momentsHitTex.write(float4(accumulatedMoment, + accumulatedMomentSquared, variance, + primaryHitDistance), pixel); outTex.write(float4(accum, 1.0), pixel); brightTex.write(float4(brightColor, 1.0), pixel); @@ -8183,7 +8302,7 @@ R"( } )", }; -static const AtlasPackedShaderSource PATH = {PATH_PARTS, 9}; +static const AtlasPackedShaderSource PATH = {PATH_PARTS, 10}; static const char* const PATH_DENOISE_PARTS[] = { R"(#include @@ -8199,6 +8318,7 @@ kernel void main0(texture2d inputTexture [[texture(0)]], texture2d guideTexture [[texture(3)]], texture2d albedoRoughnessTexture [[texture(4)]], + texture2d momentsTexture [[texture(5)]], constant DenoiseParameters ¶meters [[buffer(0)]], uint2 gid [[thread_position_in_grid]]) { uint width = outputTexture.get_width(); @@ -8214,6 +8334,7 @@ kernel void main0(texture2d inputTexture [[texture(0)]], float3 center = inputTexture.read(gid).xyz; float4 centerGuide = guideTexture.read(gid); float4 centerAlbedoRoughness = albedoRoughnessTexture.read(gid); + float4 centerMoments = momentsTexture.read(gid); bool centerSurface = centerGuide.w > 0.0; float centerNormalLength = dot(centerGuide.xyz, centerGuide.xyz); float centerLuminance = dot(center, float3(0.2126, 0.7152, 0.0722)); @@ -8285,7 +8406,17 @@ kernel void main0(texture2d inputTexture [[texture(0)]], filtered += sampleColor * weight; totalWeight += weight; } - float3 result = totalWeight > 0.0001 ? filtered / totalWeight : center; + float3 spatialResult = + totalWeight > 0.0001 ? filtered / totalWeight : center; + float roughness = clamp(centerAlbedoRoughness.w, 0.0, 1.0); + float relativeNoise = sqrt(max(centerMoments.z, 0.0)) / + max(centerLuminance, 0.05); + float filterStrength = clamp(relativeNoise * 1.5, 0.02, 1.0) * + mix(0.12, 1.0, roughness * roughness); + if (!centerSurface) { + filterStrength *= 0.25; + } + float3 result = mix(center, spatialResult, filterStrength); float brightness = dot(result, float3(0.2126, 0.7152, 0.0722)); constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; diff --git a/include/photon/illuminate.h b/include/photon/illuminate.h index 7c7aa859..4e19d175 100644 --- a/include/photon/illuminate.h +++ b/include/photon/illuminate.h @@ -88,21 +88,24 @@ class PathTracing { void init(); /** @brief Resizes path tracing output and history textures. */ void resizeOutput(int width, int height); + void configure(int samplesPerPixel, int bounceLimit, bool useDenoising, + int historyFrames); + void resetAccumulation(); const std::string &getLastError() const { return lastError; } - /** @brief Current frame output texture. */ - std::shared_ptr pathTracingTexturePrev; - /** @brief Rays traced per pixel each dispatch. */ - int raysPerPixel = 2; + int raysPerPixel = 4; /** @brief Maximum bounce count for indirect transport. */ - int maxBounces = 6; + int maxBounces = 8; /** @brief Scalar multiplier for indirect lighting contribution. */ float indirectStrength = 1.0f; /** @brief Whether normal maps are evaluated during shading. */ bool sampleNormalMaps = true; /** @brief Strength multiplier applied to sampled normal maps. */ float normalMapStrength = 1.0f; + bool denoisingEnabled = true; + int accumulationFrames = 512; + float fireflyClamp = 32.0f; private: std::shared_ptr pointLights; @@ -115,6 +118,7 @@ class PathTracing { std::shared_ptr materialBuffer; std::shared_ptr instanceDataBuffer; std::shared_ptr blasPrimitiveOffsets; + std::shared_ptr emissiveTriangles; std::vector> materialTextures; std::vector> materialTextureBindings; std::shared_ptr sceneBLAS; @@ -123,8 +127,9 @@ class PathTracing { std::shared_ptr computePathTracer; std::shared_ptr computePathDenoiser; std::array, 2> denoiseTextures; + std::array, 2> pathTracingHistoryTextures; + std::array, 2> pathTracingHistoryGuides; std::array, 4> pathTracingAovTextures; - std::shared_ptr pathTracingHistoryGuide; std::vector cachedBLASPrimitiveOffsets; std::vector cachedObjects; std::vector cachedSceneObjects; @@ -132,8 +137,10 @@ class PathTracing { std::vector cachedObjectStateHashes; std::vector cachedSceneObjectStateHashes; uint64_t cachedLightHash = 0; + int emissiveTriangleCount = 0; int frameIndex = 0; + int historyReadIndex = 0; int outputWidth = 0; int outputHeight = 0; int interactiveFramesRemaining = 0; diff --git a/photon/path_tracing.cpp b/photon/path_tracing.cpp index 7773b0fe..b2dcb011 100644 --- a/photon/path_tracing.cpp +++ b/photon/path_tracing.cpp @@ -208,6 +208,8 @@ void photon::PathTracing::init() { cachedObjectStateHashes.clear(); cachedSceneObjectStateHashes.clear(); cachedInstanceTransforms.clear(); + emissiveTriangles.reset(); + emissiveTriangleCount = 0; accelerationBuildFailed = false; lastError.clear(); @@ -237,9 +239,16 @@ void photon::PathTracing::init() { outputWidth = std::max(1, Window::mainWindow->viewportWidth); outputHeight = std::max(1, Window::mainWindow->viewportHeight); - pathTracingTexturePrev = std::make_shared( - Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, TextureType::Color)); + for (auto &texture : pathTracingHistoryTextures) { + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); + } + for (auto &texture : pathTracingHistoryGuides) { + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); + } for (auto &texture : denoiseTextures) { texture = std::make_shared(Texture::create( outputWidth, outputHeight, opal::TextureFormat::Rgba16F, @@ -250,9 +259,8 @@ void photon::PathTracing::init() { outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); } - pathTracingHistoryGuide = std::make_shared( - Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, TextureType::Color)); + historyReadIndex = 0; + frameIndex = 0; interactiveFramesRemaining = 4; interactive = true; } @@ -261,15 +269,23 @@ void photon::PathTracing::resizeOutput(int width, int height) { const int newWidth = std::max(1, width); const int newHeight = std::max(1, height); if (newWidth == outputWidth && newHeight == outputHeight && - pathTracingTexturePrev != nullptr) { + pathTracingHistoryTextures[0] != nullptr && + pathTracingHistoryTextures[1] != nullptr) { return; } outputWidth = newWidth; outputHeight = newHeight; - pathTracingTexturePrev = std::make_shared( - Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, TextureType::Color)); + for (auto &texture : pathTracingHistoryTextures) { + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); + } + for (auto &texture : pathTracingHistoryGuides) { + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); + } for (auto &texture : denoiseTextures) { texture = std::make_shared(Texture::create( outputWidth, outputHeight, opal::TextureFormat::Rgba16F, @@ -280,14 +296,36 @@ void photon::PathTracing::resizeOutput(int width, int height) { outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); } - pathTracingHistoryGuide = std::make_shared( - Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, TextureType::Color)); + historyReadIndex = 0; frameIndex = 0; interactiveFramesRemaining = 4; interactive = true; } +void photon::PathTracing::configure(int samplesPerPixel, int bounceLimit, + bool useDenoising, int historyFrames) { + const int newSamples = std::clamp(samplesPerPixel, 1, 64); + const int newBounces = std::clamp(bounceLimit, 1, 16); + const int newHistoryFrames = std::clamp(historyFrames, 1, 2048); + if (raysPerPixel == newSamples && maxBounces == newBounces && + denoisingEnabled == useDenoising && + accumulationFrames == newHistoryFrames) { + return; + } + raysPerPixel = newSamples; + maxBounces = newBounces; + denoisingEnabled = useDenoising; + accumulationFrames = newHistoryFrames; + resetAccumulation(); +} + +void photon::PathTracing::resetAccumulation() { + frameIndex = 0; + historyReadIndex = 0; + interactiveFramesRemaining = 2; + interactive = true; +} + bool photon::PathTracing::buildAccelerationStructure( const std::shared_ptr &commandBuffer) { struct MaterialData { @@ -322,8 +360,21 @@ bool photon::PathTracing::buildAccelerationStructure( float bitangent[3]; }; + struct EmissiveTriangleData { + float p0[4]; + float p1[4]; + float p2[4]; + float normal[4]; + float emission[3]; + float area; + float cdf; + float selectionPdf; + float _pad[2]; + }; + static_assert(sizeof(MaterialData) == 112); static_assert(sizeof(VertexData) == 56); + static_assert(sizeof(EmissiveTriangleData) == 96); std::vector pathTracingObjects; std::unordered_set seenPathObjects; @@ -389,6 +440,7 @@ bool photon::PathTracing::buildAccelerationStructure( std::vector allVertices; std::vector allIndices; std::vector primitiveObjects; + std::vector emissiveTriangleData; int objectID = 0; if (needsRebuild) { @@ -555,9 +607,75 @@ bool photon::PathTracing::buildAccelerationStructure( data._pad1[1] = 0; materialData.push_back(data); + const glm::vec3 emission = + glm::vec3(data.emissiveColor[0], data.emissiveColor[1], + data.emissiveColor[2]) * + std::max(data.emissiveIntensity, 0.0f); + const float emissionLuminance = + glm::dot(emission, glm::vec3(0.2126f, 0.7152f, 0.0722f)); + if (emissionLuminance > 0.0001f) { + for (size_t primitive = 0; primitive < objectPrimitiveCount; + ++primitive) { + const uint32_t i0 = + vertexOffset + objectIndices[primitive * 3 + 0]; + const uint32_t i1 = + vertexOffset + objectIndices[primitive * 3 + 1]; + const uint32_t i2 = + vertexOffset + objectIndices[primitive * 3 + 2]; + const glm::vec3 p0(allVertices[i0].position[0], + allVertices[i0].position[1], + allVertices[i0].position[2]); + const glm::vec3 p1(allVertices[i1].position[0], + allVertices[i1].position[1], + allVertices[i1].position[2]); + const glm::vec3 p2(allVertices[i2].position[0], + allVertices[i2].position[1], + allVertices[i2].position[2]); + const glm::vec3 crossValue = glm::cross(p1 - p0, p2 - p0); + const float twiceArea = glm::length(crossValue); + if (twiceArea <= 0.000001f) { + continue; + } + EmissiveTriangleData triangle{}; + const glm::vec3 normal = crossValue / twiceArea; + for (int axis = 0; axis < 3; ++axis) { + triangle.p0[axis] = p0[axis]; + triangle.p1[axis] = p1[axis]; + triangle.p2[axis] = p2[axis]; + triangle.normal[axis] = normal[axis]; + triangle.emission[axis] = emission[axis]; + } + triangle.p0[3] = 1.0f; + triangle.p1[3] = 1.0f; + triangle.p2[3] = 1.0f; + triangle.area = twiceArea * 0.5f; + triangle.selectionPdf = + triangle.area * emissionLuminance; + emissiveTriangleData.push_back(triangle); + } + } + objectID++; } + float totalEmissiveWeight = 0.0f; + for (const auto &triangle : emissiveTriangleData) { + totalEmissiveWeight += triangle.selectionPdf; + } + float cumulativeWeight = 0.0f; + if (totalEmissiveWeight > 0.0f) { + for (auto &triangle : emissiveTriangleData) { + triangle.selectionPdf /= totalEmissiveWeight; + cumulativeWeight += triangle.selectionPdf; + triangle.cdf = cumulativeWeight; + } + emissiveTriangleData.back().cdf = 1.0f; + } + emissiveTriangleCount = static_cast(emissiveTriangleData.size()); + if (emissiveTriangleData.empty()) { + emissiveTriangleData.push_back({}); + } + if (!flushAccelerationChunk()) { lastError = "Failed to allocate a scene acceleration structure chunk"; @@ -609,9 +727,13 @@ bool photon::PathTracing::buildAccelerationStructure( opal::BufferUsage::ShaderRead, cachedBLASPrimitiveOffsets.size() * sizeof(uint32_t), cachedBLASPrimitiveOffsets.data()); + emissiveTriangles = opal::Buffer::create( + opal::BufferUsage::ShaderRead, + emissiveTriangleData.size() * sizeof(EmissiveTriangleData), + emissiveTriangleData.data()); if (materialBuffer == nullptr || globalVertices == nullptr || globalIndices == nullptr || meshInfo == nullptr || - blasPrimitiveOffsets == nullptr) { + blasPrimitiveOffsets == nullptr || emissiveTriangles == nullptr) { lastError = "Failed to allocate path tracing scene buffers"; sceneBLAS.reset(); accelerationBuildFailed = true; @@ -1021,8 +1143,6 @@ bool photon::PathTracing::render( spotLightCount); pathTracingPipeline->setUniform1i("sceneData.numAreaLights", areaLightCount); - pathTracingPipeline->setUniform1i("sceneData.raysPerPixel", - this->raysPerPixel); pathTracingPipeline->setUniform1f("sceneData.indirectStrength", this->indirectStrength); @@ -1042,7 +1162,7 @@ bool photon::PathTracing::render( } if (instanceDataBuffer == nullptr || materialBuffer == nullptr || meshInfo == nullptr || globalVertices == nullptr || - globalIndices == nullptr) { + globalIndices == nullptr || emissiveTriangles == nullptr) { return fail("Required scene buffers are unavailable"); } try { @@ -1059,8 +1179,11 @@ bool photon::PathTracing::render( } commandBuffer->bindPipeline(this->pathTracingPipeline); pathTracingPipeline->bindTexture("outTex", output, 0); + const int historyWriteIndex = 1 - historyReadIndex; pathTracingPipeline->bindTexture("historyTex", - pathTracingTexturePrev->texture, 1); + pathTracingHistoryTextures[historyReadIndex] + ->texture, + 1); pathTracingPipeline->bindTexture("brightTex", brightOutput, 2); pathTracingPipeline->bindTexture("albedoRoughnessTex", pathTracingAovTextures[0]->texture, 3); @@ -1071,7 +1194,15 @@ bool photon::PathTracing::render( pathTracingPipeline->bindTexture("momentsHitTex", pathTracingAovTextures[3]->texture, 6); pathTracingPipeline->bindTexture("historyGuideTex", - pathTracingHistoryGuide->texture, 7); + pathTracingHistoryGuides[historyReadIndex] + ->texture, + 7); + pathTracingPipeline->bindTexture( + "historyOutTex", + pathTracingHistoryTextures[historyWriteIndex]->texture, 8); + pathTracingPipeline->bindTexture( + "historyGuideOutTex", + pathTracingHistoryGuides[historyWriteIndex]->texture, 9); static std::shared_ptr fallbackSkyboxTexture = nullptr; if (fallbackSkyboxTexture == nullptr) { @@ -1126,12 +1257,20 @@ bool photon::PathTracing::render( cachedAtmosphereSunSize = atmosphereSunSize; const int refinementFrame = std::max(frameIndex, 0); - const int pixelStride = interactive ? 4 : (refinementFrame < 4 ? 2 : 1); + const int pixelStride = interactive ? 2 : 1; + const int effectiveSamples = interactive ? 1 : this->raysPerPixel; const int effectiveBounces = interactive ? std::min(this->maxBounces, 2) : this->maxBounces; pathTracingPipeline->setUniform1i("sceneData.frameIndex", frameIndex); + pathTracingPipeline->setUniform1i("sceneData.raysPerPixel", + effectiveSamples); pathTracingPipeline->setUniform1i("sceneData.maxBounces", effectiveBounces); pathTracingPipeline->setUniform1i("sceneData.pixelStride", pixelStride); + pathTracingPipeline->setUniform1i("sceneData.accumulationFrameLimit", + accumulationFrames); + pathTracingPipeline->setUniform1f("sceneData.fireflyClamp", fireflyClamp); + pathTracingPipeline->setUniform1i("sceneData.numEmissiveTriangles", + emissiveTriangleCount); commandBuffer->bindPrimitiveAccelerationStructure(this->sceneBLAS, 0); @@ -1145,6 +1284,8 @@ bool photon::PathTracing::render( pathTracingPipeline->bindBuffer("areaLights", areaLights, 11); pathTracingPipeline->bindBuffer("blasPrimitiveOffsets", blasPrimitiveOffsets, 13); + pathTracingPipeline->bindBuffer("emissiveTriangles", emissiveTriangles, + 14); pathTracingPipeline->setUniform1i( "sceneData.materialTextureCount", std::min(static_cast(materialTextures.size()), @@ -1163,8 +1304,10 @@ bool photon::PathTracing::render( (outputHeight + pixelStride - 1) / pixelStride, 1); commandBuffer->computeBarrier(); + historyReadIndex = historyWriteIndex; - if (!interactive && pixelStride == 1 && pathDenoisePipeline != nullptr && + if (denoisingEnabled && !interactive && pixelStride == 1 && + pathDenoisePipeline != nullptr && denoiseTextures[0] != nullptr && denoiseTextures[1] != nullptr) { const std::array denoiseSteps = {1, 2, 4}; const size_t denoisePassCount = refinementFrame < 32 ? 2 : 3; @@ -1185,6 +1328,8 @@ bool photon::PathTracing::render( pathDenoisePipeline->bindTexture("albedoRoughnessTexture", pathTracingAovTextures[0]->texture, 4); + pathDenoisePipeline->bindTexture( + "momentsTexture", pathTracingAovTextures[3]->texture, 5); pathDenoisePipeline->setUniform1i("parameters.stepWidth", denoiseSteps[pass]); commandBuffer->dispatch(outputWidth, outputHeight, 1); diff --git a/shaders/metal/path_tracing/path.metal b/shaders/metal/path_tracing/path.metal index 9a8f9031..53ff74fb 100644 --- a/shaders/metal/path_tracing/path.metal +++ b/shaders/metal/path_tracing/path.metal @@ -96,6 +96,20 @@ struct AreaLight { float twoSided; }; +struct EmissiveTriangle { + float4 p0; + float4 p1; + float4 p2; + float4 normal; + packed_float3 emission; + float area; + float cdf; + float selectionPdf; + float2 _pad; +}; + +static_assert(sizeof(EmissiveTriangle) == 96); + struct SceneData { uint numDirectionalLights; uint numPointLights; @@ -116,6 +130,9 @@ struct SceneData { uint pixelStride; float3 ambientColor; uint environmentEnabled; + uint accumulationFrameLimit; + float fireflyClamp; + uint numEmissiveTriangles; }; static_assert(sizeof(SceneData) == 144); @@ -124,6 +141,8 @@ static_assert(__builtin_offsetof(SceneData, atmosphereSunIntensity) == 64); static_assert(__builtin_offsetof(SceneData, atmosphereSunColor) == 80); static_assert(__builtin_offsetof(SceneData, pixelStride) == 96); static_assert(__builtin_offsetof(SceneData, ambientColor) == 112); +static_assert(__builtin_offsetof(SceneData, accumulationFrameLimit) == 132); +static_assert(__builtin_offsetof(SceneData, numEmissiveTriangles) == 140); float pow5(float x) { float x2 = x * x; @@ -668,11 +687,7 @@ float3 traceShadowVisibility(intersector isect, float3 p0 = float3(vertices[i0].position); float3 p1 = float3(vertices[i1].position); float3 p2 = float3(vertices[i2].position); - InstanceData inst = instanceData[objectIndex]; - float3x3 normalMatrix = float3x3( - inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); - float3 hitNormal = normalizeOr( - normalMatrix * cross(p1 - p0, p2 - p0), -L); + float3 hitNormal = normalizeOr(cross(p1 - p0, p2 - p0), -L); hitNormal = dot(hitNormal, L) < 0.0 ? hitNormal : -hitNormal; float ior = max(material.ior, 1.0); float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); @@ -743,6 +758,16 @@ float3 F_Schlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow5(c); } +float3 materialF0(float3 albedo, float metallic, float reflectivity, + float ior) { + float dielectricF0 = pow((max(ior, 1.0001) - 1.0) / + (max(ior, 1.0001) + 1.0), + 2.0); + float dielectricScale = mix(0.5, 1.5, clamp(reflectivity, 0.0, 1.0)); + return mix(float3(clamp(dielectricF0 * dielectricScale, 0.0, 0.16)), + albedo, clamp(metallic, 0.0, 1.0)); +} + float G_Smith(float NdotV, float NdotL, float roughness) { float r = roughness + 1.0; float k = (r * r) / 8.0; @@ -817,20 +842,13 @@ float3 evalPBR(float3 albedo, float metallic, float roughness, float VdotH = max(dot(V, H), 0.0); float clampedRoughness = clamp(roughness, 0.045, 1.0); - float dielectricF0 = pow((max(ior, 1.0) - 1.0) / - (max(ior, 1.0) + 1.0), - 2.0); - float3 baseF0 = mix(float3(dielectricF0), albedo, - clamp(metallic, 0.0, 1.0)); - float3 reflectedColor = mix(float3(1.0), albedo, metallic); - float3 F0 = mix(baseF0, reflectedColor, clamp(reflectivity, 0.0, 1.0)); + float3 F0 = materialF0(albedo, metallic, reflectivity, ior); float3 F = F_Schlick(VdotH, F0); float D = D_GGX(NdotH, clampedRoughness); float G = G_Smith(NdotV, NdotL, clampedRoughness); float3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4); float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)) * - (1.0 - clamp(reflectivity, 0.0, 1.0)) * (1.0 - clamp(transmittance, 0.0, 1.0)); float diffuseFactor = disneyDiffuseFactor(NdotV, NdotL, max(dot(L, H), 0.0), clampedRoughness); @@ -871,6 +889,65 @@ float3 evalTransmission(float3 albedo, float3 N, float3 V, float3 L, transmissionLobe; } +float3 evalEmissiveTriangleLighting( + intersector isect, + primitive_acceleration_structure sceneAS, float3 P, float3 N, float3 Ng, + float3 V, float3 albedo, float metallic, float roughness, + float reflectivity, float ior, float transmittance, thread uint &rng, + constant SceneData &sceneData, + constant EmissiveTriangle *emissiveTriangles, + constant Material *materials, constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, constant VertexData *vertices, + constant uint *indices, constant InstanceData *instanceData, + PT_MATERIAL_TEXTURE_PARAMS) { + if (sceneData.numEmissiveTriangles == 0) { + return float3(0.0); + } + float selector = rand(rng); + uint first = 0; + uint last = sceneData.numEmissiveTriangles - 1; + while (first < last) { + uint middle = first + (last - first) / 2; + if (selector <= emissiveTriangles[middle].cdf) { + last = middle; + } else { + first = middle + 1; + } + } + EmissiveTriangle light = emissiveTriangles[first]; + float sqrtU = sqrt(rand(rng)); + float barycentricV = rand(rng); + float b0 = 1.0 - sqrtU; + float b1 = sqrtU * (1.0 - barycentricV); + float b2 = sqrtU * barycentricV; + float3 lightPosition = light.p0.xyz * b0 + light.p1.xyz * b1 + + light.p2.xyz * b2; + float3 toLight = lightPosition - P; + float distanceSquared = dot(toLight, toLight); + if (distanceSquared <= 1e-8) { + return float3(0.0); + } + float distanceToLight = sqrt(distanceSquared); + float3 L = toLight / distanceToLight; + float surfaceCosine = dot(N, L); + float lightCosine = abs(dot(light.normal.xyz, -L)); + if (surfaceCosine <= 0.0 || dot(Ng, L) <= 0.0 || + lightCosine <= 1e-5 || light.area <= 1e-8 || + light.selectionPdf <= 1e-8) { + return float3(0.0); + } + float solidAnglePdf = light.selectionPdf * distanceSquared / + max(lightCosine * light.area, 1e-8); + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, distanceToLight, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, indices, + instanceData, sceneData, PT_MATERIAL_TEXTURE_ARGS); + return evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, light.emission, + 1.0 / max(solidAnglePdf, 1e-8)) * + visibility; +} + // --------------------------------------------------------------------------- // Direct lighting with full PBR (replaces old evalDirectLighting) // --------------------------------------------------------------------------- @@ -887,6 +964,7 @@ float3 evalDirectLightingPBR(intersector isect, constant PointLight *pointLights, constant SpotLight *spotLights, constant AreaLight *areaLights, + constant EmissiveTriangle *emissiveTriangles, constant Material *materials, constant uint *primitiveObjects, constant uint *blasPrimitiveOffsets, @@ -998,6 +1076,12 @@ float3 evalDirectLightingPBR(intersector isect, lighting += (c + s * (1.0 - transmittance)) * visibility; } + lighting += evalEmissiveTriangleLighting( + isect, sceneAS, P, N, Ng, V, albedo, metallic, roughness, + reflectivity, ior, transmittance, rng, sceneData, emissiveTriangles, + materials, primitiveObjects, blasPrimitiveOffsets, vertices, indices, + instanceData, PT_MATERIAL_TEXTURE_ARGS); + return lighting; } @@ -1018,6 +1102,7 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, constant PointLight *pointLights, constant SpotLight *spotLights, constant AreaLight *areaLights, + constant EmissiveTriangle *emissiveTriangles, PT_MATERIAL_TEXTURE_PARAMS, texturecube skybox, thread float3 &primaryAlbedo, thread float3 &primaryNormal, @@ -1089,7 +1174,7 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, float3x3 normalMatrix = float3x3( inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); geometricNormal = normalizeOr( - normalMatrix * cross(p1 - p0, p2 - p0), + cross(p1 - p0, p2 - p0), normalizeOr(normalMatrix * localN, float3(0.0, 1.0, 0.0))); float alpha = resolveMaterialOpacity( @@ -1153,17 +1238,21 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, primaryObjectId = surfaceObjectIndex; } - float reflectivity = clamp(mat.reflectivity, 0.0, 1.0) * - (1.0 - transmittance); + float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); float sssStrength = 0.0; float sssThickness = mix(0.25, 1.75, ao); float3 direct = evalDirectLightingPBR( isect, sceneAS, P, N, Ng, V, albedo, metallic, roughness, reflectivity, ior, transmittance, sssStrength, sssThickness, rng, - dirLight, sceneData, pointLights, spotLights, areaLights, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, indices, - instanceData, PT_MATERIAL_TEXTURE_ARGS); - radiance += throughput * (direct + emissive); + dirLight, sceneData, pointLights, spotLights, areaLights, + emissiveTriangles, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, + PT_MATERIAL_TEXTURE_ARGS); + radiance += throughput * direct; + if (depth == 0 || previousEventWasDelta || + sceneData.numEmissiveTriangles == 0) { + radiance += throughput * emissive; + } if (depth == 0 && sceneData.ambientIntensity > 0.0) { float aoVisibility = mix(0.2, 1.0, ao); @@ -1181,20 +1270,15 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, } float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); - float3 baseF0 = mix(float3(dielectricF0), albedo, metallic); - float3 reflectedColor = mix(float3(1.0), albedo, metallic); - float3 F0 = mix(baseF0, reflectedColor, reflectivity); + float3 F0 = materialF0(albedo, metallic, mat.reflectivity, ior); float NdotV = max(dot(N, V), 1e-4); - float dielectricFresnel = - F_Schlick(NdotV, float3(dielectricF0)).x; - float specProb = metallic * mix(0.35, 0.9, 1.0 - roughness) + - (1.0 - metallic) * dielectricFresnel; + float3 viewFresnel = F_Schlick(NdotV, F0); + float fresnelProbability = clamp(luminance(viewFresnel), 0.001, 0.999); + float specProb = fresnelProbability; float transmitProb = transmittance * (1.0 - metallic) * - (1.0 - dielectricFresnel); - float diffuseProb = (1.0 - metallic) * (1.0 - transmittance); - specProb = mix(specProb, 1.0, reflectivity); - transmitProb *= 1.0 - reflectivity; - diffuseProb *= 1.0 - reflectivity; + (1.0 - fresnelProbability); + float diffuseProb = (1.0 - metallic) * (1.0 - transmittance) * + (1.0 - fresnelProbability); float eta = frontFace ? 1.0 / ior : ior; float3 idealRefractedDirection = refract(-V, N, eta); bool totalInternalReflection = @@ -1232,7 +1316,7 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, float VdotH = max(dot(V, H), 1e-5); float3 F = F_Schlick(VdotH, F0); float3 kD = (1.0 - F) * (1.0 - metallic) * - (1.0 - transmittance) * (1.0 - reflectivity); + (1.0 - transmittance); float diffuseFactor = disneyDiffuseFactor( NdotV, NdotEnvironment, max(dot(environmentDirection, H), 0.0), roughness); @@ -1337,7 +1421,8 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, float NdotL = max(dot(N, nextDirection), 0.0); float3 H = normalizeOr(V + nextDirection, N); float3 F = F_Schlick(max(dot(V, H), 0.0), F0); - float3 kD = (1.0 - F) * (1.0 - metallic); + float3 kD = (1.0 - F) * (1.0 - metallic) * + (1.0 - transmittance); float diffuseFactor = disneyDiffuseFactor( NdotV, NdotL, max(dot(nextDirection, H), 0.0), roughness); float3 diffuseBsdf = kD * albedo * diffuseFactor / M_PI_F; @@ -1386,13 +1471,15 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, } kernel void main0(texture2d outTex [[texture(0)]], - texture2d historyTex [[texture(1)]], + texture2d historyTex [[texture(1)]], texture2d brightTex [[texture(2)]], texture2d albedoRoughnessTex [[texture(3)]], texture2d normalDepthTex [[texture(4)]], texture2d motionObjectTex [[texture(5)]], - texture2d momentsHitTex [[texture(6)]], - texture2d historyGuideTex [[texture(7)]], + texture2d momentsHitTex [[texture(6)]], + texture2d historyGuideTex [[texture(7)]], + texture2d historyOutTex [[texture(8)]], + texture2d historyGuideOutTex [[texture(9)]], primitive_acceleration_structure sceneAS [[buffer(0)]], constant CameraUniforms &cam [[buffer(1)]], constant Material *materials [[buffer(2)]], @@ -1405,6 +1492,7 @@ kernel void main0(texture2d outTex [[texture(0)]], constant PointLight *pointLights [[buffer(9)]], constant SpotLight *spotLights [[buffer(10)]], constant AreaLight *areaLights [[buffer(11)]], + constant EmissiveTriangle *emissiveTriangles [[buffer(14)]], PT_MATERIAL_TEXTURE_BINDINGS, constant uint *blasPrimitiveOffsets [[buffer(13)]], texturecube skybox [[texture(60)]], @@ -1436,8 +1524,10 @@ kernel void main0(texture2d outTex [[texture(0)]], for (uint s = 0; s < spp; ++s) { uint cameraRng = seedBase(gid, w, sceneData.frameIndex, s + 0x9E3779B9u); - float2 pixelJitter = - float2(rand(cameraRng), rand(cameraRng)) - 0.5; + float2 pixelJitter = s == 0 + ? float2(0.0) + : float2(rand(cameraRng), rand(cameraRng)) - + 0.5; float2 sampleUv = (float2(gid) + 0.5 + pixelJitter) / float2(w, h); float2 sampleNdc = sampleUv * 2.0 - 1.0; sampleNdc.y = -sampleNdc.y; @@ -1463,13 +1553,14 @@ kernel void main0(texture2d outTex [[texture(0)]], gid, s, w, isect, sceneAS, primaryRay, materials, primitiveObjects, blasPrimitiveOffsets, vertices, indices, instanceData, dirLight, sceneData, pointLights, spotLights, areaLights, - PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, sampleNormal, - samplePosition, sampleDepth, sampleRoughness, sampleHitDistance, - sampleObjectId); + emissiveTriangles, PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, + sampleNormal, samplePosition, sampleDepth, sampleRoughness, + sampleHitDistance, sampleObjectId); if (!all(isfinite(sample))) { sample = float3(0.0); } - color += clampLuminance(max(sample, float3(0.0)), 12.0); + color += clampLuminance(max(sample, float3(0.0)), + max(sceneData.fireflyClamp, 1.0)); if (s == 0) { primaryAlbedo = sampleAlbedo; primaryNormal = sampleNormal; @@ -1486,36 +1577,66 @@ kernel void main0(texture2d outTex [[texture(0)]], color = float3(0.0); } - int frameIndex = int(sceneData.frameIndex); - - float4 prevColor = historyTex.read(gid); - float4 previousGuide = historyGuideTex.read(gid); float objectIdValue = primaryObjectId == 0xFFFFFFFFu ? -1.0 : float(primaryObjectId); float2 encodedNormal = encodeNormal(primaryNormal); float4 currentGuide = float4(encodedNormal, primaryDepth, objectIdValue); - bool historyValid = frameIndex > 0 && + float4 previousClip = cam.prevViewProj * float4(primaryPosition, 1.0); + float2 previousNdc = previousClip.xy / max(abs(previousClip.w), 0.0001); + float2 previousUv = float2(previousNdc.x * 0.5 + 0.5, + 0.5 - previousNdc.y * 0.5); + bool previousUvValid = previousClip.w > 0.0 && + all(previousUv >= float2(0.0)) && + all(previousUv <= float2(1.0)); + uint2 previousPixel = gid; + if (primaryObjectId != 0xFFFFFFFFu && previousUvValid) { + previousPixel = uint2(clamp(previousUv * float2(w, h), float2(0.0), + float2(w - 1, h - 1))); + } + float4 prevColor = historyTex.read(previousPixel); + float4 previousGuide = historyGuideTex.read(previousPixel); + float4 previousMoments = momentsHitTex.read(gid); + bool historyValid = sceneData.frameIndex > 0 && prevColor.w > 0.0 && abs(previousGuide.z - primaryDepth) < - max(0.05, primaryDepth * 0.02) && - distance(previousGuide.xy, encodedNormal) < 0.08 && + max(0.02, primaryDepth * 0.01) && + distance(previousGuide.xy, encodedNormal) < 0.04 && abs(previousGuide.w - objectIdValue) < 0.5; - if (frameIndex == 0) - prevColor = float4(0, 0, 0, 1); - float sampleLuminanceLimit = - historyValid ? max(4.0, luminance(prevColor.xyz) * 2.0 + 0.5) : 12.0; + if (!historyValid) { + prevColor = float4(0.0); + previousMoments = float4(0.0); + } + float previousMean = historyValid ? previousMoments.x : 0.0; + float previousVariance = + historyValid + ? max(previousMoments.y - previousMean * previousMean, 0.0) + : 0.0; + float sampleLuminanceLimit = max(sceneData.fireflyClamp, 1.0); + if (historyValid && prevColor.w >= 4.0) { + float statisticalLimit = previousMean + + max(0.5, 6.0 * sqrt(previousVariance)); + sampleLuminanceLimit = + min(sampleLuminanceLimit, max(4.0, statisticalLimit)); + } color = clampLuminance(color, sampleLuminanceLimit); - if (!historyValid) - prevColor = float4(color, 1.0); - - float historyLength = historyValid ? min(float(frameIndex), 255.0) : 0.0; - float3 lower = min(prevColor.xyz, color) - float3(0.35); - float3 upper = max(prevColor.xyz, color) + float3(0.35); - float3 clippedHistory = clamp(prevColor.xyz, lower, upper); - float3 accum = mix(color, clippedHistory, - historyLength / (historyLength + 1.0)); - accum = clampLuminance(accum, 24.0); + float historyLimit = max(float(sceneData.accumulationFrameLimit), 1.0); + float previousWeight = + historyValid ? min(prevColor.w, max(historyLimit - 1.0, 0.0)) : 0.0; + float newHistoryLength = min(previousWeight + 1.0, historyLimit); + float accumulationDenominator = max(previousWeight + 1.0, 1.0); + float3 accum = + (prevColor.xyz * previousWeight + color) / accumulationDenominator; + float moment = luminance(color); + float accumulatedMoment = + (previousMoments.x * previousWeight + moment) / + accumulationDenominator; + float accumulatedMomentSquared = + (previousMoments.y * previousWeight + moment * moment) / + accumulationDenominator; + float variance = max(accumulatedMomentSquared - + accumulatedMoment * accumulatedMoment, + 0.0); constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; @@ -1527,11 +1648,7 @@ kernel void main0(texture2d outTex [[texture(0)]], float contribution = max(brightness - bloomThreshold, soft) / max(brightness, 0.00001); float3 brightColor = accum * contribution; - float4 previousClip = cam.prevViewProj * float4(primaryPosition, 1.0); - float2 previousUv = previousClip.xy / max(abs(previousClip.w), 0.0001); - previousUv = previousUv * 0.5 + 0.5; - float2 motion = uv - previousUv; - float moment = luminance(color); + float2 motion = previousUvValid ? uv - previousUv : float2(0.0); for (uint y = 0; y < pixelStride; ++y) { for (uint x = 0; x < pixelStride; ++x) { @@ -1539,14 +1656,15 @@ kernel void main0(texture2d outTex [[texture(0)]], if (pixel.x >= w || pixel.y >= h) { continue; } - historyTex.write(float4(accum, 1.0), pixel); - historyGuideTex.write(currentGuide, pixel); + historyOutTex.write(float4(accum, newHistoryLength), pixel); + historyGuideOutTex.write(currentGuide, pixel); albedoRoughnessTex.write(float4(primaryAlbedo, primaryRoughness), pixel); normalDepthTex.write(float4(primaryNormal, primaryDepth), pixel); motionObjectTex.write(float4(motion, objectIdValue, 1.0), pixel); - momentsHitTex.write(float4(moment, moment * moment, - primaryRoughness, primaryHitDistance), + momentsHitTex.write(float4(accumulatedMoment, + accumulatedMomentSquared, variance, + primaryHitDistance), pixel); outTex.write(float4(accum, 1.0), pixel); brightTex.write(float4(brightColor, 1.0), pixel); diff --git a/shaders/metal/path_tracing/path_denoise.metal b/shaders/metal/path_tracing/path_denoise.metal index 9b6c5668..73987eb3 100644 --- a/shaders/metal/path_tracing/path_denoise.metal +++ b/shaders/metal/path_tracing/path_denoise.metal @@ -11,6 +11,7 @@ kernel void main0(texture2d inputTexture [[texture(0)]], texture2d guideTexture [[texture(3)]], texture2d albedoRoughnessTexture [[texture(4)]], + texture2d momentsTexture [[texture(5)]], constant DenoiseParameters ¶meters [[buffer(0)]], uint2 gid [[thread_position_in_grid]]) { uint width = outputTexture.get_width(); @@ -26,6 +27,7 @@ kernel void main0(texture2d inputTexture [[texture(0)]], float3 center = inputTexture.read(gid).xyz; float4 centerGuide = guideTexture.read(gid); float4 centerAlbedoRoughness = albedoRoughnessTexture.read(gid); + float4 centerMoments = momentsTexture.read(gid); bool centerSurface = centerGuide.w > 0.0; float centerNormalLength = dot(centerGuide.xyz, centerGuide.xyz); float centerLuminance = dot(center, float3(0.2126, 0.7152, 0.0722)); @@ -97,7 +99,17 @@ kernel void main0(texture2d inputTexture [[texture(0)]], filtered += sampleColor * weight; totalWeight += weight; } - float3 result = totalWeight > 0.0001 ? filtered / totalWeight : center; + float3 spatialResult = + totalWeight > 0.0001 ? filtered / totalWeight : center; + float roughness = clamp(centerAlbedoRoughness.w, 0.0, 1.0); + float relativeNoise = sqrt(max(centerMoments.z, 0.0)) / + max(centerLuminance, 0.05); + float filterStrength = clamp(relativeNoise * 1.5, 0.02, 1.0) * + mix(0.12, 1.0, roughness * roughness); + if (!centerSurface) { + filterStrength *= 0.25; + } + float3 result = mix(center, spatialResult, filterStrength); float brightness = dot(result, float3(0.2126, 0.7152, 0.0722)); constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; From 496d52576e9a46d6e6b4e2c840273a0b6dca36f1 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sat, 1 Aug 2026 23:48:30 +0200 Subject: [PATCH 2/4] Add synchronized cinematic Photon settings --- atlas/application/window.cpp | 13 +- cli/src/create.rs | 6 +- editor/project/projectStore.cpp | 6 +- editor/views/editor/editor.cpp | 245 +++++++++++++++++++++++++++++-- editor/views/editor/viewport.cpp | 17 +++ include/atlas/runtime/context.h | 9 +- include/atlas/window.h | 2 + include/editor/views/viewport.h | 3 + runtime/lib/context.cpp | 50 ++++++- runtime/lib/runtime.cpp | 5 + 10 files changed, 337 insertions(+), 19 deletions(-) diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index 50029d76..fdac8fcd 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -1075,7 +1075,7 @@ Window::Window(const WindowConfiguration &config) SDL_SetWindowAspectRatio(window, aspectRatio, aspectRatio); } - this->renderScale = std::clamp(config.renderScale, 0.5f, 1.0f); + this->renderScale = std::clamp(config.renderScale, 0.25f, 1.0f); this->ssaoRenderScale = std::clamp(config.ssaoScale, 0.25f, 1.0f); this->useMultisampling = config.multisampling; this->setEditorControlsEnabled(config.editorControls); @@ -4056,7 +4056,7 @@ void Window::setWindowed(const WindowConfiguration &config) { SDL_Window *window = this->windowRef; int windowWidth = config.width; int windowHeight = config.height; - this->renderScale = std::clamp(config.renderScale, 0.5f, 1.0f); + this->renderScale = std::clamp(config.renderScale, 0.25f, 1.0f); this->ssaoRenderScale = std::clamp(config.ssaoScale, 0.25f, 1.0f); this->useMultisampling = config.multisampling; this->setEditorControlsEnabled(config.editorControls); @@ -5679,6 +5679,15 @@ void Window::enablePathTracing() { pathTracer->init(); } +void Window::configurePathTracing(int samplesPerPixel, int bounceLimit, + bool denoising, int accumulationFrames) { + if (pathTracer == nullptr) { + return; + } + pathTracer->configure(samplesPerPixel, bounceLimit, denoising, + accumulationFrames); +} + bool Window::setEditorPathTracingPreview(bool enabled) { if (pathTracer == nullptr) { return false; diff --git a/cli/src/create.rs b/cli/src/create.rs index 63d57317..c3653c8e 100644 --- a/cli/src/create.rs +++ b/cli/src/create.rs @@ -34,7 +34,11 @@ ssr = false ssr_quality = 1 ssr_debug = false use_upscaling = true -upscaling_ratio = 0.5 +upscaling_ratio = 0.67 +samples_per_pixel = 4 +max_bounces = 8 +denoising = true +accumulation_frames = 512 [window] dimensions = [1280, 720] diff --git a/editor/project/projectStore.cpp b/editor/project/projectStore.cpp index 9bf109a1..98c90892 100644 --- a/editor/project/projectStore.cpp +++ b/editor/project/projectStore.cpp @@ -74,7 +74,11 @@ QString projectConfig(const QString& name, stream << "ssr_quality = 1\n"; stream << "ssr_debug = false\n"; stream << "use_upscaling = true\n"; - stream << "upscaling_ratio = 0.5\n\n"; + stream << "upscaling_ratio = 0.67\n"; + stream << "samples_per_pixel = 4\n"; + stream << "max_bounces = 8\n"; + stream << "denoising = true\n"; + stream << "accumulation_frames = 512\n\n"; stream << "[window]\n"; stream << "dimensions = [1280, 720]\n"; stream << "mouse_capture = false\n"; diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index 41515518..6c2d421a 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -189,6 +189,37 @@ QString tomlQuoted(QString value) { return QStringLiteral("\"%1\"").arg(value); } +QString tomlValue(const QStringList &lines, const QString §ion, + const QString &key) { + int start = 0; + int end = lines.size(); + if (!section.isEmpty()) { + start = lines.indexOf(QStringLiteral("[%1]").arg(section)); + if (start < 0) + return {}; + ++start; + } + for (int index = start; index < lines.size(); ++index) { + if (lines.at(index).trimmed().startsWith('[')) { + end = index; + break; + } + } + const QRegularExpression expression( + QStringLiteral("^\\s*%1\\s*=\\s*(.+?)\\s*$") + .arg(QRegularExpression::escape(key))); + for (int index = start; index < end; ++index) { + const auto match = expression.match(lines.at(index)); + if (!match.hasMatch()) + continue; + QString value = match.captured(1).trimmed(); + if (value.size() >= 2 && value.startsWith('"') && value.endsWith('"')) + value = value.mid(1, value.size() - 2); + return value; + } + return {}; +} + void setTomlValue(QStringList *lines, const QString §ion, const QString &key, const QString &value) { int start = 0; @@ -1114,6 +1145,63 @@ void EditorWindow::showProjectSettings() { QDir().mkpath(settingsDirectory); QSettings settings(QDir(settingsDirectory).filePath("project-settings.ini"), QSettings::IniFormat); + QStringList projectLines; + QFile projectManifest(projectFile); + if (projectManifest.open(QIODevice::ReadOnly | QIODevice::Text)) { + projectLines = + QString::fromUtf8(projectManifest.readAll()).split('\n'); + } + const QString configuredRenderer = + tomlValue(projectLines, "renderer", "default"); + const bool configuredGlobalIllumination = + tomlValue(projectLines, "renderer", "global_illumination") == "true"; + const QString rendererDisplay = + configuredRenderer == "pathtracing" + ? QStringLiteral("Path Tracing") + : configuredGlobalIllumination ? QStringLiteral("PBR + DDGI") + : QStringLiteral("PBR"); + const QString dimensionsValue = + tomlValue(projectLines, "window", "dimensions"); + const auto dimensionsMatch = + QRegularExpression(QStringLiteral( + "^\\[\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\]$")) + .match(dimensionsValue); + const int configuredWidth = + dimensionsMatch.hasMatch() + ? dimensionsMatch.captured(1).toInt() + : settings.value("project/windowWidth", 1280).toInt(); + const int configuredHeight = + dimensionsMatch.hasMatch() + ? dimensionsMatch.captured(2).toInt() + : settings.value("project/windowHeight", 720).toInt(); + const QString upscalingValue = + tomlValue(projectLines, "renderer", "use_upscaling"); + const bool configuredUpscaling = + upscalingValue.isEmpty() + ? settings.value("project/useUpscaling", true).toBool() + : upscalingValue == "true"; + bool scaleValid = false; + const double configuredScaleValue = + tomlValue(projectLines, "renderer", "upscaling_ratio") + .toDouble(&scaleValid); + const int configuredScale = + scaleValid + ? std::clamp(qRound(configuredScaleValue * 100.0), 25, 100) + : settings.value("project/internalScale", 67).toInt(); + bool samplesValid = false; + const int configuredSamples = + tomlValue(projectLines, "renderer", "samples_per_pixel") + .toInt(&samplesValid); + bool bouncesValid = false; + const int configuredBounces = + tomlValue(projectLines, "renderer", "max_bounces") + .toInt(&bouncesValid); + bool accumulationValid = false; + const int configuredAccumulation = + tomlValue(projectLines, "renderer", "accumulation_frames") + .toInt(&accumulationValid); + const QString denoisingValue = + tomlValue(projectLines, "renderer", "denoising"); auto addPage = [tabs](const QString &name, styling::Icon icon, const QColor &color) { auto *page = new QWidget(tabs); @@ -1146,24 +1234,64 @@ void EditorWindow::showProjectSettings() { settings.value("project/version", "1.0.0").toString(), &dialog); auto *windowWidth = new QSpinBox(&dialog); windowWidth->setRange(320, 16384); - windowWidth->setValue(settings.value("project/windowWidth", 1280).toInt()); + windowWidth->setValue(configuredWidth); auto *windowHeight = new QSpinBox(&dialog); windowHeight->setRange(240, 16384); - windowHeight->setValue(settings.value("project/windowHeight", 720).toInt()); + windowHeight->setValue(configuredHeight); + auto *resolutionPreset = new QComboBox(&dialog); + resolutionPreset->addItem("HD · 1280 × 720", QSize(1280, 720)); + resolutionPreset->addItem("Full HD · 1920 × 1080", QSize(1920, 1080)); + resolutionPreset->addItem("QHD · 2560 × 1440", QSize(2560, 1440)); + resolutionPreset->addItem("4K UHD · 3840 × 2160", QSize(3840, 2160)); + resolutionPreset->addItem("Custom", QSize()); + int matchingResolution = resolutionPreset->count() - 1; + for (int index = 0; index < resolutionPreset->count() - 1; ++index) { + if (resolutionPreset->itemData(index).toSize() == + QSize(configuredWidth, configuredHeight)) { + matchingResolution = index; + break; + } + } + resolutionPreset->setCurrentIndex(matchingResolution); + connect(resolutionPreset, &QComboBox::currentIndexChanged, &dialog, + [resolutionPreset, windowWidth, windowHeight](int index) { + const QSize resolution = resolutionPreset->itemData(index).toSize(); + if (!resolution.isValid()) + return; + windowWidth->setValue(resolution.width()); + windowHeight->setValue(resolution.height()); + }); + auto syncResolutionPreset = [=] { + int index = resolutionPreset->count() - 1; + const QSize resolution(windowWidth->value(), windowHeight->value()); + for (int candidate = 0; candidate < resolutionPreset->count() - 1; + ++candidate) { + if (resolutionPreset->itemData(candidate).toSize() == resolution) { + index = candidate; + break; + } + } + QSignalBlocker blocker(resolutionPreset); + resolutionPreset->setCurrentIndex(index); + }; + connect(windowWidth, &QSpinBox::valueChanged, &dialog, + syncResolutionPreset); + connect(windowHeight, &QSpinBox::valueChanged, &dialog, + syncResolutionPreset); auto *fullscreen = new QCheckBox("Start in fullscreen", &dialog); fullscreen->setChecked( settings.value("project/fullscreen", false).toBool()); general->addRow("Default scene", defaultScene); general->addRow("Company", companyName); general->addRow("Version", gameVersion); + general->addRow("Output resolution", resolutionPreset); general->addRow("Window width", windowWidth); general->addRow("Window height", windowHeight); general->addRow(QString(), fullscreen); auto *rendering = addPage("Rendering", styling::Icon::Aperture, "#9E897D"); auto *renderer = new QComboBox(&dialog); renderer->addItems({"PBR", "PBR + DDGI", "Path Tracing"}); - renderer->setCurrentText( - settings.value("project/renderer", "PBR").toString()); + renderer->setCurrentText(rendererDisplay); auto *frameLimit = new QSpinBox(&dialog); frameLimit->setRange(0, 1000); frameLimit->setValue(settings.value("project/frameLimit", 0).toInt()); @@ -1176,20 +1304,97 @@ void EditorWindow::showProjectSettings() { auto *ssrDebug = new QCheckBox("Show SSR hit confidence", &dialog); ssrDebug->setChecked(settings.value("project/ssrDebug", false).toBool()); auto *upscaling = new QCheckBox("Enable Metal upscaling", &dialog); - upscaling->setChecked( - settings.value("project/useUpscaling", true).toBool()); + upscaling->setChecked(configuredUpscaling); + auto *upscalingQuality = new QComboBox(&dialog); + upscalingQuality->addItem("Quality", 75); + upscalingQuality->addItem("Balanced", 67); + upscalingQuality->addItem("Performance", 50); + upscalingQuality->addItem("Ultra Performance", 33); + upscalingQuality->addItem("Custom", -1); auto *internalScale = new QSpinBox(&dialog); - internalScale->setRange(50, 100); + internalScale->setRange(25, 100); internalScale->setSuffix("%"); - internalScale->setValue( - settings.value("project/internalScale", 50).toInt()); + internalScale->setValue(configuredScale); + int matchingScale = upscalingQuality->count() - 1; + for (int index = 0; index < upscalingQuality->count() - 1; ++index) { + if (upscalingQuality->itemData(index).toInt() == configuredScale) { + matchingScale = index; + break; + } + } + upscalingQuality->setCurrentIndex(matchingScale); + auto *samplesPerPixel = new QSpinBox(&dialog); + samplesPerPixel->setRange(1, 64); + samplesPerPixel->setValue(samplesValid ? configuredSamples : 4); + auto *maxBounces = new QSpinBox(&dialog); + maxBounces->setRange(1, 16); + maxBounces->setValue(bouncesValid ? configuredBounces : 8); + auto *denoising = new QCheckBox("Variance-guided denoising", &dialog); + denoising->setChecked(denoisingValue.isEmpty() || denoisingValue == "true"); + auto *accumulationFrames = new QSpinBox(&dialog); + accumulationFrames->setRange(1, 2048); + accumulationFrames->setValue(accumulationValid ? configuredAccumulation + : 512); + auto *internalResolution = new QLabel(&dialog); + auto updateInternalResolution = [=] { + const int scale = upscaling->isChecked() ? internalScale->value() : 100; + internalResolution->setText( + QStringLiteral("%1 × %2 internal → %3 × %4 output") + .arg(std::max(1, windowWidth->value() * scale / 100)) + .arg(std::max(1, windowHeight->value() * scale / 100)) + .arg(windowWidth->value()) + .arg(windowHeight->value())); + }; + connect(upscalingQuality, &QComboBox::currentIndexChanged, &dialog, + [upscalingQuality, internalScale](int index) { + const int scale = upscalingQuality->itemData(index).toInt(); + if (scale > 0) + internalScale->setValue(scale); + }); + connect(internalScale, &QSpinBox::valueChanged, &dialog, + [upscalingQuality](int value) { + const int index = upscalingQuality->findData(value); + QSignalBlocker blocker(upscalingQuality); + upscalingQuality->setCurrentIndex( + index >= 0 ? index : upscalingQuality->count() - 1); + }); + connect(upscaling, &QCheckBox::toggled, &dialog, + [internalScale, upscalingQuality](bool enabled) { + internalScale->setEnabled(enabled); + upscalingQuality->setEnabled(enabled); + }); + for (auto *spinBox : {windowWidth, windowHeight, internalScale}) { + connect(spinBox, &QSpinBox::valueChanged, &dialog, + updateInternalResolution); + } + connect(upscaling, &QCheckBox::toggled, &dialog, + updateInternalResolution); + internalScale->setEnabled(upscaling->isChecked()); + upscalingQuality->setEnabled(upscaling->isChecked()); + updateInternalResolution(); rendering->addRow("Renderer", renderer); rendering->addRow(QString(), ssr); rendering->addRow("SSR quality", ssrQuality); rendering->addRow(QString(), ssrDebug); rendering->addRow(QString(), upscaling); + rendering->addRow("Upscaling quality", upscalingQuality); rendering->addRow("Internal render scale", internalScale); + rendering->addRow("Effective resolution", internalResolution); + rendering->addRow("Samples per pixel", samplesPerPixel); + rendering->addRow("Maximum light bounces", maxBounces); + rendering->addRow(QString(), denoising); + rendering->addRow("Temporal accumulation", accumulationFrames); rendering->addRow("Frame limit (0 = unlimited)", frameLimit); + auto updatePathTracingControls = [=] { + const bool enabled = renderer->currentText() == "Path Tracing"; + samplesPerPixel->setEnabled(enabled); + maxBounces->setEnabled(enabled); + denoising->setEnabled(enabled); + accumulationFrames->setEnabled(enabled); + }; + connect(renderer, &QComboBox::currentTextChanged, &dialog, + updatePathTracingControls); + updatePathTracingControls(); auto *physics = addPage("Physics", styling::Icon::Wrench, "#A1957D"); auto *gravity = new QLineEdit( settings.value("project/gravity", "0, -9.81, 0").toString(), &dialog); @@ -1264,6 +1469,11 @@ void EditorWindow::showProjectSettings() { settings.setValue("project/ssrDebug", ssrDebug->isChecked()); settings.setValue("project/useUpscaling", upscaling->isChecked()); settings.setValue("project/internalScale", internalScale->value()); + settings.setValue("project/pathTracingSamples", samplesPerPixel->value()); + settings.setValue("project/pathTracingBounces", maxBounces->value()); + settings.setValue("project/pathTracingDenoising", denoising->isChecked()); + settings.setValue("project/pathTracingAccumulation", + accumulationFrames->value()); settings.setValue("project/gravity", gravity->text()); settings.setValue("project/fixedStep", fixedStep->text()); settings.setValue("project/inputMap", inputMap->text()); @@ -1276,6 +1486,7 @@ void EditorWindow::showProjectSettings() { settings.setValue("project/icon", iconPath->text()); settings.setValue("project/exportBackend", backend->currentText()); settings.sync(); + bool manifestUpdated = false; QFile manifest(projectFile); if (manifest.open(QIODevice::ReadOnly | QIODevice::Text)) { QStringList lines = QString::fromUtf8(manifest.readAll()).split('\n'); @@ -1312,6 +1523,14 @@ void EditorWindow::showProjectSettings() { upscaling->isChecked() ? "true" : "false"); setTomlValue(&lines, "renderer", "upscaling_ratio", QString::number(internalScale->value() / 100.0, 'f', 2)); + setTomlValue(&lines, "renderer", "samples_per_pixel", + QString::number(samplesPerPixel->value())); + setTomlValue(&lines, "renderer", "max_bounces", + QString::number(maxBounces->value())); + setTomlValue(&lines, "renderer", "denoising", + denoising->isChecked() ? "true" : "false"); + setTomlValue(&lines, "renderer", "accumulation_frames", + QString::number(accumulationFrames->value())); QSaveFile outputFile(projectFile); const QByteArray contents = lines.join('\n').toUtf8(); if (!outputFile.open(QIODevice::WriteOnly) || @@ -1319,8 +1538,16 @@ void EditorWindow::showProjectSettings() { !outputFile.commit()) { QMessageBox::warning(this, "Project Settings", "The project manifest could not be updated."); + } else { + manifestUpdated = true; } } + if (manifestUpdated && viewportPanel != nullptr) { + viewportPanel->applyPathTracingSettings( + samplesPerPixel->value(), maxBounces->value(), + denoising->isChecked(), accumulationFrames->value(), + upscaling->isChecked(), internalScale->value() / 100.0f); + } } void EditorWindow::showExportDialog() { diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index c951ef28..94a90316 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -1430,6 +1430,23 @@ void ViewportPanel::setPathTracingPreview(bool enabled) { } } +bool ViewportPanel::applyPathTracingSettings( + int samplesPerPixel, int bounceLimit, bool denoising, + int accumulationFrames, bool upscaling, float internalScale) { + if (runtimeContext == nullptr) { + return false; + } + frameTimer->stop(); + const bool applied = runtimeContext->configurePathTracing( + samplesPerPixel, bounceLimit, denoising, accumulationFrames, upscaling, + internalScale); + const bool frameReady = applied && stepRuntime(); + if (frameReady && isVisible()) { + frameTimer->start(pbrPreview ? 16 : 1); + } + return applied; +} + void ViewportPanel::setRuntimeControlMode(int mode) { if (mode < 0 || mode > 3 || runtimeContext == nullptr) { return; diff --git a/include/atlas/runtime/context.h b/include/atlas/runtime/context.h index 20dac9e2..4a60fd2d 100644 --- a/include/atlas/runtime/context.h +++ b/include/atlas/runtime/context.h @@ -46,7 +46,11 @@ class ProjectConfig { std::string mainScene; std::string inputActions; bool useUpscaling = false; - float upscalingRatio = 0.5f; + float upscalingRatio = 0.67f; + int pathTracingSamples = 4; + int pathTracingBounces = 8; + bool pathTracingDenoising = true; + int pathTracingAccumulationFrames = 512; bool screenSpaceReflections = false; int screenSpaceReflectionQuality = 1; bool screenSpaceReflectionDebug = false; @@ -128,6 +132,9 @@ class Context { bool setEditorControlMode(int mode); bool setEditorShadingMode(int mode); bool setEditorPathTracingPreview(bool enabled); + bool configurePathTracing(int samplesPerPixel, int bounceLimit, + bool denoising, int accumulationFrames, + bool useUpscaling, float upscalingRatio); std::string getPathTracingError() const; float frameRate() const; bool editorPointerEvent(int action, float x, float y, int button, diff --git a/include/atlas/window.h b/include/atlas/window.h index 0f0df96a..bcf64da4 100644 --- a/include/atlas/window.h +++ b/include/atlas/window.h @@ -506,6 +506,8 @@ class Window { #ifdef METAL void enableGlobalIllumination(); void enablePathTracing(); + void configurePathTracing(int samplesPerPixel, int bounceLimit, + bool denoising, int accumulationFrames); bool setEditorPathTracingPreview(bool enabled); const std::string &getPathTracingError() const; #endif diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index fbef8d62..a00f59dc 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -96,6 +96,9 @@ class ViewportPanel : public QWidget { void reloadRuntime(); void setRuntimeShadingMode(int mode); void setPathTracingPreview(bool enabled); + bool applyPathTracingSettings(int samplesPerPixel, int bounceLimit, + bool denoising, int accumulationFrames, + bool upscaling, float internalScale); void setRuntimeControlMode(int mode); void toggleTransformSpace(); void toggleTransformSnapping(); diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index 237b2748..dd33bb1a 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -4519,7 +4519,7 @@ makeContextWithWindowOptions(std::string projectFile, void *metalView, bool mouseCaptured = false; bool multisampling = false; float ssaoScale = 0.4f; - float renderScale = 0.5f; + float renderScale = 0.67f; bool useUpscaling = false; bool editorControls = false; @@ -4538,7 +4538,7 @@ makeContextWithWindowOptions(std::string projectFile, void *metalView, if (auto *rendererTable = configTable["renderer"].as_table()) { useUpscaling = (*rendererTable)["use_upscaling"].value_or(false); renderScale = std::clamp( - (*rendererTable)["upscaling_ratio"].value_or(0.5f), 0.5f, 1.0f); + (*rendererTable)["upscaling_ratio"].value_or(0.67f), 0.25f, 1.0f); } if (auto *editorTable = configTable["editor"].as_table()) { editorControls = (*editorTable)["controls"].value_or(false); @@ -4833,6 +4833,30 @@ bool Context::setEditorPathTracingPreview(bool enabled) { #endif } +bool Context::configurePathTracing(int samplesPerPixel, int bounceLimit, + bool denoising, int accumulationFrames, + bool useUpscaling, float upscalingRatio) { + if (window == nullptr) { + return false; + } + config.pathTracingSamples = std::clamp(samplesPerPixel, 1, 64); + config.pathTracingBounces = std::clamp(bounceLimit, 1, 16); + config.pathTracingDenoising = denoising; + config.pathTracingAccumulationFrames = + std::clamp(accumulationFrames, 1, 2048); + config.useUpscaling = useUpscaling; + config.upscalingRatio = std::clamp(upscalingRatio, 0.25f, 1.0f); +#ifdef METAL + window->useMetalUpscaling(useUpscaling ? config.upscalingRatio : 1.0f); + window->configurePathTracing( + config.pathTracingSamples, config.pathTracingBounces, + config.pathTracingDenoising, config.pathTracingAccumulationFrames); + return true; +#else + return false; +#endif +} + std::string Context::getPathTracingError() const { #ifdef METAL return window != nullptr ? window->getPathTracingError() : std::string(); @@ -6581,7 +6605,11 @@ void Context::loadProject() { std::string mainScene = "main.ascene"; std::vector assetDirectories; bool useUpscaling = false; - float upscalingRatio = 0.5f; + float upscalingRatio = 0.67f; + int pathTracingSamples = 4; + int pathTracingBounces = 8; + bool pathTracingDenoising = true; + int pathTracingAccumulationFrames = 512; bool screenSpaceReflections = false; int screenSpaceReflectionQuality = 1; bool screenSpaceReflectionDebug = false; @@ -6590,7 +6618,15 @@ void Context::loadProject() { defaultRenderer = (*renderer)["default"].value_or("normal"); globalIllumination = (*renderer)["global_illumination"].value_or(false); useUpscaling = (*renderer)["use_upscaling"].value_or(false); - upscalingRatio = (*renderer)["upscaling_ratio"].value_or(0.5f); + upscalingRatio = (*renderer)["upscaling_ratio"].value_or(0.67f); + pathTracingSamples = + std::clamp((*renderer)["samples_per_pixel"].value_or(4), 1, 64); + pathTracingBounces = + std::clamp((*renderer)["max_bounces"].value_or(8), 1, 16); + pathTracingDenoising = + (*renderer)["denoising"].value_or(true); + pathTracingAccumulationFrames = std::clamp( + (*renderer)["accumulation_frames"].value_or(512), 1, 2048); screenSpaceReflections = (*renderer)["ssr"].value_or(false); screenSpaceReflectionQuality = std::clamp((*renderer)["ssr_quality"].value_or(1), 0, 2); @@ -6628,7 +6664,11 @@ void Context::loadProject() { config.mainScene = mainScene; config.assetDirectories = assetDirectories; config.useUpscaling = useUpscaling; - config.upscalingRatio = std::clamp(upscalingRatio, 0.5f, 1.0f); + config.upscalingRatio = std::clamp(upscalingRatio, 0.25f, 1.0f); + config.pathTracingSamples = pathTracingSamples; + config.pathTracingBounces = pathTracingBounces; + config.pathTracingDenoising = pathTracingDenoising; + config.pathTracingAccumulationFrames = pathTracingAccumulationFrames; config.screenSpaceReflections = screenSpaceReflections; config.screenSpaceReflectionQuality = screenSpaceReflectionQuality; config.screenSpaceReflectionDebug = screenSpaceReflectionDebug; diff --git a/runtime/lib/runtime.cpp b/runtime/lib/runtime.cpp index 9a715d48..0554a760 100644 --- a/runtime/lib/runtime.cpp +++ b/runtime/lib/runtime.cpp @@ -35,6 +35,11 @@ void RuntimeScene::initialize(Window &window) { runtimeContext->config.screenSpaceReflectionDebug); } else if (runtimeContext->config.renderer == "pathtracing") { window.enablePathTracing(); + window.configurePathTracing( + runtimeContext->config.pathTracingSamples, + runtimeContext->config.pathTracingBounces, + runtimeContext->config.pathTracingDenoising, + runtimeContext->config.pathTracingAccumulationFrames); } if (runtimeContext->config.useUpscaling) { From ee2bb6e378586a6cda6e2eb151c2678e0ca805e1 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sat, 1 Aug 2026 23:57:59 +0200 Subject: [PATCH 3/4] Restore the cinematic Cornell path tracing sample --- include/atlas/core/default_shaders.h | 57 +++--- include/photon/illuminate.h | 3 +- photon/path_tracing.cpp | 21 ++- shaders/metal/path_tracing/path.metal | 19 +- .../assets/materials/BoxGreen.amat | 2 +- .../path-tracing/assets/materials/BoxRed.amat | 2 +- .../assets/materials/BoxWhite.amat | 2 +- .../assets/materials/Emissive Ball.amat | 2 +- .../path-tracing/assets/materials/Glass.amat | 8 +- tests/path-tracing/main.ascene | 162 ++++++++---------- tests/path-tracing/project.atlas | 6 + 11 files changed, 147 insertions(+), 137 deletions(-) diff --git a/include/atlas/core/default_shaders.h b/include/atlas/core/default_shaders.h index a66e07fc..34d95b48 100644 --- a/include/atlas/core/default_shaders.h +++ b/include/atlas/core/default_shaders.h @@ -7171,9 +7171,9 @@ void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, metallic = mat.metallic; roughness = mat.roughness; ao = mat.ao; - emissive = clampLuminance(float3(mat.emissiveColor) * - min(max(mat.emissiveIntensity, 0.0), 8.0), - 8.0); + emissive = max(float3(mat.emissiveColor) * + max(mat.emissiveIntensity, 0.0), + float3(0.0)); outIor = max(mat.ior, 1.0); outTransmittance = clamp(mat.transmittance, 0.0, 1.0); @@ -7206,8 +7206,8 @@ void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, roughness *= clamp(roughnessValue, 0.0, 1.0); } if (mat.aoTextureIndex >= 0 && uint(mat.aoTextureIndex) < textureCount) { - ao *= clamp(sampleM)", -R"(aterialTexture(mat.aoTextureIndex, uv, + ao *= clamp(sampleMaterialTexture(mat.aoTextureIndex, )", +R"(uv, PT_MATERIAL_TEXTURE_ARGS) .x, 0.0, 1.0); @@ -7395,8 +7395,8 @@ float G_Smith(float NdotV, float NdotL, float roughness) { float r = roughness + 1.0; float k = (r * r) / 8.0; float gV = NdotV / (NdotV * (1.0 - k) + k); - )", -R"(float gL = NdotL / (NdotL * (1.0 - k) + k); + float gL = NdotL / (NdotL * (1.0 - )", +R"(k) + k); return gV * gL; } @@ -7578,9 +7578,9 @@ float3 evalEmissiveTriangleLighting( float3 evalDirectLightingPBR(intersector isect, primitive_acceleration_structure sceneAS, float3 P, - float3 N, fl)", -R"(oat3 Ng, float3 V, float3 albedo, - float metallic, float roughness, float reflectivity, + float3 N, float3 Ng, float3 V, float3 albedo, + )", +R"( float metallic, float roughness, float reflectivity, float ior, float transmittance, float sssStrength, float sssThickness, thread uint &rng, @@ -7731,9 +7731,9 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, PT_MATERIAL_TEXTURE_PARAMS, texturecube skybox, thread float3 &primaryAlbedo, thread float3 &primaryNormal, - thread f)", -R"(loat3 &primaryPosition, - thread float &primaryDepth, + thread float3 &primaryPosition, + )", +R"( thread float &primaryDepth, thread float &primaryRoughness, thread float &primaryHitDistance, thread uint &primaryObjectId) { @@ -7906,9 +7906,9 @@ R"(loat3 &primaryPosition, float diffuseProb = (1.0 - metallic) * (1.0 - transmittance) * (1.0 - fresnelProbability); float eta = frontFace ? 1.0 / ior : ior; - float3 idealRefractedDi)", -R"(rection = refract(-V, N, eta); - bool totalInternalReflection = + float3 idealRefractedDirection = refract(-V, N, eta); + )", +R"( bool totalInternalReflection = dot(idealRefractedDirection, idealRefractedDirection) < 1e-8; if (totalInternalReflection) { specProb += transmitProb; @@ -8077,9 +8077,9 @@ R"(rection = refract(-V, N, eta); if (depth >= 2) { float survival = clamp(max(throughput.x, max(throughput.y, throughput.z)), - )", -R"( 0.05, 0.95); - if (rand(rng) > survival) { + 0.05, 0.95); + )", +R"( if (rand(rng) > survival) { break; } throughput /= survival; @@ -8104,10 +8104,11 @@ kernel void main0(texture2d outTex [[texture(0)]], texture2d albedoRoughnessTex [[texture(3)]], texture2d normalDepthTex [[texture(4)]], texture2d motionObjectTex [[texture(5)]], - texture2d momentsHitTex [[texture(6)]], + texture2d historyMomentsTex [[texture(6)]], texture2d historyGuideTex [[texture(7)]], texture2d historyOutTex [[texture(8)]], texture2d historyGuideOutTex [[texture(9)]], + texture2d historyMomentsOutTex [[texture(10)]], primitive_acceleration_structure sceneAS [[buffer(0)]], constant CameraUniforms &cam [[buffer(1)]], constant Material *materials [[buffer(2)]], @@ -8225,7 +8226,7 @@ kernel void main0(texture2d outTex [[texture(0)]], } float4 prevColor = historyTex.read(previousPixel); float4 previousGuide = historyGuideTex.read(previousPixel); - float4 previousMoments = momentsHitTex.read(gid); + float4 previousMoments = historyMomentsTex.read(previousPixel); bool historyValid = sceneData.frameIndex > 0 && prevColor.w > 0.0 && abs(previousGuide.z - primaryDepth) < max(0.02, primaryDepth * 0.01) && @@ -8255,10 +8256,10 @@ kernel void main0(texture2d outTex [[texture(0)]], float accumulationDenominator = max(previousWeight + 1.0, 1.0); float3 accum = (prevColor.xyz * previousWeight + color) / accumulationDenominator; - float moment = luminance(color); + )", +R"( float moment = luminance(color); float accumulatedMoment = -)", -R"( (previousMoments.x * previousWeight + moment) / + (previousMoments.x * previousWeight + moment) / accumulationDenominator; float accumulatedMomentSquared = (previousMoments.y * previousWeight + moment * moment) / @@ -8291,10 +8292,10 @@ R"( (previousMoments.x * previousWeight + moment) / pixel); normalDepthTex.write(float4(primaryNormal, primaryDepth), pixel); motionObjectTex.write(float4(motion, objectIdValue, 1.0), pixel); - momentsHitTex.write(float4(accumulatedMoment, - accumulatedMomentSquared, variance, - primaryHitDistance), - pixel); + historyMomentsOutTex.write( + float4(accumulatedMoment, accumulatedMomentSquared, variance, + primaryHitDistance), + pixel); outTex.write(float4(accum, 1.0), pixel); brightTex.write(float4(brightColor, 1.0), pixel); } diff --git a/include/photon/illuminate.h b/include/photon/illuminate.h index 4e19d175..7797ce28 100644 --- a/include/photon/illuminate.h +++ b/include/photon/illuminate.h @@ -129,7 +129,8 @@ class PathTracing { std::array, 2> denoiseTextures; std::array, 2> pathTracingHistoryTextures; std::array, 2> pathTracingHistoryGuides; - std::array, 4> pathTracingAovTextures; + std::array, 2> pathTracingHistoryMoments; + std::array, 3> pathTracingAovTextures; std::vector cachedBLASPrimitiveOffsets; std::vector cachedObjects; std::vector cachedSceneObjects; diff --git a/photon/path_tracing.cpp b/photon/path_tracing.cpp index b2dcb011..77df689a 100644 --- a/photon/path_tracing.cpp +++ b/photon/path_tracing.cpp @@ -249,6 +249,11 @@ void photon::PathTracing::init() { outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); } + for (auto &texture : pathTracingHistoryMoments) { + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); + } for (auto &texture : denoiseTextures) { texture = std::make_shared(Texture::create( outputWidth, outputHeight, opal::TextureFormat::Rgba16F, @@ -286,6 +291,11 @@ void photon::PathTracing::resizeOutput(int width, int height) { outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); } + for (auto &texture : pathTracingHistoryMoments) { + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); + } for (auto &texture : denoiseTextures) { texture = std::make_shared(Texture::create( outputWidth, outputHeight, opal::TextureFormat::Rgba16F, @@ -1191,8 +1201,9 @@ bool photon::PathTracing::render( pathTracingAovTextures[1]->texture, 4); pathTracingPipeline->bindTexture("motionObjectTex", pathTracingAovTextures[2]->texture, 5); - pathTracingPipeline->bindTexture("momentsHitTex", - pathTracingAovTextures[3]->texture, 6); + pathTracingPipeline->bindTexture( + "historyMomentsTex", + pathTracingHistoryMoments[historyReadIndex]->texture, 6); pathTracingPipeline->bindTexture("historyGuideTex", pathTracingHistoryGuides[historyReadIndex] ->texture, @@ -1203,6 +1214,9 @@ bool photon::PathTracing::render( pathTracingPipeline->bindTexture( "historyGuideOutTex", pathTracingHistoryGuides[historyWriteIndex]->texture, 9); + pathTracingPipeline->bindTexture( + "historyMomentsOutTex", + pathTracingHistoryMoments[historyWriteIndex]->texture, 10); static std::shared_ptr fallbackSkyboxTexture = nullptr; if (fallbackSkyboxTexture == nullptr) { @@ -1329,7 +1343,8 @@ bool photon::PathTracing::render( pathTracingAovTextures[0]->texture, 4); pathDenoisePipeline->bindTexture( - "momentsTexture", pathTracingAovTextures[3]->texture, 5); + "momentsTexture", + pathTracingHistoryMoments[historyReadIndex]->texture, 5); pathDenoisePipeline->setUniform1i("parameters.stepWidth", denoiseSteps[pass]); commandBuffer->dispatch(outputWidth, outputHeight, 1); diff --git a/shaders/metal/path_tracing/path.metal b/shaders/metal/path_tracing/path.metal index 53ff74fb..3320bad0 100644 --- a/shaders/metal/path_tracing/path.metal +++ b/shaders/metal/path_tracing/path.metal @@ -549,9 +549,9 @@ void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, metallic = mat.metallic; roughness = mat.roughness; ao = mat.ao; - emissive = clampLuminance(float3(mat.emissiveColor) * - min(max(mat.emissiveIntensity, 0.0), 8.0), - 8.0); + emissive = max(float3(mat.emissiveColor) * + max(mat.emissiveIntensity, 0.0), + float3(0.0)); outIor = max(mat.ior, 1.0); outTransmittance = clamp(mat.transmittance, 0.0, 1.0); @@ -1476,10 +1476,11 @@ kernel void main0(texture2d outTex [[texture(0)]], texture2d albedoRoughnessTex [[texture(3)]], texture2d normalDepthTex [[texture(4)]], texture2d motionObjectTex [[texture(5)]], - texture2d momentsHitTex [[texture(6)]], + texture2d historyMomentsTex [[texture(6)]], texture2d historyGuideTex [[texture(7)]], texture2d historyOutTex [[texture(8)]], texture2d historyGuideOutTex [[texture(9)]], + texture2d historyMomentsOutTex [[texture(10)]], primitive_acceleration_structure sceneAS [[buffer(0)]], constant CameraUniforms &cam [[buffer(1)]], constant Material *materials [[buffer(2)]], @@ -1597,7 +1598,7 @@ kernel void main0(texture2d outTex [[texture(0)]], } float4 prevColor = historyTex.read(previousPixel); float4 previousGuide = historyGuideTex.read(previousPixel); - float4 previousMoments = momentsHitTex.read(gid); + float4 previousMoments = historyMomentsTex.read(previousPixel); bool historyValid = sceneData.frameIndex > 0 && prevColor.w > 0.0 && abs(previousGuide.z - primaryDepth) < max(0.02, primaryDepth * 0.01) && @@ -1662,10 +1663,10 @@ kernel void main0(texture2d outTex [[texture(0)]], pixel); normalDepthTex.write(float4(primaryNormal, primaryDepth), pixel); motionObjectTex.write(float4(motion, objectIdValue, 1.0), pixel); - momentsHitTex.write(float4(accumulatedMoment, - accumulatedMomentSquared, variance, - primaryHitDistance), - pixel); + historyMomentsOutTex.write( + float4(accumulatedMoment, accumulatedMomentSquared, variance, + primaryHitDistance), + pixel); outTex.write(float4(accum, 1.0), pixel); brightTex.write(float4(brightColor, 1.0), pixel); } diff --git a/tests/path-tracing/assets/materials/BoxGreen.amat b/tests/path-tracing/assets/materials/BoxGreen.amat index f393181b..65e1ea44 100644 --- a/tests/path-tracing/assets/materials/BoxGreen.amat +++ b/tests/path-tracing/assets/materials/BoxGreen.amat @@ -18,7 +18,7 @@ "metallic": 0, "normalMapStrength": 1, "reflectivity": 0.5, - "roughness": 0.5, + "roughness": 0.92, "textureOffset": [ 0, 0 diff --git a/tests/path-tracing/assets/materials/BoxRed.amat b/tests/path-tracing/assets/materials/BoxRed.amat index 884788f7..20eff5f0 100644 --- a/tests/path-tracing/assets/materials/BoxRed.amat +++ b/tests/path-tracing/assets/materials/BoxRed.amat @@ -18,7 +18,7 @@ "metallic": 0, "normalMapStrength": 1, "reflectivity": 0.5, - "roughness": 0.5, + "roughness": 0.92, "textureOffset": [ 0, 0 diff --git a/tests/path-tracing/assets/materials/BoxWhite.amat b/tests/path-tracing/assets/materials/BoxWhite.amat index de1de88c..4fc845b3 100644 --- a/tests/path-tracing/assets/materials/BoxWhite.amat +++ b/tests/path-tracing/assets/materials/BoxWhite.amat @@ -18,7 +18,7 @@ "metallic": 0, "normalMapStrength": 1, "reflectivity": 0.5, - "roughness": 0.5, + "roughness": 0.92, "textureOffset": [ 0, 0 diff --git a/tests/path-tracing/assets/materials/Emissive Ball.amat b/tests/path-tracing/assets/materials/Emissive Ball.amat index 33123ae6..86fe7044 100644 --- a/tests/path-tracing/assets/materials/Emissive Ball.amat +++ b/tests/path-tracing/assets/materials/Emissive Ball.amat @@ -18,7 +18,7 @@ "metallic": 0, "normalMapStrength": 1, "reflectivity": 0.5, - "roughness": 0.5, + "roughness": 1.0, "textureOffset": [ 0, 0 diff --git a/tests/path-tracing/assets/materials/Glass.amat b/tests/path-tracing/assets/materials/Glass.amat index f9fc6380..c6b482d6 100644 --- a/tests/path-tracing/assets/materials/Glass.amat +++ b/tests/path-tracing/assets/materials/Glass.amat @@ -1,9 +1,9 @@ { "material": { "albedo": [ - 0.8, - 0.8, - 0.8, + 1, + 1, + 1, 1 ], "ao": 1, @@ -18,7 +18,7 @@ "metallic": 0, "normalMapStrength": 1, "reflectivity": 0.5, - "roughness": 0.5, + "roughness": 0.02, "textureOffset": [ 0, 0 diff --git a/tests/path-tracing/main.ascene b/tests/path-tracing/main.ascene index e57df0dd..b66c8788 100644 --- a/tests/path-tracing/main.ascene +++ b/tests/path-tracing/main.ascene @@ -4,23 +4,23 @@ "automaticMoving": false, "controllerLookSensitivity": 180.0, "farClip": 1000.0, - "focusDepth": 20.0, - "focusRange": 10.0, - "fov": 60.0, + "focusDepth": 8.5, + "focusRange": 4.0, + "fov": 45.0, "lookSmoothness": 0.15000000596046448, "mouseSensitivity": 0.10000000149011612, "movementSpeed": 2.0, - "nearClip": 0.5, + "nearClip": 0.1, "orthoSize": 5.0, "orthographic": false, "position": [ - 0.4004000127315521, - 1.9144999980926514, - -4.536300182342529 + 0.0, + 2.5, + -7.5 ], "target": [ 0.0, - 0.5273000001907349, + 2.25, 1.0 ] }, @@ -38,72 +38,82 @@ }, "id": "main_scene", "lights": [], - "name": "Main Scene", + "name": "Cornell Box", "objects": [ { "components": [], "id": 157052340, "material": "assets/materials/BoxWhite.amat", "name": "Floor", - "position": [ - -0.31472718715667725, - -0.15528558194637299, - 0.15461790561676025 - ], - "rotation": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 4.174081802368164, - 0.05000000074505806, - 4.174081802368164 - ], + "position": [0.0, -0.05, 1.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [5.5, 0.1, 5.5], + "solid_type": "cube", + "type": "solid" + }, + { + "components": [], + "id": 157052341, + "material": "assets/materials/BoxWhite.amat", + "name": "Ceiling", + "position": [0.0, 5.05, 1.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [5.5, 0.1, 5.5], + "solid_type": "cube", + "type": "solid" + }, + { + "components": [], + "id": 157052342, + "material": "assets/materials/BoxWhite.amat", + "name": "Back Wall", + "position": [0.0, 2.5, 3.75], + "rotation": [0.0, 0.0, 0.0], + "scale": [5.5, 5.0, 0.1], + "solid_type": "cube", + "type": "solid" + }, + { + "components": [], + "id": 157052343, + "material": "assets/materials/BoxGreen.amat", + "name": "Left Wall", + "position": [-2.75, 2.5, 1.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [0.1, 5.0, 5.5], "solid_type": "cube", "type": "solid" }, { "components": [], + "id": 157052344, "material": "assets/materials/BoxRed.amat", - "name": "Floor 2", - "position": [ - -0.3160567581653595, - 1.5384644269943237, - 2.2118043899536133 - ], - "rotation": [ - -89.787841796875, - 0.0, - 0.0 - ], - "scale": [ - 4.174081802368164, - 0.05000000074505806, - 7.269232749938965 - ], + "name": "Right Wall", + "position": [2.75, 2.5, 1.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [0.1, 5.0, 5.5], + "solid_type": "cube", + "type": "solid" + }, + { + "components": [], + "id": 157052345, + "material": "assets/materials/BoxWhite.amat", + "name": "Short Box", + "position": [-1.15, 0.75, 2.0], + "rotation": [0.0, -18.0, 0.0], + "scale": [1.5, 1.5, 1.5], "solid_type": "cube", "type": "solid" }, { "components": [], + "id": 157052346, "material": "assets/materials/BoxWhite.amat", - "name": "Floor 3", - "position": [ - -0.2613140940666199, - 5.090397834777832, - 0.2858502268791199 - ], - "rotation": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 4.174081802368164, - 0.05000000074505806, - 4.174081802368164 - ], + "name": "Tall Box", + "position": [1.0, 1.4, 1.65], + "rotation": [0.0, 15.0, 0.0], + "scale": [1.4, 2.8, 1.4], "solid_type": "cube", "type": "solid" }, @@ -111,22 +121,10 @@ "components": [], "id": 6739391, "material": "assets/materials/Glass.amat", - "name": "Sphere", - "position": [ - -0.22421985864639282, - 1.5384645462036133, - 0.5111536979675293 - ], - "rotation": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ], + "name": "Glass Sphere", + "position": [-0.7, 1.0, 0.05], + "rotation": [0.0, 0.0, 0.0], + "scale": [1.1, 1.1, 1.1], "solid_type": "sphere", "type": "solid" }, @@ -134,22 +132,10 @@ "components": [], "id": 2097299432, "material": "assets/materials/Emissive Ball.amat", - "name": "Plane", - "position": [ - 0.0, - 2.497310161590576, - -5.3531646728515625 - ], - "rotation": [ - 20.657730102539063, - 0.0, - 0.0 - ], - "scale": [ - 3.9725027084350586, - 2.9564456939697266, - 3.064767360687256 - ], + "name": "Ceiling Light", + "position": [0.0, 4.92, 1.0], + "rotation": [90.0, 0.0, 0.0], + "scale": [2.0, 1.5, 1.0], "solid_type": "plane", "type": "solid" } diff --git a/tests/path-tracing/project.atlas b/tests/path-tracing/project.atlas index 55738484..9089cb5b 100644 --- a/tests/path-tracing/project.atlas +++ b/tests/path-tracing/project.atlas @@ -13,8 +13,14 @@ icon = "none" supported_platforms = "all" [renderer] +accumulation_frames = 512 default = "pathtracing" +denoising = true global_illumination = false +max_bounces = 8 +samples_per_pixel = 4 +upscaling_ratio = 0.67 +use_upscaling = true [window] dimensions = [1280, 720] From 576ecf08260326611b8adbdbe2c0a5a65cf5b9f9 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 2 Aug 2026 07:37:01 +0200 Subject: [PATCH 4/4] Finished fixing the path tracer --- .gitignore | 3 +- .../path-tracing/.atlas/project-settings.ini | 32 ++++ tests/path-tracing/main.ascene | 157 ++++++++++++++---- tests/path-tracing/project.atlas | 11 +- 4 files changed, 164 insertions(+), 39 deletions(-) create mode 100644 tests/path-tracing/.atlas/project-settings.ini diff --git a/.gitignore b/.gitignore index 2e2dd2dc..8892fa57 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ __pycache__ build_verify dist node_modules -.obsidian \ No newline at end of file +.obsidian +*.app diff --git a/tests/path-tracing/.atlas/project-settings.ini b/tests/path-tracing/.atlas/project-settings.ini new file mode 100644 index 00000000..61978377 --- /dev/null +++ b/tests/path-tracing/.atlas/project-settings.ini @@ -0,0 +1,32 @@ +[project] +autosaveMinutes=5 +buildCommand=atlas pack --backend METAL +bundleIdentifier=org.atlasengine.path-tracing +company=Neutral Software +controller=Automatic +defaultScene=main.ascene +exportBackend=METAL +exportConfiguration=Release +exportDirectory=/Users/maxvdec/Coding/Projects/Atlas/tests/path-tracing/Exports +exportPlatform=macOS +fixedStep=0.0166667 +frameLimit=0 +fullscreen=false +gravity="0, -9.81, 0" +icon=none +inputMap=input.json +internalScale=67 +pathTracingAccumulation=512 +pathTracingBounces=8 +pathTracingDenoising=true +pathTracingSamples=50 +renderer=Path Tracing +runCommand=atlas run project.atlas +snapIncrement=0.5 +ssr=false +ssrDebug=false +ssrQuality=1 +useUpscaling=true +version=1.0.0 +windowHeight=720 +windowWidth=1280 diff --git a/tests/path-tracing/main.ascene b/tests/path-tracing/main.ascene index b66c8788..de3cfac1 100644 --- a/tests/path-tracing/main.ascene +++ b/tests/path-tracing/main.ascene @@ -10,7 +10,7 @@ "lookSmoothness": 0.15000000596046448, "mouseSensitivity": 0.10000000149011612, "movementSpeed": 2.0, - "nearClip": 0.1, + "nearClip": 0.10000000149011612, "orthoSize": 5.0, "orthographic": false, "position": [ @@ -45,9 +45,21 @@ "id": 157052340, "material": "assets/materials/BoxWhite.amat", "name": "Floor", - "position": [0.0, -0.05, 1.0], - "rotation": [0.0, 0.0, 0.0], - "scale": [5.5, 0.1, 5.5], + "position": [ + 0.0, + -0.05000000074505806, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 5.5, + 0.10000000149011612, + 5.5 + ], "solid_type": "cube", "type": "solid" }, @@ -56,9 +68,21 @@ "id": 157052341, "material": "assets/materials/BoxWhite.amat", "name": "Ceiling", - "position": [0.0, 5.05, 1.0], - "rotation": [0.0, 0.0, 0.0], - "scale": [5.5, 0.1, 5.5], + "position": [ + 0.0, + 5.050000190734863, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 5.5, + 0.10000000149011612, + 5.5 + ], "solid_type": "cube", "type": "solid" }, @@ -67,9 +91,21 @@ "id": 157052342, "material": "assets/materials/BoxWhite.amat", "name": "Back Wall", - "position": [0.0, 2.5, 3.75], - "rotation": [0.0, 0.0, 0.0], - "scale": [5.5, 5.0, 0.1], + "position": [ + 0.0, + 2.5, + 3.75 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 5.5, + 5.0, + 0.10000000149011612 + ], "solid_type": "cube", "type": "solid" }, @@ -78,9 +114,21 @@ "id": 157052343, "material": "assets/materials/BoxGreen.amat", "name": "Left Wall", - "position": [-2.75, 2.5, 1.0], - "rotation": [0.0, 0.0, 0.0], - "scale": [0.1, 5.0, 5.5], + "position": [ + -2.75, + 2.5, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 0.10000000149011612, + 5.0, + 5.5 + ], "solid_type": "cube", "type": "solid" }, @@ -89,20 +137,21 @@ "id": 157052344, "material": "assets/materials/BoxRed.amat", "name": "Right Wall", - "position": [2.75, 2.5, 1.0], - "rotation": [0.0, 0.0, 0.0], - "scale": [0.1, 5.0, 5.5], - "solid_type": "cube", - "type": "solid" - }, - { - "components": [], - "id": 157052345, - "material": "assets/materials/BoxWhite.amat", - "name": "Short Box", - "position": [-1.15, 0.75, 2.0], - "rotation": [0.0, -18.0, 0.0], - "scale": [1.5, 1.5, 1.5], + "position": [ + 2.75, + 2.5, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 0.10000000149011612, + 5.0, + 5.5 + ], "solid_type": "cube", "type": "solid" }, @@ -111,9 +160,21 @@ "id": 157052346, "material": "assets/materials/BoxWhite.amat", "name": "Tall Box", - "position": [1.0, 1.4, 1.65], - "rotation": [0.0, 15.0, 0.0], - "scale": [1.4, 2.8, 1.4], + "position": [ + 1.0, + 1.5808521509170532, + 1.649999976158142 + ], + "rotation": [ + 0.0, + 15.0, + 0.0 + ], + "scale": [ + 1.7824022769927979, + 3.1824023723602295, + 1.7824022769927979 + ], "solid_type": "cube", "type": "solid" }, @@ -122,9 +183,21 @@ "id": 6739391, "material": "assets/materials/Glass.amat", "name": "Glass Sphere", - "position": [-0.7, 1.0, 0.05], - "rotation": [0.0, 0.0, 0.0], - "scale": [1.1, 1.1, 1.1], + "position": [ + -1.1136409044265747, + 1.508070468902588, + -0.5597862005233765 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.9675586223602295, + 1.9675586223602295, + 1.9675586223602295 + ], "solid_type": "sphere", "type": "solid" }, @@ -133,9 +206,21 @@ "id": 2097299432, "material": "assets/materials/Emissive Ball.amat", "name": "Ceiling Light", - "position": [0.0, 4.92, 1.0], - "rotation": [90.0, 0.0, 0.0], - "scale": [2.0, 1.5, 1.0], + "position": [ + 0.0, + 4.920000076293945, + 1.0 + ], + "rotation": [ + 90.0, + 0.0, + 0.0 + ], + "scale": [ + 2.0, + 1.5, + 1.0 + ], "solid_type": "plane", "type": "solid" } diff --git a/tests/path-tracing/project.atlas b/tests/path-tracing/project.atlas index 9089cb5b..d7240baf 100644 --- a/tests/path-tracing/project.atlas +++ b/tests/path-tracing/project.atlas @@ -1,6 +1,6 @@ app_name = "path-tracing" atlas_version = "alpha9" -backend = "AUTO" +backend = "METAL" name = "path-tracing" platform = "DESKTOP" @@ -12,18 +12,25 @@ main_scene = "main.ascene" icon = "none" supported_platforms = "all" +identifier = "org.atlasengine.path-tracing" +version = "1.0.0" [renderer] accumulation_frames = 512 default = "pathtracing" denoising = true global_illumination = false max_bounces = 8 -samples_per_pixel = 4 +samples_per_pixel = 50 upscaling_ratio = 0.67 use_upscaling = true +ssr = false +ssr_quality = 1 +ssr_debug = false [window] dimensions = [1280, 720] mouse_capture = false multisampling = false ssaoScale = 0.5 + +fullscreen = false \ No newline at end of file