This commit is contained in:
Lennart R. 2025-09-02 15:43:02 +02:00
commit 6c260999d0
14 changed files with 287 additions and 535 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
# Custom additions
.vscode/
.vsconfig
.idea/
# This .gitignore file should be placed at the root of your Unity project directory

View File

@ -1,6 +0,0 @@
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.ManagedGame"
]
}

View File

@ -28,6 +28,10 @@ Material:
m_Texture: {fileID: 2800000, guid: bbec7f79ed7356243ae0604d95144b77, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FieldColor:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FrabVarianz:
m_Texture: {fileID: 2800000, guid: 9bb4e817f09d0ea4cbab03f772e01910, type: 3}
m_Scale: {x: 1, y: 1}
@ -38,9 +42,10 @@ Material:
- _BendStrength: 1
- _BladeBow: 0.195
- _BladeWidth: 0.0274
- _BladeWidthTexStrength: 1
- _BladeWidthTexStrength: 0.74
- _LOD: 0
- _Metallic: 0
- _NormalCurvature: 1
- _Roughness: 0
- _SSSStrength: 1
- _Translucency: 1.81

View File

@ -64477,7 +64477,7 @@ MeshFilter:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 434496868}
m_Mesh: {fileID: 1114929778}
m_Mesh: {fileID: 2061544436}
--- !u!23 &434496871
MeshRenderer:
m_ObjectHideFlags: 0
@ -64498,7 +64498,7 @@ MeshRenderer:
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: d979d9bbf3760d948a452ba192cbf563, type: 2}
- {fileID: 2100000, guid: 91fb1ec3cf2133a4b931798a8822d055, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
@ -140790,6 +140790,7 @@ GameObject:
- component: {fileID: 963194227}
- component: {fileID: 963194226}
- component: {fileID: 963194229}
- component: {fileID: 963194230}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@ -140864,14 +140865,26 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
serializedVersion: 2
m_LocalRotation: {x: 0.23506844, y: 0.3851214, z: -0.1021131, w: 0.8865649}
m_LocalPosition: {x: 6.34, y: 2.02, z: 3.8}
m_LocalRotation: {x: 0.23339066, y: 0.37273654, z: -0.097438015, w: 0.8928058}
m_LocalPosition: {x: 6.6, y: 3.5, z: 4.2}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 29.7, y: 46.96, z: 0}
m_LocalEulerAnglesHint: {x: 29.3, y: 45.32, z: 0}
--- !u!114 &963194229
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 78c9485abe801d74184e00172d4b42c4, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &963194230
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -140880,7 +140893,7 @@ MonoBehaviour:
m_GameObject: {fileID: 963194225}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 78c9485abe801d74184e00172d4b42c4, type: 3}
m_Script: {fileID: 11500000, guid: d9be733811a3b0342953552aa8d5ab8a, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!43 &963631122
@ -241007,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
@ -290519,7 +290427,6 @@ MonoBehaviour:
numChunks: 20
bladesPerSquareMeter: 100
chunkPrefab: {fileID: 6414307754184109852, guid: abb47bb4e9622f7428302dfa4730d722, type: 3}
ignoreHeightForLOD: 0
grassMaterialLOD0: {fileID: 2100000, guid: d979d9bbf3760d948a452ba192cbf563, type: 2}
minDistanceLOD0: 3.5
densityLOD0: 1
@ -290946,7 +290853,7 @@ Transform:
m_GameObject: {fileID: 2016058935}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 5.11, y: 0, z: 2.71}
m_LocalPosition: {x: 5.31, y: 0, z: 2.19}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@ -308722,4 +308629,3 @@ SceneRoots:
- {fileID: 2016058937}
- {fileID: 513204364}
- {fileID: 934953871}
- {fileID: 1663409495}

View File

