Search

유리 공예 프로젝트 일지 #0 - 프로토타입 개발 회고

프로젝트 시작 동기

어느 날 쇼츠에서 유리 공예 영상을 보게 되었다. 시뻘겋게 달아오른 유리가 꿀처럼 움직이는 모습이 신기했다. "이거 VR로 하면 재밌겠는데?" 하는 생각이 들었다.
관련 자료를 찾아보니 간단한 논문 정도는 있었지만, 대부분 미리 정의된 애니메이션 방식이었다. 실시간, 그것도 VR에서는 불가능해 보였다. 하지만 그래서 더 궁금해졌다. 잘하면 가능할 것도 같았다.
관련 자료를 찾아보니 간단한 논문 정도는 있었지만, 대부분 미리 정의된 애니메이션 방식이었다. 딱 봐도 실시간, 그것도 VR에서는 불가능할 것 같았다. 하지만 그래서 더 궁금해졌다. 잘하면 가능할 것도 같은데... 개인적으로 뭔가 딱 이 정도로 불가능해 보일 때, 증명해보고 싶은 욕망이 생긴다.
기존 시뮬레이션들이 단순한 변형 효과로만 유리를 표현하는 것과 달리, 진짜 온도라는 물리적 변수가 실시간으로 메쉬 변형과 렌더링에 직접 영향을 미치는 시스템을 만들어보고 싶었다.

초기 기술적 고민과 방향 설정

메쉬 변형의 한계 인식

처음에는 단순히 Vertex Shader에서 정점들을 움직이거나, CPU에서 메쉬의 vertices 배열을 직접 조작하는 방식을 고려했다. 하지만 이런 접근법은 몇 가지 근본적인 문제가 있었다.
첫째, 유리의 변형은 예측할 수 없는 복잡한 형태를 가진다. 미리 정의된 수식으로는 자연스러운 흘러내림이나 늘어짐을 표현하기 어렵다. 둘째, 온도에 따른 점성 변화를 단순한 변형으로는 구현할 수 없다. 셋째, 사용자의 도구(예: 집게, 블로우 파이프 등)와의 상호작용을 고려하면 물리 기반 시뮬레이션이 필수적이었다.

Position Based Dynamics 선택

사실 처음에는 유체 시뮬레이션 쪽을 더 살펴보았다. SPH(Smoothed Particle Hydrodynamics)나 MPM(Material Point Method) 등을 찾아보긴 했는데, 유리는 점성 물체라서 유체와 고체 그 어딘가에 있다 보니 더 적합한 방식이 없을까 고민되었다.
PBD는 Unity Asset Store에서 Obi Softbody를 보다가 떠올린 생각이었다. 제약 조건에서 필요 없는 탄성 같은 거 빼고, 우리한테 필요한 점성 따위를 넣으면 딱 좋지 않을까? 하는 느낌이었다. Verlet Integration을 기반으로 하면서도 제약 조건을 통해 물체의 형태를 유지할 수 있고, 온도에 따라 제약 강도를 조절하면 점성 변화를 표현할 수 있을 것 같았다.
// 파티클 기본 구조 설계 public class Particle { public Vector3 currentPos; public Vector3 previousPos; public Vector3 acceleration; public float mass = 1.0f; public float temperature; // 온도 정보 추가 public float viscosity; // 점성 정보 추가 }
C#
복사

첫 번째 구현: CPU 기반 소프트바디

기본 시뮬레이션 구축

2024년 12월부터 본격적인 구현을 시작했다. 먼저 CPU에서 간단한 메쉬 변형 시스템을 만들어 기본 개념을 검증했다. MeshDeformer 클래스를 통해 Inflate와 Pinch 기능을 구현하면서 메쉬 조작의 기초를 다졌다.
// 초기 메쉬 변형 테스트 void ApplyInflate() { Vector3 inflateCenterLocal = transform.InverseTransformPoint(inflateCenter); for (int i = 0; i < displacedVertices.Length; i++) { Vector3 direction = displacedVertices[i] - inflateCenterLocal; float distance = direction.magnitude; if (distance < inflateRadius) { float factor = 1.0f - (distance / inflateRadius); displacedVertices[i] += direction.normalized * inflateStrength * factor * Time.deltaTime; } } }
C#
복사
원본
블로잉
핀칭

Distance Constraint 구현

메쉬의 형태를 유지하면서도 변형을 허용하기 위해 Distance Constraint를 구현했다. 메쉬의 삼각형 정보에서 에지를 추출하고, 각 에지의 원래 길이를 제약 조건으로 사용했다.
// 에지 기반 제약 조건 생성 HashSet<Edge> edges = new HashSet<Edge>(); for (int i = 0; i < triangles.Length; i += 3) { int i0 = triangles[i]; int i1 = triangles[i + 1]; int i2 = triangles[i + 2]; edges.Add(new Edge(i0, i1)); edges.Add(new Edge(i1, i2)); edges.Add(new Edge(i2, i0)); }
C#
복사
온도에 따른 점성 변화는 제약 조건의 강도를 조절하는 방식으로 구현했다. 가열된 영역의 파티클들은 제약이 약해져서 더 쉽게 변형되도록 했다.

