#if SURFACE_CACHE using System; using System.Collections.Generic; using UnityEngine.PathTracing.Core; using InstanceHandle = UnityEngine.PathTracing.Core.Handle; using LightHandle = UnityEngine.PathTracing.Core.Handle; using MaterialHandle = UnityEngine.PathTracing.Core.Handle; using ObjectDispatcher = UnityEngine.InternalBridge.ObjectDispatcher; namespace UnityEngine.Rendering.Universal { class SurfaceCacheWorldAdapter { readonly SharedMaterialSet _sharedMaterials; readonly LightSet _lights; readonly MeshRendererSet _meshRenderers; #if ENABLE_TERRAIN_MODULE readonly TerrainSet _terrains; // Per-frame set of TerrainData EntityIds that were reported changed by ObjectDispatcher. readonly HashSet _dirtyTerrainDataThisFrame = new(); #endif // Defaults to Everything so that, absent a Surface Cache volume override, every instance contributes. uint _renderingLayerMaskFilter = 0xFFFFFFFF; public SurfaceCacheWorldAdapter(ObjectDispatcher objDispatcher, SurfaceCacheWorld world, Material fallbackMaterial) { _lights = new LightSet(); _sharedMaterials = new SharedMaterialSet(fallbackMaterial); _meshRenderers = new MeshRendererSet(fallbackMaterial); #if ENABLE_TERRAIN_MODULE _terrains = new TerrainSet(fallbackMaterial); #endif #if UNITY_EDITOR objDispatcher.maxDispatchHistoryFramesCount = int.MaxValue; #endif objDispatcher.EnableTypeTracking(ObjectDispatcher.TypeTrackingFlags.SceneObjects); objDispatcher.EnableTransformTracking(ObjectDispatcher.TransformTrackingType.GlobalTRS); objDispatcher.EnableTypeTracking(ObjectDispatcher.TypeTrackingFlags.SceneObjects); objDispatcher.EnableTransformTracking(ObjectDispatcher.TransformTrackingType.GlobalTRS); objDispatcher.EnableTypeTracking(ObjectDispatcher.TypeTrackingFlags.SceneObjects | ObjectDispatcher.TypeTrackingFlags.Assets); #if ENABLE_TERRAIN_MODULE objDispatcher.EnableTypeTracking(ObjectDispatcher.TypeTrackingFlags.SceneObjects); objDispatcher.EnableTransformTracking(ObjectDispatcher.TransformTrackingType.GlobalTRS); objDispatcher.EnableTypeTracking(ObjectDispatcher.TypeTrackingFlags.SceneObjects | ObjectDispatcher.TypeTrackingFlags.Assets); #endif } public void CleanUp(SurfaceCacheWorld world) { _meshRenderers.CleanUp(_sharedMaterials, world); #if ENABLE_TERRAIN_MODULE _terrains.CleanUp(_sharedMaterials, world); #endif _lights.CleanUp(world); _sharedMaterials.CleanUp(world); } public void Update(ObjectDispatcher objDispatcher, AmbientMode ambientMode, Material skyboxMaterial, Color ambientSkycolor, Color ambientEquatorColor, Color ambientGroundColor, float materialEnvIntensityMultiplier, bool divideEnvIntensityByPI, uint renderingLayerMaskFilter, SurfaceCacheWorld world) { bool filterChanged = renderingLayerMaskFilter != _renderingLayerMaskFilter; _renderingLayerMaskFilter = renderingLayerMaskFilter; UpdateMeshRenderers(objDispatcher, world); #if ENABLE_TERRAIN_MODULE UpdateTerrains(objDispatcher, world); #endif UpdateLights(objDispatcher, world); // A change to the global rendering layer mask filter is not reported as a per-object change by the // ObjectDispatcher, so re-evaluate every known instance to add the newly-included and remove the // newly-excluded ones. Filter changes are rare (authoring), so this rescan is acceptable; the common // unchanged path skips it entirely. if (filterChanged) { _meshRenderers.ReevaluateAll(_sharedMaterials, world, _renderingLayerMaskFilter); #if ENABLE_TERRAIN_MODULE _terrains.ReevaluateAll(_sharedMaterials, world, _renderingLayerMaskFilter); #endif _lights.ReevaluateAll(world, _renderingLayerMaskFilter); } UpdateMaterials(objDispatcher, world); UpdateEnvironment( ambientMode, skyboxMaterial, ambientSkycolor, ambientEquatorColor, ambientGroundColor, materialEnvIntensityMultiplier, divideEnvIntensityByPI, world); } void UpdateMeshRenderers(ObjectDispatcher objDispatcher, SurfaceCacheWorld world) { var transformChanges = objDispatcher.GetTransformChangesAndClear(ObjectDispatcher.TransformTrackingType.GlobalTRS, false); foreach (var component in transformChanges) { var meshRenderer = (MeshRenderer)component; _meshRenderers.Refresh(meshRenderer, _sharedMaterials, world, true, _renderingLayerMaskFilter); } using (var typeChanges = objDispatcher.GetTypeChangesAndClear(Unity.Collections.Allocator.Temp)) { foreach (var component in typeChanges.changed) { var meshRenderer = (MeshRenderer)component; _meshRenderers.Refresh(meshRenderer, _sharedMaterials, world, false, _renderingLayerMaskFilter); } foreach (var entityId in typeChanges.destroyedID) { if (_meshRenderers.Contains(entityId)) _meshRenderers.Remove(entityId, _sharedMaterials, world); } } } #if ENABLE_TERRAIN_MODULE void UpdateTerrains(ObjectDispatcher objDispatcher, SurfaceCacheWorld world) { Debug.Assert(_dirtyTerrainDataThisFrame.Count == 0); using (var terrainDataChanges = objDispatcher.GetTypeChangesAndClear(Unity.Collections.Allocator.Temp)) { foreach (var obj in terrainDataChanges.changed) _dirtyTerrainDataThisFrame.Add(((TerrainData)obj).GetEntityId()); } var transformChanges = objDispatcher.GetTransformChangesAndClear(ObjectDispatcher.TransformTrackingType.GlobalTRS, false); foreach (var component in transformChanges) _terrains.Refresh((Terrain)component, _sharedMaterials, world, true, _renderingLayerMaskFilter); using (var typeChanges = objDispatcher.GetTypeChangesAndClear(Unity.Collections.Allocator.Temp)) { foreach (var component in typeChanges.changed) _terrains.Refresh((Terrain)component, _sharedMaterials, world, false, _renderingLayerMaskFilter); foreach (var entityId in typeChanges.destroyedID) { if (_terrains.Contains(entityId)) _terrains.Remove(entityId, _sharedMaterials, world); } } // TerrainData changes require remove + re-add of the World instance because terrain geometry is baked at AddInstance time and currently cannot be patched in place. if (_dirtyTerrainDataThisFrame.Count > 0) _terrains.RebuildConsumers(_dirtyTerrainDataThisFrame, _sharedMaterials, world); _dirtyTerrainDataThisFrame.Clear(); #if UNITY_EDITOR _terrains.ProcessDeferredRebuilds(_sharedMaterials, world); #endif } #endif void UpdateEnvironment(AmbientMode ambientMode, Material skyboxMaterial, Color ambientSkycolor, Color ambientEquatorColor, Color ambientGroundColor, float materialEnvIntensityMultiplier, bool divideEnvIntensityByPI, SurfaceCacheWorld world) { float envIntensityMultiplier = divideEnvIntensityByPI ? 1.0f / Mathf.PI : 1.0f; switch (ambientMode) { case AmbientMode.Skybox: world.SetEnvironmentMode(CubemapRender.Mode.Material); world.SetEnvironmentMaterial(skyboxMaterial); world.SetEnvironmentIntensityMultiplier(materialEnvIntensityMultiplier * envIntensityMultiplier); break; case AmbientMode.Flat: world.SetEnvironmentMode(CubemapRender.Mode.Color); world.SetEnvironmentColor(ambientSkycolor); world.SetEnvironmentIntensityMultiplier(envIntensityMultiplier); break; case AmbientMode.Trilight: world.SetEnvironmentMode(CubemapRender.Mode.Color); world.SetEnvironmentGradientColors(ambientSkycolor, ambientEquatorColor, ambientGroundColor); world.SetEnvironmentIntensityMultiplier(envIntensityMultiplier); break; default: world.SetEnvironmentMode(CubemapRender.Mode.Color); world.SetEnvironmentColor(Color.black); world.SetEnvironmentIntensityMultiplier(envIntensityMultiplier); break; } } void UpdateLights(ObjectDispatcher objDispatcher, SurfaceCacheWorld world) { var transformChanges = objDispatcher.GetTransformChangesAndClear(ObjectDispatcher.TransformTrackingType.GlobalTRS, false); foreach (var component in transformChanges) _lights.Refresh((Light)component, world, _renderingLayerMaskFilter); using (var typeChanges = objDispatcher.GetTypeChangesAndClear(Unity.Collections.Allocator.Temp)) { foreach (var component in typeChanges.changed) _lights.Refresh((Light)component, world, _renderingLayerMaskFilter); foreach (var entityId in typeChanges.destroyedID) { if (_lights.Contains(entityId)) _lights.Remove(entityId, world); } } } void UpdateMaterials(ObjectDispatcher objDispatcher, SurfaceCacheWorld world) { #if UNITY_EDITOR _sharedMaterials.Update(world); #endif using (var typeChanges = objDispatcher.GetTypeChangesAndClear(Unity.Collections.Allocator.Temp)) { foreach (var obj in typeChanges.changed) { var material = (Material)obj; var matEntityId = material.GetEntityId(); if (_sharedMaterials.IsReferenced(matEntityId)) { _sharedMaterials.Update(matEntityId, material, world); } } // For now we do not explicitly handle material _removal_. This is acceptable for these reasons: // 1) Even without explicit handling, the user experience is decent. If a user removes a material currently // being used by a mesh renderer, they can assign a new material and everything will be in sync again. // 2) Keeping the associated data structures consistent is not easy and requires extra complexity and // and tracking. It is a lot of work and cost for little gain. } } // Ensures the right MeshRenderers are present in the SurfaceCacheWorld at the right time, according to game // object state (enabled/active), a valid mesh, and the rendering layer mask filter. class MeshRendererSet { readonly Dictionary _slots = new(); readonly List _entityIds = new(); readonly List _components = new(); readonly List _inWorld = new(); readonly List _handles = new(); // Materials assigned to the renderer the last time we checked ("null materials" are EntityId.None). readonly List _inputMaterialIds = new(); // Materials currently acquired from the shared set (may include the fallback material). readonly List _acquiredMaterialIds = new(); readonly List _reevaluateScratch = new(); readonly Material _fallbackMaterial; readonly List _scratch = new(); public MeshRendererSet(Material fallbackMaterial) { _fallbackMaterial = fallbackMaterial; } public void CleanUp(SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { for (int i = 0; i < _components.Count; i++) { if (_inWorld[i]) RemoveFromWorld(i, sharedMaterials, world); } } public bool Contains(EntityId rendererId) { return _slots.ContainsKey(rendererId); } public void Refresh(MeshRenderer renderer, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world, bool transformChange, uint renderingLayerMaskFilter) { var id = renderer.GetEntityId(); bool isRegistered = _slots.TryGetValue(id, out int slot); bool isInWorld = isRegistered && _inWorld[slot]; var mesh = renderer.GetComponent()?.sharedMesh; bool shouldBeRegistered = renderer.enabled && renderer.gameObject.activeInHierarchy && mesh != null && mesh.vertexCount != 0; bool matchesWorldFilter = (renderer.renderingLayerMask & renderingLayerMaskFilter) != 0; bool shouldBeInWorld = shouldBeRegistered && matchesWorldFilter; bool shouldAddToWorld = shouldBeInWorld && !isInWorld; bool shouldUpdateInWorld = shouldBeInWorld && isInWorld; bool shouldRemoveFromWorld = isInWorld && !shouldBeInWorld; bool shouldUnregister = isRegistered && !shouldBeRegistered; bool shouldRegister = shouldBeRegistered && !isRegistered; // Note that ordering of the following operations matter. if (shouldRemoveFromWorld) RemoveFromWorld(slot, sharedMaterials, world); if (shouldUnregister) Unregister(slot); else if (shouldRegister) slot = Register(id, renderer); if (shouldAddToWorld) AddToWorld(slot, renderer, mesh, sharedMaterials, world); else if (shouldUpdateInWorld) UpdateInWorld(slot, renderer, mesh, transformChange, sharedMaterials, world); } public void Remove(EntityId rendererId, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { Debug.Assert(_slots.ContainsKey(rendererId)); int slot = _slots[rendererId]; if (_inWorld[slot]) RemoveFromWorld(slot, sharedMaterials, world); Unregister(slot); } public void ReevaluateAll(SharedMaterialSet sharedMaterials, SurfaceCacheWorld world, uint renderingLayerMaskFilter) { // A copy is required because we mutate below. Debug.Assert(_reevaluateScratch.Count == 0); _reevaluateScratch.AddRange(_components); foreach (var component in _reevaluateScratch) { Debug.Assert(component != null); Refresh(component, sharedMaterials, world, false, renderingLayerMaskFilter); } _reevaluateScratch.Clear(); } int Register(EntityId id, MeshRenderer component) { int slot = _components.Count; _slots[id] = slot; _entityIds.Add(id); _components.Add(component); _inWorld.Add(false); _handles.Add(default); _inputMaterialIds.Add(null); _acquiredMaterialIds.Add(null); return slot; } void Unregister(int slot) { Debug.Assert(!_inWorld[slot]); int last = _components.Count - 1; _slots.Remove(_entityIds[slot]); if (slot != last) { _entityIds[slot] = _entityIds[last]; _components[slot] = _components[last]; _inWorld[slot] = _inWorld[last]; _handles[slot] = _handles[last]; _inputMaterialIds[slot] = _inputMaterialIds[last]; _acquiredMaterialIds[slot] = _acquiredMaterialIds[last]; _slots[_entityIds[slot]] = slot; } _entityIds.RemoveAt(last); _components.RemoveAt(last); _inWorld.RemoveAt(last); _handles.RemoveAt(last); _inputMaterialIds.RemoveAt(last); _acquiredMaterialIds.RemoveAt(last); } void AddToWorld(int slot, MeshRenderer renderer, Mesh mesh, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { Debug.Assert(!_inWorld[slot]); Debug.Assert(!renderer.isPartOfStaticBatch, "Static Batching is not supported by Surface Cache GI."); Debug.Assert(mesh != null && mesh.vertexCount != 0); Debug.Assert(_scratch.Count == 0); var inputMats = _scratch; renderer.GetSharedMaterials(inputMats); // We call GetSharedMaterials to avoid heap allocation. Span inputMatIds = stackalloc EntityId[mesh.subMeshCount]; ResolveMaterialIds(inputMats, inputMatIds); Span acquiredMatHandles = stackalloc MaterialHandle[mesh.subMeshCount]; Span acquiredMatIds = stackalloc EntityId[mesh.subMeshCount]; Span masks = stackalloc uint[mesh.subMeshCount]; ResolveMaterialHandlesAndMasks(world, sharedMaterials, renderer, inputMats, _fallbackMaterial, acquiredMatHandles, acquiredMatIds, masks); _handles[slot] = world.AddInstance(mesh, acquiredMatHandles, masks, renderer.transform.localToWorldMatrix); _inputMaterialIds[slot] = inputMatIds.ToArray(); _acquiredMaterialIds[slot] = acquiredMatIds.ToArray(); _inWorld[slot] = true; _scratch.Clear(); } void UpdateInWorld(int slot, MeshRenderer renderer, Mesh mesh, bool transformChange, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { if (transformChange) { world.UpdateInstanceTransform(_handles[slot], renderer.transform.localToWorldMatrix); return; } Debug.Assert(mesh != null && mesh.vertexCount != 0); Debug.Assert(_scratch.Count == 0); var inputMats = _scratch; renderer.GetSharedMaterials(inputMats); Span inputMatIds = stackalloc EntityId[mesh.subMeshCount]; ResolveMaterialIds(inputMats, inputMatIds); bool materialsChanged = !((ReadOnlySpan)inputMatIds).SequenceEqual(_inputMaterialIds[slot]); if (materialsChanged) { Span acquiredMatHandles = stackalloc MaterialHandle[mesh.subMeshCount]; Span acquiredMatIds = stackalloc EntityId[mesh.subMeshCount]; Span masks = stackalloc uint[mesh.subMeshCount]; ResolveMaterialHandlesAndMasks(world, sharedMaterials, renderer, inputMats, _fallbackMaterial, acquiredMatHandles, acquiredMatIds, masks); foreach (var matEntityId in _acquiredMaterialIds[slot]) sharedMaterials.Release(matEntityId, world); _inputMaterialIds[slot] = inputMatIds.ToArray(); _acquiredMaterialIds[slot] = acquiredMatIds.ToArray(); world.UpdateInstanceMaterials(_handles[slot], acquiredMatHandles); world.UpdateInstanceMask(_handles[slot], masks); } _scratch.Clear(); } void RemoveFromWorld(int slot, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { Debug.Assert(_inWorld[slot]); world.RemoveInstance(_handles[slot]); foreach (var matEntityId in _acquiredMaterialIds[slot]) sharedMaterials.Release(matEntityId, world); _handles[slot] = default; _inputMaterialIds[slot] = null; _acquiredMaterialIds[slot] = null; _inWorld[slot] = false; } static void ResolveMaterialHandlesAndMasks(SurfaceCacheWorld world, SharedMaterialSet sharedMaterials, MeshRenderer renderer, List rendererMats, Material fallbackMaterial, Span acquiredMaterialHandles, Span acquiredMaterialEntityIds, Span masks) { for (int i = 0; i < acquiredMaterialHandles.Length; i++) { Material inputMat = i < rendererMats.Count ? rendererMats[i] : null; masks[i] = inputMat == null ? 0u : 1u; bool matIsSet = inputMat != null; bool matIsSetAndHasMetaPass = matIsSet && inputMat.FindPass("Meta") != -1; bool matIsUnsetOrNoMetaPass = !matIsSetAndHasMetaPass; bool matIsSetAndNoMetaPass = matIsSet && matIsUnsetOrNoMetaPass; if (matIsSetAndNoMetaPass) Debug.LogError($"The material '{inputMat.name}' used by Game Object '{renderer.gameObject.name}' does not have a 'Meta' shader pass and cannot be used by Surface Cache Global Illumination. A fallback material will be used for this material instead.", inputMat); // Using fallback material when there is no input material is redundant, but we do it anyway // because this buys us the simplifying assumption that we always have some material handle. var acquiredMat = inputMat; if (matIsUnsetOrNoMetaPass) acquiredMat = fallbackMaterial; var acquiredMatEntityId = acquiredMat.GetEntityId(); acquiredMaterialHandles[i] = sharedMaterials.Acquire(acquiredMatEntityId, acquiredMat, world); acquiredMaterialEntityIds[i] = acquiredMatEntityId; } } static void ResolveMaterialIds(List rendererMaterials, Span materialEntityIds) { for (int i = 0; i < materialEntityIds.Length; i++) { Material material = i < rendererMaterials.Count ? rendererMaterials[i] : null; materialEntityIds[i] = material != null ? material.GetEntityId() : EntityId.None; } } } #if ENABLE_TERRAIN_MODULE // Ensures the right Terrains are present in the SurfaceCacheWorld at the right time, according to game object // state (enabled/active), terrain data validity, and the rendering layer mask filter. class TerrainSet { readonly Dictionary _slots = new(); readonly List _entityIds = new(); readonly List _components = new(); readonly List _inWorld = new(); readonly List _handles = new(); // The material assigned the last time we checked. This may be EntityId.None. readonly List _inputMaterialIds = new(); // The material currently acquired (may be the fallback material). readonly List _acquiredMaterialIds = new(); readonly List _terrainDataIds = new(); readonly List _reevaluateScratch = new(); // Scratch buffer reused by RebuildConsumers to snapshot ids before mutating the world. readonly List _terrainEntityIdScratch = new(); readonly Material _fallbackMaterial; #if UNITY_EDITOR struct DeferredRebuild { public uint TickAtLastChange; } const uint k_TerrainRebuildDelayInTicks = 30; uint _tickCounter; readonly Dictionary _deferredRebuilds = new(); readonly List _deferredRebuildScratch = new(); #endif public TerrainSet(Material fallbackMaterial) { _fallbackMaterial = fallbackMaterial; } public void CleanUp(SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { for (int i = 0; i < _components.Count; i++) { if (_inWorld[i]) RemoveFromWorld(i, sharedMaterials, world); } } public bool Contains(EntityId terrainEntityId) { return _slots.ContainsKey(terrainEntityId); } public void Refresh(Terrain terrain, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world, bool transformChange, uint renderingLayerMaskFilter) { var id = terrain.GetEntityId(); bool isRegistered = _slots.TryGetValue(id, out int slot); bool isInWorld = isRegistered && _inWorld[slot]; bool shouldBeRegistered = terrain.isActiveAndEnabled && terrain.terrainData != null; bool matchesWorldFilter = (terrain.renderingLayerMask & renderingLayerMaskFilter) != 0; bool shouldBeInWorld = shouldBeRegistered && matchesWorldFilter; bool shouldAddToWorld = shouldBeInWorld && !isInWorld; bool shouldUpdateInWorld = shouldBeInWorld && isInWorld; bool shouldRemoveFromWorld = isInWorld && !shouldBeInWorld; bool shouldUnregister = isRegistered && !shouldBeRegistered; bool shouldRegister = shouldBeRegistered && !isRegistered; // Note that ordering of the following operations matter. if (shouldRemoveFromWorld) RemoveFromWorld(slot, sharedMaterials, world); if (shouldUnregister) { #if UNITY_EDITOR _deferredRebuilds.Remove(id); #endif Unregister(slot); } else if (shouldRegister) { slot = Register(id, terrain); } if (shouldAddToWorld) AddToWorld(slot, terrain, sharedMaterials, world); else if (shouldUpdateInWorld) UpdateInWorld(slot, terrain, transformChange, sharedMaterials, world); } public void Remove(EntityId terrainEntityId, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { Debug.Assert(_slots.ContainsKey(terrainEntityId)); int slot = _slots[terrainEntityId]; if (_inWorld[slot]) RemoveFromWorld(slot, sharedMaterials, world); Unregister(slot); #if UNITY_EDITOR _deferredRebuilds.Remove(terrainEntityId); #endif } public void ReevaluateAll(SharedMaterialSet sharedMaterials, SurfaceCacheWorld world, uint renderingLayerMaskFilter) { // A copy is required because we mutate below. Debug.Assert(_reevaluateScratch.Count == 0); _reevaluateScratch.AddRange(_components); foreach (var component in _reevaluateScratch) { Debug.Assert(component != null); Refresh(component, sharedMaterials, world, false, renderingLayerMaskFilter); } _reevaluateScratch.Clear(); } // For every in-world Terrain whose TerrainData appears in the dirty set, schedule (Editor) or perform (Player) a geometry rebuild. public void RebuildConsumers(HashSet dirtyTerrainDataIds, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { _terrainEntityIdScratch.Clear(); for (int i = 0; i < _components.Count; i++) { if (_inWorld[i] && dirtyTerrainDataIds.Contains(_terrainDataIds[i])) _terrainEntityIdScratch.Add(_entityIds[i]); } foreach (var terrainEntityId in _terrainEntityIdScratch) { #if UNITY_EDITOR _deferredRebuilds[terrainEntityId] = new DeferredRebuild { TickAtLastChange = _tickCounter, }; #else RebuildInstance(_slots[terrainEntityId], sharedMaterials, world); #endif } } #if UNITY_EDITOR public void ProcessDeferredRebuilds(SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { _tickCounter++; if (_deferredRebuilds.Count == 0) return; _deferredRebuildScratch.Clear(); foreach (var deferredRebuild in _deferredRebuilds) { if (_tickCounter - deferredRebuild.Value.TickAtLastChange >= k_TerrainRebuildDelayInTicks) _deferredRebuildScratch.Add(deferredRebuild.Key); } foreach (var entityId in _deferredRebuildScratch) { _deferredRebuilds.Remove(entityId); if (_slots.TryGetValue(entityId, out int slot) && _inWorld[slot]) RebuildInstance(slot, sharedMaterials, world); } } #endif int Register(EntityId id, Terrain component) { int slot = _components.Count; _slots[id] = slot; _entityIds.Add(id); _components.Add(component); _inWorld.Add(false); _handles.Add(default); _inputMaterialIds.Add(EntityId.None); _acquiredMaterialIds.Add(EntityId.None); _terrainDataIds.Add(EntityId.None); return slot; } void Unregister(int slot) { Debug.Assert(!_inWorld[slot]); int last = _components.Count - 1; _slots.Remove(_entityIds[slot]); if (slot != last) { _entityIds[slot] = _entityIds[last]; _components[slot] = _components[last]; _inWorld[slot] = _inWorld[last]; _handles[slot] = _handles[last]; _inputMaterialIds[slot] = _inputMaterialIds[last]; _acquiredMaterialIds[slot] = _acquiredMaterialIds[last]; _terrainDataIds[slot] = _terrainDataIds[last]; _slots[_entityIds[slot]] = slot; } _entityIds.RemoveAt(last); _components.RemoveAt(last); _inWorld.RemoveAt(last); _handles.RemoveAt(last); _inputMaterialIds.RemoveAt(last); _acquiredMaterialIds.RemoveAt(last); _terrainDataIds.RemoveAt(last); } void AddToWorld(int slot, Terrain terrain, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { Debug.Assert(!_inWorld[slot]); var terrainData = terrain.terrainData; Debug.Assert(terrainData != null); var inputMaterial = terrain.splatBaseMaterial; EntityId inputMatEntityId = inputMaterial != null ? inputMaterial.GetEntityId() : EntityId.None; ResolveMaterialHandleAndMask(terrain, inputMaterial, inputMatEntityId, sharedMaterials, world, _fallbackMaterial, out EntityId acquiredMatEntityId, out MaterialHandle matHandle, out uint mask); _handles[slot] = world.AddInstance(terrain, matHandle, mask, terrain.transform.localToWorldMatrix); _inputMaterialIds[slot] = inputMatEntityId; _acquiredMaterialIds[slot] = acquiredMatEntityId; _terrainDataIds[slot] = terrainData.GetEntityId(); _inWorld[slot] = true; } void UpdateInWorld(int slot, Terrain terrain, bool transformChange, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { if (transformChange) { world.UpdateInstanceTransform(_handles[slot], terrain.transform.localToWorldMatrix); return; } var newTerrainDataEntityId = terrain.terrainData.GetEntityId(); if (newTerrainDataEntityId != _terrainDataIds[slot]) { RebuildInstance(slot, sharedMaterials, world); return; } var inputMat = terrain.splatBaseMaterial; var inputMatEntityId = inputMat != null ? inputMat.GetEntityId() : EntityId.None; if (inputMatEntityId != _inputMaterialIds[slot]) { ResolveMaterialHandleAndMask(terrain, inputMat, inputMatEntityId, sharedMaterials, world, _fallbackMaterial, out EntityId acquiredMatEntityId, out MaterialHandle acquiredMatHandle, out uint mask); Span matHandles = stackalloc MaterialHandle[1] { acquiredMatHandle }; Span masks = stackalloc uint[1] { mask }; world.UpdateInstanceMaterials(_handles[slot], matHandles); world.UpdateInstanceMask(_handles[slot], masks); sharedMaterials.Release(_acquiredMaterialIds[slot], world); _inputMaterialIds[slot] = inputMatEntityId; _acquiredMaterialIds[slot] = acquiredMatEntityId; } } void RemoveFromWorld(int slot, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { Debug.Assert(_inWorld[slot]); world.RemoveInstance(_handles[slot]); sharedMaterials.Release(_acquiredMaterialIds[slot], world); _handles[slot] = default; _inputMaterialIds[slot] = EntityId.None; _acquiredMaterialIds[slot] = EntityId.None; _terrainDataIds[slot] = EntityId.None; _inWorld[slot] = false; } // Terrain geometry is baked at AddInstance time and cannot be patched in place, so a TerrainData change is // applied as a remove + re-add of the world instance. The instance stays registered and in the world throughout. void RebuildInstance(int slot, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world) { Debug.Assert(_inWorld[slot]); Debug.Assert(_components[slot] != null && _components[slot].terrainData != null); RemoveFromWorld(slot, sharedMaterials, world); AddToWorld(slot, _components[slot], sharedMaterials, world); } static void ResolveMaterialHandleAndMask(Terrain terrain, Material inputMat, EntityId inputMatEntityId, SharedMaterialSet sharedMaterials, SurfaceCacheWorld world, Material fallbackMaterial, out EntityId acquiredMatEntityId, out MaterialHandle acquiredMatHandle, out uint mask) { mask = inputMat == null ? 0u : 1u; bool matIsSet = inputMat != null; bool matIsSetAndHasMetaPass = matIsSet && inputMat.FindPass("Meta") != -1; bool matIsUnsetOrNoMetaPass = !matIsSetAndHasMetaPass; bool matIsSetAndNoMetaPass = matIsSet && matIsUnsetOrNoMetaPass; if (matIsSetAndNoMetaPass) Debug.LogError($"The material '{inputMat.name}' used by terrain '{terrain.gameObject.name}' does not have a 'Meta' shader pass and cannot be used by Surface Cache Global Illumination. A fallback material will be used for this material instead.", inputMat); // Using fallback material when there is no input material is redundant, but we do it anyway // because this buys us the simplifying assumption that we always have some material handle. Material acquiredMat; if (matIsUnsetOrNoMetaPass) { acquiredMatEntityId = fallbackMaterial.GetEntityId(); acquiredMat = fallbackMaterial; } else { acquiredMatEntityId = inputMatEntityId; acquiredMat = inputMat; } acquiredMatHandle = sharedMaterials.Acquire(acquiredMatEntityId, acquiredMat, world); } } #endif // Ensures the right Lights are present in the SurfaceCacheWorld at the right time, according to game object // state (enabled/active), bake state, and the rendering layer mask filter. class LightSet { readonly Dictionary _slots = new(); readonly List _entityIds = new(); readonly List _components = new(); readonly List _inWorld = new(); readonly List _handles = new(); readonly List _reevaluateScratch = new(); public void CleanUp(SurfaceCacheWorld world) { for (int i = 0; i < _components.Count; i++) { if (_inWorld[i]) RemoveFromWorld(i, world); } } public bool Contains(EntityId lightId) { return _slots.ContainsKey(lightId); } public void Refresh(Light light, SurfaceCacheWorld world, uint renderingLayerMaskFilter) { var id = light.GetEntityId(); bool isRegistered = _slots.TryGetValue(id, out int slot); bool isInWorld = isRegistered && _inWorld[slot]; bool shouldBeRegistered = light.isActiveAndEnabled && !light.bakingOutput.isBaked; bool matchesWorldFilter = ((uint)light.renderingLayerMask & renderingLayerMaskFilter) != 0; bool shouldBeInWorld = shouldBeRegistered && matchesWorldFilter; bool shouldAddToWorld = shouldBeInWorld && !isInWorld; bool shouldUpdateInWorld = shouldBeInWorld && isInWorld; bool shouldRemoveFromWorld = isInWorld && !shouldBeInWorld; bool shouldUnregister = isRegistered && !shouldBeRegistered; bool shouldRegister = shouldBeRegistered && !isRegistered; // Note that ordering of the following operations matter. if (shouldRemoveFromWorld) RemoveFromWorld(slot, world); if (shouldUnregister) Unregister(slot); else if (shouldRegister) slot = Register(id, light); if (shouldAddToWorld) AddToWorld(slot, light, world); else if (shouldUpdateInWorld) UpdateInWorld(slot, light, world); } public void Remove(EntityId lightId, SurfaceCacheWorld world) { Debug.Assert(_slots.ContainsKey(lightId)); int slot = _slots[lightId]; if (_inWorld[slot]) RemoveFromWorld(slot, world); Unregister(slot); } public void ReevaluateAll(SurfaceCacheWorld world, uint renderingLayerMaskFilter) { // A copy is required because we mutate below. Debug.Assert(_reevaluateScratch.Count == 0); _reevaluateScratch.AddRange(_components); foreach (var component in _reevaluateScratch) { Debug.Assert(component != null); Refresh(component, world, renderingLayerMaskFilter); } _reevaluateScratch.Clear(); } int Register(EntityId id, Light component) { int slot = _components.Count; _slots[id] = slot; _entityIds.Add(id); _components.Add(component); _inWorld.Add(false); _handles.Add(default); return slot; } void Unregister(int slot) { Debug.Assert(!_inWorld[slot]); int last = _components.Count - 1; _slots.Remove(_entityIds[slot]); if (slot != last) { _entityIds[slot] = _entityIds[last]; _components[slot] = _components[last]; _inWorld[slot] = _inWorld[last]; _handles[slot] = _handles[last]; _slots[_entityIds[slot]] = slot; } _entityIds.RemoveAt(last); _components.RemoveAt(last); _inWorld.RemoveAt(last); _handles.RemoveAt(last); } void AddToWorld(int slot, Light light, SurfaceCacheWorld world) { Debug.Assert(!_inWorld[slot]); _handles[slot] = world.AddLight(CreateLightDescriptor(light)); _inWorld[slot] = true; } void UpdateInWorld(int slot, Light light, SurfaceCacheWorld world) { world.UpdateLight(_handles[slot], CreateLightDescriptor(light)); } void RemoveFromWorld(int slot, SurfaceCacheWorld world) { Debug.Assert(_inWorld[slot]); world.RemoveLight(_handles[slot]); _handles[slot] = default; _inWorld[slot] = false; } static SurfaceCacheWorld.LightDescriptor CreateLightDescriptor(Light light) { const bool multiplyPunctualLightIntensityByPI = false; var desc = new SurfaceCacheWorld.LightDescriptor(); desc.Type = light.type; desc.LinearLightColor = Util.GetLinearLightColor(light, light.bounceIntensity); if (multiplyPunctualLightIntensityByPI && Util.IsPunctualLightType(light.type)) desc.LinearLightColor *= Mathf.PI; desc.Transform = light.transform.localToWorldMatrix; desc.ColorTemperature = light.colorTemperature; desc.OuterSpotAngle = light.spotAngle; desc.InnerSpotAngle = light.innerSpotAngle; desc.Range = light.range; return desc; } } class SharedMaterialSet { struct Entry { public Material Material; public MaterialHandle WorldHandle; public uint RefCount; public MaterialPool.MaterialDescriptor Descriptor; } const EmissionMode kEmissionMode = EmissionMode.Realtime; const UVChannel kUVChannel = UVChannel.UV0; readonly Dictionary _entries = new(); readonly HashSet _pendingMetaPassEvals = new(); readonly Material _fallbackMaterial; public SharedMaterialSet(Material fallbackMaterial) { _fallbackMaterial = fallbackMaterial; } public MaterialHandle Acquire(EntityId matEntityId, Material mat, SurfaceCacheWorld world) { if (_entries.TryGetValue(matEntityId, out var entry)) { entry.RefCount += 1; _entries[matEntityId] = entry; return entry.WorldHandle; } else { var metaPassIndex = mat.FindPass("Meta"); Debug.Assert(metaPassIndex != -1, "The material has no metapass."); MaterialPool.MaterialDescriptor descriptor; #if UNITY_EDITOR if (UnityEditor.ShaderUtil.IsPassCompiled(mat, metaPassIndex)) { descriptor = MaterialPool.ConvertUnityMaterialToMaterialDescriptor(mat, kEmissionMode); } else { var oldAllowAsyncCompilation = UnityEditor.ShaderUtil.allowAsyncCompilation; UnityEditor.ShaderUtil.allowAsyncCompilation = false; descriptor = MaterialPool.ConvertUnityMaterialToMaterialDescriptor(_fallbackMaterial, kEmissionMode); UnityEditor.ShaderUtil.allowAsyncCompilation = oldAllowAsyncCompilation; _pendingMetaPassEvals.Add(matEntityId); UnityEditor.ShaderUtil.CompilePass(mat, metaPassIndex); } #else descriptor = MaterialPool.ConvertUnityMaterialToMaterialDescriptor(mat, EmissionMode.Realtime); #endif var newHandle = world.AddMaterial(descriptor, kUVChannel); var newEntry = new Entry { RefCount = 1, WorldHandle = newHandle, Descriptor = descriptor, Material = mat }; _entries[matEntityId] = newEntry; return newHandle; } } #if UNITY_EDITOR public void Update(SurfaceCacheWorld world) { if (_pendingMetaPassEvals.Count != 0) { var evaluatedMaterials = new List(); foreach (var matEntityId in _pendingMetaPassEvals) { var entry = _entries[matEntityId]; var metaPassIndex = entry.Material.FindPass("Meta"); Debug.Assert(metaPassIndex != -1); if (UnityEditor.ShaderUtil.IsPassCompiled(entry.Material, metaPassIndex)) { DestroyDescriptorTextures(entry.Descriptor); entry.Descriptor = MaterialPool.ConvertUnityMaterialToMaterialDescriptor(entry.Material, kEmissionMode); world.UpdateMaterial(entry.WorldHandle, entry.Descriptor, kUVChannel); _entries[matEntityId] = entry; evaluatedMaterials.Add(matEntityId); } } foreach (var matEntityId in evaluatedMaterials) { _pendingMetaPassEvals.Remove(matEntityId); } } } #endif public void Update(EntityId matEntityId, Material material, SurfaceCacheWorld world) { Debug.Assert(_entries.ContainsKey(matEntityId)); if (!_pendingMetaPassEvals.Contains(matEntityId)) { var entry = _entries[matEntityId]; DestroyDescriptorTextures(entry.Descriptor); entry.Descriptor = MaterialPool.ConvertUnityMaterialToMaterialDescriptor(material, kEmissionMode); _entries[matEntityId] = entry; world.UpdateMaterial(entry.WorldHandle, in entry.Descriptor, kUVChannel); } } public bool IsReferenced(EntityId matEntityId) { return _entries.ContainsKey(matEntityId); } public void Release(EntityId matEntityId, SurfaceCacheWorld world) { Debug.Assert(_entries.ContainsKey(matEntityId)); var entry = _entries[matEntityId]; if (entry.RefCount == 1) { RemoveHandle(matEntityId, world); } else { entry.RefCount -= 1; _entries[matEntityId] = entry; } } public void CleanUp(SurfaceCacheWorld world) { var ids = new EntityId[_entries.Count]; int i = 0; foreach (var key in _entries.Keys) ids[i++] = key; foreach (var id in ids) RemoveHandle(id, world); } void RemoveHandle(EntityId matEntityId, SurfaceCacheWorld world) { _pendingMetaPassEvals.Remove(matEntityId); var entry = _entries[matEntityId]; world.RemoveMaterial(entry.WorldHandle); _entries.Remove(matEntityId); DestroyDescriptorTextures(entry.Descriptor); } static void DestroyDescriptorTextures(in MaterialPool.MaterialDescriptor desc) { CoreUtils.Destroy(desc.Albedo); CoreUtils.Destroy(desc.Emission); CoreUtils.Destroy(desc.Transmission); } } } } #endif