From 3e23fff486f2d29451cad59eafc7a6a2d4ac460b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 15:37:39 +0100 Subject: [PATCH] Add Unity fields projection adapter --- ReleaseConsumerProject/Packages/manifest.json | 3 +- TestProject/Packages/manifest.json | 1 + TestProject/Packages/packages-lock.json | 7 ++ .../org.gamecult.eve.unity-scene/README.md | 5 + .../Runtime/Fields.meta | 8 ++ .../Runtime/Fields/EveFieldsSplatBuffer.cs | 105 ++++++++++++++++ .../Fields/EveFieldsSplatBuffer.cs.meta | 2 + .../Fields/EveFieldsSplatRasterizer.cs | 117 ++++++++++++++++++ .../Fields/EveFieldsSplatRasterizer.cs.meta | 2 + .../Runtime/Fields/EveFieldsSplats.shader | 49 ++++++++ .../Fields/EveFieldsSplats.shader.meta | 10 ++ .../Runtime/Fields/EveFieldsSplatsCore.hlsl | 87 +++++++++++++ .../Fields/EveFieldsSplatsCore.hlsl.meta | 7 ++ .../Runtime/GameCult.Eve.UnityScene.asmdef | 3 +- .../EveFieldsUnitySceneProjectionTests.cs | 32 +++++ ...EveFieldsUnitySceneProjectionTests.cs.meta | 2 + .../GameCult.Eve.UnityScene.Tests.asmdef | 3 +- .../eve-runtime-capability.json | 8 ++ .../org.gamecult.eve.unity-scene/package.json | 3 +- 19 files changed, 450 insertions(+), 4 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs.meta diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 9d0ec36..823b2d1 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -6,8 +6,9 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-source-v1.0.3-r2", + "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.1.0", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.1.1", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.1.2", "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/TestProject/Packages/manifest.json b/TestProject/Packages/manifest.json index b263290..b08495a 100644 --- a/TestProject/Packages/manifest.json +++ b/TestProject/Packages/manifest.json @@ -6,6 +6,7 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "org.gamecult.cultlib": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib", + "org.gamecult.eve.plugin-fields": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields", "org.gamecult.eve.surface": "file:../../packages/org.gamecult.eve.surface", "org.gamecult.eve.unity-scene": "file:../../packages/org.gamecult.eve.unity-scene", "org.gamecult.eve.unity-uitoolkit": "file:../../packages/org.gamecult.eve.unity-uitoolkit" diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index 37d1b94..6b032e8 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -22,6 +22,12 @@ "source": "local", "dependencies": {} }, + "org.gamecult.eve.plugin-fields": { + "version": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields", + "depth": 0, + "source": "local", + "dependencies": {} + }, "org.gamecult.eve.surface": { "version": "file:../../packages/org.gamecult.eve.surface", "depth": 0, @@ -36,6 +42,7 @@ "source": "local", "dependencies": { "org.gamecult.cultlib": "1.0.3", + "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.1.0" } }, diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index f8e0ddc..43d7650 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -14,3 +14,8 @@ provides every bootstrap port. Provider, surface, and Verse IDs are optional selection filters; they are not product knowledge required by the client. The package never imports provider product assemblies. + +`fields.surface` projection consumes the plugin-owned +`org.gamecult.eve.plugin-fields` contracts. `EveFieldsSplatRasterizer` owns the +Unity structured-buffer and RenderTexture lowering without importing provider +document classes. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields.meta new file mode 100644 index 0000000..cdf8720 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2c2c7df6e8d34f3ba491b20a450d17cb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs new file mode 100644 index 0000000..ddd6abb --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using GameCult.Eve.PluginFields; +using UnityEngine; + +namespace GameCult.Eve.UnityScene.Fields +{ + public sealed class EveFieldsSplatBuffer : IDisposable + { + public const string SplatBufferPropertyName = "_EveFieldsSplats"; + public const string SplatCountPropertyName = "_EveFieldsSplatCount"; + public const string ViewportToClipPropertyName = "_EveFieldsViewportToClip"; + public const string ChannelFilterPropertyName = "_EveFieldsChannelFilter"; + + private const int SplatStrideBytes = 80; + private EveFieldsGpuSplat[] splats = Array.Empty(); + private GraphicsBuffer buffer; + private int capacity; + + public int Count { get; private set; } + public GraphicsBuffer Buffer => buffer; + public bool HasGpuBuffer => buffer != null && buffer.IsValid() && Count > 0; + + public bool Upload(IEveFieldsSplatSoa source, int channelFilter = -1, int layerFilter = -1) + { + Count = Pack(source, channelFilter, layerFilter); + EnsureGpuCapacity(Count); + if (Count <= 0) + return false; + buffer.SetData(splats, 0, 0, Count); + return true; + } + + private int Pack(IEveFieldsSplatSoa source, int channelFilter, int layerFilter) + { + if (source == null || source.Count <= 0) + return 0; + EnsureCpuCapacity(source.Count); + var count = 0; + for (var index = 0; index < source.Count; index++) + { + var channel = Read(source.Channel, index, 0); + var layer = Read(source.LayerIndex, index, -1); + if ((channelFilter >= 0 && channel != channelFilter) || + (layerFilter >= 0 && layer != layerFilter)) + continue; + var halfExtentX = (float)Read(source.HalfExtentX, index, 0); + var halfExtentY = (float)Read(source.HalfExtentY, index, 0); + if (halfExtentX <= 0 || halfExtentY <= 0) + continue; + splats[count++] = new EveFieldsGpuSplat + { + CenterHalfExtent = new Vector4((float)Read(source.CenterX, index, 0), (float)Read(source.CenterY, index, 0), halfExtentX, halfExtentY), + RotationChannelFalloff = new Vector4((float)Read(source.RotationCos, index, 1), (float)Read(source.RotationSin, index, 0), channel, Read(source.Falloff, index, EveFieldsSplatFalloffs.Smooth)), + LayerSource = new Vector4(layer, Read(source.SourceKind, index, EveFieldsSplatSourceKinds.Constant), (float)Read(source.AnimationSpeed, index, 0), (float)Read(source.SourceFlags, index, 0)), + SourceFrequencyPhase = new Vector4((float)Read(source.FrequencyX, index, 1), (float)Read(source.FrequencyY, index, 1), (float)Read(source.PhaseX, index, 0), (float)Read(source.PhaseY, index, 0)), + Value = new Vector4((float)Read(source.ValueR, index, 0), (float)Read(source.ValueG, index, 0), (float)Read(source.ValueB, index, 0), (float)Read(source.ValueA, index, 1)) + }; + } + return count; + } + + private void EnsureCpuCapacity(int count) + { + if (splats.Length >= count) return; + var next = 1; + while (next < count) next <<= 1; + splats = new EveFieldsGpuSplat[next]; + } + + private void EnsureGpuCapacity(int count) + { + if (buffer != null && buffer.IsValid() && capacity >= count) return; + buffer?.Release(); + buffer = null; + capacity = 0; + if (count <= 0) return; + capacity = 1; + while (capacity < count) capacity <<= 1; + buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, capacity, SplatStrideBytes); + } + + public void Dispose() + { + buffer?.Release(); + buffer = null; + capacity = 0; + Count = 0; + } + + private static double Read(IReadOnlyList values, int index, double fallback) => + values != null && index >= 0 && index < values.Count ? values[index] : fallback; + private static int Read(IReadOnlyList values, int index, int fallback) => + values != null && index >= 0 && index < values.Count ? values[index] : fallback; + + private struct EveFieldsGpuSplat + { + public Vector4 CenterHalfExtent; + public Vector4 RotationChannelFalloff; + public Vector4 LayerSource; + public Vector4 SourceFrequencyPhase; + public Vector4 Value; + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs.meta new file mode 100644 index 0000000..62074ad --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0638cfafda354c8785255c44861df08d diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs new file mode 100644 index 0000000..b8638a5 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs @@ -0,0 +1,117 @@ +using GameCult.Eve.PluginFields; +using UnityEngine; +using UnityEngine.Experimental.Rendering; +using UnityEngine.Rendering; + +namespace GameCult.Eve.UnityScene.Fields +{ + public class EveFieldsSplatRasterizer : MonoBehaviour + { + [SerializeField] private Material splatMaterial; + [SerializeField] private RenderTexture targetTexture; + [SerializeField] private GraphicsFormat targetFormat = GraphicsFormat.R16_SFloat; + [SerializeField] private int width = 512; + [SerializeField] private int height = 512; + [SerializeField] private int channelFilter = -1; + [SerializeField] private int layerFilter = -1; + [SerializeField] private int materialPass; + [SerializeField] private Color clearColor = Color.clear; + [SerializeField] private bool clearBeforeDraw = true; + + private readonly EveFieldsSplatBuffer splatBuffer = new EveFieldsSplatBuffer(); + private CommandBuffer commandBuffer; + private RenderTexture ownedTargetTexture; + private Material ownedSplatMaterial; + private static readonly int SplatBufferId = Shader.PropertyToID(EveFieldsSplatBuffer.SplatBufferPropertyName); + private static readonly int SplatCountId = Shader.PropertyToID(EveFieldsSplatBuffer.SplatCountPropertyName); + private static readonly int ViewportToClipId = Shader.PropertyToID(EveFieldsSplatBuffer.ViewportToClipPropertyName); + private static readonly int ChannelFilterId = Shader.PropertyToID(EveFieldsSplatBuffer.ChannelFilterPropertyName); + + public RenderTexture TargetTexture => targetTexture != null ? targetTexture : ownedTargetTexture; + public int LastDrawnCount { get; private set; } + public int ChannelFilter { get => channelFilter; set => channelFilter = value; } + public int LayerFilter { get => layerFilter; set => layerFilter = value; } + + public RenderTexture Render(IEveFieldsSplatsDocument document, int overrideWidth = 0, int overrideHeight = 0) + { + if (document == null) { LastDrawnCount = 0; return TargetTexture; } + var output = EnsureTarget(overrideWidth, overrideHeight); + var material = ResolveMaterial(); + if (output == null || material == null) { LastDrawnCount = 0; return output; } + splatBuffer.Upload(document.Splats, channelFilter, layerFilter); + LastDrawnCount = splatBuffer.Count; + if (!splatBuffer.HasGpuBuffer) { if (clearBeforeDraw) Clear(output); return output; } + commandBuffer ??= new CommandBuffer { name = "Eve Fields Splats" }; + commandBuffer.Clear(); + commandBuffer.SetRenderTarget(output); + if (clearBeforeDraw) commandBuffer.ClearRenderTarget(false, true, clearColor); + commandBuffer.SetGlobalBuffer(SplatBufferId, splatBuffer.Buffer); + commandBuffer.SetGlobalInt(SplatCountId, splatBuffer.Count); + commandBuffer.SetGlobalInt(ChannelFilterId, channelFilter); + commandBuffer.SetGlobalMatrix(ViewportToClipId, BuildViewportToClip(document.Viewport)); + commandBuffer.DrawProcedural(Matrix4x4.identity, material, Mathf.Max(0, materialPass), MeshTopology.Triangles, 6, splatBuffer.Count); + Graphics.ExecuteCommandBuffer(commandBuffer); + return output; + } + + public RenderTexture RenderLayerToTarget(IEveFieldsSplatsDocument document, RenderTexture output, int overrideLayerFilter, int overrideMaterialPass, Color overrideClearColor) + { + var oldTarget = targetTexture; var oldLayer = layerFilter; var oldPass = materialPass; var oldClear = clearColor; + targetTexture = output; layerFilter = overrideLayerFilter; materialPass = overrideMaterialPass; clearColor = overrideClearColor; + try { return Render(document, output != null ? output.width : 0, output != null ? output.height : 0); } + finally { targetTexture = oldTarget; layerFilter = oldLayer; materialPass = oldPass; clearColor = oldClear; } + } + + private Material ResolveMaterial() + { + if (splatMaterial != null) return splatMaterial; + if (ownedSplatMaterial != null) return ownedSplatMaterial; + var shader = Shader.Find("Eve/Fields/Splats"); + if (shader == null) return null; + ownedSplatMaterial = new Material(shader) { hideFlags = HideFlags.DontSave }; + return ownedSplatMaterial; + } + + private RenderTexture EnsureTarget(int overrideWidth, int overrideHeight) + { + if (targetTexture != null) return targetTexture; + var targetWidth = Mathf.Max(1, overrideWidth > 0 ? overrideWidth : width); + var targetHeight = Mathf.Max(1, overrideHeight > 0 ? overrideHeight : height); + if (ownedTargetTexture != null && ownedTargetTexture.width == targetWidth && ownedTargetTexture.height == targetHeight && ownedTargetTexture.graphicsFormat == targetFormat) return ownedTargetTexture; + ReleaseOwnedTarget(); + var descriptor = new RenderTextureDescriptor(targetWidth, targetHeight) { depthBufferBits = 0, graphicsFormat = targetFormat, msaaSamples = 1, sRGB = false, useMipMap = false, autoGenerateMips = false }; + ownedTargetTexture = new RenderTexture(descriptor) { name = "Eve Fields Splats", filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp }; + ownedTargetTexture.Create(); + return ownedTargetTexture; + } + + private void Clear(RenderTexture output) + { + commandBuffer ??= new CommandBuffer { name = "Eve Fields Splats" }; + commandBuffer.Clear(); commandBuffer.SetRenderTarget(output); commandBuffer.ClearRenderTarget(false, true, clearColor); Graphics.ExecuteCommandBuffer(commandBuffer); + } + + private static Matrix4x4 BuildViewportToClip(IEveFieldsViewport viewport) + { + var minX = (float)System.Math.Min(viewport?.MinX ?? 0, viewport?.MaxX ?? 0); + var minY = (float)System.Math.Min(viewport?.MinY ?? 0, viewport?.MaxY ?? 0); + var maxX = (float)System.Math.Max(viewport?.MinX ?? 0, viewport?.MaxX ?? 0); + var maxY = (float)System.Math.Max(viewport?.MinY ?? 0, viewport?.MaxY ?? 0); + var spanX = Mathf.Max(0.0001f, maxX - minX); var spanY = Mathf.Max(0.0001f, maxY - minY); + return new Matrix4x4(new Vector4(2f / spanX, 0, 0, 0), new Vector4(0, 2f / spanY, 0, 0), new Vector4(0, 0, 1, 0), new Vector4(-(maxX + minX) / spanX, -(maxY + minY) / spanY, 0, 1)); + } + + private void OnDisable() + { + splatBuffer.Dispose(); commandBuffer?.Release(); commandBuffer = null; + if (ownedSplatMaterial != null) { Destroy(ownedSplatMaterial); ownedSplatMaterial = null; } + ReleaseOwnedTarget(); LastDrawnCount = 0; + } + + private void ReleaseOwnedTarget() + { + if (ownedTargetTexture == null) return; + ownedTargetTexture.Release(); Destroy(ownedTargetTexture); ownedTargetTexture = null; + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs.meta new file mode 100644 index 0000000..427fe55 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3c38a5ac2cea4201943817fbdf766974 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader new file mode 100644 index 0000000..893f327 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader @@ -0,0 +1,49 @@ +Shader "Eve/Fields/Splats" +{ + Properties { _ValueScale("Value Scale", Float) = 1 } + SubShader + { + Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } + Pass + { + Name "Add" + Cull Off ZWrite Off ZTest Always + BlendOp Add + Blend One One + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #include "UnityCG.cginc" + #include "EveFieldsSplatsCore.hlsl" + ENDHLSL + } + Pass + { + Name "Max" + Cull Off ZWrite Off ZTest Always + BlendOp Max + Blend One One + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #include "UnityCG.cginc" + #include "EveFieldsSplatsCore.hlsl" + ENDHLSL + } + Pass + { + Name "Alpha" + Cull Off ZWrite Off ZTest Always + Blend SrcAlpha OneMinusSrcAlpha + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #include "UnityCG.cginc" + #include "EveFieldsSplatsCore.hlsl" + ENDHLSL + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader.meta new file mode 100644 index 0000000..9a1b640 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplats.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 830df1cf91684771a9254a4b28613f6f +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + preprocessorOverride: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl new file mode 100644 index 0000000..a7ec7a9 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl @@ -0,0 +1,87 @@ +#ifndef EVE_FIELDS_SPLATS_CORE_INCLUDED +#define EVE_FIELDS_SPLATS_CORE_INCLUDED + +struct EveFieldsSplat +{ + float4 centerHalfExtent; + float4 rotationChannelFalloff; + float4 layerSource; + float4 sourceFrequencyPhase; + float4 value; +}; + +StructuredBuffer _EveFieldsSplats; +float4x4 _EveFieldsViewportToClip; +int _EveFieldsSplatCount; +int _EveFieldsChannelFilter; +float _ValueScale; + +float hash21(float2 p) +{ + p = frac(p * float2(123.34, 456.21)); + p += dot(p, p + 45.32); + return frac(p.x * p.y); +} + +float valueNoise(float2 p) +{ + float2 i = floor(p); + float2 f = frac(p); + float a = hash21(i); + float b = hash21(i + float2(1, 0)); + float c = hash21(i + float2(0, 1)); + float d = hash21(i + float2(1, 1)); + float2 u = f * f * (3 - 2 * f); + return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y) * 2 - 1; +} + +struct Varyings +{ + float4 position : SV_POSITION; + float2 localUv : TEXCOORD0; + nointerpolation uint instanceId : TEXCOORD1; +}; + +Varyings Vert(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID) +{ + static const float2 corners[6] = { float2(-1,-1), float2(1,-1), float2(1,1), float2(-1,-1), float2(1,1), float2(-1,1) }; + EveFieldsSplat splat = _EveFieldsSplats[instanceId]; + float2 local = corners[vertexId]; + float2 scaled = local * splat.centerHalfExtent.zw; + float c = splat.rotationChannelFalloff.x; + float s = splat.rotationChannelFalloff.y; + float2 world = splat.centerHalfExtent.xy + float2(scaled.x*c-scaled.y*s, scaled.x*s+scaled.y*c); + Varyings output; + output.position = mul(_EveFieldsViewportToClip, float4(world, 0, 1)); + output.localUv = local; + output.instanceId = instanceId; + return output; +} + +float ResolveFalloff(float2 localUv, int falloff) +{ + float distance01 = saturate(length(localUv)); + if (falloff == 0) return 1; + if (falloff == 1) return saturate(1 - distance01); + if (falloff == 3) return smoothstep(0, 1, distance01); + return 1 - smoothstep(0, 1, distance01); +} + +float4 Frag(Varyings input) : SV_Target +{ + EveFieldsSplat splat = _EveFieldsSplats[input.instanceId]; + int channel = (int)round(splat.rotationChannelFalloff.z); + if (_EveFieldsChannelFilter >= 0 && channel != _EveFieldsChannelFilter) discard; + float alpha = ResolveFalloff(input.localUv, (int)round(splat.rotationChannelFalloff.w)); + clip(alpha - 0.0001); + int sourceKind = (int)round(splat.layerSource.y); + float source = 1; + if (sourceKind == 1 || sourceKind == 2) + { + float timeOffset = sourceKind == 2 ? _Time.y * splat.layerSource.z : 0; + source = valueNoise(input.localUv * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw + timeOffset); + if (splat.layerSource.w != 0) source = abs(source); + } + return splat.value * (alpha * source * _ValueScale); +} +#endif diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl.meta new file mode 100644 index 0000000..e42c653 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c57c2586f57942f39323935e46133e86 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef b/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef index 395e095..6651097 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef @@ -2,7 +2,8 @@ "name": "GameCult.Eve.UnityScene", "rootNamespace": "GameCult.Eve.UnityScene", "references": [ - "GameCult.Eve.Surface" + "GameCult.Eve.Surface", + "GameCult.Eve.PluginFields" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs new file mode 100644 index 0000000..0f899e3 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Linq; +using GameCult.Eve.PluginFields; +using GameCult.Eve.UnityScene.Fields; +using NUnit.Framework; +using UnityEngine; + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveFieldsUnitySceneProjectionTests + { + [Test] + public void RasterizerConsumesPluginOwnedDocumentContract() + { + var render = typeof(EveFieldsSplatRasterizer).GetMethod( + "Render", + new[] { typeof(IEveFieldsSplatsDocument), typeof(int), typeof(int) }); + + Assert.That(render, Is.Not.Null); + Assert.That(typeof(EveFieldsSplatRasterizer).Assembly.GetReferencedAssemblies().Any(assembly => + assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); + } + + [Test] + public void PackageContainsGenericFieldsShader() + { + Assert.That(Shader.Find("Eve/Fields/Splats"), Is.Not.Null); + Assert.That(File.Exists("Packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl"), Is.True); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs.meta new file mode 100644 index 0000000..9586cef --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2d07cc4537e64f12adfa8930ded4fe96 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef index 392a1b5..2f8a14e 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef @@ -3,7 +3,8 @@ "rootNamespace": "GameCult.Eve.UnityScene.Tests", "references": [ "GameCult.Eve.Surface", - "GameCult.Eve.UnityScene" + "GameCult.Eve.UnityScene", + "GameCult.Eve.PluginFields" ], "includePlatforms": [ "Editor" diff --git a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json index 3f2f5a8..55177a9 100644 --- a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json @@ -39,6 +39,14 @@ "embeddedDocuments" ], "supportedPlugins": [ + { + "pluginId": "fields.surface", + "capabilities": [ + "field.surface2d", + "gravity.surface" + ], + "projectionAdapter": "EveFieldsSplatRasterizer" + }, { "pluginId": "sai.vn", "capabilities": [ diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 72d85a1..4c9c546 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,12 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.1.1", + "version": "0.1.2", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.3", + "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.1.0" }, "keywords": [