온도 시뮬레이션의 도전

UV 좌표를 활용한 온도 전이

3D 공간에서의 온도 계산이 고민이었다. 셰이더에서 이 정보를 쉽게 활용할 수 있어야 했고, 여러모로 고민했다. 파티클 간의 거리 계산은 비용이 크고, 메쉬의 토폴로지를 고려한 이웃 탐색은 복잡했다.
결국 무슨 정보가 있지 하고 보다가 "UV에 넣을까?" 하는 이상한 생각이 들었다. 연산할 때 마스크를 씌워서 확산하기도 쉬울 것 같고, 접근하기도 쉽겠고... 이상하긴 한데 해볼까? 해서 구현해보았다. 메쉬의 UV2 좌표에 온도 시뮬레이션용 그리드를 매핑하고, 2D 텍스처에서 열 확산을 계산하는 방식이었다.
결과적으로 여러 제한점들을 알게 되긴 했지만, 꽤 쉽게 구현할 수 있어서 테스트 단계에서는 좋았다.
// 컴퓨트 쉐이더에서 열 확산 계산 [numthreads(8,8,1)] void CS_HeatDiffusion (uint3 id : SV_DispatchThreadID) { int2 coord = int2(id.x, id.y); float2 current = _CurrentState[coord]; float temperature = current.x; // 4방향 이웃의 온도 평균 계산 float sumTemp = 0.0; int count = 0; int2 offsets[4] = {{1,0}, {-1,0}, {0,1}, {0,-1}}; for (int i = 0; i < 4; i++) { int2 neighbor = coord + offsets[i]; // Wrap-around 처리로 경계 문제 해결 if (neighbor.x < 0) neighbor.x += gridSize.x; else if (neighbor.x >= gridSize.x) neighbor.x -= gridSize.x; float neighborTemp = _CurrentState[neighbor].x; sumTemp += neighborTemp; count++; } float avgTemp = sumTemp / count; float laplacian = avgTemp - temperature; float newTemperature = temperature + diffusionRate * laplacian * deltaTime; _OutputState[coord] = float2(newTemperature, current.y); }
Plain Text
복사
가열된 유리

컴퓨트 쉐이더 학습 과정

컴퓨트 쉐이더를 처음 다뤄보면서 GPGPU의 개념부터 차근차근 학습해야 했다. CPU와 GPU의 메모리 구조 차이, 스레드 그룹의 동작 방식, 텍스처와 버퍼 간의 데이터 전송 등 기초적인 부분부터 이해가 필요했다.
특히 RenderTexture를 사용한 ping-pong 렌더링 패턴을 구현하면서 GPU 프로그래밍의 비동기적 특성에 대해 깊이 이해하게 되었다.

GPU 기반 시뮬레이션으로 진화

성능 최적화의 필요성

CPU 기반 구현으로는 VR 환경에서 필요한 90fps를 유지하기 어려웠다. 특히 파티클 수가 증가하면 제약 해결 단계에서 급격한 성능 저하가 발생했다. 이를 해결하기 위해 모든 계산을 GPU로 이전하기로 결정했다.
// GPU 버퍼를 통한 파티클 데이터 관리 struct Particle { public Vector3 currentPos; public Vector3 previousPos; public float mass; } private ComputeBuffer particleBuffer; private ComputeBuffer constraintBuffer; void InitializeGPUSimulation() { particleBuffer = new ComputeBuffer(particles.Length, sizeof(float) * 7); constraintBuffer = new ComputeBuffer(constraints.Length, sizeof(int) * 2 + sizeof(float)); particleBuffer.SetData(particles); constraintBuffer.SetData(constraints); }
C#
복사

Constraint Coloring 구현

GPU에서 제약 조건을 병렬로 처리할 때 발생하는 데이터 경쟁(race condition) 문제를 해결하기 위해 Constraint Coloring 기법을 구현했다. 같은 파티클을 공유하지 않는 제약들을 같은 그룹으로 묶어서 안전하게 병렬 처리할 수 있도록 했다.
// 제약 조건 색칠 알고리즘 List<List<DistanceConstraint>> ColorConstraints(DistanceConstraint[] constraints, int numParticles) { int[] colors = new int[constraints.Length]; for (int i = 0; i < constraints.Length; i++) { HashSet<int> usedColors = new HashSet<int>(); // 현재 제약과 파티클을 공유하는 이전 제약들의 색상 수집 foreach (int relatedConstraint in GetRelatedConstraints(constraints[i])) { if (relatedConstraint < i && colors[relatedConstraint] != -1) usedColors.Add(colors[relatedConstraint]); } // 사용되지 않은 가장 작은 색상 할당 int color = 0; while (usedColors.Contains(color)) color++; colors[i] = color; } return GroupConstraintsByColor(constraints, colors); }
C#
복사
온도와 점성으로 인해 녹는 유리컵

