diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs index ecbb2a4..dc94d57 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs @@ -11,6 +11,7 @@ namespace GameCult.Eve.UnityScene.Fields public const string SplatCountPropertyName = "_EveFieldsSplatCount"; public const string ViewportToClipPropertyName = "_EveFieldsViewportToClip"; public const string ChannelFilterPropertyName = "_EveFieldsChannelFilter"; + public const string SimulationTimePropertyName = "_EveFieldsSimulationTimeSeconds"; private const int SplatStrideBytes = 96; private EveFieldsGpuSplat[] splats = Array.Empty(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs index b8638a5..c87f90b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs @@ -26,6 +26,7 @@ namespace GameCult.Eve.UnityScene.Fields 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); + private static readonly int SimulationTimeId = Shader.PropertyToID(EveFieldsSplatBuffer.SimulationTimePropertyName); public RenderTexture TargetTexture => targetTexture != null ? targetTexture : ownedTargetTexture; public int LastDrawnCount { get; private set; } @@ -48,6 +49,7 @@ namespace GameCult.Eve.UnityScene.Fields commandBuffer.SetGlobalBuffer(SplatBufferId, splatBuffer.Buffer); commandBuffer.SetGlobalInt(SplatCountId, splatBuffer.Count); commandBuffer.SetGlobalInt(ChannelFilterId, channelFilter); + commandBuffer.SetGlobalFloat(SimulationTimeId, (float)document.SimulationTimeSeconds); commandBuffer.SetGlobalMatrix(ViewportToClipId, BuildViewportToClip(document.Viewport)); commandBuffer.DrawProcedural(Matrix4x4.identity, material, Mathf.Max(0, materialPass), MeshTopology.Triangles, 6, splatBuffer.Count); Graphics.ExecuteCommandBuffer(commandBuffer); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl index a37a411..d40dce3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl @@ -15,31 +15,105 @@ StructuredBuffer _EveFieldsSplats; float4x4 _EveFieldsViewportToClip; int _EveFieldsSplatCount; int _EveFieldsChannelFilter; +float _EveFieldsSimulationTimeSeconds; float _ValueScale; -float hash21(float2 p) +// Ashima Arts webgl-noise, translated by Keijiro Takahashi. +// Copyright (C) 2011 Ashima Arts. Distributed under the MIT License. +float3 EveFieldsMod289(float3 x) { - p = frac(p * float2(123.34, 456.21)); - p += dot(p, p + 45.32); - return frac(p.x * p.y); + return x - floor(x / 289.0) * 289.0; } -float valueNoise(float2 p) +float4 EveFieldsMod289(float4 x) { - 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; + return x - floor(x / 289.0) * 289.0; +} + +float4 EveFieldsPermute(float4 x) +{ + return EveFieldsMod289((x * 34.0 + 1.0) * x); +} + +float4 EveFieldsTaylorInvSqrt(float4 r) +{ + return 1.79284291400159 - r * 0.85373472095314; +} + +float EveFieldsSimplexNoise3D(float3 v) +{ + const float2 C = float2(1.0 / 6.0, 1.0 / 3.0); + float3 i = floor(v + dot(v, C.yyy)); + float3 x0 = v - i + dot(i, C.xxx); + float3 g = step(x0.yzx, x0.xyz); + float3 l = 1.0 - g; + float3 i1 = min(g.xyz, l.zxy); + float3 i2 = max(g.xyz, l.zxy); + float3 x1 = x0 - i1 + C.xxx; + float3 x2 = x0 - i2 + C.yyy; + float3 x3 = x0 - 0.5; + i = EveFieldsMod289(i); + float4 p = EveFieldsPermute(EveFieldsPermute(EveFieldsPermute( + i.z + float4(0.0, i1.z, i2.z, 1.0)) + + i.y + float4(0.0, i1.y, i2.y, 1.0)) + + i.x + float4(0.0, i1.x, i2.x, 1.0)); + float4 j = p - 49.0 * floor(p / 49.0); + float4 x_ = floor(j / 7.0); + float4 y_ = floor(j - 7.0 * x_); + float4 x = (x_ * 2.0 + 0.5) / 7.0 - 1.0; + float4 y = (y_ * 2.0 + 0.5) / 7.0 - 1.0; + float4 h = 1.0 - abs(x) - abs(y); + float4 b0 = float4(x.xy, y.xy); + float4 b1 = float4(x.zw, y.zw); + float4 s0 = floor(b0) * 2.0 + 1.0; + float4 s1 = floor(b1) * 2.0 + 1.0; + float4 sh = -step(h, 0.0); + float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + float4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + float3 g0 = float3(a0.xy, h.x); + float3 g1 = float3(a0.zw, h.y); + float3 g2 = float3(a1.xy, h.z); + float3 g3 = float3(a1.zw, h.w); + float4 norm = EveFieldsTaylorInvSqrt(float4( + dot(g0, g0), dot(g1, g1), dot(g2, g2), dot(g3, g3))); + g0 *= norm.x; + g1 *= norm.y; + g2 *= norm.z; + g3 *= norm.w; + float4 m = max(0.6 - float4( + dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m *= m; + m *= m; + return 42.0 * dot(m, float4( + dot(x0, g0), dot(x1, g1), dot(x2, g2), dot(x3, g3))); +} + +float EveFieldsCellDistanceB(float2 p) +{ + p = frac(p) - 0.5; + return (length(p) * 1.5 + 0.25) * + max(abs(p.x) * 0.866 + p.y * 0.5, -p.y); +} + +float EveFieldsAnimatedCellNoiseB(float2 p, float time) +{ + float2 o = sin(float2(1.93, 0.0) + time) * 0.166; + float a = EveFieldsCellDistanceB(p + float2(o.x, 0)); + float b = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); + p = mul(p + 0.5, -float2x2(0.5, -0.866, 0.866, 0.5)); + float c = EveFieldsCellDistanceB(p + float2(o.x, 0)); + float d = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); + p = mul(p + 0.5, -float2x2(0.5, -0.866, 0.866, 0.5)); + float e = EveFieldsCellDistanceB(p + float2(o.x, 0)); + float f = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); + return min(min(min(a, b), min(c, d)), min(e, f)) * 2.0; } struct Varyings { float4 position : SV_POSITION; float2 localUv : TEXCOORD0; + float2 fieldWorld : TEXCOORD2; nointerpolation uint instanceId : TEXCOORD1; }; @@ -55,6 +129,7 @@ Varyings Vert(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID) Varyings output; output.position = mul(_EveFieldsViewportToClip, float4(world, 0, 1)); output.localUv = local; + output.fieldWorld = world; output.instanceId = instanceId; return output; } @@ -88,10 +163,20 @@ float4 Frag(Varyings input) : SV_Target 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); + float timeCoordinate = sourceKind == 2 + ? _EveFieldsSimulationTimeSeconds * splat.layerSource.z + : 0; + source = EveFieldsSimplexNoise3D(float3( + input.fieldWorld * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw, + timeCoordinate)); } + else if (sourceKind == 3) + { + source = EveFieldsAnimatedCellNoiseB( + input.fieldWorld * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw, + _EveFieldsSimulationTimeSeconds * splat.layerSource.z); + } + if (((int)round(splat.layerSource.w) & 1) != 0) source = abs(source); return splat.value * (alpha * source * _ValueScale); } #endif diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 365d2e4..32d9451 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -174,6 +174,94 @@ namespace GameCult.Eve.UnityScene.Tests UnityEngine.Object.DestroyImmediate(host); } + [Test] + public void RasterizerAnchorsAnimatedSimplexToFieldWorldAndPublishedSimulationTime() + { + var first = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); + var shifted = ProceduralDocument( + 25, 125, 75, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); + var later = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 7); + + var firstValue = RenderFieldPixel(first, 64, 64); + var shiftedValue = RenderFieldPixel(shifted, 32, 64); + var laterValue = RenderFieldPixel(later, 64, 64); + + Assert.That(shiftedValue, Is.EqualTo(firstValue).Within(0.015f), + "the same field-world coordinate must not swim when the viewport moves"); + Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.02f), + "animation must use the document's simulation time rather than Unity wall time"); + } + + [Test] + public void RasterizerLowersPortableAnimatedCellNoiseB() + { + var first = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 2); + var later = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 11); + + var firstValue = RenderFieldPixel(first, 57, 71); + var laterValue = RenderFieldPixel(later, 57, 71); + + Assert.That(firstValue, Is.GreaterThanOrEqualTo(0)); + Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.005f)); + } + + private static EveFieldsSplatsDocument ProceduralDocument( + double min, + double max, + double center, + int sourceKind, + double simulationTime) + { + return new EveFieldsSplatsDocument + { + SimulationTimeSeconds = simulationTime, + Viewport = new EveFieldsViewport { MinX = min, MinY = 0, MaxX = max, MaxY = 100 }, + Splats = new EveFieldsSplatSoa + { + Count = 1, + CenterX = new[] { center }, + CenterY = new[] { 50d }, + HalfExtentX = new[] { 50d }, + HalfExtentY = new[] { 50d }, + RotationCos = new[] { 1d }, + RotationSin = new[] { 0d }, + Channel = new[] { 0 }, + Falloff = new[] { EveFieldsSplatFalloffs.Solid }, + ValueR = new[] { 1d }, + ValueA = new[] { 1d }, + LayerIndex = new[] { 0 }, + SourceKind = new[] { sourceKind }, + FrequencyX = new[] { 0.037d }, + FrequencyY = new[] { 0.041d }, + PhaseX = new[] { 0.17d }, + PhaseY = new[] { -0.23d }, + AnimationSpeed = new[] { 0.31d }, + SourceFlags = new[] { (double)EveFieldsSplatSourceFlags.AbsoluteValue } + } + }; + } + + private static float RenderFieldPixel(EveFieldsSplatsDocument document, int x, int y) + { + var host = new GameObject("eve-fields-procedural-capture"); + var rasterizer = host.AddComponent(); + var output = rasterizer.Render(document, 128, 128); + var previous = RenderTexture.active; + RenderTexture.active = output; + var readback = new Texture2D(128, 128, TextureFormat.RGBAFloat, false, true); + readback.ReadPixels(new Rect(0, 0, 128, 128), 0, 0); + readback.Apply(); + RenderTexture.active = previous; + var value = readback.GetPixel(x, y).r; + UnityEngine.Object.DestroyImmediate(readback); + UnityEngine.Object.DestroyImmediate(host); + return value; + } + private sealed class CaptureDocument : IEveFieldsSplatsDocument { public string Schema => EveFieldsSchemas.Splats; diff --git a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md new file mode 100644 index 0000000..e66c613 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md @@ -0,0 +1,27 @@ +# Third-party notices + +## Ashima Arts webgl-noise + +The portable 3D simplex implementation in `Runtime/Fields/EveFieldsSplatsCore.hlsl` +is derived from Ashima Arts' `webgl-noise`, translated to Unity shader syntax by +Keijiro Takahashi. + +Copyright (C) 2011 Ashima Arts. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta new file mode 100644 index 0000000..4745e42 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5702faba00994033870c858bb2124d91 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 87d9d36..0c48786 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,12 +1,12 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.42", + "version": "0.3.43", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.1", + "org.gamecult.eve.plugin-fields": "0.2.2", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0"