Last active
August 14, 2023 05:06
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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