From 3b1f2d407357c205fba02018d0581c56c154da54 Mon Sep 17 00:00:00 2001 From: Timo Eberl Date: Sun, 31 Aug 2025 16:54:24 +0200 Subject: [PATCH] fix normals --- Assets/Scenes/SampleScene.unity | 106 ---------------- Assets/Shaders/GrassBlade.shader | 2 +- Assets/Shaders/include/GrassBladePass.hlsl | 135 +++++++++++---------- Assets/Shaders/include/GrassHelpers.hlsl | 4 +- 4 files changed, 73 insertions(+), 174 deletions(-) diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 97cbab5..e1a64d5 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -241020,111 +241020,6 @@ Mesh: offset: 0 size: 0 path: ---- !u!1 &1663409491 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1663409495} - - component: {fileID: 1663409494} - - component: {fileID: 1663409493} - - component: {fileID: 1663409492} - m_Layer: 0 - m_Name: Cube - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!65 &1663409492 -BoxCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1663409491} - m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_LayerOverridePriority: 0 - m_IsTrigger: 0 - m_ProvidesContacts: 0 - m_Enabled: 1 - serializedVersion: 3 - m_Size: {x: 1, y: 1, z: 1} - m_Center: {x: 0, y: 0, z: 0} ---- !u!23 &1663409493 -MeshRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1663409491} - m_Enabled: 1 - m_CastShadows: 1 - m_ReceiveShadows: 1 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 1 - m_LightProbeUsage: 1 - m_ReflectionProbeUsage: 1 - m_RayTracingMode: 2 - m_RayTraceProcedural: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: 944161d6703f1e244a9216da9f82501b, type: 2} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_AdditionalVertexStreams: {fileID: 0} ---- !u!33 &1663409494 -MeshFilter: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1663409491} - m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} ---- !u!4 &1663409495 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1663409491} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 7.656, y: 0.872, z: 0.544} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!43 &1663750196 Mesh: m_ObjectHideFlags: 0 @@ -308734,4 +308629,3 @@ SceneRoots: - {fileID: 2016058937} - {fileID: 513204364} - {fileID: 934953871} - - {fileID: 1663409495} diff --git a/Assets/Shaders/GrassBlade.shader b/Assets/Shaders/GrassBlade.shader index d5cb8f6..094ea57 100644 --- a/Assets/Shaders/GrassBlade.shader +++ b/Assets/Shaders/GrassBlade.shader @@ -24,7 +24,7 @@ } SubShader { LOD 100 - // Cull Off + Cull Off //Render Pass Pass { diff --git a/Assets/Shaders/include/GrassBladePass.hlsl b/Assets/Shaders/include/GrassBladePass.hlsl index 9998f3c..a8dd538 100644 --- a/Assets/Shaders/include/GrassBladePass.hlsl +++ b/Assets/Shaders/include/GrassBladePass.hlsl @@ -24,7 +24,7 @@ struct v2g { struct g2f { float4 pos : SV_POSITION; #ifdef IS_IN_BASE_PASS - float3 normal : NORMAL; + float3 localNormal : NORMAL; float2 uv : TEXCOORD0; // bottom left 0.0 top right 1.1 (values converge - 0 and 1 meet at the tip) SHADOW_COORDS(1) // put shadows data into TEXCOORD1 float3 worldPos : TEXCOORD2; @@ -40,10 +40,6 @@ v2g vert (MeshData v) { return o; } -float getCameraDistance(float3 pos) { - return distance(_MainCameraPosition, pos); -} - #if _LOD_LOD_0 [MaxVertexCount(4 + ( 11 - 1 ) * 2)] #elif _LOD_LOD_1 @@ -56,7 +52,6 @@ float getCameraDistance(float3 pos) { void geom(point v2g IN[1], inout TriangleStream triStream) { const float3 basePos = IN[0].vertex.xyz; const float3 tipOffset = IN[0].tipOffset; - const float3 tipPosObjectSpace = basePos + tipOffset; // const float4 basePosClipSpace = UnityObjectToClipPos(float4(basePos, 1)); // const float4 tipPosClipSpace = UnityObjectToClipPos(float4(tipPosObjectSpace, 1)); @@ -64,9 +59,9 @@ void geom(point v2g IN[1], inout TriangleStream triStream) { const float3 basePosWS = mul(unity_ObjectToWorld, float4(basePos.xyz, 1.0)).xyz; - const float cameraDistance = getCameraDistance(basePosWS); + const float mainCamDist = distance(_MainCameraPosition, basePosWS); - float interpolator = invLerp(_TransitionRange.x, _TransitionRange.y, cameraDistance); + float interpolator = invLerp(_TransitionRange.x, _TransitionRange.y, mainCamDist); float halfWidth = _BladeWidth/2; @@ -78,18 +73,16 @@ void geom(point v2g IN[1], inout TriangleStream triStream) { } // randomly shrink shadow blades to create a smooth transition between shadowed and non-shadowed area - #ifdef IS_IN_SHADOW_PASS - #if _LOD_LOD_1 +#ifdef IS_IN_SHADOW_PASS +#if _LOD_LOD_1 float widthMultiplier = remap(N21(basePos.xz) * 0.5, 1, 1, 0, interpolator); widthMultiplier = max(0, widthMultiplier); halfWidth *= widthMultiplier; - #endif - #endif +#endif +#endif g2f o; o.debug = float3(0,0,0); - - float3 previousSegmentCenter = basePos - float3(0,-1,0); // point beneath the basePos int numSegments; #if _LOD_LOD_0 @@ -102,13 +95,12 @@ void geom(point v2g IN[1], inout TriangleStream triStream) { numSegments = 1; #endif - float3 localCameraPos = mul(unity_WorldToObject, float4(_MainCameraPosition.xyz, 1.0)).xyz; const float lowerThickness = 0.8; // LOD 3 has a different algorithm #if _LOD_LOD_3 const float segmentWidth = halfWidth * pow(sin(lowerThickness * 3.141), .8); - const float3 widthOffset = getWidthOffset(segmentWidth, tipOffset.xz) * max(1, cameraDistance / 10); + const float3 widthOffset = getWidthOffset(tipOffset.xz) * segmentWidth * max(1, mainCamDist / 10); float3 segmentCenterBase = basePos; float3 segmentCenterTip = basePos + tipOffset; const float3 vertLeftBase = segmentCenterBase + widthOffset * 2.5; @@ -118,22 +110,13 @@ void geom(point v2g IN[1], inout TriangleStream triStream) { const float3 surfTangent1 = vertRightBase - segmentCenterTip; const float3 surfTangent2 = vertLeftBase - segmentCenterTip; - const float3 normal = normalize(cross(surfTangent1, surfTangent2)); // TODO remove normalize - float3 normalWS = UnityObjectToWorldNormal(normal); - - const float3 camVec = normalize(segmentCenterTip - localCameraPos); - - // at which side of the blade are we looking? - const bool lookingFromAbove = dot(camVec, normal) > 0; - if (lookingFromAbove) { - // normalWS = -normalWS; - } + const float3 normal = cross(surfTangent1, surfTangent2); o.pos = UnityObjectToClipPos(vertRightBase); #ifdef IS_IN_BASE_PASS o.uv = float2(1, 0); TRANSFER_SHADOW(o) - o.normal = normalWS; + o.localNormal = normal; o.worldPos = mul(unity_ObjectToWorld, float4(vertRightBase,1.0)).xyz; #endif triStream.Append(o); @@ -142,7 +125,7 @@ void geom(point v2g IN[1], inout TriangleStream triStream) { #ifdef IS_IN_BASE_PASS o.uv = float2(0, 0); TRANSFER_SHADOW(o) - o.normal = normalWS; + o.localNormal = normal; o.worldPos = mul(unity_ObjectToWorld, float4(vertLeftBase,1.0)).xyz; #endif triStream.Append(o); @@ -151,7 +134,7 @@ void geom(point v2g IN[1], inout TriangleStream triStream) { #ifdef IS_IN_BASE_PASS o.uv = float2(1, 1); TRANSFER_SHADOW(o) - o.normal = normalWS; + o.localNormal = normal; o.worldPos = mul(unity_ObjectToWorld, float4(vertRightTip,1.0)).xyz; #endif triStream.Append(o); @@ -160,83 +143,105 @@ void geom(point v2g IN[1], inout TriangleStream triStream) { #ifdef IS_IN_BASE_PASS o.uv = float2(0, 1); TRANSFER_SHADOW(o) - o.normal = normalWS; + o.localNormal = normal; o.worldPos = mul(unity_ObjectToWorld, float4(vertLeftTip,1.0)).xyz; #endif triStream.Append(o); #else - for (int i = 0; i < numSegments; i++) { - // in "blade space" - const float segmentWidth = halfWidth * pow(sin(lowerThickness * 3.141 * (numSegments - i) / numSegments), .8); - const float segmentHeightNormalized = i / (float)numSegments; - // float nextSegmentHeightNormalized = (i+1) / (float)numSegments; + const float3 widthOffset = getWidthOffset(tipOffset.xz) * max(1, mainCamDist / 10); - // generate new verts - const float3 widthOffset = getWidthOffset(segmentWidth, tipOffset.xz) * max(1, cameraDistance / 10); - float3 segmentCenter = basePos + bendParabula(tipOffset, segmentHeightNormalized, _BendStrength); - const float3 segmentCenterSnapshot = segmentCenter; - - const float3 vertLeft = segmentCenter + widthOffset; - const float3 vertRight = segmentCenter - widthOffset; - //const float3 nextSegmentCenter = basePos + bendParabula(tipPos, nextSegmentHeight01); + // previous vertex positions, used for normal calculation + float3 previousVertLeft; + float3 previousVertRight; + for (int i = 0; i < numSegments; i++) { + const float segmentWidth = halfWidth * pow(sin(lowerThickness * 3.141 * (numSegments - i) / numSegments), .8); + const float3 segmentWidthOffset = widthOffset * segmentWidth; + const float segmentHeightNormalized = i / (float)numSegments; + + const float3 segmentCenter = basePos + bendParabula(tipOffset, segmentHeightNormalized, _BendStrength); + const float3 vertLeft = segmentCenter + segmentWidthOffset; + const float3 vertRight = segmentCenter - segmentWidthOffset; #ifdef IS_IN_BASE_PASS // calculate normal - const float3 surfTangent1 = previousSegmentCenter - segmentCenter; - const float3 surfTangent2 = previousSegmentCenter - vertLeft; - const float3 normal = normalize(cross(surfTangent1, surfTangent2)); // TODO remove normalize - float3 normalWS = UnityObjectToWorldNormal(normal); - - const float3 camVec = normalize(segmentCenter - localCameraPos); - - // at which side of the blade are we looking? - const bool lookingFromAbove = dot(camVec, normalWS) > 0; - if (lookingFromAbove) { - normalWS = -normalWS; + float3 surfTangent1, surfTangent2; + if (i == 0) { + // for the normals at the bottom vertices, use the normals of the next segment + const float nextSegmentHeightNormalized = (i+1) / (float)numSegments; + const float3 nextSegmentCenter = basePos + bendParabula(tipOffset, nextSegmentHeightNormalized, _BendStrength); + surfTangent1 = nextSegmentCenter - vertRight; + surfTangent2 = nextSegmentCenter - vertLeft; } + else { + surfTangent1 = segmentCenter - previousVertRight; + surfTangent2 = segmentCenter - previousVertLeft; + } + const float3 normal = cross(surfTangent1, surfTangent2); + + // TODO fake curvature + // o.localNormal = normalize(lerp(normalWS, UnityObjectToWorldNormal(-normalize(segmentWidthOffset)), _BladeBow)); + + previousVertLeft = vertLeft; + previousVertRight = vertRight; #endif + o.pos = UnityObjectToClipPos(vertRight); #ifdef IS_IN_BASE_PASS o.uv = float2(1, segmentHeightNormalized); TRANSFER_SHADOW(o) - o.normal = normalize(lerp(normalWS, UnityObjectToWorldNormal(-normalize(widthOffset)), _BladeBow)); + o.localNormal = normal; o.worldPos = mul(unity_ObjectToWorld, float4(vertRight,1.0)).xyz; #endif triStream.Append(o); - previousSegmentCenter = segmentCenterSnapshot; o.pos = UnityObjectToClipPos(vertLeft); #ifdef IS_IN_BASE_PASS o.uv = float2(0, segmentHeightNormalized); TRANSFER_SHADOW(o) - o.normal = normalize(lerp(normalWS, UnityObjectToWorldNormal(normalize(widthOffset)), _BladeBow)); + o.localNormal = normal; o.worldPos = mul(unity_ObjectToWorld, float4(vertLeft,1.0)).xyz; #endif triStream.Append(o); } -#endif - - o.pos = UnityObjectToClipPos(basePos + tipOffset * float3(_BendStrength, 1, _BendStrength)); + // tip vertex + const float3 tipPosObjectSpace = basePos + tipOffset; + const float3 surfTangent1 = tipPosObjectSpace - previousVertRight; + const float3 surfTangent2 = tipPosObjectSpace - previousVertLeft; + const float3 normal = cross(surfTangent1, surfTangent2); + o.pos = UnityObjectToClipPos(tipPosObjectSpace * float3(_BendStrength, 1, _BendStrength)); #ifdef IS_IN_BASE_PASS o.uv = float2(.5, 1); TRANSFER_SHADOW(o) - o.normal = o.normal > 0 ? float3(0, 1, 0) : float3(0, -1, 0); + o.localNormal = normal; + o.worldPos = mul(unity_ObjectToWorld, float4(tipPosObjectSpace,1.0)).xyz; #endif triStream.Append(o); + +#endif } float4 frag(g2f i) : SV_Target{ #ifdef IS_IN_BASE_PASS float3 localPos = mul(unity_WorldToObject, float4(i.worldPos, 1)).xyz; - float d = distance(localPos, i.debug) * 5; - return float4(i.debug, 1); + float d = distance(localPos, i.debug) * 50; + float k = frac(d); + // return float4(k,k,k, 1); + // return float4(i.debug,1); + + float3 worldNormal = UnityObjectToWorldNormal(i.localNormal); + float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); + + // at which side of the blade are we looking? + // NOT ACCURATE, because the normals don't exactly match the geometry + const bool lookingFromAbove = dot(worldViewDir, worldNormal) > 0; + if (!lookingFromAbove) { + worldNormal = -worldNormal; + } - float3 worldNormal = normalize(i.normal); float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz); - float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); float3 worldRefl = reflect(-worldViewDir, worldNormal); // same as in previous shader diff --git a/Assets/Shaders/include/GrassHelpers.hlsl b/Assets/Shaders/include/GrassHelpers.hlsl index 269252a..224c9ba 100644 --- a/Assets/Shaders/include/GrassHelpers.hlsl +++ b/Assets/Shaders/include/GrassHelpers.hlsl @@ -1,7 +1,7 @@ // tipPos2D = xz coordinates of tip -float3 getWidthOffset(float width, float2 tipPos2D) { +float3 getWidthOffset(float2 tipPos2D) { float2 dirBaseToTip = normalize(tipPos2D); - return float3(-dirBaseToTip.y * width, 0, dirBaseToTip.x * width); + return float3(-dirBaseToTip.y, 0, dirBaseToTip.x); } float3 bendParabula(float3 tipPos, float posOnBlade, float bendStrength) {