Clock
출처: Three.js in practice - 3D clock - tutorial for beginners 2022
일단 three.js의 기본 씬 설정 코드이다. three.js를 화면에 띄우기 위해서는 기본적으로 scene, camera, renderer 세 가지가 필요하다.
let scene = new Scene();
scene.background = new Color("white");
let camera = new PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 1000);
// Perspective Camera의 인자로는 fov, aspect ratio, near, far이 있다.
camera.position.set(0, 0, 10);
let renderer = new WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = ACESFilmicToneMapping;
// toneMapping은 HDR 이미지를 HDR을 지원하지 않는 일반모니터에서 추론할 수 있도록 해준다. (default: NoToneMapping)
renderer.outputEncoding = sRGBEncoding;
// 결과물의 인코딩을 정의한다. (default: LinearEncoding)
document.body.appendChild(renderer.domElement);
(function init() {
renderer.setAnumationLoop(() => {
renderer.render(scene, camera);
});
})();
orbit control과 PMREMGenerator을 추가해준다.
let controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
let pmrem = new PMREMGenerator(renderer);
// Prefiltered, Minimapped Radiance Environment Map (PMREM)을 생성해 재질의 roughness에 따라 다양한 수준의 블러를 빠르게 적용할 수 있다.
pmrem.compileEquirectangularShader();
// 큐브 맵 셰이더를 미리 컴파일한다. 텍스쳐의 네트워크 로딩 중 메서드를 호출하여 더 빨리 시작할 수 있다.
// in init function
let envHdrTexture = await new RGBELoader().loadAsync(
"./../assets/vestibule_4k.hdr"
);
// hdr 파일은 poly haven에서 아무거나 가져왔다.
let envRT = pmrem.fromEquirectangular(envHdrTexture);
// in animationloop function
controls.update();
다음으로 링을 생성하는 함수를 만들어준다.
function customRing(envRT, thickness, color) {
let ring = new Mesh(
new RingGeometry(2, 2 + thickness, 70),
// RingGeometry의 인자로는 innerRadius, outerRadius, thetaSegments(roundness)가 있다.
new MeshStandardMaterial({
envMap: envRT.texture,
roughness: 0,
metalness: 1,
side: DoubleSide,
color,
envMapIntensity: 1,
})
);
ring.position.set(0, 0, 0.25 * 0.5);
// RingGeometry는 2D이므로 입체감을 위해 바깥쪽과 안쪽에 cylinder를 만들어준다.
let outerCylinder = new Mesh(
new CylinderBufferGeometry(2 + thickness, 2 + thickness, 0.25, 70, 1, true),
// 인자는 radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded
new MeshStandardMaterial({
envMap: envRT.texture,
roughness: 0,
metalness: 1,
side: DoubleSide,
color,
envMapIntensity: 1,
})
);
outerCylinder.rotation.x = Math.PI * 0.5;
let innerCylinder = new Mesh(
new CylinderBufferGeometry(2, 2, 0.25, 140, 1, true),
new MeshStandardMaterial({
envMap: envRT.texture,
roughness: 0,
metalness: 1,
side: DoubleSide,
color,
envMapIntensity: 1,
})
);
innerCylinder.rotation.x = Math.PI * 0.5;
// 그룹으로 묶어서 리턴
let group = new Group();
group.add(ring, outerCylinder, innerCylinder);
return group;
}
마우스의 움직임을 트래킹하는 이벤트 리스너를 추가해준다.
let mousePos = new Vector2(0, 0);
window.addEventListener("mousemove", (e) => {
// 화면의 중앙을 0, 0으로 설정
let x = e.clientX - innerWidth * 0.5;
let y = e.clientY - innerHeight * 0.5;
// 배율 조정
mousePos.x = x * 0.001;
mousePos.y = y * 0.001;
});
init 함수에 링을 추가해준다.
// in init function
let ring1 = customRing(envRT, 0.65, "white");
ring1.scale.set(0.75, 0.75);
scene.add(ring1);
let ring2 = customRing(envRT, 0.35, new Color(0.25, 0.225, 0.215));
ring2.scale.set(1.05, 1.05);
scene.add(ring2);
let ring3 = customRing(envRT, 0.15, new Color(0.7, 0.7, 0.7));
ring3.scale.set(1.3, 1.3);
scene.add(ring3);
그리고 animation loop 함수에 마우스 포지션에 따른 이동을 반영해준다.
// in animationLoop function
ring1.rotation.x = ring1.rotation.x * 0.95 + mousePos.y * 1.2 * 0.05;
ring1.rotation.y = ring1.rotation.y * 0.95 + mousePos.x * 1.2 * 0.05;
ring2.rotation.x = ring2.rotation.x * 0.95 + mousePos.y * 0.375 * 0.05;
ring2.rotation.y = ring2.rotation.y * 0.95 + mousePos.x * 0.375 * 0.05;
ring3.rotation.x = ring3.rotation.x * 0.95 - mousePos.y * 0.275 * 0.05;
ring3.rotation.y = ring3.rotation.y * 0.95 - mousePos.x * 0.275 * 0.05;
다음으로는 시간 표기를 위한 선을 생성하는 함수를 만들어준다.
function customLine(height, width, depth, envRT, color, envMapIntensity) {
// 박스 위아래로 cylinder를 넣어 뭉뚝한 선을 만들어준다.
let box = new Mesh(
new BoxBufferGeometry(width, height, depth),
new MeshStandardMaterial({
envMap: envRT.texture,
roughness: 0,
metalness: 1,
side: DoubleSide,
color,
envMapIntensity,
})
);
box.position.set(0, 0, 0);
let topCap = new Mesh(
new CylinderBufferGeometry(width * 0.5, width * 0.5, depth, 10),
new MeshStandardMaterial({
envMap: envRT.texture,
roughness: 0,
metalness: 1,
side: DoubleSide,
color,
envMapIntensity,
})
);
topCap.rotation.x = Math.PI * 0.5;
topCap.position.set(0, +height * 0.5, 0);
let bottomCap = new Mesh(
new CylinderBufferGeometry(width * 0.5, width * 0.5, depth, 10),
new MeshStandardMaterial({
envMap: envRT.texture,
roughness: 0,
metalness: 1,
side: DoubleSide,
color,
envMapIntensity,
})
);
bottomCap.rotation.x = Math.PI * 0.5;
bottomCap.position.set(0, -height * 0.5, 0);
let group = new Group();
group.add(box, topCap, bottomCap);
return group;
}
생성된 선을 움직이는 함수를 만들어준다. (3개의 matrix를 곱해서 이동시켜주는데 이 부분은 이해가 더 필요하다.)
function rotateLine(
line,
angle,
ringRotation,
topTranslation,
depthTranslation
) {
let tmatrix = new Matrix4().makeTranslation(
0,
topTranslation,
depthTranslation
);
let rmatrix = new Matrix4().makeRotationAxis(new Vector3(0, 0, 1), -angle);
let r1matrix = new Matrix4().makeRotationFromEuler(
new Euler().copy(ringRotation)
);
line.matrix.copy(
new Matrix4().multiply(r1matrix).multiply(rmatrix).multiply(tmatrix)
);
line.matrixAutoUpdate = false;
line.matrixWorldNeedsUpdate = false;
}
시침, 분침, 초침을 추가해준다.
// in init function
let hourLine = customLine(0.4, 0.135, 0.07, envRT, "white", 3);
scene.add(hourLine);
let minuteLine = customLine(
0.8,
0.135,
0.07,
envRT,
new Color(0.5, 0.5, 0.5),
1
);
scene.add(minuteLine);
let secoundLine = customLine(
1,
0.075,
0.07,
envRT,
new Color(0.2, 0.2, 0.2),
1
);
scene.add(secoundLine);
// in animationLoop function
let date = new Date();
let hourAngle = (date.getHours() / 12) * Math.PI * 2;
rotateLine(hourLine, hourAngle, ring1.rotation, 1.0, 0);
let minuteAngle = (date.getMinutes() / 60) * Math.PI * 2;
rotateLine(minuteLine, minuteAngle, ring1.rotation, 0.8, 0.1);
let secondAngle = (date.getSeconds() / 60) * Math.PI * 2;
rotateLine(secoundLine, secondAngle, ring1.rotation, 0.75, -0.1);
마지막으로 시계의 시간을 표시해 출 선을 추가해준다.
function clockLines(envRT) {
let group = new Group();
for (let i = 0; i < 12; i++) {
let line = customLine(
0.1,
0.075,
0.025,
envRT,
new Color(0.65, 0.65, 0.65),
1
);
group.add(line);
}
return group;
}
// in init function
let cLines = clockLines(envRT);
scene.add(cLines);
// in animationLoop function
cLines.children.forEach((c, i) => {
rotateLine(c, (i / 12) * Math.PI * 2, ring1.rotation, 1.72, 0.2);
});
마우스 움직임에 따라 회전하는 시계가 완성되었다.