멜트 이펙트와 볼륨 제약

온도 기반 변형 구현

진정한 유리 느낌을 위해서는 단순한 변형을 넘어서 온도에 따른 "녹아내림" 효과가 필요했다. 특정 온도 이상에서는 제약 조건이 약해지고, 추가적인 중력이 적용되도록 구현했다.
// 멜트 효과가 적용된 제약 해결 float3 worldPosA = mul(localToWorld, float4(pA.currentPos, 1.0)).xyz; float3 worldPosB = mul(localToWorld, float4(pB.currentPos, 1.0)).xyz; bool meltA = (distance(worldPosA, meltRegionPosition) < meltRadius); bool meltB = (distance(worldPosB, meltRegionPosition) < meltRadius); // 멜트 영역의 파티클은 제약 강도가 약해짐 float factor = (meltA || meltB) ? (1.0 - meltStrength) : 1.0; float3 correction = (delta / currentLength) * (error * 0.5 * factor);
Plain Text
복사

볼륨 보존과 형태 유지

유리가 변형되더라도 전체 부피는 보존되어야 한다는 물리적 특성을 반영하기 위해 Volume Constraint를 추가했다. 또한 과도한 변형을 방지하기 위한 Shape Constraint도 함께 구현했다.
// 볼륨 제약 적용 [numthreads(256, 1, 1)] void ApplyVolumeConstraint(uint id : SV_DispatchThreadID) { Particle p = particles[id]; float3 dir = p.currentPos - volumeCenter; p.currentPos = volumeCenter + dir * volumeCorrectionFactor; particles[id] = p; }
Plain Text
복사
온도와 점도 분포

메쉬 적응적 분할의 시도와 좌절

토폴로지 문제의 심화

시뮬레이션이 발전할수록 기존 메쉬의 한계가 더욱 명확해졌다. 특히 곡률이 큰 영역에서 각진 형태가 나타나거나, 과도한 변형으로 인해 삼각형이 뒤집히는 문제가 빈번히 발생했다.
이를 해결하기 위해 실시간 메쉬 분할(Adaptive Mesh Subdivision)을 시도했다. 에지의 길이가 임계값을 초과하면 중간점을 추가하여 메쉬를 세분화하는 방식이었다.
// 적응적 메쉬 분할 시도 Mesh SubdivideMesh(Mesh mesh, float threshold) { Dictionary<Edge, int> edgeMidpointDict = new Dictionary<Edge, int>(); for (int i = 0; i < triangles.Length; i += 3) { int i0 = triangles[i]; int i1 = triangles[i + 1]; int i2 = triangles[i + 2]; // 각 에지의 중점이 필요한지 확인 int m01 = GetOrCreateMidpoint(i0, i1, threshold, edgeMidpointDict); int m12 = GetOrCreateMidpoint(i1, i2, threshold, edgeMidpointDict); int m20 = GetOrCreateMidpoint(i2, i0, threshold, edgeMidpointDict); // 분할 패턴에 따라 새로운 삼각형 생성 SubdivideTriangle(i0, i1, i2, m01, m12, m20, newTriangles); } return newMesh; }
C#
복사

예상치 못한 복잡성

하지만 실시간 메쉬 분할은 예상보다 훨씬 복잡한 문제였다. 새로 생성된 정점들의 물리적 속성을 어떻게 보간할 것인가, 제약 조건은 어떻게 업데이트할 것인가, GPU 버퍼의 크기 변화를 어떻게 처리할 것인가 등 수많은 부차적 문제들이 연쇄적으로 발생했다.
무엇보다 메쉬 분할로 인한 성능 부담이 시뮬레이션 자체보다 훨씬 컸다. VR 환경에서 실시간 처리가 불가능한 수준이었다.

터셀레이션과 지오메트리 쉐이더 탐구

GPU 기반 메쉬 세분화 모색

메쉬 분할의 CPU 부담을 줄이기 위해 GPU의 테셀레이션 기능을 활용하는 방법을 연구했다. Hull Shader와 Domain Shader를 통해 실시간으로 메쉬를 세분화할 수 있다면 이상적일 것이라 생각했다.
하지만 Unity에서는 테셀레이션 지원이 제한적이고, 특히 URP에서는 기본적으로 지원하지 않는다는 사실을 뒤늦게 알게 되었다. Geometry Shader 역시 성능상 권장되지 않는 기술이었다.

