2026-07-10 노예계약서 서명 기능
This commit is contained in:
135
Assets/02_Scripts/Interaction/DrawablePaper.cs
Normal file
135
Assets/02_Scripts/Interaction/DrawablePaper.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
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<Renderer>();
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Interaction/DrawablePaper.cs.meta
Normal file
2
Assets/02_Scripts/Interaction/DrawablePaper.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7cc7a36389579e4eb095cb50751ced7
|
||||
49
Assets/02_Scripts/Interaction/PenTip.cs
Normal file
49
Assets/02_Scripts/Interaction/PenTip.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using UnityEngine;
|
||||
|
||||
// 펜촉. 이 오브젝트의 +Z(파란 축)가 펜촉이 가리키는 방향이 되도록
|
||||
// 펜 프리팹의 촉 끝에 빈 오브젝트로 배치할 것.
|
||||
// 촉이 DrawablePaper에 닿아 있는 동안 매 프레임 잉크를 찍는다.
|
||||
// 펜 자체는 XRGrabInteractable로 잡는다 — 이 스크립트는 잡혔는지 여부와 무관하게
|
||||
// 접촉만 검사하므로, 잡지 않은 펜이 종이에 꽂혀 있으면 그때도 그려진다는 점만 유의.
|
||||
public class PenTip : MonoBehaviour
|
||||
{
|
||||
[Header("Ink")]
|
||||
[SerializeField] private Color _inkColor = new Color(0.08f, 0.08f, 0.1f, 1f); // 순검정보다 자연스러운 잉크색
|
||||
[Tooltip("선 굵기 — 종이 가로 대비 지름 비율 (0.015 = 1.5%)")]
|
||||
[SerializeField, Range(0.003f, 0.1f)] private float _brushSize = 0.015f;
|
||||
|
||||
[Header("Contact")]
|
||||
[Tooltip("펜촉 끝에서 이 거리 안에 종이가 있으면 접촉으로 판정")]
|
||||
[SerializeField] private float _contactDistance = 0.01f;
|
||||
[SerializeField] private LayerMask _paperMask = ~0;
|
||||
|
||||
// 펜촉이 종이를 살짝 뚫고 들어가도 인식되도록 촉 뒤쪽에서부터 레이를 쏜다
|
||||
private const float CastBack = 0.03f;
|
||||
|
||||
private DrawablePaper _currentPaper;
|
||||
private Vector2 _lastUv;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Vector3 origin = transform.position - transform.forward * CastBack;
|
||||
if (Physics.Raycast(origin, transform.forward, out var hit, CastBack + _contactDistance, _paperMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
var paper = hit.collider.GetComponentInParent<DrawablePaper>();
|
||||
if (paper != null)
|
||||
{
|
||||
Vector2 uv = hit.textureCoord; // MeshCollider 필수
|
||||
|
||||
if (paper == _currentPaper)
|
||||
paper.DrawStroke(_lastUv, uv, _inkColor, _brushSize); // 이어 그리기
|
||||
else
|
||||
paper.StampAt(uv, _inkColor, _brushSize); // 새 접촉 — 점 하나
|
||||
|
||||
_currentPaper = paper;
|
||||
_lastUv = uv;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_currentPaper = null; // 접촉 끊김 — 다음에 닿으면 새 선 시작
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Interaction/PenTip.cs.meta
Normal file
2
Assets/02_Scripts/Interaction/PenTip.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 993c2a069a0c98144be2ba85ef774def
|
||||
Reference in New Issue
Block a user