@ -1,37 +1,30 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
public class GrassField : MonoBehaviour {
public int size = 100;
public int numChunks = 20;
public int bladesPerSquareMeter = 100;
public GrassChunk chunkPrefab;
public bool ignoreHeightForLOD = true;
[Header("LOD 0")]
public Material grassMaterialLOD0;
[Header("LOD 0")] public Material grassMaterialLOD0;
public float minDistanceLOD0 = 2f;
[Range(0f, 1f)] public float densityLOD0 = 1f;
public bool enableShadowsLOD0 = true;
[Header("LOD 1")]
public float distanceLOD1 = 5f;
[Header("LOD 1")] public float distanceLOD1 = 5f;
public Material grassMaterialLOD1;
[Range(0f, 1f)] public float densityLOD1 = 0.5f;
public bool enableShadowsLOD1 = true;
[Header("LOD 2")]
public float distanceLOD2 = 15f;
[Header("LOD 2")] public float distanceLOD2 = 15f;
public Material grassMaterialLOD2;
[Range(0f, 1f)] public float densityLOD2 = 0.25f;
public bool enableShadowsLOD2 = false;
[Header("LOD 3")]
public float distanceLOD3 = 25f;
[Header("LOD 3")] public float distanceLOD3 = 25f;
public Material grassMaterialLOD3;
[Range(0f, 1f)] public float densityLOD3 = 0.125f;
public bool enableShadowsLOD3 = false;
private const float SQRT_2 = 1.4142135623730f;
private static readonly int pId_WorldSpaceCameraPosEditor = Shader.PropertyToID("_WorldSpaceCameraPosEditor");
private static readonly int pId_TransitionRange = Shader.PropertyToID("_TransitionRange");
// a 2D array would be cleaner, but it can't be serialized
@ -49,12 +42,12 @@ public class GrassField : MonoBehaviour {
var camPosWorld = Camera.main ? Camera.main.transform.position : Vector3.zero;
UpdateLODs(camPosWorld);
UpdateMaterials(camPosWorld);
UpdateMaterials();
}
public void UpdateLODsAndMaterials() {
var camPosWorld = Camera.main ? Camera.main.transform.position : Vector3.zero;
UpdateMaterials(camPosWorld);
UpdateMaterials();
UpdateLODs(camPosWorld);
}
@ -64,11 +57,12 @@ public class GrassField : MonoBehaviour {
if (distanceLOD2 < minDistLOD2) {
Debug.LogWarning("It is recommended that minDistLOD2 is greater than " + minDistLOD2 + ".");
}
var minDistLOD3 = distanceLOD2 + chunkSize * SQRT_2;
if (distanceLOD3 < minDistLOD3) {
Debug.LogWarning("It is recommended that minDistLOD3 is greater than " + minDistLOD3 + ".");
}
if (_chunks.Length != numChunks * numChunks) {
Debug.LogError(this.name + ": Existing chunks does not match numChunks.");
gameObject.SetActive(false);
@ -81,26 +75,24 @@ public class GrassField : MonoBehaviour {
UpdateLODsAndMaterials();
}
private void FixedUpdate() {
private void Update() {
#if UNITY_EDITOR
// LOD distances might have been changed via Inspector -> Update materials
// Camera might have been moved -> Update LODs
UpdateLODsAndMaterials();
#else
if (_chunks.Length != numChunks * numChunks) return;
var cam = Camera.main;
if (!cam) return;
var camPosWorld = cam.transform.position;
#if UNITY_EDITOR
// update LOD distances and camera position
UpdateMaterials(camPosWorld);
// while the game is running, only update the chunks near the camera
// if the camera moved a great distance since the last frame, this fails
UpdateLODs(cam.transform.position, false);
#endif
if (_chunks.Length != numChunks * numChunks) return;
UpdateLODs(camPosWorld, false); // while the game is running, only the chunks near the camera are updated
}
private void UpdateLODs(Vector3 camPosWorld, bool forceCompleteCheck = true) {
var camPos = camPosWorld - transform.position; // cam position relative to grass field
if (ignoreHeightForLOD) {
camPos.y = 0;
}
var chunkSize = ((float)size) / numChunks;
var bounds = new Bounds(
new Vector3(0.5f * chunkSize, 0f, 0.5f * chunkSize),
@ -109,7 +101,7 @@ public class GrassField : MonoBehaviour {
// small performance difference (0.13ms vs 0.02ms on a 100m 30x30 grid, LOD dist 26.6, camera at corner)
var range = forceCompleteCheck
? new GridRange{ fromX = 0, toX = numChunks-1, fromZ = 0, toZ = numChunks-1 }
? new GridRange { fromX = 0, toX = numChunks - 1, fromZ = 0, toZ = numChunks - 1 }
: GetLODGridRange(camPos, chunkSize);
for (int x = range.fromX; x <= range.toX; x++)
@ -142,17 +134,17 @@ public class GrassField : MonoBehaviour {
private GridRange GetLODGridRange(Vector3 camPos, float chunkSize) {
GridRange range;
var maxCamMove = 3f; // assume that the camera did not move further than this distance
var fromXWorld = camPos.x - distanceLOD3 - maxCamMove;
var toXWorld = camPos.x + distanceLOD3 + maxCamMove;
var fromZWorld = camPos.z - distanceLOD3 - maxCamMove;
var toZWorld = camPos.z + distanceLOD3 + maxCamMove;
range.fromX = Math.Max( 0, (int) (fromXWorld / chunkSize) );
range.toX = Math.Max( 0, (int) (toXWorld / chunkSize) );
range.fromZ = Math.Max( 0, (int) (fromZWorld / chunkSize) );
range.toZ = (int) (toZWorld / chunkSize);
range.fromX = Math.Max(0, (int)(fromXWorld / chunkSize));
range.toX = Math.Max(0, (int)(toXWorld / chunkSize));
range.fromZ = Math.Max(0, (int)(fromZWorld / chunkSize));
range.toZ = (int)(toZWorld / chunkSize);
range.fromX = Math.Clamp(range.fromX, 0, numChunks - 1);
range.toX = Math.Clamp(range.toX, 0, numChunks - 1);
range.fromZ = Math.Clamp(range.fromZ, 0, numChunks - 1);
@ -161,28 +153,19 @@ public class GrassField : MonoBehaviour {
return range;
}
private void UpdateMaterials(Vector3 camPosWorld) {
private void UpdateMaterials() {
var chunkSize = ((float)size) / numChunks;
var chunkDiagonalLength = chunkSize * SQRT_2;
var transition0From = minDistanceLOD0;
var transition0To = distanceLOD1;
var transition0To = distanceLOD1;
var transition1From = distanceLOD1 + chunkDiagonalLength;
var transition1To = distanceLOD2;
var transition1To = distanceLOD2;
var transition2From = distanceLOD2 + chunkDiagonalLength;
var transition2To = distanceLOD3;
var transition2To = distanceLOD3;
grassMaterialLOD0.SetVector(pId_TransitionRange, new Vector2(transition0From, transition0To));
grassMaterialLOD1.SetVector(pId_TransitionRange, new Vector2(transition1From, transition1To));
grassMaterialLOD2.SetVector(pId_TransitionRange, new Vector2(transition2From, transition2To));
#if UNITY_EDITOR
if (ignoreHeightForLOD) {
camPosWorld.y = 0;
}
grassMaterialLOD0.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
grassMaterialLOD1.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
grassMaterialLOD2.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
grassMaterialLOD3.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
#endif
}
private void CreateChunks(
@ -195,6 +178,7 @@ public class GrassField : MonoBehaviour {
// Destroy(obj) does not work in edit mode
DestroyImmediate(chunksTransform.gameObject);
}
// create GameObject "Chunks" as a container for all GrassChunk objects
chunksContainer = new GameObject("Chunks");
chunksContainer.transform.SetParent(this.transform, false);
@ -216,7 +200,7 @@ public class GrassField : MonoBehaviour {
var meshLOD3 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD3, "grass_chunk_" + x + "_" + y + "_lod3");
var chunk = Instantiate(chunkPrefab, chunksContainer.transform);
chunk.name = "Chunk [" + x + ", " + y +"]";
chunk.name = "Chunk [" + x + ", " + y + "]";
chunk.transform.localPosition = chunkPos;
chunk.MaterialLOD0 = grassMaterialLOD0;
@ -260,6 +244,7 @@ public class GrassField : MonoBehaviour {
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
Debug.LogWarning(meshName + " has more than 65536 grass blades. Mesh.indexFormat set to UInt32.");
}
mesh.SetVertices(rootPositions);
mesh.SetUVs(0, tipOffsets_missingInNextLOD); // Vector4 UVs
// AABB gets calculated automatically (considers all root positions)
@ -272,13 +257,16 @@ public class GrassField : MonoBehaviour {
const float height = 1f; // a blade should not be higher than this
mesh.bounds = new Bounds(
previousBounds.center + new Vector3(0f, height * 0.5f, 0f),
previousBounds.size + new Vector3(2f*width, height, 2f*width)
previousBounds.size + new Vector3(2f * width, height, 2f * width)
);
return mesh;
}
private void OnDrawGizmos() {
// when in editor, update LODs every frame. nothing to do with gizmos
UpdateLODsAndMaterials();
var chunkSize = ((float)size) / numChunks;
var tPos = transform.position;
@ -299,17 +287,12 @@ public class GrassField : MonoBehaviour {
if (!cam) return;
var center = cam.transform.position;
var oldMatrix = Gizmos.matrix;
if (ignoreHeightForLOD) {
center.y = 0.01f;
// draw a circle instead of a sphere
Gizmos.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1f, 0.001f, 1f));
}
Gizmos.color = new Color(0f, 0.3f, 1f, 1f);
Gizmos.DrawWireSphere(center, distanceLOD1);
Gizmos.DrawWireSphere(center, distanceLOD2);
Gizmos.DrawWireSphere(center, distanceLOD3);
Gizmos.color = new Color(1.0f, 1.0f, 0.0f, 1f);
var minDistLOD0 = minDistanceLOD0;
var minDistLOD1 = distanceLOD1 + chunkSize * SQRT_2;
@ -317,7 +300,7 @@ public class GrassField : MonoBehaviour {
Gizmos.DrawWireSphere(center, minDistLOD0);
Gizmos.DrawWireSphere(center, minDistLOD1);
Gizmos.DrawWireSphere(center, minDistLOD2);
Gizmos.matrix = oldMatrix;
}
}

View File

@ -0,0 +1,18 @@
using System;
using UnityEngine;
using UnityEditor;
[ExecuteAlways]
public class MainCameraPositionShaderSetter : MonoBehaviour {
private static readonly int pId_MainCameraPosition = Shader.PropertyToID("_MainCameraPosition");
private void OnEnable() {
if (!Camera.main || Camera.main.gameObject != this.gameObject) {
Debug.LogWarning("MainCameraPositionShaderSetter set on object that is not the main camera");
}
}
private void Update() {
Shader.SetGlobalVector(pId_MainCameraPosition, this.transform.position);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9be733811a3b0342953552aa8d5ab8a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -5,7 +5,7 @@
_BladeWidth("Blade Width", Range(0.003, .1)) = 0.01
_BendStrength("Bend Strength", Range(0, 1)) = 1
_FrabVarianz("Farb Varianz", 2D) = "white" {}
_FieldColor("Field Color", 2D) = "white" {}
_BladeWidthTex("Blade Width Texture", 2D) = "white" {}
_AOStrength("Ambient Occlusion Strength", Range(0,2)) = 1
@ -13,14 +13,9 @@
_SSSStrength("Subsurface Scattering Strength", Range(0,1)) = 1
_Metallic("Metallic", Range(0,1)) = 0
_Roughness("Roughness", Range(0,1)) = 0.3
_BladeBow("Blade Bow", Range(0,1)) = 0.3
_NormalCurvature("Normal Curvature", Range(0,1)) = 0.3
_BladeWidthTexStrength("BladeWidthTexStrength", Range(0,1)) = 0.3
_InnerColor("Inner Color", Color) = (1,1,1,1)
_Color0("Color LOD 0", Color) = (0.3, 1.0, 0.3, 1.0)
_Color1("Color LOD 1", Color) = (1.0, 0.7, 0.3, 1.0)
_Color2("Color LOD 2", Color) = (1.0, 0.3, 0.1, 1.0)
_Color3("Color LOD 3", Color) = (0.8, 0.0, 0.0, 1.0)
}
SubShader {
LOD 100

View File

@ -1,46 +0,0 @@
Shader "Unlit/SkyColorTestShader"
{
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
half3 worldRefl : TEXCOORD0;
float4 pos : SV_POSITION;
};
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
// compute world space position of the vertex
float3 worldPos = mul(unity_ObjectToWorld, vertex).xyz;
// compute world space view direction
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// world space normal
float3 worldNormal = UnityObjectToWorldNormal(normal);
// world space reflection vector
o.worldRefl = reflect(-worldViewDir, worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the default reflection cubemap, using the reflection vector
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);
// decode cubemap data into actual color
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
// output it!
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDHLSL
}
}
}

View File

@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: 633e3c163a26aee4395de6b81b1aae3b
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,83 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: SkyTest
m_Shader: {fileID: 4800000, guid: 633e3c163a26aee4395de6b81b1aae3b, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 944161d6703f1e244a9216da9f82501b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,14 +1,14 @@
// Material Properties
float _BladeWidth, _BendStrength;
float4 _Color0, _Color1, _Color2, _Color3, _InnerColor;
float _SegmentsMinusOne, _FieldSize, _AOStrength, _Translucency, _SSSStrength, _Metallic, _Roughness, _BladeBow, _MaxBladeHeight, _HighlightStrength, _BladeWidthTexStrength;
sampler2D _FrabVarianz, _BladeWidthTex;
float4 _InnerColor;
float _AOStrength, _Translucency, _SSSStrength, _Metallic, _Roughness, _NormalCurvature, _BladeWidthTexStrength;
sampler2D _FieldColor, _BladeWidthTex;
float2 _TransitionRange;
#if UNITY_EDITOR
float3 _WorldSpaceCameraPosEditor;
#endif
// Custom variable that is set to the position of the main camera in the scene
// (instead of Unity's _WorldSpaceCameraPos, which uses the scene view camera in editor mode)
float3 _MainCameraPosition;
struct MeshData {
float4 vertex : POSITION;
@ -23,12 +23,12 @@ struct v2g {
struct g2f {
float4 pos : SV_POSITION;
#ifdef IS_IN_BASE_PASS
float3 normal : NORMAL;
float3 worldPos : TEXCOORD3;
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
float transitionInterpolator : TEXCOORD2;
float3 worldPos : TEXCOORD2;
#endif
float3 debug : TEXCOORD3;
};
v2g vert (MeshData v) {
@ -39,14 +39,6 @@ v2g vert (MeshData v) {
return o;
}
float getCameraDistance(float3 pos) {
#if UNITY_EDITOR
return distance(_WorldSpaceCameraPosEditor, pos);
#else
return distance(_WorldSpaceCameraPos, pos);
#endif
}
#if _LOD_LOD_0
[MaxVertexCount(4 + ( 11 - 1 ) * 2)]
#elif _LOD_LOD_1
@ -59,7 +51,6 @@ float getCameraDistance(float3 pos) {
void geom(point v2g IN[1], inout TriangleStream<g2f> 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));
@ -67,9 +58,9 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> 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;
@ -81,225 +72,219 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> 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;
float3 previousSegmentCenter = basePos - float3(0,-1,0); // point beneath the basePos
o.debug = float3(0,0,0);
int numSegments;
#if _LOD_LOD_0
int numSegments = lerp(11.99, 5.0, interpolator); // 11 - 5
numSegments = lerp(11.99, 5.0, interpolator); // 11 - 5
#elif _LOD_LOD_1
int numSegments = lerp(5.99, 2.0, interpolator); // 5 - 2
numSegments = lerp(5.99, 2.0, interpolator); // 5 - 2
#elif _LOD_LOD_2
int numSegments = 2;
numSegments = 2;
#elif _LOD_LOD_3
int numSegments = 1;
numSegments = 1;
#endif
float3 localCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1.0)).xyz;
const float lowerThickness = 0.8;
for (int i = 0; i < numSegments; i++) {
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);
float3 segmentCenterBase = basePos;
float3 segmentCenterTip = basePos + tipOffset;
const float3 vertLeftBase = segmentCenterBase + widthOffset * 2.5;
const float3 vertRightBase = segmentCenterBase - widthOffset * 2.5;
const float3 vertLeftTip = segmentCenterTip + widthOffset * 0.4;
const float3 vertRightTip = segmentCenterTip - widthOffset * 0.4;
const float3 surfTangent1 = vertLeftBase - vertLeftTip;
const float3 surfTangent2 = vertLeftBase - vertRightBase;
const float3 normal = normalize(cross(surfTangent1, surfTangent2)); // TODO remove normalize
float3 normalWS = UnityObjectToWorldNormal(normal);
const float3 camVec = normalize(vertLeftBase - localCameraPos);
// at which side of the blade are we looking?
const bool lookingFromAbove = dot(camVec, normalWS) > 0;
if (lookingFromAbove) {
normalWS = -normalWS;
}
//normalWS = float3(1,1,1) * lookingFromAbove;
//normalWS = basePosWS - _WorldSpaceCameraPos.xyz;
//normalWS = camVec;
o.pos = UnityObjectToClipPos(vertLeftBase);
#ifndef IS_IN_SHADOW_PASS
o.uv = float2(0, 0);
TRANSFER_SHADOW(o)
o.transitionInterpolator = interpolator;
o.normal = normalWS;
o.worldPos = mul(unity_ObjectToWorld, vertLeftBase).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertRightBase);
#ifndef IS_IN_SHADOW_PASS
o.uv = float2(1, 0);
TRANSFER_SHADOW(o)
o.transitionInterpolator = interpolator;
o.normal = normalWS;
o.worldPos = mul(unity_ObjectToWorld, vertRightBase).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertLeftTip);
#ifndef IS_IN_SHADOW_PASS
o.uv = float2(0, 1);
TRANSFER_SHADOW(o)
o.transitionInterpolator = interpolator;
o.normal = normalWS;
o.worldPos = mul(unity_ObjectToWorld, vertLeftTip).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertRightTip);
#ifndef IS_IN_SHADOW_PASS
o.uv = float2(1, 1);
TRANSFER_SHADOW(o)
o.transitionInterpolator = interpolator;
o.normal = normalWS;
o.worldPos = mul(unity_ObjectToWorld, vertRightTip).xyz;
#endif
triStream.Append(o);
}
continue;
#endif
// 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;
// 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);
#ifndef IS_IN_SHADOW_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;
}
#endif
o.pos = UnityObjectToClipPos(vertLeft);
#ifndef IS_IN_SHADOW_PASS
o.uv = float2(0, segmentHeightNormalized);
TRANSFER_SHADOW(o)
o.transitionInterpolator = interpolator;
o.normal = normalize(lerp(normalWS, UnityObjectToWorldNormal(normalize(widthOffset)), _BladeBow));
o.worldPos = mul(unity_ObjectToWorld, vertLeft).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertRight);
#ifndef IS_IN_SHADOW_PASS
o.uv = float2(1, segmentHeightNormalized);
TRANSFER_SHADOW(o)
o.transitionInterpolator = interpolator;
o.normal = normalize(lerp(normalWS, UnityObjectToWorldNormal(-normalize(widthOffset)), _BladeBow));
o.worldPos = mul(unity_ObjectToWorld, vertRight).xyz;
#endif
triStream.Append(o);
previousSegmentCenter = segmentCenterSnapshot;
}
const float segmentWidth = halfWidth * pow(sin(lowerThickness * 3.141), .8);
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;
const float3 vertRightBase = segmentCenterBase - widthOffset * 2.5;
const float3 vertLeftTip = segmentCenterTip + widthOffset * 0.4;
const float3 vertRightTip = segmentCenterTip - widthOffset * 0.4;
o.pos = UnityObjectToClipPos(basePos + tipOffset * float3(_BendStrength, 1, _BendStrength));
#ifndef IS_IN_SHADOW_PASS
o.uv = float2(.5, 1);
const float3 surfTangent1 = vertRightBase - segmentCenterTip;
const float3 surfTangent2 = vertLeftBase - segmentCenterTip;
const float3 normal = cross(surfTangent1, surfTangent2);
o.pos = UnityObjectToClipPos(vertRightBase);
#ifdef IS_IN_BASE_PASS
o.uv = float2(1, 0);
TRANSFER_SHADOW(o)
o.transitionInterpolator = interpolator;
o.normal = o.normal > 0 ? float3(0, 1, 0) : float3(0, -1, 0);
o.localNormal = normal;
o.worldPos = mul(unity_ObjectToWorld, float4(vertRightBase,1.0)).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertLeftBase);
#ifdef IS_IN_BASE_PASS
o.uv = float2(0, 0);
TRANSFER_SHADOW(o)
o.localNormal = normal;
o.worldPos = mul(unity_ObjectToWorld, float4(vertLeftBase,1.0)).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertRightTip);
#ifdef IS_IN_BASE_PASS
o.uv = float2(1, 1);
TRANSFER_SHADOW(o)
o.localNormal = normal;
o.worldPos = mul(unity_ObjectToWorld, float4(vertRightTip,1.0)).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertLeftTip);
#ifdef IS_IN_BASE_PASS
o.uv = float2(0, 1);
TRANSFER_SHADOW(o)
o.localNormal = normal;
o.worldPos = mul(unity_ObjectToWorld, float4(vertLeftTip,1.0)).xyz;
#endif
triStream.Append(o);
#else
const float3 widthOffset = getWidthOffset(tipOffset.xz) * max(1, mainCamDist / 10);
// 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
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);
#if _LOD_LOD_0
const float curvature = _NormalCurvature;
#elif _LOD_LOD_1
const float curvature = lerp(_NormalCurvature, 0.0, interpolator);
#else
const float curvature = 0.0;
#endif
float3 leftCurvedNormal = normalize(normalize(normal) + widthOffset * curvature);
float3 rightCurvedNormal = normalize(normalize(normal) - widthOffset * curvature);
previousVertLeft = vertLeft;
previousVertRight = vertRight;
#endif
o.pos = UnityObjectToClipPos(vertRight);
#ifdef IS_IN_BASE_PASS
o.uv = float2(1, segmentHeightNormalized);
TRANSFER_SHADOW(o)
o.localNormal = rightCurvedNormal;
o.worldPos = mul(unity_ObjectToWorld, float4(vertRight,1.0)).xyz;
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertLeft);
#ifdef IS_IN_BASE_PASS
o.uv = float2(0, segmentHeightNormalized);
TRANSFER_SHADOW(o)
o.localNormal = leftCurvedNormal;
o.worldPos = mul(unity_ObjectToWorld, float4(vertLeft,1.0)).xyz;
#endif
triStream.Append(o);
}
// 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.localNormal = normal;
o.worldPos = mul(unity_ObjectToWorld, float4(tipPosObjectSpace,1.0)).xyz;
#endif
triStream.Append(o);
#endif
}
float4 frag(g2f i) : SV_Target{
#ifndef IS_IN_SHADOW_PASS
float4 frag(g2f i, bool isFrontFace : SV_IsFrontFace) : SV_Target {
#ifdef IS_IN_BASE_PASS
float3 localPos = mul(unity_WorldToObject, float4(i.worldPos, 1)).xyz;
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);
// at which side of the blade are we looking?
// VERY INACCURATE, because the normals don't match the geometry (smoothing, fake curvature)
if (!isFrontFace) {
worldNormal = -worldNormal;
}
float3 worldNormal = normalize(i.normal);
float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, worldNormal);
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 worldRefl = reflect(-worldViewDir, worldNormal);
// same as in previous shader
half4 skyData = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldRefl, 0);
half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
float4 skyData = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldRefl, 0);
float3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
float3 lightReflectDirection = reflect(-lightDirection, worldNormal);
float3 viewDirection = normalize(float3(float4(_WorldSpaceCameraPos.xyz, 1.0) - i.worldPos.xyz));
float atten = 1.0;
float3 viewDirection = normalize(float3(float4(_MainCameraPosition.xyz, 1.0) - i.worldPos.xyz));
float3 translucency = atten * _LightColor0.xyz * max(0.0, dot(worldNormal, -lightDirection)) * _Translucency;
float3 translucency = _LightColor0.xyz * max(0.0, dot(worldNormal, -lightDirection)) * _Translucency;
float3 subsurf = max(0.0, dot(lightReflectDirection, -viewDirection)) * _SSSStrength * _InnerColor;
const fixed shadow = SHADOW_ATTENUATION(i);
const float shadow = max(.35, SHADOW_ATTENUATION(i));
float3 lightFinal = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldNormal, 2.0);
lightFinal += translucency;
lightFinal += subsurf;
lightFinal *= max(.35, shadow);
lightFinal *= 1.0;
lightFinal *= shadow;
// ATTENTION DO NOT TRY TO UNDERSTAND THE CODE BELOW (or you will go crazy)
fixed4 middleCol = min(1, abs(i.uv.x - 0.5) * 1);
fixed4 col = max(0.87, pow(middleCol, .01));
fixed4 farbvarianz = tex2Dlod(_FrabVarianz, float4(i.worldPos.xz / 40, 0, 3));
fixed4 farbvarianz2 = tex2Dlod(_FrabVarianz, float4(i.worldPos.xz / 10, 0, 3));
fixed4 farbvarianz3 = tex2Dlod(_FrabVarianz, float4(i.worldPos.xz / 3, 0, 3));
fixed4 bladeWidthTex = tex2Dlod(_BladeWidthTex, float4(i.uv.x, 0, 0, 3));
float4 middleCol = min(1, abs(i.uv.x - 0.5) * 1);
float4 col = max(0.87, pow(middleCol, .01));
float3 c0 = tex2Dlod(_FieldColor, float4(i.worldPos.xz / 40, 0, 3)).xyz;
float3 c1 = tex2Dlod(_FieldColor, float4(i.worldPos.xz / 10, 0, 3)).xyz;
float3 c2 = tex2Dlod(_FieldColor, float4(i.worldPos.xz / 3, 0, 3)).xyz;
float4 bladeWidthTex = tex2Dlod(_BladeWidthTex, float4(i.uv.x, 0, 0, 3));
fixed4 albedo = lerp(farbvarianz2, farbvarianz3, 0.4);
albedo = lerp(albedo, farbvarianz, _Metallic) * 0.7;
float3 albedo = lerp(c1, c2, 0.4);
albedo = lerp(albedo, c0, _Metallic) * 0.7;
albedo *= 1 + _AOStrength;
albedo *= lerp(1, bladeWidthTex.x * 2.5, _BladeWidthTexStrength);
fixed3 color = fixed3(skyColor * max(.35, shadow) * .2 + lightFinal * albedo * col);
fixed3 highlightedColor = lerp(color, fixed3(2.5, 0.7, 0.5), 0.5);
fixed3 finalTestColor = albedo.xyz ;
float3 color = float3(skyColor * shadow * .2 + lightFinal * albedo * col);
float3 highlightedColor = lerp(color, float3(2.5, 0.7, 0.5), 0.5);
float3 finalTestColor = albedo.xyz ;
lightFinal = min(1.4, max(0.4, lightFinal));
return float4(lightFinal * albedo * 2, 0);
//return float4(worldNormal, 0); dot(worldNormal, lightDirection)
#endif
return 0;
return float4(color, 1);
#endif
return 0; // shadow pass
}

View File

@ -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) {