Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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