using System; using System.Linq; using NUnit.Framework; using Unity.Mathematics; using UnityEngine.PathTracing.Core; using UnityEngine.Rendering; using UnityEngine.Rendering.Sampling; using UnityEngine.Rendering.UnifiedRayTracing; using RayTracingBackend = UnityEngine.Rendering.UnifiedRayTracing.RayTracingBackend; namespace UnityEngine.PathTracing.Tests { using InstanceHandle = Handle; using MaterialHandle = Handle; [TestFixture("Compute")] [TestFixture("Hardware")] internal class WorldTests { private readonly RayTracingBackend _backend; private World _world; private bool _respectLightLayers; private bool _autoEstimateLUTRange; private GraphicsBuffer _buildScratchBuffer; private RayTracingContext _rayTracingContext; private Material _defaultMaterial; private CommandBuffer _cmd; private SamplingResources _samplingResources; public WorldTests(string backendAsString) { _backend = Enum.Parse(backendAsString); } [SetUp] public void Setup() { if (!SystemInfo.supportsRayTracing && _backend == RayTracingBackend.Hardware) Assert.Ignore("Cannot run test on this Graphics API. Hardware RayTracing is not supported"); var resources = new RayTracingResources(); resources.Load(); _rayTracingContext = new RayTracingContext(_backend, resources); _defaultMaterial = UnityEditor.AssetDatabase.GetBuiltinExtraResource("Default-Material.mat"); _respectLightLayers = false; _autoEstimateLUTRange = false; _cmd = new CommandBuffer(); _cmd.name = "PathTracing.Tests Command Buffer"; _world = new World(); var worldResources = new WorldResourceSet(); worldResources.LoadFromAssetDatabase(); _world.Init(_rayTracingContext, worldResources); _world.AddMaterial(MaterialPool.ConvertUnityMaterialToMaterialDescriptor(_defaultMaterial), UVChannel.UV0); _samplingResources = new SamplingResources(); _samplingResources.Load((uint)SamplingResources.ResourceType.All); } [TearDown] public void TearDown() { _world?.Dispose(); _buildScratchBuffer?.Dispose(); _rayTracingContext?.Dispose(); _cmd?.Dispose(); _samplingResources?.Dispose(); // Null this buffer so nobody attempts to access properties of a disposed buffer _buildScratchBuffer = null; } private static World.LightDescriptor[] CreateLights(int lightCount) { var lights = new Light[lightCount]; for (int i = 0; i < lightCount; ++i) { var lightGameObject = new GameObject($"Test Light {i}"); var light = lightGameObject.AddComponent(); lights[i] = light; } return Util.ConvertUnityLightsToLightDescriptors(lights, false); } [Test] [TestCase(0)] [TestCase(1)] [TestCase(2)] public void World_AddLight_IncreaseLightCount(int lightCount) { #if UNITY_EDITOR_OSX if (System.Runtime.InteropServices.RuntimeInformation.OSArchitecture != System.Runtime.InteropServices.Architecture.Arm64) NUnit.Framework.Assert.Ignore("Fails on MacOS13 UUM-111386"); #endif var lights = CreateLights(lightCount); _world.AddLights(lights, _respectLightLayers, _autoEstimateLUTRange, MixedLightingMode.IndirectOnly); _world.lightPickingMethod = LightPickingMethod.LightGrid; _world.Build(new Bounds(), _cmd, ref _buildScratchBuffer, _samplingResources, false); Graphics.ExecuteCommandBuffer(_cmd); Assert.AreEqual(lightCount, _world.LightCount); } [Test] public void World_AddAndRemoveLight_CountUpdates() { Assert.AreEqual(0, _world.LightCount); var light = CreateLights(1); var handles = _world.AddLights(light, _respectLightLayers, _autoEstimateLUTRange, MixedLightingMode.IndirectOnly); Assert.AreEqual(1, _world.LightCount); _world.RemoveLights(handles); Assert.AreEqual(0, _world.LightCount); } static Bounds CalculateBounds(Mesh mesh, Matrix4x4 localToWorld) { Bounds bounds = new Bounds(localToWorld.GetPosition(), Vector3.zero); var verts = mesh.vertices; foreach (var vert in verts) { var worldPos = localToWorld.MultiplyPoint(vert); bounds.Encapsulate(worldPos); } return bounds; } InstanceHandle AddInstanceToWorld(Mesh mesh, Matrix4x4 localToWorld, Material material) { Bounds bounds = CalculateBounds(mesh, localToWorld); const bool isStatic = true; uint objectLayerMask = 1; bool enableEmissiveSampling = true; var materialHandle = _world.AddMaterial(MaterialPool.ConvertUnityMaterialToMaterialDescriptor(material), UVChannel.UV0); var mask = World.GetInstanceMask(ShadowCastingMode.On, isStatic, RenderedGameObjectsFilter.OnlyStatic); return _world.AddInstance(mesh, new MaterialHandle[] { materialHandle }, new uint[] { mask }, objectLayerMask, localToWorld, bounds, isStatic, RenderedGameObjectsFilter.OnlyStatic, enableEmissiveSampling); } static AccelStructInstances GetAccelStructInstancesFromWorld(World world) { return world.GetAccelerationStructure().Instances; } [Test] [TestCase(0)] [TestCase(1)] [TestCase(2)] public void World_AddInstance_IncreasesInstanceCount(int instanceCount) { Mesh mesh = Resources.GetBuiltinResource("Cube.fbx"); for (int i = 0; i < instanceCount; i++) { Matrix4x4 localToWorld = Matrix4x4.Translate(Vector3.one * i); AddInstanceToWorld(mesh, localToWorld, _defaultMaterial); } _world.Build(new Bounds(), _cmd, ref _buildScratchBuffer, _samplingResources, false); Graphics.ExecuteCommandBuffer(_cmd); var instances = GetAccelStructInstancesFromWorld(_world); Assert.AreEqual(instanceCount, instances.GetInstanceCount()); } // Struct for reading geometry pool data back to CPU struct VertexData { public Vector3 Position; public float2 UV0; public float2 UV1; public float OctahedralNormal; } [Test] public void World_AddInstances_UploadedVertexDataMatches() { // Add some meshes to world Mesh[] meshes = new[] { Resources.GetBuiltinResource("Cube.fbx"), Resources.GetBuiltinResource("Sphere.fbx") }; for (int i = 0; i < meshes.Length; i++) { Matrix4x4 localToWorld = Matrix4x4.Translate(Vector3.one * i); AddInstanceToWorld(meshes[i], localToWorld, _defaultMaterial); } // Build world _world.Build(new Bounds(), _cmd, ref _buildScratchBuffer, _samplingResources, false); Graphics.ExecuteCommandBuffer(_cmd); // Readback vertex buffer data from geo pool var instances = GetAccelStructInstancesFromWorld(_world); int totalVertexCount = meshes.Sum(x => x.vertexCount); var geoPoolVertices = new VertexData[totalVertexCount]; instances.vertexBuffer.GetData(geoPoolVertices); // Check that data matches source int vertexOffset = 0; for (int i = 0; i < meshes.Length; i++) { var meshVerts = meshes[i].vertices; var meshUVs = meshes[i].uv; for (int j = 0; j < meshVerts.Length; j++) { Assert.AreEqual(meshVerts[j], geoPoolVertices[vertexOffset + j].Position, $"Vertex positions at index {vertexOffset + j} didn't match"); Assert.AreEqual(new float2(meshUVs[j].x, meshUVs[j].y), geoPoolVertices[vertexOffset + j].UV0, $"UVs at index {vertexOffset + j} didn't match"); } vertexOffset += meshVerts.Length; } } [Test] public void World_AddAndRemoveInstances_CountIsCorrect() { // Add 3 instances var mesh = Resources.GetBuiltinResource("Cube.fbx"); var handles = new InstanceHandle[3]; for (int i = 0; i < 3; i++) { Matrix4x4 localToWorld = Matrix4x4.Translate(Vector3.one * i); handles[i] = AddInstanceToWorld(mesh, localToWorld, _defaultMaterial); } // Remove 1 instance _world.RemoveInstance(handles[0]); // Build world _world.Build(new Bounds(), _cmd, ref _buildScratchBuffer, _samplingResources, false); Graphics.ExecuteCommandBuffer(_cmd); // Count should be 2 var instances = GetAccelStructInstancesFromWorld(_world); Assert.AreEqual(2, instances.GetInstanceCount()); } [Test] [TestCase(true)] [TestCase(false)] public void World_AddInstance_IncreasesMeshLightCountIfEmissive(bool isEmissive) { // Make material with emission Material material = new Material(_defaultMaterial); if (isEmissive) { material.EnableKeyword("_EMISSION"); material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.BakedEmissive; material.SetColor("_EmissionColor", Color.green); } // Add instance var mesh = Resources.GetBuiltinResource("Cube.fbx"); Matrix4x4 localToWorld = Matrix4x4.Translate(Vector3.one); AddInstanceToWorld(mesh, localToWorld, material); // Build world _world.Build(new Bounds(), _cmd, ref _buildScratchBuffer, _samplingResources, true); Graphics.ExecuteCommandBuffer(_cmd); // Check that mesh light count increased Assert.AreEqual(isEmissive ? 1 : 0, _world.MeshLightCount); CoreUtils.Destroy(material); } [Test] public void World_AddAndRemoveEmissiveInstance_MeshLightCountIsZero() { // Make material with emission Material material = new Material(_defaultMaterial); material.EnableKeyword("_EMISSION"); material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.BakedEmissive; material.SetColor("_EmissionColor", Color.green); // Add instance var mesh = Resources.GetBuiltinResource("Cube.fbx"); Matrix4x4 localToWorld = Matrix4x4.Translate(Vector3.one); InstanceHandle instance = AddInstanceToWorld(mesh, localToWorld, material); _world.RemoveInstance(instance); // Build world _world.Build(new Bounds(), _cmd, ref _buildScratchBuffer, _samplingResources, true); Graphics.ExecuteCommandBuffer(_cmd); // Check that mesh light count is 0 Assert.AreEqual(0, _world.MeshLightCount); CoreUtils.Destroy(material); } [Test] public void World_UpdateInstanceTransform_UploadsCorrectData() { #if UNITY_EDITOR_OSX if (System.Runtime.InteropServices.RuntimeInformation.OSArchitecture != System.Runtime.InteropServices.Architecture.Arm64) NUnit.Framework.Assert.Ignore("Fails on MacOS13 UUM-111386"); #endif // Add instance var mesh = Resources.GetBuiltinResource("Cube.fbx"); Matrix4x4 localToWorld = Matrix4x4.Translate(Vector3.one); InstanceHandle instance = AddInstanceToWorld(mesh, localToWorld, _defaultMaterial); // Update its transform Matrix4x4 newLocalToWorld = Matrix4x4.Translate(Vector3.one * 5); Assert.AreNotEqual(localToWorld, newLocalToWorld); _world.UpdateInstanceTransform(instance, newLocalToWorld); // Build world _world.Build(new Bounds(), _cmd, ref _buildScratchBuffer, _samplingResources, false); Graphics.ExecuteCommandBuffer(_cmd); // Readback instance buffer var instances = GetAccelStructInstancesFromWorld(_world); var instanceBuffer = instances.instanceBuffer.GetGpuBuffer(_cmd); Graphics.ExecuteCommandBuffer(_cmd); // Check that transform was updated var instancesArray = new AccelStructInstances.RTInstance[1]; instanceBuffer.GetData(instancesArray); Assert.AreEqual((float4x4)newLocalToWorld, instancesArray[0].localToWorld); } } }