Skip to content

Instantly share code, notes, and snippets.

@forkercat
Last active August 14, 2023 05:06
Show Gist options
  • Save forkercat/fb6c030c17fe1e109a34f1c92571943f to your computer and use it in GitHub Desktop.
Save forkercat/fb6c030c17fe1e109a34f1c92571943f to your computer and use it in GitHub Desktop.
Making Sky's Stylized Grass with Compute Shader in Unity. Check out junhaow.com for an article! Update: https://www.patreon.com/posts/urp-compute-54164790
// Credit: https://www.patreon.com/posts/grass-geometry-1-40090373
using UnityEngine;
public class ShaderInteractor : MonoBehaviour
{
private void Update()
{
// Set player position
Shader.SetGlobalVector("_MovingPosition", transform.position);
// Set player movement speed if you can have the value
// When the value is greater than zero, surround grass
// will be highlighted. Set it 0 to ignore this effect!
Shader.SetGlobalFloat("_MovingSpeedPercent", 0);
}
}
//
// Created by @Forkercat on 03/04/2021.
//
// A URP grass shader using compute shader rather than geometry shader.
// This file contains vertex and fragment functions. It also defines the
// structures which should be the same with the ones used in SkylikeGrassCompute.compute.
//
// References & Credits:
// 1. GrassBlades.hlsl (https://gist.github.com/NedMakesGames/3e67fabe49e2e3363a657ef8a6a09838)
// 2. GrassGeometry.shader (https://pastebin.com/VQHj0Uuc)
//
// Make sure this file is not included twice
#ifndef SKYLIKE_GRASS_INCLUDED
#define SKYLIKE_GRASS_INCLUDED
// Include some helper functions
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
// This describes a vertex on the generated mesh
struct DrawVertex
{
float3 positionWS; // The position in world space
float2 uv;
float3 brushColor;
};
// A triangle on the generated mesh
struct DrawTriangle
{
float3 normalWS;
float3 pivotWS;
DrawVertex vertices[3]; // The three points on the triangle
};
// A buffer containing the generated mesh
StructuredBuffer<DrawTriangle> _DrawTriangles;
struct v2f
{
float4 positionCS : SV_POSITION; // Position in clip space
float2 uv : TEXCOORD0; // The height of this vertex on the grass blade
float3 positionWS : TEXCOORD1; // Position in world space
float3 normalWS : TEXCOORD2; // Normal vector in world space
float3 brushColor : COLOR;
};
// Properties
float4 _BaseTex_ST;
TEXTURE2D(_BaseTex);
SAMPLER(sampler_BaseTex);
float4 _TopColor;
float4 _BaseColor;
float _AmbientStrength;
float _DiffuseStrength;
float _FogStartDistance;
float _FogEndDistance;
uniform float _HighlightRadius; // set by compute renderer (not exposed)
uniform float3 _MovingPosition; // set by player controller
uniform float _MovingSpeedPercent;
// ----------------------------------------
// Vertex function
// -- retrieve data generated from compute shader
v2f vert(uint vertexID : SV_VertexID)
{
// Initialize the output struct
v2f output;
// Get the vertex from the buffer
// Since the buffer is structured in triangles, we need to divide the vertexID by three
// to get the triangle, and then modulo by 3 to get the vertex on the triangle
DrawTriangle tri = _DrawTriangles[vertexID / 3];
DrawVertex input = tri.vertices[vertexID % 3];
// No Billboard
// output.positionCS = TransformWorldToHClip(input.positionWS);
// Billboard
float4 pivotWS = float4(tri.pivotWS, 1);
float4 pivotVS = mul(UNITY_MATRIX_V, pivotWS);
float4 worldPos = float4(input.positionWS, 1);
float4 flippedWorldPos = float4(-1, 1, -1, 1) * (worldPos - pivotWS) + pivotWS;
float4 viewPos = flippedWorldPos - pivotWS + pivotVS;
output.positionCS = mul(UNITY_MATRIX_P, viewPos);
output.positionWS = input.positionWS;
output.normalWS = normalize(tri.normalWS);
output.uv = input.uv;
output.brushColor = input.brushColor;
return output;
}
// ----------------------------------------
// Fragment function
half4 frag(v2f input) : SV_Target
{
#ifdef SHADERPASS_SHADOWCASTER
// For Shadow Caster Pass
return 0;
#else
// float shadow = 0;
// #if SHADOWS_SCREEN
// half4 shadowCoord = ComputeScreenPos(input.positionCS);
// #else
// half4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
// #endif // SHADOWS_SCREEN
//
// Light mainLight = GetMainLight(shadowCoord);
//
// #ifdef _MAIN_LIGHT_SHADOWS
// shadow = mainLight.shadowAttenuation;
// #endif
Light mainLight = GetMainLight();
float3 baseColor = lerp(_BaseColor, _TopColor, saturate(input.uv.y)) * input.brushColor;
float3 ambient = baseColor * _AmbientStrength;
float3 diffuse = baseColor * _DiffuseStrength;
float NdotL = max(0, dot(mainLight.direction, input.normalWS));
diffuse *= NdotL;
// Combine
float4 combined = float4(ambient + diffuse, 1);
// Fog
float distFromCamera = distance(_WorldSpaceCameraPos, input.positionWS);
float fogFactor = (distFromCamera - _FogStartDistance) / (_FogEndDistance - _FogStartDistance);
combined.rgb = MixFog(combined.rgb, 1 - saturate(fogFactor));
// Interactor Highlight
float distFromMovingPosition = distance(_MovingPosition, input.positionWS);
if (distFromMovingPosition < _HighlightRadius)
{
combined.rgb *= (1 + _MovingSpeedPercent);
}
// Texture Mask Color (pure white + alpha)
half4 texMaskColor = SAMPLE_TEXTURE2D(_BaseTex, sampler_BaseTex, input.uv);
return combined * texMaskColor;
#endif // SHADERPASS_SHADOWCASTER
}
#endif // SKYLIKE_GRASS_INCLUDED
//
// Created by @Forkercat on 03/04/2021.
//
// A URP grass shader using compute shader rather than geometry shader.
// It includes "SkylikeGrass.hlsl" which contains vertex and fragment functions.
//
// Note that this shader works with the grass painter tool created by MinionsArt.
// Checkout the website for the tool scripts. I also made an updated version that
// introduces shortcuts just for convenience.
// https://www.patreon.com/posts/geometry-grass-46836032
//
Shader "Grass/SkylikeGrass"
{
Properties
{
_BaseTex ("Base Texture", 2D) = "white" {}
_TopColor("Top color", Color) = (1, 1, 1, 1) // Color of the highest layer
_BaseColor("Base color", Color) = (1, 1, 1, 1) // Color of the lowest layer
_AmbientStrength("Ambient Strength", Float) = 1.2
_DiffuseStrength("Diffuse Strength", Float) = 2
}
SubShader {
// UniversalPipeline needed to have this render in URP
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" }
// Forward Lit Pass
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
Cull Off // No culling since the grass must be double sided
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
// Signal this shader requires a compute buffer
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 5.0
// Alpha
#pragma shader_feature_local_fragment _ALPHATEST_ON
#pragma shader_feature_local_fragment _ALPHAPREMULTIPLY_ON
// Lighting and shadow keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _ADDITIONAL_LIGHTS
#pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile _ _SHADOWS_SOFT
#pragma multi_compile_fog
// Register our functions
#pragma vertex vert
#pragma fragment frag
// Include vertex and fragment functions
#include "SkylikeGrass.hlsl"
ENDHLSL
}
// Shadow Casting Pass
// In my use-case, I do not need it.
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
HLSLPROGRAM
// Signal this shader requires geometry function support
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 5.0
// Support all the various light ypes and shadow paths
#pragma multi_compile_shadowcaster
// Register our functions
#pragma vertex vert
#pragma fragment frag
// A custom keyword to modify logic during the shadow caster pass
#define SHADERPASS_SHADOWCASTER
#pragma shader_feature_local _ DISTANCE_DETAIL
// Include vertex and fragment functions
#include "SkylikeGrass.hlsl"
ENDHLSL
}
}
}
//
// Created by @Forkercat on 03/04/2021.
//
// A URP grass shader using compute shader rather than geometry shader.
// This file contains kernel function which works like a geometry function.
// It defines the buffers needed to pass data from renderer C# script to here
// and from here to our vertex and fragment functions.
//
// Note that data flows differently in different cases.
// [Compute Shader] Data Flow : Mesh -> Compute Shader -> Vertex Shader -> Fragment Shader
// [Geometry shader] Data Flow : Mesh -> Vertex Shader -> Geometry Shader -> Fragment Shader
//
// Please check out NedMakesGames for learning compute shaders and MinionsArt for
// the logic of generating grass, although the scripts are pretty different though.
// Let me know if you have any question!
//
// Note that this shader works with the grass painter tool created by MinionsArt.
// Checkout the website for the tool scripts. I also made an updated version that
// introduces shortcuts just for convenience.
// https://www.patreon.com/posts/geometry-grass-46836032
//
// References & Credits:
// 1. GrassBladesCompute.hlsl (NedMakesGames, https://gist.github.com/NedMakesGames/3e67fabe49e2e3363a657ef8a6a09838)
// 2. GrassGeometry.shader (MinionsArt, https://pastebin.com/VQHj0Uuc)
//
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel Main
// Import some helper functions
// ...
// Define some constants
#define PI 3.14159265358979323846
#define TWO_PI 6.28318530717958647693
// This describes a vertex on the source mesh
struct SourceVertex
{
float3 positionOS; // position in object space
float3 normalOS;
float2 uv; // contains widthMultiplier, heightMultiplier
float3 color;
};
// Source buffers, arranged as a vertex buffer and index buffer
StructuredBuffer<SourceVertex> _SourceVertices;
// This describes a vertex on the generated mesh
struct DrawVertex
{
float3 positionWS; // The position in world space
float2 uv;
float3 brushColor;
};
// A triangle on the generated mesh
struct DrawTriangle
{
float3 normalWS;
float3 pivotWS; // For Billboard Effect
DrawVertex vertices[3]; // The three points on the triangle
};
// A buffer containing the generated mesh
AppendStructuredBuffer<DrawTriangle> _DrawTriangles;
// The indirect draw call args, as described in the renderer script
struct IndirectArgs
{
uint numVerticesPerInstance;
uint numInstances;
uint startVertexIndex;
uint startInstanceIndex;
};
// The kernel will count the number of vertices, so this must be RW enabled
RWStructuredBuffer<IndirectArgs> _IndirectArgsBuffer;
// ----------------------------------------
// Variables set by the renderer
int _NumSourceVertices;
// Local to world matrix
float4x4 _LocalToWorld;
// Time
float _CurrentTime;
// Texture
half _TexHeight;
half _TexWidth;
float _TexRandomHeight;
float _TexRandomWidth;
float _TexRandomSize;
float _TexFloatHeight;
// Wind
half _WindSpeed;
float _WindStrength;
float _WindLeaningDist;
// Interactor
half _InteractorRadius, _InteractorStrength;
// Camera
float _HideDistance;
// Uniforms
uniform float3 _MovingPosition;
uniform float3 _CameraPositionWS;
// ----------------------------------------
// Helper Functions
float rand(float3 co)
{
return frac(
sin(dot(co.xyz, float3(12.9898, 78.233, 53.539))) * 43758.5453);
}
// A function to compute an rotation matrix which rotates a point
// by angle radians around the given axis
// By Keijiro Takahashi
float3x3 AngleAxis3x3(float angle, float3 axis)
{
float c, s;
sincos(angle, s, c);
float t = 1 - c;
float x = axis.x;
float y = axis.y;
float z = axis.z;
return float3x3(
t * x * x + c, t * x * y - s * z, t * x * z + s * y,
t * x * y + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c);
}
// Generate each vertex for output triangles
DrawVertex GenerateVertex(float3 positionWS, float3 rightDirWS, float3 forwardDirWS,
float startHeight, float verticalOffset, float horizonOffset,
float2 windOffset, float windLeaningOffset, float2 interactorOffset,
float2 uv, float3 color)
{
DrawVertex output;
// Position
positionWS += float3(0, 1, 0) * (startHeight + verticalOffset);
positionWS += float3(1, 0, 0) * horizonOffset;
// Offset
positionWS += rightDirWS * (windOffset.x + windLeaningOffset - interactorOffset.x); // right
positionWS += forwardDirWS * (windOffset.y - interactorOffset.y); // forward
output.positionWS = positionWS;
// UV
output.uv = uv;
// Color
output.brushColor = color;
return output;
}
// ----------------------------------------
// The main kernel
[numthreads(128, 1, 1)]
void Main(uint3 id : SV_DispatchThreadID)
{
// Return if every triangle has been processed
if ((int)id.x >= _NumSourceVertices)
{
return;
}
SourceVertex sv = _SourceVertices[id.x];
// Camera distance for culling
float3 positionWS = mul(_LocalToWorld, float4(sv.positionOS, 1)).xyz;
float distanceFromCamera = distance(positionWS, _CameraPositionWS);
if (distanceFromCamera > _HideDistance)
{
return;
}
float3 normalOS = normalize(sv.normalOS);
float3 surfaceRightDirOS = normalize(cross(float3(0, 0, 1), normalOS)); // parallel to the surface
float3 surfaceForwardDirOS = normalize(cross(surfaceRightDirOS, sv.normalOS));
float3 worldUp = float3(0, 1, 0); // world up rather than surface up!
// Random size
_TexWidth *= sv.uv.x; // UV.x == width multiplier (set in TexGeometryGrassPainter.cs)
_TexHeight *= sv.uv.y; // UV.y == height multiplier (set in TexGeometryGrassPainter.cs)
_TexWidth *= clamp(rand(sv.positionOS.zyx), 1 - _TexRandomWidth, 1 + _TexRandomWidth);
_TexHeight *= clamp(rand(sv.positionOS.xyz), 1 - _TexRandomHeight, 1 + _TexRandomHeight);
// for uniform size
float sizeMultiplier = clamp(rand(sv.positionOS.yxz), 1 - _TexRandomSize, 1 + _TexRandomSize);
_TexWidth *= sizeMultiplier;
_TexHeight *= sizeMultiplier;
// Wind
float3 v0 = sv.positionOS.xyz;
float2 windOffset = float2(sin(_CurrentTime.x * _WindSpeed + v0.x)
+ sin(_CurrentTime.x * _WindSpeed + v0.z * 2)
+ sin(_CurrentTime.x * _WindSpeed * 0.1 + v0.x), // right
cos(_CurrentTime.x * _WindSpeed + v0.x * 2)
+ cos(_CurrentTime.x * _WindSpeed + v0.z)); // forward
float windLeaningOffset = windOffset.x * _WindLeaningDist * 0.01;
windOffset *= _WindStrength * 0.1;
// Interactivity
float3 dis = distance(_MovingPosition, positionWS);
float3 radius = 1 - saturate(dis / _InteractorRadius);
// in world radius based on objects interaction radius
float2 interactorOffset = positionWS.xz - _MovingPosition.xz; // position comparison
interactorOffset *= radius; // position multiplied by radius for falloff
// increase strength
interactorOffset = clamp(interactorOffset.xy * _InteractorStrength, -1, 1);
// Convert directional vectors to world space
float3 surfaceRightDirWS = normalize(mul(_LocalToWorld, float4(surfaceRightDirOS, 0)).xyz);
float3 surfaceForwardDirWS = normalize(mul(_LocalToWorld, float4(surfaceForwardDirOS, 0)).xyz);
// Bottom-Left Triangle
DrawTriangle tri = (DrawTriangle) 0;
tri.vertices[0] = GenerateVertex(positionWS, surfaceRightDirWS, surfaceForwardDirWS,
_TexFloatHeight, 0, -_TexWidth / 2, windOffset, -windLeaningOffset,
interactorOffset, float2(1, 0), sv.color);
tri.vertices[1] = GenerateVertex(positionWS, surfaceRightDirWS, surfaceForwardDirWS,
_TexFloatHeight, 0, _TexWidth / 2, windOffset, -windLeaningOffset,
interactorOffset, float2(0, 0), sv.color);
tri.vertices[2] = GenerateVertex(positionWS, surfaceRightDirWS, surfaceForwardDirWS,
_TexFloatHeight, _TexHeight, -_TexWidth / 2, windOffset, windLeaningOffset,
interactorOffset, float2(1, 1), sv.color);
tri.normalWS = worldUp;
float3 pivotWS = (tri.vertices[1].positionWS + tri.vertices[2].positionWS) / 2.0;
tri.pivotWS = pivotWS;
_DrawTriangles.Append(tri);
// Top-Right Triangle
tri = (DrawTriangle) 0;
tri.vertices[0] = GenerateVertex(positionWS, surfaceRightDirWS, surfaceForwardDirWS,
_TexFloatHeight, 0, _TexWidth / 2, windOffset, -windLeaningOffset,
interactorOffset, float2(0, 0), sv.color);
tri.vertices[1] = GenerateVertex(positionWS, surfaceRightDirWS, surfaceForwardDirWS,
_TexFloatHeight, _TexHeight, _TexWidth / 2, windOffset, windLeaningOffset,
interactorOffset, float2(0, 1), sv.color);
tri.vertices[2] = GenerateVertex(positionWS, surfaceRightDirWS, surfaceForwardDirWS,
_TexFloatHeight, _TexHeight, -_TexWidth / 2, windOffset, windLeaningOffset,
interactorOffset, float2(1, 1), sv.color);
tri.normalWS = worldUp;
tri.pivotWS = pivotWS; // two triangles share the same pivot
_DrawTriangles.Append(tri);
// InterlockedAdd(a, b) adds b to a and stores the value in a. It is thread-safe
// This call counts the number of vertices, storing it in the indirect arguments
// This tells the renderer how many vertices are in the mesh in DrawProcedural
InterlockedAdd(_IndirectArgsBuffer[0].numVerticesPerInstance, 3 * 2); // 2 triangles
}
//
// Created by @Forkercat on 03/04/2021.
//
// A URP compute shader renderer to upload data (created by grass painter tool)
// to compute shader. Check out shader files for mode definitions and comments.
//
// [Added Features]
// 1. Override Material Properties (color, ambient strength)
// 2. Cast Shadow Checkbox (since we are not using MeshRenderer anymore, we can do it here)
//
// [Usage]
// 1. Create an empty object and create the material
// 2. Put SkylikeGrassPainterEditor.cs to Asset/Editor
// 3. Drag SkylikeGrassPainter.cs to the object (you can use the old one as well)
// 4. Drag this script (SkylikeGrassComputeRenderer.cs) to the object
// 5. Set up material and compute shader in the inspector
//
// Please check out NedMakesGames for learning compute shaders and MinionsArt for
// the logic of generating grass, although the scripts are pretty different though.
// Let me know if you have any question!
//
// Note that this shader works with the grass painter tool created by MinionsArt.
// Checkout the website for the tool scripts. I also made an updated version that
// introduces shortcuts just for convenience.
// https://www.patreon.com/posts/geometry-grass-46836032
//
// References & Credits:
// 1. ProceduralGrassRenderer.cs (NedMakesGames, https://gist.github.com/NedMakesGames/3e67fabe49e2e3363a657ef8a6a09838)
// 2. Geometry Grass Shader Tool (MinionsArt, https://www.patreon.com/posts/grass-geometry-1-40090373)
//
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]
public class SkylikeGrassComputeRenderer : MonoBehaviour
{
[Header("Components")]
[SerializeField] private SkylikeGrassPainter grassPainter = default;
[SerializeField] private Mesh sourceMesh = default;
[SerializeField] private Material material = default;
[SerializeField] private ComputeShader computeShader = default;
// Texture
[Header("Texture")]
public float texHeight = 0.1f;
public float texWidth = 0.04f;
public float texRandomSize = 0.3f;
public float texRandomHeight = 0f;
public float texRandomWidth = 0f;
public float texFloatHeight = 0.05f;
// Wind
[Header("Wind")]
public float windSpeed = 9;
public float windStrength = 0.1f;
public float windLeaningDist = 0.4f;
// Interactor
[Header("Interactor")]
public float affectRadius = 0.7f;
public float affectStrength = 2f;
public float highlightRadius = 1.5f;
// LOD
[Header("LOD")]
public float hideDistance = 40;
// Material
[Header("Material")]
public bool overrideMaterial;
public Color topColor = new Color(0, 1, 0);
public Color baseColor = new Color(1, 1, 0);
public float ambientStrength = 1.2f;
public float diffuseStrength = 2.5f;
// Other
[Header("Other")]
public bool castShadow;
private Camera m_MainCamera;
// The structure to send to the compute shader
// This layout kind assures that the data is laid out sequentially
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind
.Sequential)]
private struct SourceVertex
{
public Vector3 position;
public Vector3 normal;
public Vector2 uv;
public Vector3 color;
}
// A state variable to help keep track of whether compute buffers have been set up
private bool m_Initialized;
// A compute buffer to hold vertex data of the source mesh
private ComputeBuffer m_SourceVertBuffer;
// A compute buffer to hold vertex data of the generated mesh
private ComputeBuffer m_DrawBuffer;
// A compute buffer to hold indirect draw arguments
private ComputeBuffer m_ArgsBuffer;
// Instantiate the shaders so data belong to their unique compute buffers
private ComputeShader m_InstantiatedComputeShader;
private Material m_InstantiatedMaterial;
// The id of the kernel in the grass compute shader
private int m_IdGrassKernel;
// The x dispatch size for the grass compute shader
private int m_DispatchSize;
// The local bounds of the generated mesh
private Bounds m_LocalBounds;
// The size of one entry in the various compute buffers
private const int SOURCE_VERT_STRIDE = sizeof(float) * (3 + 3 + 2 + 3);
private const int DRAW_STRIDE = sizeof(float) * (3 + 3 + (3 + 2 + 3) * 3);
private const int INDIRECT_ARGS_STRIDE = sizeof(int) * 4;
// The data to reset the args buffer with every frame
// 0: vertex count per draw instance. We will only use one instance
// 1: instance count. One
// 2: start vertex location if using a Graphics Buffer
// 3: and start instance location if using a Graphics Buffer
private int[] argsBufferReset = new int[] {0, 1, 0, 0};
private void OnValidate()
{
// Set up components
m_MainCamera = Camera.main;
grassPainter = GetComponent<SkylikeGrassPainter>();
sourceMesh = grassPainter.mesh;
}
private void OnEnable()
{
// If initialized, call on disable to clean things up
if (m_Initialized)
{
OnDisable();
}
// Setup compute shader and material manually
// Don't do anything if resources are not found,
// or no vertex is put on the mesh.
if (grassPainter == null || sourceMesh == null || computeShader == null || material == null)
{
return;
}
sourceMesh = grassPainter.mesh; // update mesh
if (sourceMesh.vertexCount == 0)
{
return;
}
m_Initialized = true;
// Instantiate the shaders so they can point to their own buffers
m_InstantiatedComputeShader = Instantiate(computeShader);
m_InstantiatedMaterial = Instantiate(material);
// Grab data from the source mesh
Vector3[] positions = sourceMesh.vertices;
Vector3[] normals = sourceMesh.normals;
Vector2[] uvs = sourceMesh.uv;
Color[] colors = sourceMesh.colors;
// Create the data to upload to the source vert buffer
SourceVertex[] vertices = new SourceVertex[positions.Length];
for (int i = 0; i < vertices.Length; i++)
{
Color color = colors[i];
vertices[i] = new SourceVertex()
{
position = positions[i],
normal = normals[i],
uv = uvs[i],
color = new Vector3(color.r, color.g, color.b) // Color --> Vector3
};
}
int numSourceVertices = vertices.Length;
// Create compute buffers
// The stride is the size, in bytes, each object in the buffer takes up
m_SourceVertBuffer = new ComputeBuffer(vertices.Length, SOURCE_VERT_STRIDE,
ComputeBufferType.Structured, ComputeBufferMode.Immutable);
m_SourceVertBuffer.SetData(vertices);
m_DrawBuffer = new ComputeBuffer(numSourceVertices * 2, DRAW_STRIDE,
ComputeBufferType.Append);
m_DrawBuffer.SetCounterValue(0);
m_ArgsBuffer =
new ComputeBuffer(1, INDIRECT_ARGS_STRIDE, ComputeBufferType.IndirectArguments);
// Cache the kernel IDs we will be dispatching
m_IdGrassKernel = m_InstantiatedComputeShader.FindKernel("Main");
// Set buffer data
m_InstantiatedComputeShader.SetBuffer(m_IdGrassKernel, "_SourceVertices",
m_SourceVertBuffer);
m_InstantiatedComputeShader.SetBuffer(m_IdGrassKernel, "_DrawTriangles", m_DrawBuffer);
m_InstantiatedComputeShader.SetBuffer(m_IdGrassKernel, "_IndirectArgsBuffer",
m_ArgsBuffer);
// Set vertex data
m_InstantiatedComputeShader.SetInt("_NumSourceVertices", numSourceVertices);
m_InstantiatedMaterial.SetBuffer("_DrawTriangles", m_DrawBuffer);
m_InstantiatedMaterial.SetShaderPassEnabled("ShadowCaster", castShadow);
if (overrideMaterial)
{
m_InstantiatedMaterial.SetColor("_TopColor", topColor);
m_InstantiatedMaterial.SetColor("_BaseColor", baseColor);
m_InstantiatedMaterial.SetFloat("_AmbientStrength", ambientStrength);
m_InstantiatedMaterial.SetFloat("_DiffuseStrength", diffuseStrength);
}
// Calculate the number of threads to use. Get the thread size from the kernel
// Then, divide the number of triangles by that size
m_InstantiatedComputeShader.GetKernelThreadGroupSizes(m_IdGrassKernel,
out uint threadGroupSize, out _, out _);
m_DispatchSize = Mathf.CeilToInt((float) numSourceVertices / threadGroupSize);
// Get the bounds of the source mesh and then expand by the maximum blade width and height
m_LocalBounds = sourceMesh.bounds;
m_LocalBounds.Expand(Mathf.Max(texHeight + texRandomHeight + texRandomSize,
texWidth + texRandomWidth + texRandomSize));
// localBounds.Expand(1); // default
}
private void OnDisable()
{
// Dispose of buffers and copied shaders here
if (m_Initialized)
{
// If the application is not in play mode, we have to call DestroyImmediate
if (Application.isPlaying)
{
Destroy(m_InstantiatedComputeShader);
Destroy(m_InstantiatedMaterial);
}
else
{
DestroyImmediate(m_InstantiatedComputeShader);
DestroyImmediate(m_InstantiatedMaterial);
}
// Release each buffer
m_SourceVertBuffer?.Release();
m_DrawBuffer?.Release();
m_ArgsBuffer?.Release();
}
m_Initialized = false;
}
// LateUpdate is called after all Update calls
private void LateUpdate()
{
// If in edit mode, we need to update the shaders each Update to make sure settings changes are applied
// Don't worry, in edit mode, Update isn't called each frame
if (Application.isPlaying == false)
{
OnDisable();
OnEnable();
}
// Debug.Log("LateUpdate : " + m_Initialized + " " + sourceMesh.vertexCount);
// If not initialized, do nothing (creating zero-length buffer will crash)
if (!m_Initialized)
{
// Initialization is not done, please check if there are null components
// or just because there is not vertex being painted.
return;
}
// Clear the draw and indirect args buffers of last frame's data
m_DrawBuffer.SetCounterValue(0);
m_ArgsBuffer.SetData(argsBufferReset);
// Transform the bounds to world space
Bounds bounds = TransformBounds(m_LocalBounds);
// Update the shader with frame specific data
SetGrassData();
// Dispatch the grass shader. It will run on the GPU
m_InstantiatedComputeShader.Dispatch(m_IdGrassKernel, m_DispatchSize, 1, 1);
// DrawProceduralIndirect queues a draw call up for our generated mesh
Graphics.DrawProceduralIndirect(m_InstantiatedMaterial, bounds, MeshTopology.Triangles,
m_ArgsBuffer, 0, null, null, ShadowCastingMode.On, true, gameObject.layer);
}
private void SetGrassData()
{
// Compute Shader
m_InstantiatedComputeShader.SetMatrix("_LocalToWorld", transform.localToWorldMatrix);
m_InstantiatedComputeShader.SetFloat("_CurrentTime", Time.time);
m_InstantiatedComputeShader.SetVector("_CameraPositionWS",
m_MainCamera.transform.position);
m_InstantiatedComputeShader.SetFloat("_TexHeight", texHeight);
m_InstantiatedComputeShader.SetFloat("_TexWidth", texWidth);
m_InstantiatedComputeShader.SetFloat("_TexRandomHeight", texRandomHeight);
m_InstantiatedComputeShader.SetFloat("_TexRandomWidth", texRandomWidth);
m_InstantiatedComputeShader.SetFloat("_TexRandomSize", texRandomSize);
m_InstantiatedComputeShader.SetFloat("_TexFloatHeight", texFloatHeight);
m_InstantiatedComputeShader.SetFloat("_WindSpeed", windSpeed);
m_InstantiatedComputeShader.SetFloat("_WindStrength", windStrength);
m_InstantiatedComputeShader.SetFloat("_WindLeaningDist", windLeaningDist);
m_InstantiatedComputeShader.SetFloat("_InteractorRadius", affectRadius);
m_InstantiatedComputeShader.SetFloat("_InteractorStrength", affectStrength);
m_InstantiatedComputeShader.SetFloat("_HideDistance", hideDistance);
// Material
m_InstantiatedMaterial.SetFloat("_FogStartDistance", RenderSettings.fogStartDistance);
m_InstantiatedMaterial.SetFloat("_FogEndDistance", RenderSettings.fogEndDistance);
m_InstantiatedMaterial.SetFloat("_HighlightRadius", highlightRadius);
}
// This applies the game object's transform to the local bounds
// Code by benblo from https://answers.unity.com/questions/361275/cant-convert-bounds-from-world-coordinates-to-loca.html
private Bounds TransformBounds(Bounds boundsOS)
{
var center = transform.TransformPoint(boundsOS.center);
// transform the local extents' axes
var extents = boundsOS.extents;
var axisX = transform.TransformVector(extents.x, 0, 0);
var axisY = transform.TransformVector(0, extents.y, 0);
var axisZ = transform.TransformVector(0, 0, extents.z);
// sum their absolute value to get the world extents
extents.x = Mathf.Abs(axisX.x) + Mathf.Abs(axisY.x) + Mathf.Abs(axisZ.x);
extents.y = Mathf.Abs(axisX.y) + Mathf.Abs(axisY.y) + Mathf.Abs(axisZ.y);
extents.z = Mathf.Abs(axisX.z) + Mathf.Abs(axisY.z) + Mathf.Abs(axisZ.z);
return new Bounds {center = center, extents = extents};
}
}
// Updated Version of The Grass Painter by MinionsArt
// 1. Some renames
// 2. Added several shortcuts
// 3. Added mesh name
// Source: https://pastebin.com/xwhSJkFV
// Tutorial: https://www.patreon.com/posts/geometry-grass-46836032
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;
using UnityEditor;
// Only requires mesh filter
[RequireComponent(typeof(MeshFilter))] // for drawing tool graphics (e.g. solid disc)
[ExecuteInEditMode]
public class SkylikeGrassPainter : MonoBehaviour
{
public Mesh mesh;
private MeshFilter filter;
[SerializeField]
List<Vector3> positions = new List<Vector3>();
[SerializeField]
List<Color> colors = new List<Color>();
[SerializeField]
List<int> indices = new List<int>();
[SerializeField]
List<Vector3> normals = new List<Vector3>();
[SerializeField]
List<Vector2> grassSizeMultipliers = new List<Vector2>();
// Grass Limit
public int grassLimit = 10000;
public int currentGrassAmount = 0;
// Paint Status
public bool painting;
public bool removing;
public bool editing;
public int toolbarInt = 0;
// Brush Settings
public LayerMask hitMask = 1;
public LayerMask paintMask = 1;
public float brushSize = 1f;
public float density = 2f;
public float normalLimit = 1;
// Grass Size
public float widthMultiplier = 1f;
public float heightMultiplier = 1f;
// Color
public Color adjustedColor = Color.white;
public float rangeR, rangeG, rangeB;
// Stored Values
[HideInInspector] public Vector3 hitPosGizmo;
[HideInInspector] public Vector3 hitNormal;
private Vector3 mousePos;
private Vector3 hitPos;
private Vector3 lastPosition = Vector3.zero;
int[] indi;
#if UNITY_EDITOR
void OnFocus()
{
// Remove delegate listener if it has previously
// been assigned.
SceneView.duringSceneGui -= this.OnScene;
// Add (or re-add) the delegate.
SceneView.duringSceneGui += this.OnScene;
}
void OnDestroy()
{
// When the window is destroyed, remove the delegate
// so that it will no longer do any drawing.
SceneView.duringSceneGui -= this.OnScene;
}
private void OnEnable()
{
filter = GetComponent<MeshFilter>();
SceneView.duringSceneGui += this.OnScene;
}
public void ClearMesh()
{
currentGrassAmount = 0;
positions = new List<Vector3>();
indices = new List<int>();
colors = new List<Color>();
normals = new List<Vector3>();
grassSizeMultipliers = new List<Vector2>();
}
void OnScene(SceneView scene)
{
// Fix an error
// https://forum.unity.com/threads/the-object-of-type-x-has-been-destroyed-but-you-are-still-trying-to-access-it.454891/
if (!this)
{
return;
}
// only allow painting while this object is selected
if (Selection.Contains(gameObject))
{
Event e = Event.current;
RaycastHit terrainHit;
mousePos = e.mousePosition;
float ppp = EditorGUIUtility.pixelsPerPoint;
mousePos.y = scene.camera.pixelHeight - mousePos.y * ppp;
mousePos.x *= ppp;
// ray for gizmo (disc)
Ray rayGizmo = scene.camera.ScreenPointToRay(mousePos);
RaycastHit hitGizmo;
if (Physics.Raycast(rayGizmo, out hitGizmo, 200f, hitMask.value))
{
hitPosGizmo = hitGizmo.point;
}
// Events
if (e.type == EventType.MouseDown && e.button == 2)
{
if (e.control)
{
Event.current.Use();
toolbarInt = (toolbarInt + 1) % 3;
}
}
// Change Brush Settings
// -- Brush Size : Control + Scroll Wheel
// -- Brush Density : Alt/Shift/Command + Scroll Wheel
// -- Grass Height Multiplier : Control + Alt/Shift/Command + Scroll Wheel
// ----------------------------------------
// -- Shift + Mouse does not work on macOS
// -- but Shift + Trackpad works fine
if (e.type == EventType.ScrollWheel)
{
float deltaY = -e.delta.y;
if (e.control)
{
Event.current.Use(); // ignore zooming in scene
if (e.alt || e.command || e.shift)
// Change Grass Height Multiplier
heightMultiplier += 0.005f * deltaY;
else
// Change Brush Size
brushSize += 0.03f * deltaY;
}
else if (e.alt || e.command || e.shift)
{
Event.current.Use(); // ignore zooming in scene
// Change Brush Density
density += 0.03f * deltaY;
}
}
// when any of the modifier keys is pressed
bool isModifiedHold = e.control || e.alt || e.shift || e.command;
// Adding
// -- In ADDING mode, ANY MODIFIER KEYS + RIGHT BUTTON
bool isAdding = isModifiedHold && e.type == EventType.MouseDrag && e.button == 1 && toolbarInt == 0;
if (isAdding)
{
// place based on density
for (int k = 0; k < density; k++)
{
// brush range
float t = 2f * Mathf.PI * Random.Range(0f, brushSize);
float u = Random.Range(0f, brushSize) + Random.Range(0f, brushSize);
float r = (u > 1 ? 2 - u : u);
Vector3 origin = Vector3.zero;
// place random in radius, except for first one
if (k != 0)
{
origin.x += r * Mathf.Cos(t);
origin.y += r * Mathf.Sin(t);
}
else
{
origin = Vector3.zero;
}
// add random range to ray
Ray ray = scene.camera.ScreenPointToRay(mousePos);
ray.origin += origin;
// if the ray hits something thats on the layer mask,
// within the grass limit and within the y normal limit
if (Physics.Raycast(ray, out terrainHit, 200f, hitMask.value) &&
currentGrassAmount < grassLimit &&
terrainHit.normal.y <= (1 + normalLimit) &&
terrainHit.normal.y >= (1 - normalLimit))
{
if ((paintMask.value & (1 << terrainHit.transform.gameObject.layer)) > 0)
{
hitPos = terrainHit.point;
hitNormal = terrainHit.normal;
if (k != 0)
{
var grassPosition = hitPos; // + Vector3.Cross(origin, hitNormal);
grassPosition -= this.transform.position;
positions.Add((grassPosition));
indices.Add(currentGrassAmount);
grassSizeMultipliers.Add(new Vector2(widthMultiplier, heightMultiplier));
// add random color variations
colors.Add(new Color(
adjustedColor.r + (Random.Range(0, 1.0f) * rangeR),
adjustedColor.g + (Random.Range(0, 1.0f) * rangeG),
adjustedColor.b + (Random.Range(0, 1.0f) * rangeB), 1));
//colors.Add(temp);
normals.Add(terrainHit.normal);
currentGrassAmount++;
}
else
{
// to not place everything at once, check if the first placed point far enough away from the last placed first one
if (Vector3.Distance(terrainHit.point, lastPosition) > brushSize)
{
var grassPosition = hitPos;
grassPosition -= this.transform.position;
positions.Add((grassPosition));
indices.Add(currentGrassAmount);
grassSizeMultipliers.Add(new Vector2(widthMultiplier, heightMultiplier));
colors.Add(new Color(
adjustedColor.r + (Random.Range(0, 1.0f) * rangeR),
adjustedColor.g + (Random.Range(0, 1.0f) * rangeG),
adjustedColor.b + (Random.Range(0, 1.0f) * rangeB), 1));
normals.Add(terrainHit.normal);
currentGrassAmount++;
if (origin == Vector3.zero)
{
lastPosition = hitPos;
}
}
}
}
}
}
e.Use();
}
// removing mesh points
// -- In REMOVING Mode, ANY MODIFIER KEYS + RIGHT BUTTON
// -- In ANY modes, ANY MODIFIER KEYS + LEFT BUTTON
bool isRemoving = isModifiedHold && e.type == EventType.MouseDrag && e.button == 1 && toolbarInt == 1;
isRemoving |= (e.alt || e.command) && e.type == EventType.MouseDrag && e.button == 0; // ALT / COMMAND + RIGHT CLICK
if (isRemoving)
{
Event.current.Use(); // ignore selecting in scene
Ray ray = scene.camera.ScreenPointToRay(mousePos);
if (Physics.Raycast(ray, out terrainHit, 200f, hitMask.value))
{
hitPos = terrainHit.point;
hitPosGizmo = hitPos;
hitNormal = terrainHit.normal;
for (int j = 0; j < positions.Count; j++)
{
Vector3 pos = positions[j];
pos += this.transform.position;
float dist = Vector3.Distance(terrainHit.point, pos);
// if its within the radius of the brush, remove all info
if (dist <= brushSize)
{
positions.RemoveAt(j);
colors.RemoveAt(j);
normals.RemoveAt(j);
grassSizeMultipliers.RemoveAt(j);
indices.RemoveAt(j);
currentGrassAmount--;
for (int i = 0; i < indices.Count; i++)
{
indices[i] = i;
}
}
}
}
e.Use();
}
// Editing
// -- In EDITING mode, ANY MODIFIER KEYS + RIGHT BUTTON
bool isEditing = isModifiedHold && e.type == EventType.MouseDrag && e.button == 1 && toolbarInt == 2;
if (isEditing)
{
Ray ray = scene.camera.ScreenPointToRay(mousePos);
if (Physics.Raycast(ray, out terrainHit, 200f, hitMask.value))
{
hitPos = terrainHit.point;
hitPosGizmo = hitPos;
hitNormal = terrainHit.normal;
for (int j = 0; j < positions.Count; j++)
{
Vector3 pos = positions[j];
pos += this.transform.position;
float dist = Vector3.Distance(terrainHit.point, pos);
// if its within the radius of the brush, remove all info
if (dist <= brushSize)
{
colors[j] = (new Color(
adjustedColor.r + (Random.Range(0, 1.0f) * rangeR),
adjustedColor.g + (Random.Range(0, 1.0f) * rangeG),
adjustedColor.b + (Random.Range(0, 1.0f) * rangeB), 1));
grassSizeMultipliers[j] = new Vector2(widthMultiplier, heightMultiplier);
}
}
}
e.Use();
}
// set all info to mesh
mesh = new Mesh();
mesh.name = "Grass Mesh - " + name;
mesh.SetVertices(positions);
indi = indices.ToArray();
mesh.SetIndices(indi, MeshTopology.Points, 0);
mesh.SetUVs(0, grassSizeMultipliers);
mesh.SetColors(colors);
mesh.SetNormals(normals);
filter.mesh = mesh;
}
}
#endif
}
// Updated Version of The Grass Paint Editor by MinionsArt
// 1. Some renames
// 2. Added shortcut hint
// 3. Updated UI (progress bar, etc)
// Source: https://pastebin.com/Y7dCRAd3
// Tutorial: https://www.patreon.com/posts/geometry-grass-46836032
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
[CustomEditor(typeof(SkylikeGrassPainter))]
public class SkylikeGrassPainterEditor : Editor
{
SkylikeGrassPainter grassPainter;
readonly string[] toolbarStrings = {"Add", "Remove", "Edit"};
private string shortcutText;
private void OnEnable()
{
grassPainter = (SkylikeGrassPainter) target;
shortcutText = "[ Use Tool ] Modifier Key + Right Click\n";
shortcutText += "[ Remove Grass ] ALT + Left Click\n";
shortcutText += "[ Switch Tool ] Modifier Key + Middle Click\n";
shortcutText += "[ Brush Size ] CTRL + Scroll\n";
shortcutText += "[ Density ] ALT/SHIFT/COMMAND + Scroll\n";
shortcutText += "[ Height Multiplier ] CTRL + ALT/SHIFT/COMMAND + Scroll\n";
}
void OnSceneGUI()
{
Handles.color = Color.cyan;
Handles.DrawWireDisc(grassPainter.hitPosGizmo, grassPainter.hitNormal,
grassPainter.brushSize);
Handles.color = new Color(0, 0.5f, 0.5f, 0.4f);
Handles.DrawSolidDisc(grassPainter.hitPosGizmo, grassPainter.hitNormal,
grassPainter.brushSize);
if (grassPainter.toolbarInt == 1)
{
Handles.color = Color.red;
Handles.DrawWireDisc(grassPainter.hitPosGizmo, grassPainter.hitNormal,
grassPainter.brushSize);
Handles.color = new Color(0.5f, 0f, 0f, 0.4f);
Handles.DrawSolidDisc(grassPainter.hitPosGizmo, grassPainter.hitNormal,
grassPainter.brushSize);
}
if (grassPainter.toolbarInt == 2)
{
Handles.color = Color.yellow;
Handles.DrawWireDisc(grassPainter.hitPosGizmo, grassPainter.hitNormal,
grassPainter.brushSize);
Handles.color = new Color(0.5f, 0.5f, 0f, 0.4f);
Handles.DrawSolidDisc(grassPainter.hitPosGizmo, grassPainter.hitNormal,
grassPainter.brushSize);
}
}
public override void OnInspectorGUI()
{
float barOffset = 20f;
// Grass Limit
EditorGUILayout.LabelField("Grass Limit", EditorStyles.boldLabel);
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUI.ProgressBar(
new Rect(barOffset, barOffset + 10, EditorGUIUtility.currentViewWidth - barOffset * 2,
20f),
(float) grassPainter.currentGrassAmount / grassPainter.grassLimit,
grassPainter.currentGrassAmount.ToString() + " / " + grassPainter.grassLimit);
EditorGUILayout.Space();
EditorGUILayout.Space();
grassPainter.grassLimit =
EditorGUILayout.IntField("Max Grass Amount", grassPainter.grassLimit);
EditorGUILayout.Space();
// Paint Settings
EditorGUILayout.LabelField("Paint Status", EditorStyles.boldLabel);
grassPainter.toolbarInt = GUILayout.Toolbar(grassPainter.toolbarInt, toolbarStrings,
GUI.skin.button, GUILayout.Height(25));
EditorGUILayout.LabelField(new GUIContent("Shortcuts (hover here)", shortcutText));
EditorGUILayout.Space();
// Brush Settings
EditorGUILayout.LabelField("Brush Settings", EditorStyles.boldLabel);
LayerMask tempMask = EditorGUILayout.MaskField("Hit Mask",
InternalEditorUtility.LayerMaskToConcatenatedLayersMask(grassPainter.hitMask),
InternalEditorUtility.layers);
grassPainter.hitMask = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(tempMask);
LayerMask tempMask2 = EditorGUILayout.MaskField("Painting Mask",
InternalEditorUtility.LayerMaskToConcatenatedLayersMask(grassPainter.paintMask),
InternalEditorUtility.layers);
grassPainter.paintMask = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(tempMask2);
grassPainter.brushSize =
EditorGUILayout.Slider("Brush Size", grassPainter.brushSize, 0.1f, 10f);
grassPainter.density = EditorGUILayout.Slider("Density", grassPainter.density, 0.1f, 10f);
grassPainter.normalLimit =
EditorGUILayout.Slider("Normal Limit", grassPainter.normalLimit, 0f, 1f);
EditorGUILayout.Space();
// Grass Size
EditorGUILayout.LabelField("Grass Size", EditorStyles.boldLabel);
grassPainter.heightMultiplier =
EditorGUILayout.Slider("Height Multiplier", grassPainter.heightMultiplier, 0f, 2f);
grassPainter.widthMultiplier =
EditorGUILayout.Slider("Width Multiplier", grassPainter.widthMultiplier, 0f, 2f);
EditorGUILayout.Space();
// Color
EditorGUILayout.LabelField("Color", EditorStyles.boldLabel);
grassPainter.adjustedColor =
EditorGUILayout.ColorField("Brush Color", grassPainter.adjustedColor);
grassPainter.rangeR =
EditorGUILayout.Slider("Random Red", grassPainter.rangeR, 0f, 1f);
grassPainter.rangeG =
EditorGUILayout.Slider("Random Green", grassPainter.rangeG, 0f, 1f);
grassPainter.rangeB =
EditorGUILayout.Slider("Random Blue", grassPainter.rangeB, 0f, 1f);
EditorGUILayout.Space();
// Clear Button
GUI.backgroundColor = new Color(252/255f, 142/255f, 134/255f);
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Clear Mesh", GUILayout.Width(150), GUILayout.Height(25)))
{
if (EditorUtility.DisplayDialog("Clear Painted Mesh?",
"Are you sure you want to clear the mesh?", "Clear", "Don't Clear"))
{
grassPainter.ClearMesh();
}
}
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment