어느새 threejs 이론 편 마지막 주차이다. 이번 주차는 그림자, 안개, 렌더타겟, 사용자 지정 BufferGeometry를 다뤄야 했지만, BufferGeometry 편에서 약간 응용 편 같이 구현된 예시가 너무 마음에 들어서 비슷한 걸 만들어보고 그 경험을 공유하는 식으로 스터디를 준비해 보았다.
예시에서는 구체를 가로방향으로 24조각, 세로방향으로 16조각 낸다고 했을 때의 꼭짓점의 위치 값을 직접 계산해서 BufferGeometry를 만든다. 이후 시간의 흐름에 따라 BufferGeometry의 모든 꼭짓점들을 일정하게 움직이도록 만든다.
홀린 듯이 예시를 보다가, 하트가 박동하며 산산조각 났다가 다시 합쳐지면 예쁘겠단 생각이 들어 구현해 보기로 했다.
// 예시코드 https://threejs.org/manual/#ko/custom-buffergeometry
function makeSpherePositions(segmentsAround, segmentsDown) {
const numVertices = segmentsAround * segmentsDown * 6;
const numComponents = 3;
const positions = new Float32Array(numVertices * numComponents);
const indices = [];
const longHelper = new THREE.Object3D();
const latHelper = new THREE.Object3D();
const pointHelper = new THREE.Object3D();
longHelper.add(latHelper);
latHelper.add(pointHelper);
pointHelper.position.z = 1;
const temp = new THREE.Vector3();
function getPoint(lat, long) {
latHelper.rotation.x = lat;
longHelper.rotation.y = long;
longHelper.updateMatrixWorld(true);
return pointHelper.getWorldPosition(temp).toArray();
}
let posNdx = 0;
let ndx = 0;
for (let down = 0; down < segmentsDown; ++down) {
const v0 = down / segmentsDown;
const v1 = (down + 1) / segmentsDown;
const lat0 = (v0 - 0.5) * Math.PI;
const lat1 = (v1 - 0.5) * Math.PI;
for (let across = 0; across < segmentsAround; ++across) {
const u0 = across / segmentsAround;
const u1 = (across + 1) / segmentsAround;
const long0 = u0 * Math.PI * 2;
const long1 = u1 * Math.PI * 2;
positions.set(getPoint(lat0, long0), posNdx); posNdx += numComponents;
positions.set(getPoint(lat1, long0), posNdx); posNdx += numComponents;
positions.set(getPoint(lat0, long1), posNdx); posNdx += numComponents;
positions.set(getPoint(lat1, long1), posNdx); posNdx += numComponents;
indices.push(
ndx, ndx + 1, ndx + 2,
ndx + 2, ndx + 1, ndx + 3,
);
ndx += 4;
}
}
return { positions, indices };
}
아이디어와 실패
처음엔 예시와 같은 방식으로 3D 하트도 만들 수 있지 않을까 했다.
그렇지만 3D 하트를 그리는 수학적인 방정식은 쉽지 않아 보였다. ㅎ
두 번째 아이디어로는 ExtrudedGeometry로 3D 하트를 만든 뒤, 꼭짓점마다 순회하며 위치정보를 찾아 예시처럼 BufferGeometry를 만들려고 했다. 그러나 하트가 만들어지지 않거나 울퉁불퉁한 구체가 되어버렸다. 중복된 꼭짓점을 제거하는 과정에서 문제가 발생한 걸로 보이는 데 어디서부터 접근해야 할지 감도 잡히지 않았다.
1조각 1 BufferGeometry 객체
세 번째 아이디어는, 수많은 조각들로 이루어진 하트를 굳이 하나의 BufferGeometry 객체로 만들기보다, 각 조각마다 BufferGeometry 객체로 만들면 간단하지 않을까 싶었다. 물론 비용면에선 객체 하나가 훨씬 낫겠지만 일단 구현을 하고 싶었기에 흐린 눈으로 넘어갔고 구현에 성공했다.
const shape = new THREE.Shape();
const x = -2.5;
const y = -5;
shape.moveTo(x + 2.5, y + 2.5);
shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);
const extrudeSettings = {
steps: 1,
depth: 1.0,
bevelEnabled: true,
bevelThickness: 1.26,
bevelSize: 2.59,
bevelSegments: 4,
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
geometry.scale(1, -1, 1); // Y축 반전
const positions = geometry.attributes.position.array;
const material = new THREE.MeshStandardMaterial({
color: 0xff1537,
side: THREE.DoubleSide,
roughness: 0.4,
metalness: 0.4
});
const cubes = []
// Loop through the vertices in sets of 3 to access faces (triangles)
for (let i = 0; i < positions.length; i += 9) {
// Each face consists of three vertices (3 vertices = 9 values: 3 x 3 coordinates)
const trianglePositions = [
positions[i], positions[i + 1], positions[i + 2],
positions[i + 3], positions[i + 4], positions[i + 5],
positions[i + 6], positions[i + 7], positions[i + 8]
]
const geometry = new THREE.BufferGeometry();
const bufferAttribute = new THREE.BufferAttribute(
new Float32Array(trianglePositions),
3
)
geometry.setAttribute('position', bufferAttribute );
geometry.setAttribute('normal', bufferAttribute);
const cube = new THREE.Mesh(geometry, material);
cubes.push(cube);
container.add(cube);
}
움직임 구현
박동하면서 멀어지는 모습을 구현하기 위해서 하트를 이루는 모든 조각들이,
현재의 위치에서 하트의 중심점으로부터의 방향과 사인그래프 값을 곱해준 만큼 위치가 변하도록 해주었다.
이때 부드럽게 움직이게 하기 위해 lerp 함수를 사용했고,
하트가 커졌다가 다시 돌아와야 하기 때문에 사인그래프의 값이 상수보다 작을 때 -1을 곱해주었다.
마지막으로 메탈릭 표면이 빛에 따라 반짝거리는 걸 잘 보여주고 싶어 y축을 중심으로 하트의 박동에 맞춰 조금씩 회전할 수 있게 했다.
const cubePositions = []
// Loop through the vertices in sets of 3 to access faces (triangles)
for (let i = 0; i < positions.length; i += 9) {
// Each face consists of three vertices (3 vertices = 9 values: 3 x 3 coordinates)
const trianglePositions = [
positions[i], positions[i + 1], positions[i + 2],
positions[i + 3], positions[i + 4], positions[i + 5],
positions[i + 6], positions[i + 7], positions[i + 8]
]
cubePositions.push(new THREE.Vector3().subVectors(
new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]),
new THREE.Vector3(0, 0, 0)
).normalize())
...
}
...
const sizeFactor = A * Math.sin(B * time) + D; // 심장박동 함수
cubes.forEach((cube, index) => {
const moveDirection = sizeFactor >= D ? 1 : -1;
const newPosition = cubePositions[index].clone().multiplyScalar(sizeFactor * moveDirection);
const dampingFactor = 0.01;
cube.position.lerp(newPosition, dampingFactor);
});
container.rotation.y = Math.sin( time) * 0.2 ;
결과
결과적으론 아래처럼 나왔는 데, 생각보다 예쁘게 나와 구경하는 재미가 있었다. 끗
'개발 > js' 카테고리의 다른 글
네이버 앱에서 css가 깨질 땐..! (1) | 2024.11.20 |
---|---|
three.js 스터디 4주차) 카메라 컨트롤, 여러가지 재질과 텍스처를 곁들인 (2) | 2024.11.14 |
three.js 스터디 3주차) 씬 그래프 탐구 feat.태양계 (0) | 2024.11.14 |
three.js 스터디 2주차) Primitives (2) | 2024.11.01 |
three.js 스터디 1주차) Hello Cube! (0) | 2024.10.29 |