diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 38a93db..6e802c3 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -181,7 +181,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var root = new GameObject("Generic Eve World Projection"); var cameraObject = new GameObject("Generic Eve Capture Camera"); var mapCameraObject = new GameObject("Generic Eve Map Camera"); - var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; EveUnityCultMeshPlayableWorldProvider provider = null; @@ -415,10 +414,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) Assert.That(combatRenderer.HitMarkerVisible, Is.True); - var light = lightObject.AddComponent(); - light.type = LightType.Directional; - light.intensity = 1.4f; - lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); @@ -478,6 +473,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); + MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); + var playerRendererFacts = playerRenderers + .Select(renderer => RendererFact.From(camera, renderer)) + .ToArray(); + var texturedPlayerMaterialCount = playerRenderers + .SelectMany(renderer => renderer.sharedMaterials) + .Where(material => material != null) + .Distinct() + .Count(HasAnyTexture); + var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() + .Where(light => light != null && light.enabled && light.type == LightType.Directional) + .Sum(light => light.intensity); + Assert.That(directionalLightIntensity, + Is.EqualTo(runtime.ActiveWorld.KeyLightIntensity).Within(0.001f), + "The live witness contains directional lighting not owned by the advertised provider surface."); var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; @@ -489,10 +499,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); + MeasureLuminance(pixels, out var pilotAverageLuminance, out var pilotBrightPixelCount); Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + MeasureRendererOutputs( + camera, + target, + pixels, + root.GetComponentsInChildren(includeInactive: true), + playerRenderers, + playerRendererFacts); + var playerMapRendererFacts = playerRendererFacts + .Where(fact => fact.layer == mapLayer) + .ToArray(); + Assert.That(playerMapRendererFacts, Is.Not.Empty, + "The provider-authored player prefab has no renderer on its advertised map channel."); + Assert.That(playerMapRendererFacts.Sum(fact => fact.renderedPixelCount), Is.Zero, + "A provider-prefab map-channel renderer leaked into the pilot camera."); mapRenderers = mapRenderers.Where(renderer => renderer != null).ToList(); Assert.That(mapRenderers, Is.Not.Empty, @@ -539,7 +564,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, (float)observedShot.LockQuality, - aimDotDistance); + aimDotDistance, + playerViewportWidth, + playerViewportHeight, + texturedPlayerMaterialCount, + directionalLightIntensity, + pilotAverageLuminance, + pilotBrightPixelCount, + playerRendererFacts); } finally { @@ -551,7 +583,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); host?.Disconnect(); - UnityEngine.Object.DestroyImmediate(lightObject); UnityEngine.Object.DestroyImmediate(mapCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); @@ -579,7 +610,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float hullRatioBefore, float hullRatioAfter, float lockProgress, - float aimDotDistance) + float aimDotDistance, + float playerViewportWidth, + float playerViewportHeight, + int texturedPlayerMaterialCount, + float directionalLightIntensity, + float pilotAverageLuminance, + int pilotBrightPixelCount, + RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -608,7 +646,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementDistance = movementDistance, aimDotDistance = aimDotDistance, playerRendererCount = playerRendererCount, + playerViewportWidth = playerViewportWidth, + playerViewportHeight = playerViewportHeight, + texturedPlayerMaterialCount = texturedPlayerMaterialCount, + directionalLightIntensity = directionalLightIntensity, pilotChangedPixels = pilotChangedPixels, + pilotAverageLuminance = pilotAverageLuminance, + pilotBrightPixelCount = pilotBrightPixelCount, + playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -645,7 +690,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float movementDistance; public float aimDotDistance; public int playerRendererCount; + public float playerViewportWidth; + public float playerViewportHeight; + public int texturedPlayerMaterialCount; + public float directionalLightIntensity; public int pilotChangedPixels; + public float pilotAverageLuminance; + public int pilotBrightPixelCount; + public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -685,6 +737,68 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + [Serializable] + private sealed class RendererFact + { + public string name; + public string rendererType; + public bool enabled; + public bool activeInHierarchy; + public int layer; + public string boundsSize; + public float viewportWidth; + public float viewportHeight; + public int renderedPixelCount; + public float renderedAverageLuminance; + public int renderedBrightPixelCount; + public MaterialFact[] materials; + + public static RendererFact From(Camera camera, Renderer renderer) + { + MeasureViewportCoverage(camera, new[] { renderer }, out var width, out var height); + return new RendererFact + { + name = renderer.name, + rendererType = renderer.GetType().Name, + enabled = renderer.enabled, + activeInHierarchy = renderer.gameObject.activeInHierarchy, + layer = renderer.gameObject.layer, + boundsSize = FormatVector(renderer.bounds.size), + viewportWidth = width, + viewportHeight = height, + materials = renderer.sharedMaterials + .Where(material => material != null) + .Select(MaterialFact.From) + .ToArray() + }; + } + } + + [Serializable] + private sealed class MaterialFact + { + public string name; + public string shader; + public bool shaderSupported; + public string baseColor; + public float surface; + public int textureCount; + public int renderQueue; + + public static MaterialFact From(Material material) => new MaterialFact + { + name = material.name, + shader = material.shader?.name ?? "", + shaderSupported = material.shader != null && material.shader.isSupported, + baseColor = material.HasProperty("_BaseColor") + ? FormatColor(material.GetColor("_BaseColor")) + : "", + surface = material.HasProperty("_Surface") ? material.GetFloat("_Surface") : -1f, + textureCount = material.GetTexturePropertyNames().Count(name => material.GetTexture(name) != null), + renderQueue = material.renderQueue + }; + } + private static void AssertReconciledProviderReceipt( EveUnitySceneCommandReceipt receipt, string commandId, @@ -714,6 +828,108 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } + private static bool HasAnyTexture(Material material) + { + foreach (var propertyName in material.GetTexturePropertyNames()) + { + if (material.GetTexture(propertyName) != null) + return true; + } + return false; + } + + private static string FormatVector(Vector3 value) => + $"{value.x:R},{value.y:R},{value.z:R}"; + + private static string FormatColor(Color value) => + $"{value.r:R},{value.g:R},{value.b:R},{value.a:R}"; + + private static void MeasureViewportCoverage( + Camera camera, + IReadOnlyList renderers, + out float width, + out float height) + { + var min = new Vector2(float.PositiveInfinity, float.PositiveInfinity); + var max = new Vector2(float.NegativeInfinity, float.NegativeInfinity); + foreach (var renderer in renderers) + { + var bounds = renderer.bounds; + for (var x = -1; x <= 1; x += 2) + for (var y = -1; y <= 1; y += 2) + for (var z = -1; z <= 1; z += 2) + { + var point = bounds.center + Vector3.Scale(bounds.extents, new Vector3(x, y, z)); + var viewport = camera.WorldToViewportPoint(point); + if (viewport.z <= 0f) continue; + min = Vector2.Min(min, viewport); + max = Vector2.Max(max, viewport); + } + } + width = float.IsInfinity(min.x) ? 0f : Mathf.Max(0f, max.x - min.x); + height = float.IsInfinity(min.y) ? 0f : Mathf.Max(0f, max.y - min.y); + } + + private static void MeasureLuminance( + Texture2D texture, + out float average, + out int brightPixelCount) + { + double total = 0; + brightPixelCount = 0; + var pixels = texture.GetPixels32(); + foreach (var pixel in pixels) + { + var luminance = (0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b) / 255.0; + total += luminance; + if (luminance >= 0.2) + brightPixelCount++; + } + average = pixels.Length == 0 ? 0f : (float)(total / pixels.Length); + } + + private static void MeasureRendererOutputs( + Camera camera, + RenderTexture target, + Texture2D pixels, + IReadOnlyList allRenderers, + IReadOnlyList measuredRenderers, + IReadOnlyList facts) + { + var enabled = allRenderers.Select(renderer => renderer.enabled).ToArray(); + var clearFlags = camera.clearFlags; + var background = camera.backgroundColor; + try + { + foreach (var renderer in allRenderers) + renderer.enabled = false; + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = Color.black; + for (var index = 0; index < measuredRenderers.Count; index++) + { + var fact = facts[index]; + measuredRenderers[index].enabled = true; + camera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); + pixels.Apply(); + fact.renderedPixelCount = CountChangedPixels(pixels, Color.black); + MeasureLuminance( + pixels, + out fact.renderedAverageLuminance, + out fact.renderedBrightPixelCount); + measuredRenderers[index].enabled = false; + } + } + finally + { + for (var index = 0; index < allRenderers.Count; index++) + allRenderers[index].enabled = enabled[index]; + camera.clearFlags = clearFlags; + camera.backgroundColor = background; + } + } + private static int CountChangedPixels(Texture2D texture, Color background) { var changed = 0; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 036f757..3fba6bd 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.26", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.27", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 984f6f4..65fec3a 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.26", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.27", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5" + "hash": "63a41712c5a68ce835bc1820980fc4786c34eb7b" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 38a93db..6e802c3 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -181,7 +181,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var root = new GameObject("Generic Eve World Projection"); var cameraObject = new GameObject("Generic Eve Capture Camera"); var mapCameraObject = new GameObject("Generic Eve Map Camera"); - var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; EveUnityCultMeshPlayableWorldProvider provider = null; @@ -415,10 +414,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) Assert.That(combatRenderer.HitMarkerVisible, Is.True); - var light = lightObject.AddComponent(); - light.type = LightType.Directional; - light.intensity = 1.4f; - lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); @@ -478,6 +473,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); + MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); + var playerRendererFacts = playerRenderers + .Select(renderer => RendererFact.From(camera, renderer)) + .ToArray(); + var texturedPlayerMaterialCount = playerRenderers + .SelectMany(renderer => renderer.sharedMaterials) + .Where(material => material != null) + .Distinct() + .Count(HasAnyTexture); + var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() + .Where(light => light != null && light.enabled && light.type == LightType.Directional) + .Sum(light => light.intensity); + Assert.That(directionalLightIntensity, + Is.EqualTo(runtime.ActiveWorld.KeyLightIntensity).Within(0.001f), + "The live witness contains directional lighting not owned by the advertised provider surface."); var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; @@ -489,10 +499,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); + MeasureLuminance(pixels, out var pilotAverageLuminance, out var pilotBrightPixelCount); Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + MeasureRendererOutputs( + camera, + target, + pixels, + root.GetComponentsInChildren(includeInactive: true), + playerRenderers, + playerRendererFacts); + var playerMapRendererFacts = playerRendererFacts + .Where(fact => fact.layer == mapLayer) + .ToArray(); + Assert.That(playerMapRendererFacts, Is.Not.Empty, + "The provider-authored player prefab has no renderer on its advertised map channel."); + Assert.That(playerMapRendererFacts.Sum(fact => fact.renderedPixelCount), Is.Zero, + "A provider-prefab map-channel renderer leaked into the pilot camera."); mapRenderers = mapRenderers.Where(renderer => renderer != null).ToList(); Assert.That(mapRenderers, Is.Not.Empty, @@ -539,7 +564,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, (float)observedShot.LockQuality, - aimDotDistance); + aimDotDistance, + playerViewportWidth, + playerViewportHeight, + texturedPlayerMaterialCount, + directionalLightIntensity, + pilotAverageLuminance, + pilotBrightPixelCount, + playerRendererFacts); } finally { @@ -551,7 +583,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); host?.Disconnect(); - UnityEngine.Object.DestroyImmediate(lightObject); UnityEngine.Object.DestroyImmediate(mapCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); @@ -579,7 +610,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float hullRatioBefore, float hullRatioAfter, float lockProgress, - float aimDotDistance) + float aimDotDistance, + float playerViewportWidth, + float playerViewportHeight, + int texturedPlayerMaterialCount, + float directionalLightIntensity, + float pilotAverageLuminance, + int pilotBrightPixelCount, + RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -608,7 +646,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementDistance = movementDistance, aimDotDistance = aimDotDistance, playerRendererCount = playerRendererCount, + playerViewportWidth = playerViewportWidth, + playerViewportHeight = playerViewportHeight, + texturedPlayerMaterialCount = texturedPlayerMaterialCount, + directionalLightIntensity = directionalLightIntensity, pilotChangedPixels = pilotChangedPixels, + pilotAverageLuminance = pilotAverageLuminance, + pilotBrightPixelCount = pilotBrightPixelCount, + playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -645,7 +690,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float movementDistance; public float aimDotDistance; public int playerRendererCount; + public float playerViewportWidth; + public float playerViewportHeight; + public int texturedPlayerMaterialCount; + public float directionalLightIntensity; public int pilotChangedPixels; + public float pilotAverageLuminance; + public int pilotBrightPixelCount; + public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -685,6 +737,68 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + [Serializable] + private sealed class RendererFact + { + public string name; + public string rendererType; + public bool enabled; + public bool activeInHierarchy; + public int layer; + public string boundsSize; + public float viewportWidth; + public float viewportHeight; + public int renderedPixelCount; + public float renderedAverageLuminance; + public int renderedBrightPixelCount; + public MaterialFact[] materials; + + public static RendererFact From(Camera camera, Renderer renderer) + { + MeasureViewportCoverage(camera, new[] { renderer }, out var width, out var height); + return new RendererFact + { + name = renderer.name, + rendererType = renderer.GetType().Name, + enabled = renderer.enabled, + activeInHierarchy = renderer.gameObject.activeInHierarchy, + layer = renderer.gameObject.layer, + boundsSize = FormatVector(renderer.bounds.size), + viewportWidth = width, + viewportHeight = height, + materials = renderer.sharedMaterials + .Where(material => material != null) + .Select(MaterialFact.From) + .ToArray() + }; + } + } + + [Serializable] + private sealed class MaterialFact + { + public string name; + public string shader; + public bool shaderSupported; + public string baseColor; + public float surface; + public int textureCount; + public int renderQueue; + + public static MaterialFact From(Material material) => new MaterialFact + { + name = material.name, + shader = material.shader?.name ?? "", + shaderSupported = material.shader != null && material.shader.isSupported, + baseColor = material.HasProperty("_BaseColor") + ? FormatColor(material.GetColor("_BaseColor")) + : "", + surface = material.HasProperty("_Surface") ? material.GetFloat("_Surface") : -1f, + textureCount = material.GetTexturePropertyNames().Count(name => material.GetTexture(name) != null), + renderQueue = material.renderQueue + }; + } + private static void AssertReconciledProviderReceipt( EveUnitySceneCommandReceipt receipt, string commandId, @@ -714,6 +828,108 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } + private static bool HasAnyTexture(Material material) + { + foreach (var propertyName in material.GetTexturePropertyNames()) + { + if (material.GetTexture(propertyName) != null) + return true; + } + return false; + } + + private static string FormatVector(Vector3 value) => + $"{value.x:R},{value.y:R},{value.z:R}"; + + private static string FormatColor(Color value) => + $"{value.r:R},{value.g:R},{value.b:R},{value.a:R}"; + + private static void MeasureViewportCoverage( + Camera camera, + IReadOnlyList renderers, + out float width, + out float height) + { + var min = new Vector2(float.PositiveInfinity, float.PositiveInfinity); + var max = new Vector2(float.NegativeInfinity, float.NegativeInfinity); + foreach (var renderer in renderers) + { + var bounds = renderer.bounds; + for (var x = -1; x <= 1; x += 2) + for (var y = -1; y <= 1; y += 2) + for (var z = -1; z <= 1; z += 2) + { + var point = bounds.center + Vector3.Scale(bounds.extents, new Vector3(x, y, z)); + var viewport = camera.WorldToViewportPoint(point); + if (viewport.z <= 0f) continue; + min = Vector2.Min(min, viewport); + max = Vector2.Max(max, viewport); + } + } + width = float.IsInfinity(min.x) ? 0f : Mathf.Max(0f, max.x - min.x); + height = float.IsInfinity(min.y) ? 0f : Mathf.Max(0f, max.y - min.y); + } + + private static void MeasureLuminance( + Texture2D texture, + out float average, + out int brightPixelCount) + { + double total = 0; + brightPixelCount = 0; + var pixels = texture.GetPixels32(); + foreach (var pixel in pixels) + { + var luminance = (0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b) / 255.0; + total += luminance; + if (luminance >= 0.2) + brightPixelCount++; + } + average = pixels.Length == 0 ? 0f : (float)(total / pixels.Length); + } + + private static void MeasureRendererOutputs( + Camera camera, + RenderTexture target, + Texture2D pixels, + IReadOnlyList allRenderers, + IReadOnlyList measuredRenderers, + IReadOnlyList facts) + { + var enabled = allRenderers.Select(renderer => renderer.enabled).ToArray(); + var clearFlags = camera.clearFlags; + var background = camera.backgroundColor; + try + { + foreach (var renderer in allRenderers) + renderer.enabled = false; + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = Color.black; + for (var index = 0; index < measuredRenderers.Count; index++) + { + var fact = facts[index]; + measuredRenderers[index].enabled = true; + camera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); + pixels.Apply(); + fact.renderedPixelCount = CountChangedPixels(pixels, Color.black); + MeasureLuminance( + pixels, + out fact.renderedAverageLuminance, + out fact.renderedBrightPixelCount); + measuredRenderers[index].enabled = false; + } + } + finally + { + for (var index = 0; index < allRenderers.Count; index++) + allRenderers[index].enabled = enabled[index]; + camera.clearFlags = clearFlags; + camera.backgroundColor = background; + } + } + private static int CountChangedPixels(Texture2D texture, Color background) { var changed = 0; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 99ad2a7..79fdb49 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.26`, commit - `7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5`; +- `org.gamecult.eve.unity-scene` `0.3.27`, commit + `63a41712c5a68ce835bc1820980fc4786c34eb7b`; - `org.gamecult.cultlib` `1.0.13`, commit `feb5c71513e71d681699f462fe3682b3168c6f73`; - the generic `ReleaseConsumerProject` client connected directly to the @@ -18,19 +18,27 @@ and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the active camera rig's leased Unity environment. +The witness supplies no client-authored light. Aetheria advertises the key-light +direction, color, and intensity; the generic camera rig lowers that contract to +the only live directional light (`0.75` intensity). Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `230,332`; +- pilot changed pixels: `230,359`; - map-channel renderers: `11`; -- map changed pixels: `5,496`; +- map changed pixels: `5,153`; - the pilot camera excludes the advertised map layer; +- the player prefab's embedded layer-14 map icon contributes exactly `0` pilot + pixels; - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. The pilot presentation remains visually dark; -the native art and lighting pass is not complete. +present in the map-only frame. The pilot presentation remains visually sparse +and dark. Renderer-isolation facts prove that the hull draws under the +provider-owned light, so the next native-art work is world composition and +available pre-generated provider textures, not a client-side lighting fallback. +Substance is not part of this path; later texture baking belongs in Blender. Primary artifacts: diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 2b8b81f..fac33e1 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5" +$expectedEveUnityCommit = "63a41712c5a68ce835bc1820980fc4786c34eb7b" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject @@ -66,7 +66,10 @@ foreach ($ephemeralPath in @( "$replicaPath.cultmesh", $statePath, "$statePath.records", - "$statePath.cultmesh" + "$statePath.cultmesh", + "$statePath.ymir.cc", + "$statePath.ymir.cc.records", + "$statePath.ymir.cc.cultmesh" )) { $resolved = [IO.Path]::GetFullPath($ephemeralPath) if (-not $resolved.StartsWith([IO.Path]::GetFullPath($outputRoot), [StringComparison]::OrdinalIgnoreCase)) {