using UnityEngine; using UnityEngine.Events; // 펜(PenTip)으로 그릴 수 있는 종이. // 시작 시 RenderTexture를 만들어 원본 텍스처(계약서 인쇄물)를 복사해 넣고, // 렌더러의 텍스처를 그 RT로 교체한다. 이후 펜이 UV 좌표로 스탬프를 찍는다. // // 요구 사항: // - 이 오브젝트(또는 콜라이더)가 MeshCollider여야 한다 — RaycastHit.textureCoord는 MeshCollider에서만 나온다 // - 머티리얼은 DinoLove/Paper 셰이더 기준 (_FrontTex). 다른 셰이더면 프로퍼티 이름만 맞춰주면 됨 public class DrawablePaper : MonoBehaviour { [Header("Canvas")] [SerializeField] private Renderer _renderer; // 비우면 자기 자신의 Renderer [SerializeField] private string _texturePropertyName = "_FrontTex"; [Tooltip("그리기 전 원본(계약서 인쇄물). 비우면 머티리얼의 현재 텍스처, 그것도 없으면 흰 종이")] [SerializeField] private Texture _sourceTexture; [SerializeField] private int _textureSize = 1024; [Header("서명란 판정")] [Tooltip("서명으로 인정할 UV 영역 (좌하단 0,0 ~ 우상단 1,1)")] [SerializeField] private Rect _signatureZone = new Rect(0.55f, 0.05f, 0.4f, 0.2f); [Tooltip("서명란 안에 스탬프가 이만큼 찍히면 서명 완료로 판정")] [SerializeField] private int _requiredStamps = 80; [Tooltip("서명이 완료되는 순간 1회 호출 (계약 진행 이벤트 연결용)")] public UnityEvent OnSigned; public bool IsSigned { get; private set; } private RenderTexture _rt; private int _zoneStamps; private static Texture2D s_softBrush; // 절차 생성 소프트 원 브러시 (전 종이 공유) private void Awake() { if (_renderer == null) _renderer = GetComponent(); _rt = new RenderTexture(_textureSize, _textureSize, 0, RenderTextureFormat.ARGB32); _rt.Create(); // 원본 텍스처로 초기화 (없으면 흰 종이) var src = _sourceTexture != null ? _sourceTexture : _renderer.material.GetTexture(_texturePropertyName); if (src != null) { Graphics.Blit(src, _rt); } else { var prev = RenderTexture.active; RenderTexture.active = _rt; GL.Clear(false, true, Color.white); RenderTexture.active = prev; } _renderer.material.SetTexture(_texturePropertyName, _rt); } private void OnDestroy() { if (_rt != null) { _rt.Release(); Destroy(_rt); } } // 두 UV 사이를 브러시 간격으로 보간해서 연속된 선으로 찍는다 (프레임 사이 점선 방지) public void DrawStroke(Vector2 fromUv, Vector2 toUv, Color ink, float brushSize) { float dist = Vector2.Distance(fromUv, toUv); int steps = Mathf.Max(1, Mathf.CeilToInt(dist / Mathf.Max(brushSize * 0.25f, 0.0005f))); for (int i = 1; i <= steps; i++) StampAt(Vector2.Lerp(fromUv, toUv, (float)i / steps), ink, brushSize); } // UV 위치에 브러시 한 번 찍기. brushSize는 종이 가로 대비 지름 비율 (0.02 = 2%) public void StampAt(Vector2 uv, Color ink, float brushSize) { if (_rt == null) return; var prev = RenderTexture.active; RenderTexture.active = _rt; GL.PushMatrix(); GL.LoadPixelMatrix(0, _rt.width, _rt.height, 0); // 좌상단 원점 픽셀 좌표계 float d = brushSize * _rt.width; // 지름(픽셀) float px = uv.x * _rt.width - d * 0.5f; float py = (1f - uv.y) * _rt.height - d * 0.5f; // DrawTexture는 y가 위→아래 // DrawTexture의 색 변조는 0.5가 중립(×2 곱)이라 절반으로 넘긴다 Graphics.DrawTexture(new Rect(px, py, d, d), GetBrush(), new Rect(0, 0, 1, 1), 0, 0, 0, 0, ink * 0.5f); GL.PopMatrix(); RenderTexture.active = prev; // 서명란 판정 if (!IsSigned && _signatureZone.Contains(uv)) { _zoneStamps++; if (_zoneStamps >= _requiredStamps) { IsSigned = true; OnSigned?.Invoke(); } } } // 가장자리로 갈수록 투명해지는 원형 브러시를 한 번만 생성 private static Texture2D GetBrush() { if (s_softBrush != null) return s_softBrush; const int size = 64; s_softBrush = new Texture2D(size, size, TextureFormat.RGBA32, false); s_softBrush.hideFlags = HideFlags.HideAndDontSave; var pixels = new Color32[size * size]; float half = size * 0.5f; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { float r = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(half, half)) / half; // 중심은 진하게, 70% 지점부터 부드럽게 빠지는 잉크 느낌 float a = Mathf.Clamp01(1f - Mathf.InverseLerp(0.7f, 1f, r)); pixels[y * size + x] = new Color32(255, 255, 255, (byte)(a * 255f)); } } s_softBrush.SetPixels32(pixels); s_softBrush.Apply(); return s_softBrush; } }