// These tests relies on RayTracingResources.Load() so cannot run in a player.
#if UNITY_EDITOR
using System.Runtime.InteropServices;
using NUnit.Framework;
using Unity.Mathematics;
using UnityEditor;
using UnityEngine.PathTracing.Core;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Sampling;
using UnityEngine.Rendering.UnifiedRayTracing;
namespace UnityEngine.PathTracing.Tests
{
internal class PathIteratorTests
{
private static class MeshUtil
{
internal static Mesh CreateQuadMesh()
{
Mesh mesh = new Mesh();
Vector3[] vertices =
{
new(-0.5f, -0.5f, 0.0f),
new(0.5f, -0.5f, 0.0f),
new(-0.5f, 0.5f, 0.0f),
new(0.5f, 0.5f, 0.0f)
};
mesh.vertices = vertices;
Vector3[] normals =
{
Vector3.forward,
Vector3.forward,
Vector3.forward,
Vector3.forward
};
mesh.normals = normals;
Vector2[] uv =
{
new(0, 0),
new(1, 0),
new(0, 1),
new(1, 1)
};
mesh.uv = uv;
int[] tris =
{
0, 2, 1,
2, 3, 1
};
mesh.triangles = tris;
return mesh;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct TestRay
{
public float3 Origin;
public float3 Direction;
}
private World _world;
private RayTracingContext _rayTracingContext;
private CommandBuffer _cmd;
private SamplingResources _samplingResources;
private RTHandle _emptyExposureTexture;
private const RayTracingBackend _backend = RayTracingBackend.Compute;
[SetUp]
public void Setup()
{
if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLCore ||
SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES3)
{
Assert.Ignore("PathTracingCore is incompatible with OpenGL as it places a harsh limitation of buffer count.");
return;
}
var resources = new RayTracingResources();
resources.Load();
_rayTracingContext = new RayTracingContext(_backend, resources);
_cmd = new CommandBuffer();
_cmd.name = "PathTracingCore/PathIteratorTests";
var worldResources = new WorldResourceSet();
worldResources.LoadFromAssetDatabase();
_world = new World();
_world.Init(_rayTracingContext, worldResources);
_samplingResources = new SamplingResources();
_samplingResources.Load((uint)SamplingResources.ResourceType.All);
_emptyExposureTexture = RTHandles.Alloc(1, 1, enableRandomWrite: true, name: "Empty EV100 Exposure");
}
[TearDown]
public void TearDown()
{
_world?.Dispose();
_rayTracingContext?.Dispose();
_cmd?.Dispose();
_emptyExposureTexture?.Release();
_samplingResources?.Dispose();
}
[Test]
[TestCase(1, 1, 1, 1u)]
[TestCase(10, 20, 30, 1u)]
[TestCase(10, 20, 30, 8u)]
[TestCase(0, 42, 0, 1u)]
[TestCase(0, 42, 0, 8u)]
public void EmptyWorldWithEnvironmentLight_ShouldOutputEnvironmentLight(float envRed, float envGreen, float envBlue, uint sampleCount)
{
using var deviceInputBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 1, Marshal.SizeOf<TestRay>());
{
var inputHost = new Ray[1];
inputHost[0].origin = float3.zero;
inputHost[0].direction = new float3(0, 1, 0);
deviceInputBuffer.SetData(inputHost);
}
using var deviceOutputBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 1, sizeof(float) * 3);
var cubemapMat = new Material(Shader.Find("PathIteratorTesting/UniformCubemap"));
cubemapMat.SetVector("_Radiance", new Vector4(envRed, envGreen, envBlue, 1.0f));
_world.SetEnvironmentMaterial(cubemapMat);
GraphicsBuffer buildScratchBuffer = null;
_world.Build(new Bounds(), _cmd, ref buildScratchBuffer, _samplingResources, true);
var shader = _rayTracingContext.LoadRayTracingShader("Packages/com.unity.render-pipelines.core/Tests/Runtime/PathTracing/PathIteratorTest.urtshader");
Util.BindWorld(_cmd, shader, _world, 8);
Util.BindPathTracingInputs(_cmd, shader, false, 1, false, 4, 1.0f, RenderedGameObjectsFilter.OnlyStatic, _samplingResources, _emptyExposureTexture);
shader.SetIntParam(_cmd, Shader.PropertyToID("g_SampleCount"), (int)sampleCount);
shader.SetBufferParam(_cmd, Shader.PropertyToID("_Output"), deviceOutputBuffer);
shader.SetBufferParam(_cmd, Shader.PropertyToID("_InputRay"), deviceInputBuffer);
GraphicsBuffer traceScratchBuffer = null;
RayTracingHelper.ResizeScratchBufferForTrace(shader, 1, 1, 1, ref traceScratchBuffer);
shader.Dispatch(_cmd, traceScratchBuffer, 1, 1, 1);
Graphics.ExecuteCommandBuffer(_cmd);
var hostOutputBuffer = new float3[1];
deviceOutputBuffer.GetData(hostOutputBuffer);
const float tolerance = 0.001f;
Assert.AreEqual(envRed, hostOutputBuffer[0].x, tolerance);
Assert.AreEqual(envGreen, hostOutputBuffer[0].y, tolerance);
Assert.AreEqual(envBlue, hostOutputBuffer[0].z, tolerance);
CoreUtils.Destroy(cubemapMat);
buildScratchBuffer.Dispose();
traceScratchBuffer.Dispose();
}
[Test]
[TestCase(1, 0, 0, 1u)]
[TestCase(0, 1, 0, 1u)]
[TestCase(0, 0, 1, 1u)]
[TestCase(0, 0, 1, 8u)]
public void RayHittingPlaneLitByWhiteEnvironmentLight_ShouldMatchAnalyticDerivation(float albedoRed, float albedoGreen, float albedoBlue, uint sampleCount)
{
Assert.Ignore("Fails on MacEditor ARM64, Windows x64 and other platforms, probably due to it looking for output in the wrong place. See https://jira.unity3d.com/browse/GFXLIGHT-1796");
using var deviceInputBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 1, Marshal.SizeOf<TestRay>());
{
var hostInputBuffer = new Ray[1];
hostInputBuffer[0].origin = new float3(0.1f, 0.1f, -1); // We avoid numerical precision issues at the diagonal.
hostInputBuffer[0].direction = new float3(0, 0, 1);
deviceInputBuffer.SetData(hostInputBuffer);
}
using var deviceOutputBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 1, sizeof(float) * 3);
var cubemapMaterial = new Material(Shader.Find("PathIteratorTesting/UniformCubemap"));
cubemapMaterial.SetVector("_Radiance", new Vector4(1.0f, 1.0f, 1.0f, 1.0f));
GraphicsBuffer buildScratchBuffer = null;
var mesh = MeshUtil.CreateQuadMesh();
var material = new Material(Shader.Find("PathIteratorTesting/UniformAlbedoMetaPass"));
material.SetColor("_Albedo", new Color(albedoRed, albedoGreen, albedoBlue, 1));
var matDesc = MaterialPool.ConvertUnityMaterialToMaterialDescriptor(material);
_world.SetEnvironmentMaterial(cubemapMaterial);
var matHandle = _world.AddMaterial(matDesc, UVChannel.UV0);
var mask = World.GetInstanceMask(ShadowCastingMode.On, true, RenderedGameObjectsFilter.OnlyStatic);
_world.AddInstance(
mesh,
new [] { matHandle },
new[] { mask },
1,
Matrix4x4.identity,
new Bounds(Vector3.zero, Vector3.one * 20.0f),
true,
RenderedGameObjectsFilter.OnlyStatic,
true);
_world.Build(new Bounds(), _cmd, ref buildScratchBuffer, _samplingResources, true);
var shader = _rayTracingContext.LoadRayTracingShader("Packages/com.unity.render-pipelines.core/Tests/Runtime/PathTracing/PathIteratorTest.urtshader");
Util.BindWorld(_cmd, shader, _world, 8);
Util.BindPathTracingInputs(_cmd, shader, false, 1, false, 4, 1.0f, RenderedGameObjectsFilter.OnlyStatic, _samplingResources, _emptyExposureTexture);
shader.SetIntParam(_cmd, Shader.PropertyToID("g_SampleCount"), (int)sampleCount);
shader.SetBufferParam(_cmd, Shader.PropertyToID("_Output"), deviceOutputBuffer);
shader.SetBufferParam(_cmd, Shader.PropertyToID("_InputRay"), deviceInputBuffer);
GraphicsBuffer traceScratchBuffer = null;
RayTracingHelper.ResizeScratchBufferForTrace(shader, 1, 1, 1, ref traceScratchBuffer);
shader.Dispatch(_cmd, traceScratchBuffer, 1, 1, 1);
Graphics.ExecuteCommandBuffer(_cmd);
var outputHost = new float3[1];
deviceOutputBuffer.GetData(outputHost);
// We have a non-emissive lambertian plane with albedo c which receives light from a uniform environment light with radiance 1. How much light is reflected in the direction of the normal of the plane?
// The incoming light L_i is 1 and the BRDF is c/π.
// L_o = ∫ c/π L_i(ω) cos(θ) dω = c/π ∫ cos(θ) dω = c.
// The last equality holds because the hemispherical integral of cos(θ) is π.
Assert.AreEqual(albedoRed, outputHost[0].x);
Assert.AreEqual(albedoGreen, outputHost[0].y);
Assert.AreEqual(albedoBlue, outputHost[0].z);
CoreUtils.Destroy(cubemapMaterial);
CoreUtils.Destroy(material);
buildScratchBuffer.Dispose();
traceScratchBuffer.Dispose();
}
}
}
#endif