기술적 한계의 인정

며칠간의 연구 끝에 현재 Unity 환경에서는 실시간 메쉬 적응적 분할이 현실적이지 않다는 결론에 도달했다. 대신 초기 메쉬의 품질을 높이고, 물리 시뮬레이션의 안정성을 개선하는 방향으로 접근 방식을 수정했다.

발견된 근본적 문제들

UV 기반 온도 시뮬레이션의 한계

UV 좌표를 활용한 온도 전이 방식은 작동은 했지만, 여러 한계가 있었다. 첫째, 모델링 단계에서 온도 시뮬레이션을 고려한 UV 설계가 필요했다. 복잡한 형태의 유리 제품에서는 UV 접힘이나 왜곡으로 인해 부정확한 결과가 나왔다.
둘째, UV 그리드의 해상도와 메쉬의 정점 밀도가 일치하지 않는 영역에서는 온도 정보의 손실이 발생했다. 이는 특히 곡률이 큰 부분에서 문제가 되었다.

토폴로지 안정성 문제

유리의 자유로운 변형을 허용하다 보니 메쉬의 토폴로지가 완전히 무너지는 경우가 빈번했다. 삼각형의 뒤집힘, 정점의 겹침, 에지의 교차 등으로 인해 렌더링이 깨지거나 물리 시뮬레이션이 불안정해졌다.
이런 문제들을 방지하기 위한 추가적인 제약 조건들을 도입하면, 결국 유리의 자연스러운 변형이 제한되는 딜레마에 빠졌다.

성능과 정확도의 트레이드오프

정확한 물리 시뮬레이션을 위해서는 높은 정점 밀도와 빈번한 제약 해결이 필요했다. 하지만 이는 VR 환경에서 요구되는 90fps를 만족시키기 어려운 계산 부담을 가져왔다.
특히 외부 포스 필드(사용자의 도구)와 상호작용할 때 성능 저하가 심각했다. 실시간 물리 기반 렌더링까지 고려하면 현재 하드웨어로는 한계가 명확했다.

프로토타입의 성과와 한계

구현된 핵심 기능들

3개월간의 프로토타입 개발을 통해 다음과 같은 핵심 기능들을 구현할 수 있었다:
Position Based Dynamics 기반 소프트바디 시뮬레이션
컴퓨트 쉐이더를 활용한 GPU 가속 물리 계산
UV 기반 온도 시뮬레이션과 열 확산
온도에 따른 점성 변화와 제약 강도 조절
볼륨 보존과 형태 유지를 위한 추가 제약들
실시간 메쉬 변형과 시각적 피드백

명확해진 기술적 한계들

동시에 다음과 같은 근본적 한계들도 확인되었다:
1.
모델링 의존성: UV 기반 접근법은 특별히 설계된 모델에서만 효과적이다
2.
토폴로지 불안정성: 자유로운 변형과 메쉬 안정성은 양립하기 어렵다
3.
성능 제약: 정확한 시뮬레이션과 실시간 성능 사이의 근본적 갈등
4.
정밀도 문제: 매 프레임 누적되는 수치 오차로 인한 결과 왜곡

향후 방향성과 교훈

기술적 접근법의 재고

프로토타입 개발 과정에서 얻은 가장 중요한 교훈은 “완벽한 물리 시뮬레이션보다 재미 있는 공예 경험”이 더 중요하다는 것이었다. 더 완벽하게 대응할 방법을 고민할 수록 들던 생각이었다. 항상 모든 변형에 대응할 필요는 없다, 블로잉 하면서 동시에 마버링 하는 등의 케이스는 존재하지 않을 것이다. 사실 사용자는 물리적 정확성보다는 직관적이고 일관된 반응을 원한다.
차세대 구현에서는 하이브리드 접근법을 고려하고 있다. 핵심 변형은 단순화된 물리 모델로 처리하고, 세부적인 시각적 효과는 절차적 생성이나 사전 계산된 애니메이션으로 보완하는 방식이다.

성능 최적화 전략

VR 환경에서의 안정적인 성능을 위해서는 기획적으로 각 단계에서 요구되는 시뮬레이션의 복잡도를 영리하게 제한하는 방향을 고려하고 있다. 도구별로 아예 셰이더나 기능들을 스위칭하는 식으로 최적화하려고 한다.
또한 GPU의 힘을 깨달았기 때문에 적극 활용하되, 시뮬레이션과 렌더링 사이의 동기화 오버헤드를 최소화하는 방향으로 아키텍처를 재설계할 예정이다.
프로토타입은 실패했지만 성공적이었다. 가능성이 보였다. 실패를 통해 얻은 통찰이야말로 다음 단계로 나아가는 가장 소중한 자산이다. 이제 현실적이면서도 효과적인 유리 공예 시뮬레이션 시스템을 구축할 준비가 되었다.