using System.Collections.Generic; using UnityEngine; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif /// /// Live mesh painting in Play mode. /// Paints a fixed color (brush) onto the mesh's texture with the mouse. /// /// TWO BACKENDS (switchable via , also live in Play mode): /// /// * GPU (default): True shader painting into a RenderTexture. The brush is /// drawn via a vertex/fragment shader - the mesh is unwrapped into the texture /// layout using its UVs, and per texel the actual 3D position is tested against /// the brush sphere. Edge padding against seams runs as a GPU blit. /// Fast, because the GPU rasterizes all texels in parallel. /// /// * CPU: Rasterization of the hit triangles on the CPU; per texel the 3D /// position is computed barycentrically and tested against the brush sphere. /// Edge padding via CPU dilation. Identical visual result, but more expensive. /// /// BOTH backends paint seamlessly: a 3D sphere around the hit point captures /// triangles on both sides of a UV seam at once (front/back share the same 3D /// edge), no matter where they lie in the UV layout. /// /// Simply attach to the GameObject you want to paint (e.g. paintman_UV/paintman_UV). /// [DisallowMultipleComponent] public class MeshPainter : MonoBehaviour { [Header("Backend")] [Tooltip("ON = GPU painting (shader/RenderTexture, fast). OFF = CPU painting. " + "Can be toggled live in Play mode - the painted content is carried over.")] public bool useGpu = true; [Header("Brush")] [Tooltip("Color used for painting.")] public Color brushColor = Color.red; [Tooltip("Brush radius in texels (visual size). Converted into a 3D radius per hit.")] [Range(1, 256)] public int brushSize = 16; [Header("Canvas")] [Tooltip("Resolution of the paint texture (square).")] public int textureSize = 1024; [Tooltip("Base color of the texture at startup (if none exists).")] public Color baseColor = Color.white; [Tooltip("Edge padding (texels) against visible UV seams. The color is 'bled' beyond the " + "UV island edges so that bilinear sampling there does not pull in the unpainted " + "base color. 0 = off.")] [Range(0, 16)] public int paddingTexels = 4; [Header("Setup")] [Tooltip("Camera for the mouse ray. Empty = Camera.main.")] public Camera paintCamera; [Tooltip("Only for animated meshes: re-bake the collider every frame (expensive!).")] public bool updateColliderEveryFrame = false; // --- shared references --- private Renderer _renderer; private SkinnedMeshRenderer _skinned; private MeshCollider _collider; private Mesh _bakedMesh; private Material _materialInstance; // Mesh caches (in the transform's local space = collider space). private Vector3[] _verts; private int[] _tris; private Vector2[] _uvs; private Vector3[] _triCenter; // centroid per triangle (for fast CPU culling) private float[] _triRadius; // circumscribed radius per triangle (for fast CPU culling) private float _avgUvToLocal = 1f; // average conversion: UV unit -> local unit private bool _hasLastScreen; private Vector2 _lastScreen; private bool _gpuActive; // currently configured backend private bool _setupValid; // --- CPU backend --- private Texture2D _paintTexture; private Color32[] _pixels; private bool[] _painted; // true = painted by the brush (persistent dilation source) private int[] _fillStamp; // transient marker for padding texels per dilation call private int _dilateStamp; // incremented per call -> padding from previous frames does not count as a source private readonly List _dilateScratch = new List(512); private int _dx0, _dy0, _dx1, _dy1; // dirty rectangle (CPU upload) // --- GPU backend --- public Shader brushShader; // optional; otherwise Shader.Find("Hidden/MeshPaintBrush") public Shader dilateShader; // optional; otherwise Shader.Find("Hidden/MeshPaintDilate") private Material _brushMat; private Material _dilateMat; private RenderTexture _displayRT; // assigned to the material as _BaseMap private RenderTexture _tempRT; // scratch for the dilation blit (ping-pong) private bool _drewThisStroke; private bool _prevSRGB; // Step size (in screen pixels) for interpolation along the mouse movement. private const float ScreenStepPixels = 5f; // Upper bound of raycasts per frame (safeguard for very fast movements). private const int MaxStepsPerFrame = 256; private static readonly int BaseMapId = Shader.PropertyToID("_BaseMap"); private static readonly int MainTexId = Shader.PropertyToID("_MainTex"); private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); private static readonly int ColorId = Shader.PropertyToID("_Color"); private static readonly int BrushColorId = Shader.PropertyToID("_BrushColor"); private static readonly int BrushCenterId = Shader.PropertyToID("_BrushCenter"); private static readonly int BrushRadiusId = Shader.PropertyToID("_BrushRadius"); private static readonly int PadId = Shader.PropertyToID("_Pad"); private void Start() { if (paintCamera == null) paintCamera = Camera.main; _renderer = GetComponent(); _skinned = GetComponent(); if (_renderer == null) { Debug.LogError("[MeshPainter] No Renderer found on this GameObject.", this); enabled = false; return; } textureSize = Mathf.Clamp(textureSize, 32, 4096); SetupCollider(); BuildMeshCaches(); if (_uvs == null || _uvs.Length == 0) { Debug.LogError("[MeshPainter] Mesh has no UVs - painting not possible.", this); enabled = false; return; } // Own material instance so the shared asset is not modified. _materialInstance = _renderer.material; // automatically creates an instance // Tint to white so the painted color appears unaltered. if (_materialInstance.HasProperty(BaseColorId)) _materialInstance.SetColor(BaseColorId, Color.white); else if (_materialInstance.HasProperty(ColorId)) _materialInstance.SetColor(ColorId, Color.white); SetupBackend(useGpu, fromColor32: BuildBaseColors()); _setupValid = true; Debug.Log($"[MeshPainter] Ready ({(_gpuActive ? "GPU" : "CPU")} painting, seamless). " + "Hold the left mouse button and drag over the mesh.", this); } // ------------------------------------------------------------------ // Setup (shared) // ------------------------------------------------------------------ private void SetupCollider() { _collider = GetComponent(); if (_collider == null) _collider = gameObject.AddComponent(); _collider.convex = false; // non-convex provides triangleIndex / textureCoord _bakedMesh = new Mesh { name = "MeshPainter_Baked" }; BakeColliderMesh(); } private void BakeColliderMesh() { if (_skinned != null) { // Static skinned mesh: bake into local coordinates (scale = 1). _skinned.BakeMesh(_bakedMesh); _collider.sharedMesh = null; _collider.sharedMesh = _bakedMesh; } else { var mf = GetComponent(); if (mf != null) _collider.sharedMesh = mf.sharedMesh; } } /// Reads vertices/triangles/UVs of the collider mesh and computes culling data. private void BuildMeshCaches() { Mesh m = _collider.sharedMesh; if (m == null) return; _verts = m.vertices; _tris = m.triangles; _uvs = m.uv; // Fallback: UVs from the original mesh (BakeMesh usually keeps them, but better safe than sorry). if ((_uvs == null || _uvs.Length != _verts.Length) && _skinned != null && _skinned.sharedMesh != null) _uvs = _skinned.sharedMesh.uv; if (_uvs == null || _uvs.Length != _verts.Length) return; int triCount = _tris.Length / 3; _triCenter = new Vector3[triCount]; _triRadius = new float[triCount]; double sumArea3D = 0.0, sumAreaUv = 0.0; for (int t = 0; t < triCount; t++) { int i0 = _tris[t * 3], i1 = _tris[t * 3 + 1], i2 = _tris[t * 3 + 2]; Vector3 a = _verts[i0], b = _verts[i1], c = _verts[i2]; Vector3 center = (a + b + c) / 3f; _triCenter[t] = center; float ra = (a - center).sqrMagnitude; float rb = (b - center).sqrMagnitude; float rc = (c - center).sqrMagnitude; _triRadius[t] = Mathf.Sqrt(Mathf.Max(ra, Mathf.Max(rb, rc))); sumArea3D += 0.5 * Vector3.Cross(b - a, c - a).magnitude; Vector2 ua = _uvs[i0], ub = _uvs[i1], uc = _uvs[i2]; sumAreaUv += 0.5 * Mathf.Abs(Cross2D(ub - ua, uc - ua)); } if (sumAreaUv > 1e-9) _avgUvToLocal = (float)System.Math.Sqrt(sumArea3D / sumAreaUv); } /// Builds the base-color texels (copy of the existing material texture or baseColor). private Color32[] BuildBaseColors() { var px = new Color32[textureSize * textureSize]; Texture sourceTex = null; var shared = _renderer.sharedMaterial; if (shared != null) { if (shared.HasProperty(BaseMapId)) sourceTex = shared.GetTexture(BaseMapId); if (sourceTex == null && shared.HasProperty(MainTexId)) sourceTex = shared.GetTexture(MainTexId); } var src = sourceTex as Texture2D; if (src != null) { try { if (src.width == textureSize && src.height == textureSize && src.isReadable) return src.GetPixels32(); } catch { /* not readable -> fall back to baseColor */ } } Color32 c = baseColor; for (int i = 0; i < px.Length; i++) px[i] = c; return px; } // ------------------------------------------------------------------ // Backend selection / switching // ------------------------------------------------------------------ /// Sets up the desired backend and carries over the initial image content. private void SetupBackend(bool gpu, Color32[] fromColor32) { if (gpu) SetupGpu(fromColor32); else SetupCpu(fromColor32); _gpuActive = gpu; } /// Switches the backend at runtime and carries over the currently painted content. private void SwitchBackend(bool toGpu) { // save the current content as Color32 Color32[] current = (_gpuActive ? ReadGpuPixels() : (Color32[])_pixels.Clone()); if (_gpuActive) TeardownGpu(); else TeardownCpu(); SetupBackend(toGpu, current); _hasLastScreen = false; Debug.Log($"[MeshPainter] Backend switched -> {(toGpu ? "GPU" : "CPU")} painting.", this); } // ------------------------------------------------------------------ // CPU backend // ------------------------------------------------------------------ private void SetupCpu(Color32[] initial) { _paintTexture = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false) { name = "MeshPainter_Canvas", wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear }; _pixels = (initial != null && initial.Length == textureSize * textureSize) ? initial : BuildBaseColors(); _painted = new bool[textureSize * textureSize]; _fillStamp = new int[textureSize * textureSize]; _dilateStamp = 0; _paintTexture.SetPixels32(_pixels); _paintTexture.Apply(false); AssignTextureToMaterial(_paintTexture); } private void TeardownCpu() { if (_paintTexture != null) { Destroy(_paintTexture); _paintTexture = null; } _pixels = null; _painted = null; _fillStamp = null; } // ------------------------------------------------------------------ // GPU backend // ------------------------------------------------------------------ private void SetupGpu(Color32[] initial) { if (brushShader == null) brushShader = Shader.Find("Hidden/MeshPaintBrush"); if (dilateShader == null) dilateShader = Shader.Find("Hidden/MeshPaintDilate"); if (brushShader == null || dilateShader == null) { Debug.LogError("[MeshPainter] GPU shaders not found (Hidden/MeshPaintBrush or " + "Hidden/MeshPaintDilate). Falling back to CPU painting.", this); useGpu = false; SetupCpu(initial); _gpuActive = false; return; } _brushMat = new Material(brushShader) { hideFlags = HideFlags.HideAndDontSave }; _dilateMat = new Material(dilateShader) { hideFlags = HideFlags.HideAndDontSave }; _displayRT = CreateRT("MeshPainter_DisplayRT"); _tempRT = CreateRT("MeshPainter_TempRT"); // Bring the initial image content into the display RT. Alpha = 0 marks "unpainted" // (so the base color is not mistaken for a dilation source). var seed = initial ?? BuildBaseColors(); UploadColorsToRT(seed, _displayRT, paintedAlpha: false); AssignTextureToMaterial(_displayRT); } private void TeardownGpu() { if (_displayRT != null) { _displayRT.Release(); Destroy(_displayRT); _displayRT = null; } if (_tempRT != null) { _tempRT.Release(); Destroy(_tempRT); _tempRT = null; } if (_brushMat != null) { Destroy(_brushMat); _brushMat = null; } if (_dilateMat != null) { Destroy(_dilateMat); _dilateMat = null; } } private RenderTexture CreateRT(string rtName) { var rt = new RenderTexture(textureSize, textureSize, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB) { name = rtName, wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, antiAliasing = 1, useMipMap = false, autoGenerateMips = false }; rt.Create(); return rt; } /// Writes Color32 data into a RenderTexture via a temporary texture. private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool paintedAlpha) { var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false); if (paintedAlpha) { // mark the entire content as "painted" (alpha = 255) -> no dilation needed. for (int i = 0; i < colors.Length; i++) colors[i].a = 255; } else { // mark the base color as "unpainted" (alpha = 0). for (int i = 0; i < colors.Length; i++) colors[i].a = 0; } tmp.SetPixels32(colors); tmp.Apply(false); Graphics.Blit(tmp, rt); Destroy(tmp); } /// Reads the display RT back as Color32 (for the backend switch). private Color32[] ReadGpuPixels() { var prev = RenderTexture.active; RenderTexture.active = _displayRT; var tex = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false); tex.ReadPixels(new Rect(0, 0, textureSize, textureSize), 0, 0, false); tex.Apply(false); RenderTexture.active = prev; var px = tex.GetPixels32(); Destroy(tex); return px; } // ------------------------------------------------------------------ // Material // ------------------------------------------------------------------ private void AssignTextureToMaterial(Texture tex) { if (_materialInstance == null) return; if (_materialInstance.HasProperty(BaseMapId)) _materialInstance.SetTexture(BaseMapId, tex); if (_materialInstance.HasProperty(MainTexId)) _materialInstance.SetTexture(MainTexId, tex); } // ------------------------------------------------------------------ // Update / input // ------------------------------------------------------------------ private void Update() { if (!_setupValid) return; // Live backend switching. if (useGpu != _gpuActive) SwitchBackend(useGpu); if (updateColliderEveryFrame && _skinned != null) { BakeColliderMesh(); BuildMeshCaches(); } if (!TryGetMouse(out bool pressed, out Vector2 screenPos)) return; if (!pressed) { _hasLastScreen = false; return; } if (paintCamera == null) return; if (_gpuActive) BeginGpuStroke(); else { _dx0 = textureSize; _dy0 = textureSize; _dx1 = -1; _dy1 = -1; } if (_hasLastScreen) { // Interpolate along the mouse movement in screen space and re-raycast at // each step -> the stroke follows the mesh surface. float dist = Vector2.Distance(_lastScreen, screenPos); int steps = Mathf.Clamp(Mathf.CeilToInt(dist / ScreenStepPixels), 1, MaxStepsPerFrame); for (int s = 1; s <= steps; s++) PaintAtScreen(Vector2.Lerp(_lastScreen, screenPos, s / (float)steps)); } else { PaintAtScreen(screenPos); } if (_gpuActive) EndGpuStroke(); else { if (paddingTexels > 0) DilatePadding(paddingTexels); FlushDirty(); } _lastScreen = screenPos; _hasLastScreen = true; } /// /// Raycast at a screen position. If the ray hits the mesh, a 3D sphere is painted /// around the hit point (seamless) - via GPU or CPU depending on the backend. /// private void PaintAtScreen(Vector2 screenPos) { Ray ray = paintCamera.ScreenPointToRay(screenPos); if (!(Physics.Raycast(ray, out RaycastHit hit, 1000f) && hit.collider == _collider)) return; if (_verts == null || hit.triangleIndex < 0) return; // Bring the hit point into local mesh space (= collider/vertex space). Vector3 pLocal = transform.InverseTransformPoint(hit.point); // Convert the brush radius (texels) into a local 3D radius using the UV // density of the hit triangle -> the brush stays visually the same size. float scale = _avgUvToLocal; int t = hit.triangleIndex; if (t * 3 + 2 < _tris.Length) { int i0 = _tris[t * 3], i1 = _tris[t * 3 + 1], i2 = _tris[t * 3 + 2]; float area3D = 0.5f * Vector3.Cross(_verts[i1] - _verts[i0], _verts[i2] - _verts[i0]).magnitude; float areaUv = 0.5f * Mathf.Abs(Cross2D(_uvs[i1] - _uvs[i0], _uvs[i2] - _uvs[i0])); if (areaUv > 1e-9f) { float s = Mathf.Sqrt(area3D / areaUv); // guard against degenerate UVs: keep close to the average. scale = Mathf.Clamp(s, _avgUvToLocal * 0.25f, _avgUvToLocal * 4f); } } float radiusLocal = (brushSize / (float)textureSize) * scale; if (radiusLocal <= 0f) return; if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal); else PaintSphere3D(pLocal, radiusLocal); } // ------------------------------------------------------------------ // GPU painting // ------------------------------------------------------------------ private void BeginGpuStroke() { _drewThisStroke = false; _prevSRGB = GL.sRGBWrite; GL.sRGBWrite = true; // correct sRGB encoding when writing into the sRGB RT (linear project) Graphics.SetRenderTarget(_displayRT); _brushMat.SetColor(BrushColorId, brushColor); } private void DrawBrushGpu(Vector3 pLocal, float radiusLocal) { _brushMat.SetVector(BrushCenterId, pLocal); _brushMat.SetFloat(BrushRadiusId, radiusLocal); if (!_brushMat.SetPass(0)) return; int sub = _bakedMesh.subMeshCount; for (int i = 0; i < sub; i++) Graphics.DrawMeshNow(_bakedMesh, Matrix4x4.identity, i); _drewThisStroke = true; } private void EndGpuStroke() { Graphics.SetRenderTarget(null); // Edge padding against UV seams: blit the display RT through the dilation // shader into _tempRT and then swap (the result ends up in _displayRT again). if (_drewThisStroke && paddingTexels > 0) { _dilateMat.SetInt(PadId, paddingTexels); Graphics.Blit(_displayRT, _tempRT, _dilateMat, 0); var swap = _displayRT; _displayRT = _tempRT; _tempRT = swap; AssignTextureToMaterial(_displayRT); } GL.sRGBWrite = _prevSRGB; } // ------------------------------------------------------------------ // CPU painting (rasterization + dilation) // ------------------------------------------------------------------ /// /// Paints a sphere in local 3D space: rasterizes all triangles in range into /// the texture and colors every texel whose 3D position lies inside the sphere. /// private void PaintSphere3D(Vector3 pLocal, float radiusLocal) { Color32 col = brushColor; float r2 = radiusLocal * radiusLocal; int triCount = _tris.Length / 3; float ts = textureSize; for (int t = 0; t < triCount; t++) { // Fast culling: sphere against the triangle's circumscribed circle. float reach = radiusLocal + _triRadius[t]; if ((_triCenter[t] - pLocal).sqrMagnitude > reach * reach) continue; int i0 = _tris[t * 3], i1 = _tris[t * 3 + 1], i2 = _tris[t * 3 + 2]; Vector3 v0 = _verts[i0], v1 = _verts[i1], v2 = _verts[i2]; Vector2 uv0 = _uvs[i0], uv1 = _uvs[i1], uv2 = _uvs[i2]; // UV -> pixel. float p0x = uv0.x * ts, p0y = uv0.y * ts; float p1x = uv1.x * ts, p1y = uv1.y * ts; float p2x = uv2.x * ts, p2y = uv2.y * ts; // Bounding box in pixel space. int minX = Mathf.Clamp(Mathf.FloorToInt(Mathf.Min(p0x, Mathf.Min(p1x, p2x))), 0, textureSize - 1); int maxX = Mathf.Clamp(Mathf.CeilToInt(Mathf.Max(p0x, Mathf.Max(p1x, p2x))), 0, textureSize - 1); int minY = Mathf.Clamp(Mathf.FloorToInt(Mathf.Min(p0y, Mathf.Min(p1y, p2y))), 0, textureSize - 1); int maxY = Mathf.Clamp(Mathf.CeilToInt(Mathf.Max(p0y, Mathf.Max(p1y, p2y))), 0, textureSize - 1); if (maxX < minX || maxY < minY) continue; // Barycentric denominator. float d = (p1y - p2y) * (p0x - p2x) + (p2x - p1x) * (p0y - p2y); if (Mathf.Abs(d) < 1e-7f) continue; float invD = 1f / d; const float eps = -1e-4f; // small tolerance against 1px cracks between triangles for (int y = minY; y <= maxY; y++) { float py = y + 0.5f; int rowStart = y * textureSize; for (int x = minX; x <= maxX; x++) { float px = x + 0.5f; float a = ((p1y - p2y) * (px - p2x) + (p2x - p1x) * (py - p2y)) * invD; if (a < eps) continue; float b = ((p2y - p0y) * (px - p2x) + (p0x - p2x) * (py - p2y)) * invD; if (b < eps) continue; float c = 1f - a - b; if (c < eps) continue; // 3D position of the texel via barycentric interpolation. float wx = a * v0.x + b * v1.x + c * v2.x; float wy = a * v0.y + b * v1.y + c * v2.y; float wz = a * v0.z + b * v1.z + c * v2.z; float ddx = wx - pLocal.x, ddy = wy - pLocal.y, ddz = wz - pLocal.z; if (ddx * ddx + ddy * ddy + ddz * ddz > r2) continue; _pixels[rowStart + x] = col; _painted[rowStart + x] = true; if (x < _dx0) _dx0 = x; if (x > _dx1) _dx1 = x; if (y < _dy0) _dy0 = y; if (y > _dy1) _dy1 = y; } } } } /// /// Edge padding / texture dilation against visible UV seams (CPU). /// Colors a thin border (pad texels) around the freshly painted areas with their /// color. The dilation source is exclusively brush-painted texels (_painted). /// private void DilatePadding(int pad) { if (_dx1 < _dx0 || _dy1 < _dy0) return; // nothing painted int x0 = Mathf.Max(0, _dx0 - pad); int y0 = Mathf.Max(0, _dy0 - pad); int x1 = Mathf.Min(textureSize - 1, _dx1 + pad); int y1 = Mathf.Min(textureSize - 1, _dy1 + pad); _dilateStamp++; // unique per call -> padding from previous frames does not count as a source for (int iter = 0; iter < pad; iter++) { _dilateScratch.Clear(); for (int y = y0; y <= y1; y++) { int row = y * textureSize; for (int x = x0; x <= x1; x++) { int idx = row + x; if (_painted[idx] || _fillStamp[idx] == _dilateStamp) continue; if (TryFindPaddingSource(x, y, out Color32 c)) { _pixels[idx] = c; _dilateScratch.Add(idx); if (x < _dx0) _dx0 = x; if (x > _dx1) _dx1 = x; if (y < _dy0) _dy0 = y; if (y > _dy1) _dy1 = y; } } } for (int k = 0; k < _dilateScratch.Count; k++) _fillStamp[_dilateScratch[k]] = _dilateStamp; if (_dilateScratch.Count == 0) break; } } /// Searches the 8-neighborhood for a source texel (painted or already padded) and returns its color. private bool TryFindPaddingSource(int x, int y, out Color32 col) { for (int oy = -1; oy <= 1; oy++) { int ny = y + oy; if (ny < 0 || ny >= textureSize) continue; int nrow = ny * textureSize; for (int ox = -1; ox <= 1; ox++) { if (ox == 0 && oy == 0) continue; int nx = x + ox; if (nx < 0 || nx >= textureSize) continue; int nidx = nrow + nx; if (_painted[nidx] || _fillStamp[nidx] == _dilateStamp) { col = _pixels[nidx]; return true; } } } col = default; return false; } /// Uploads only the region changed this frame to the GPU once (CPU backend). private void FlushDirty() { if (_dx1 < _dx0 || _dy1 < _dy0) return; // nothing painted int bw = _dx1 - _dx0 + 1; int bh = _dy1 - _dy0 + 1; var block = new Color32[bw * bh]; for (int y = 0; y < bh; y++) System.Array.Copy(_pixels, (_dy0 + y) * textureSize + _dx0, block, y * bw, bw); _paintTexture.SetPixels32(_dx0, _dy0, bw, bh, block); _paintTexture.Apply(false); } // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ private static float Cross2D(Vector2 a, Vector2 b) => a.x * b.y - a.y * b.x; private bool TryGetMouse(out bool pressed, out Vector2 screenPos) { pressed = false; screenPos = Vector2.zero; #if ENABLE_INPUT_SYSTEM var mouse = Mouse.current; if (mouse != null) { pressed = mouse.leftButton.isPressed; screenPos = mouse.position.ReadValue(); return true; } #endif #if ENABLE_LEGACY_INPUT_MANAGER pressed = Input.GetMouseButton(0); screenPos = Input.mousePosition; return true; #else return false; #endif } private void OnDestroy() { if (_bakedMesh != null) Destroy(_bakedMesh); if (_materialInstance != null) Destroy(_materialInstance); TeardownCpu(); TeardownGpu(); } }