using System; using UnityEngine; namespace StixGames.GrassShader { [Serializable] public class GrassPainter { private const float Epsilon = 0.01f; public TextureTarget Target = TextureTarget.Density; public DensityChannel DensityChannel = DensityChannel.R; public ColorHeightChannel ColorHeightChannel = ColorHeightChannel.Color; public BrushMode Brush = BrushMode.Blend; public Color PaintColor = Color.white; public float PaintDensity = 1; public float Strength = 1; public float Size = 1; public float Softness = 0.5f; /// /// The spacing isn't checked by the painter itself, you will have to restrict ApplyBrush calls on the outside. /// public float Spacing = 0.5f; public float Rotation = 0; /// /// The grass collider the brush will be applied to. Only set this when using ApplyBrush directly. /// public Collider[] GrassColliders = new Collider[0]; /// /// The color height texture the brush will be applied to. Only set this when using ApplyBrush directly. /// public Texture2D ColorHeightTexture; /// /// The density texture the brush will be applied to. Only set this when using ApplyBrush directly. /// public Texture2D DensityTexture; public Texture2D CurrentTexture { get { switch (Target) { case TextureTarget.ColorHeight: return ColorHeightTexture; case TextureTarget.Density: return DensityTexture; default: throw new ArgumentOutOfRangeException(); } } set { switch (Target) { case TextureTarget.ColorHeight: ColorHeightTexture = value; break; case TextureTarget.Density: DensityTexture = value; break; default: throw new ArgumentOutOfRangeException(); } } } public bool Draw(Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { GrassColliders = new [] { hit.collider }; ColorHeightTexture = hit.collider.GetComponent().material.GetTexture("_ColorMap") as Texture2D; DensityTexture = hit.collider.GetComponent().material.GetTexture("_DensityTexture") as Texture2D; ApplyBrush(hit.point); return true; } return false; } public void ApplyBrush(Vector3 hitPoint) { if (CurrentTexture == null) { Debug.Log("You haven't created a texture for the current mode yet."); return; } RaycastHit hit; Vector2 texForward, texRight; if (!GrassManipulationUtility.GetWorldToTextureSpaceMatrix(new Ray(hitPoint + Vector3.up * 1000, Vector3.down), Epsilon, GrassColliders, out hit, out texForward, out texRight)) { return; } Vector2 texCoord = hit.textureCoord; //Convert the world space radius to a pixel radius in texture space. This requires square textures. int pixelRadius = (int)(Size * texForward.magnitude * CurrentTexture.width); //Calculate the pixel coordinates of the point where the raycast hit the texture. Vector2 mid = new Vector2(texCoord.x * CurrentTexture.width, texCoord.y * CurrentTexture.height); //Calculate the pixel area where the texture will be changed int targetStartX = (int)(mid.x - pixelRadius); int targetStartY = (int)(mid.y - pixelRadius); int startX = Mathf.Clamp(targetStartX, 0, CurrentTexture.width); int startY = Mathf.Clamp(targetStartY, 0, CurrentTexture.height); int width = Mathf.Min(targetStartX + pixelRadius * 2, CurrentTexture.width) - targetStartX; int height = Mathf.Min(targetStartY + pixelRadius * 2, CurrentTexture.height) - targetStartY; mid -= new Vector2(startX, startY); //Get pixels Color[] pixels = CurrentTexture.GetPixels(startX, startY, width, height); //Iterate trough all pixels for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = y * width + x; Vector2 uv = ((new Vector2(x, y) - mid) / pixelRadius) * 0.5f + new Vector2(0.5f, 0.5f); pixels[index] = ApplyBrushToPixel(pixels[index], uv); } } //Save pixels and apply them to the texture CurrentTexture.SetPixels(startX, startY, width, height, pixels); CurrentTexture.Apply(); } private Color ApplyBrushToPixel(Color c, Vector2 v) { switch (Target) { case TextureTarget.ColorHeight: c = ColorHeightOperation(c, v); break; case TextureTarget.Density: c = DensityOperation(c, v); break; default: throw new ArgumentOutOfRangeException(); } return c; } private Color ColorHeightOperation(Color c, Vector2 v) { Color newColor; switch (ColorHeightChannel) { case ColorHeightChannel.Color: newColor = BrushOperation(c, v); newColor.a = c.a; return newColor; case ColorHeightChannel.Height: newColor = BrushOperation(c, v); c.a = newColor.a; return c; case ColorHeightChannel.Both: return BrushOperation(c, v); default: throw new InvalidOperationException("Channel invalid"); } } private Color DensityOperation(Color c, Vector2 v) { switch (DensityChannel) { case DensityChannel.R: c.r = BrushOperation(c.r, v); break; case DensityChannel.G: c.g = BrushOperation(c.g, v); break; case DensityChannel.B: c.b = BrushOperation(c.b, v); break; case DensityChannel.A: c.a = BrushOperation(c.a, v); break; default: throw new InvalidOperationException("Channel invalid"); } return c; } private Color BrushOperation(Color x, Vector2 v) { v -= new Vector2(0.5f, 0.5f); v *= 2; var distance = v.magnitude; //Calculate brush smoothness var value = SmoothStep(1, Mathf.Min(1 - Softness, 0.999f), distance); var result = x; switch (Brush) { case BrushMode.Add: result = x + PaintColor * value * Strength; break; case BrushMode.Subtract: result = x - PaintColor * value * Strength; break; case BrushMode.Blend: result = x * (1 - value * Strength) + PaintColor * Strength * value; break; } return new Color( Mathf.Clamp01(result.r), Mathf.Clamp01(result.g), Mathf.Clamp01(result.b), Mathf.Clamp01(result.a)); } private float BrushOperation(float x, Vector2 v) { v -= new Vector2(0.5f, 0.5f); v *= 2; var distance = v.magnitude; //Calculate brush smoothness var value = SmoothStep(1, Mathf.Min(1 - Softness, 0.999f), distance); var result = x; switch (Brush) { case BrushMode.Add: result = x + value * Strength; break; case BrushMode.Subtract: result = x - value * Strength; break; case BrushMode.Blend: result = x * (1 - value * Strength) + PaintDensity * Strength * value; break; } return Mathf.Clamp01(result); } //Taken from wikipedia, smoothstep private float SmoothStep(float edge0, float edge1, float x) { // Scale, bias and saturate x to 0..1 range x = Mathf.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); // Evaluate polynomial return x * x * (3 - 2 * x); } public bool RaycastColliders(Ray ray, out RaycastHit hit) { hit = new RaycastHit(); foreach (var collider in GrassColliders) { if (collider.Raycast(ray, out hit, Mathf.Infinity)) { return true; } } return false; } } public enum TextureTarget { ColorHeight, Density } public enum BrushMode { Blend, Add, Subtract } public enum ColorHeightChannel { Color, Height, Both } public enum DensityChannel { R,G,B,A } }