THREE.JS 퀵 레퍼런스 코드

Three.js를 이용한 개발 시 개인적으로 빠르게 참조하기 위해 작성한 글입니다.

three.js 기본 프로젝트 생성 (WebGL)

git clone https://github.com/GISDEVCODE/threejs-with-javascript-starter.git .

three.js 기본 프로젝트 생성 (WebGPU)

git clone https://github.com/GISDEVCODE/threejs-webgpu-with-javascript-starter.git .

R3F 기본 프로젝트 생성 (Javascript)

git clone https://github.com/GISDEVCODE/r3f-with-javascript-starter.git .

Shader 기본 프로젝트 생성 (Javascript)

git clone https://github.com/GISDEVCODE/shader-with-threejs-javascript-starter .

그림자 적용에 대한 코드

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.VSMShadowMap;
const shadowLight = new THREE.DirectionalLight(0xffe79d, 0.7);
shadowLight.position.set(150, 220, 100);
shadowLight.target.position.set(0,0,0);
shadowLight.castShadow = true;
shadowLight.shadow.mapSize.width = 1024*10;
shadowLight.shadow.mapSize.height = 1024*10;
shadowLight.shadow.camera.top = shadowLight.shadow.camera.right = 1000;
shadowLight.shadow.camera.bottom = shadowLight.shadow.camera.left = -1000;
shadowLight.shadow.camera.far = 800;
shadowLight.shadow.radius = 5;
shadowLight.shadow.blurSamples = 5;
shadowLight.shadow.bias = -0.0002;
const cameraHelper = new THREE.CameraHelper(shadowLight.shadow.camera);
this._scene.add(cameraHelper);
island.receiveShadow = true;
island.castShadow = true;
renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.VSMShadowMap; const shadowLight = new THREE.DirectionalLight(0xffe79d, 0.7); shadowLight.position.set(150, 220, 100); shadowLight.target.position.set(0,0,0); shadowLight.castShadow = true; shadowLight.shadow.mapSize.width = 1024*10; shadowLight.shadow.mapSize.height = 1024*10; shadowLight.shadow.camera.top = shadowLight.shadow.camera.right = 1000; shadowLight.shadow.camera.bottom = shadowLight.shadow.camera.left = -1000; shadowLight.shadow.camera.far = 800; shadowLight.shadow.radius = 5; shadowLight.shadow.blurSamples = 5; shadowLight.shadow.bias = -0.0002; const cameraHelper = new THREE.CameraHelper(shadowLight.shadow.camera); this._scene.add(cameraHelper); island.receiveShadow = true; island.castShadow = true;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.VSMShadowMap;

const shadowLight = new THREE.DirectionalLight(0xffe79d, 0.7);
shadowLight.position.set(150, 220, 100);
shadowLight.target.position.set(0,0,0);
shadowLight.castShadow = true;
shadowLight.shadow.mapSize.width = 1024*10;
shadowLight.shadow.mapSize.height = 1024*10;
shadowLight.shadow.camera.top = shadowLight.shadow.camera.right = 1000;
shadowLight.shadow.camera.bottom = shadowLight.shadow.camera.left = -1000;
shadowLight.shadow.camera.far = 800;
shadowLight.shadow.radius = 5;
shadowLight.shadow.blurSamples = 5;
shadowLight.shadow.bias = -0.0002;

const cameraHelper = new THREE.CameraHelper(shadowLight.shadow.camera);
this._scene.add(cameraHelper);

island.receiveShadow = true;
island.castShadow = true;

지오메트리의 좌표 수정

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const sphereGeom = new THREE.SphereGeometry(6 + Math.floor(Math.random() * 12), 8, 8);
const sphereGeomPosition = sphereGeom.attributes.position;
for (var i = 0; i < sphereGeomPosition.count; i++) {
sphereGeomPosition.setY(i, sphereGeomPosition.getY(i) + Math.random() * 4 - 2);
sphereGeomPosition.setX(i, sphereGeomPosition.getX(i) + Math.random() * 3 - 1.5);
sphereGeomPosition.setZ(i, sphereGeomPosition.getZ(i) + Math.random() * 3 - 1.5);
}
sphereGeom.computeVertexNormals();
sphereGeom.attributes.position.needsUpdate = true;
const sphereGeom = new THREE.SphereGeometry(6 + Math.floor(Math.random() * 12), 8, 8); const sphereGeomPosition = sphereGeom.attributes.position; for (var i = 0; i < sphereGeomPosition.count; i++) { sphereGeomPosition.setY(i, sphereGeomPosition.getY(i) + Math.random() * 4 - 2); sphereGeomPosition.setX(i, sphereGeomPosition.getX(i) + Math.random() * 3 - 1.5); sphereGeomPosition.setZ(i, sphereGeomPosition.getZ(i) + Math.random() * 3 - 1.5); } sphereGeom.computeVertexNormals(); sphereGeom.attributes.position.needsUpdate = true;
const sphereGeom = new THREE.SphereGeometry(6 + Math.floor(Math.random() * 12), 8, 8);
const sphereGeomPosition = sphereGeom.attributes.position;
for (var i = 0; i < sphereGeomPosition.count; i++) {
    sphereGeomPosition.setY(i, sphereGeomPosition.getY(i) + Math.random() * 4 - 2);
    sphereGeomPosition.setX(i, sphereGeomPosition.getX(i) + Math.random() * 3 - 1.5);
    sphereGeomPosition.setZ(i, sphereGeomPosition.getZ(i) + Math.random() * 3 - 1.5);
}

sphereGeom.computeVertexNormals();
sphereGeom.attributes.position.needsUpdate = true;

지오메트리에 사용자 정의 데이터 주입

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 주입
const waves = [];
const waterGeoPositions = waterGeo.attributes.position;
for (let i = 0; i < waterGeoPositions.count; i++) {
waves[i] = Math.random() * 100;
}
waterGeo.setAttribute("wave", new THREE.Float32BufferAttribute(waves, 1));
// 읽기
const waves = sea.geometry.attributes.wave;
for(let i=0; i<positions.count; i++) {
const v = waves.getX(i);
}
// 주입 const waves = []; const waterGeoPositions = waterGeo.attributes.position; for (let i = 0; i < waterGeoPositions.count; i++) { waves[i] = Math.random() * 100; } waterGeo.setAttribute("wave", new THREE.Float32BufferAttribute(waves, 1)); // 읽기 const waves = sea.geometry.attributes.wave; for(let i=0; i<positions.count; i++) { const v = waves.getX(i); }
// 주입
const waves = [];
const waterGeoPositions = waterGeo.attributes.position;
for (let i = 0; i < waterGeoPositions.count; i++) {
    waves[i] = Math.random() * 100;
}
waterGeo.setAttribute("wave", new THREE.Float32BufferAttribute(waves, 1));

// 읽기
const waves = sea.geometry.attributes.wave;

for(let i=0; i<positions.count; i++) {
    const v = waves.getX(i);
}

안개 설정 코드

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
scene.fog = new THREE.Fog("rgba(54,219,214,1)", 1000, 1400);
scene.fog = new THREE.Fog("rgba(54,219,214,1)", 1000, 1400);
scene.fog = new THREE.Fog("rgba(54,219,214,1)", 1000, 1400);

OrbitControls 관련 코드

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const controls = new OrbitControls(this._camera, this._divContainer);
controls.minPolarAngle = -Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2 + 0.1;
controls.enableZoom = true;
controls.enablePan = false;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.2;
this._controls = controls;
this._controls.update();
const controls = new OrbitControls(this._camera, this._divContainer); controls.minPolarAngle = -Math.PI / 2; controls.maxPolarAngle = Math.PI / 2 + 0.1; controls.enableZoom = true; controls.enablePan = false; controls.autoRotate = true; controls.autoRotateSpeed = 0.2; this._controls = controls; this._controls.update();
const controls = new OrbitControls(this._camera, this._divContainer);
controls.minPolarAngle = -Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2 + 0.1;
controls.enableZoom = true;
controls.enablePan = false;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.2;

this._controls = controls;

this._controls.update();

Object3D의 MBR 얻기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const board = this._scene.getObjectByName("Board");
const box = new THREE.Box3().setFromObject(board);
console.log(box);
const board = this._scene.getObjectByName("Board"); const box = new THREE.Box3().setFromObject(board); console.log(box);
const board = this._scene.getObjectByName("Board");
const box = new THREE.Box3().setFromObject(board);
console.log(box);

Mesh의 월드좌표에 대한 position 얻기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
mesh.updateMatrixWorld();
const worldPos = new THREE.Vector3();
worldPos.setFromMatrixPosition(mesh.matrixWorld);
mesh.updateMatrixWorld(); const worldPos = new THREE.Vector3(); worldPos.setFromMatrixPosition(mesh.matrixWorld);
mesh.updateMatrixWorld();

const worldPos = new THREE.Vector3();
worldPos.setFromMatrixPosition(mesh.matrixWorld);

Faked Shadow

그림자를 위한 매시에 대한 재질 속성 지정이 핵심. 참고로 shadow에 대한 이미지는 투명 이미지가 아님. 즉, 배경색이 하얀색인 이미지임.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );
const mesh = new THREE.Mesh(
new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
new THREE.MeshBasicMaterial( {
map: shadow,
blending: THREE.MultiplyBlending,
toneMapped: false,
transparent: true
} )
);
mesh.rotation.x = - Math.PI / 2;
mesh.renderOrder = 2;
carModel.add( mesh );
const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' ); const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ), new THREE.MeshBasicMaterial( { map: shadow, blending: THREE.MultiplyBlending, toneMapped: false, transparent: true } ) ); mesh.rotation.x = - Math.PI / 2; mesh.renderOrder = 2; carModel.add( mesh );
const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );

const mesh = new THREE.Mesh(
    new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
    new THREE.MeshBasicMaterial( {
        map: shadow, 
        blending: THREE.MultiplyBlending, 
        toneMapped: false, 
        transparent: true
    } )
);
mesh.rotation.x = - Math.PI / 2;
mesh.renderOrder = 2;
carModel.add( mesh );

텍스쳐 이미지 품질 올리기

샘플링 횟수를 올리는 것으로 속도는 느려질 수 있으나 품질은 향상됨

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();

async 리소스 로딩

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
async function init() {
const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/');
const gltfLoader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/');
const [texture, gltf] = await Promise.all([
rgbeLoader.loadAsync( 'venice_sunset_1k.hdr' ),
gltfLoader.loadAsync( 'DamagedHelmet.gltf' ),
]);
}
init().catch(function(err) {
console.error(err);
});
async function init() { const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/'); const gltfLoader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); const [texture, gltf] = await Promise.all([ rgbeLoader.loadAsync( 'venice_sunset_1k.hdr' ), gltfLoader.loadAsync( 'DamagedHelmet.gltf' ), ]); } init().catch(function(err) { console.error(err); });
async function init() {
    const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/');
    const gltfLoader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/');

    const [texture, gltf] = await Promise.all([
        rgbeLoader.loadAsync( 'venice_sunset_1k.hdr' ),
        gltfLoader.loadAsync( 'DamagedHelmet.gltf' ),
    ]);
}

init().catch(function(err) {
    console.error(err);
});

텍스쳐를 Canvas로 후다닥 만들기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const canvas = document.createElement( 'canvas' );
canvas.width = 1;
canvas.height = 32;
const context = canvas.getContext( '2d' );
const gradient = context.createLinearGradient( 0, 0, 0, 32 );
gradient.addColorStop( 0.0, '#ff0000' );
gradient.addColorStop( 0.5, '#00ff00' );
gradient.addColorStop( 1.0, '#0000ff' );
context.fillStyle = gradient;
context.fillRect( 0, 0, 1, 32 );
const sky = new THREE.Mesh(
new THREE.SphereGeometry( 10 ),
new THREE.MeshBasicMaterial( { map: new THREE.CanvasTexture( canvas ), side: THREE.BackSide } )
);
scene.add( sky );
const canvas = document.createElement( 'canvas' ); canvas.width = 1; canvas.height = 32; const context = canvas.getContext( '2d' ); const gradient = context.createLinearGradient( 0, 0, 0, 32 ); gradient.addColorStop( 0.0, '#ff0000' ); gradient.addColorStop( 0.5, '#00ff00' ); gradient.addColorStop( 1.0, '#0000ff' ); context.fillStyle = gradient; context.fillRect( 0, 0, 1, 32 ); const sky = new THREE.Mesh( new THREE.SphereGeometry( 10 ), new THREE.MeshBasicMaterial( { map: new THREE.CanvasTexture( canvas ), side: THREE.BackSide } ) ); scene.add( sky );
const canvas = document.createElement( 'canvas' );
canvas.width = 1;
canvas.height = 32;

const context = canvas.getContext( '2d' );
const gradient = context.createLinearGradient( 0, 0, 0, 32 );
gradient.addColorStop( 0.0, '#ff0000' );
gradient.addColorStop( 0.5, '#00ff00' );
gradient.addColorStop( 1.0, '#0000ff' );
context.fillStyle = gradient;
context.fillRect( 0, 0, 1, 32 );

const sky = new THREE.Mesh(
	new THREE.SphereGeometry( 10 ),
	new THREE.MeshBasicMaterial( { map: new THREE.CanvasTexture( canvas ), side: THREE.BackSide } )
);
scene.add( sky );

GLTF 파일 로딩

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { GLTFLoader } from "../examples/jsm/loaders/GLTFLoader.js"
const loader = new GLTFLoader();
loader.load("./data/ring.glb", gltf => {
const object = gltf.scene;
this._scene.add(object);
});
import { GLTFLoader } from "../examples/jsm/loaders/GLTFLoader.js" const loader = new GLTFLoader(); loader.load("./data/ring.glb", gltf => { const object = gltf.scene; this._scene.add(object); });
import { GLTFLoader } from "../examples/jsm/loaders/GLTFLoader.js"

const loader = new GLTFLoader();
loader.load("./data/ring.glb", gltf => {
    const object = gltf.scene;
    this._scene.add(object);
});    

InstancedMesh

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const mesh = new THREE.InstancedMesh(geometry, material, 10000)
const matrix = new THREE.Matrix4()
const dummy = new THREE.Object3D()
for(let i = 0; i < 10000; i++) {
mesh.getMatrixAt(i, matrix)
matrix.decompose(dummy.position, dummy.rotation, dummy.scale)
dummy.rotation.x = Math.random()
dummy.rotation.y = Math.random()
dummy.rotation.z = Math.random()
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
mesh.setColorAt(i, new THREE.Color(Math.random() * 0xffffff)
}
mesh.instanceMatrix.needsUpdate()
const mesh = new THREE.InstancedMesh(geometry, material, 10000) const matrix = new THREE.Matrix4() const dummy = new THREE.Object3D() for(let i = 0; i < 10000; i++) { mesh.getMatrixAt(i, matrix) matrix.decompose(dummy.position, dummy.rotation, dummy.scale) dummy.rotation.x = Math.random() dummy.rotation.y = Math.random() dummy.rotation.z = Math.random() dummy.updateMatrix() mesh.setMatrixAt(i, dummy.matrix) mesh.setColorAt(i, new THREE.Color(Math.random() * 0xffffff) } mesh.instanceMatrix.needsUpdate()
const mesh = new THREE.InstancedMesh(geometry, material, 10000)

const matrix = new THREE.Matrix4()
const dummy = new THREE.Object3D()
 
for(let i = 0; i < 10000; i++) {
  mesh.getMatrixAt(i, matrix)
  matrix.decompose(dummy.position, dummy.rotation, dummy.scale)
  
  dummy.rotation.x = Math.random()
  dummy.rotation.y = Math.random()
  dummy.rotation.z = Math.random()

  dummy.updateMatrix()

  mesh.setMatrixAt(i, dummy.matrix)
  mesh.setColorAt(i, new THREE.Color(Math.random() * 0xffffff)
}

mesh.instanceMatrix.needsUpdate()

Image 기반 광원(IBL)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { RGBELoader } from 'three/examples/jsm/Addons.js'
...
new RGBELoader().setPath("./").load("pine_attic_2k.hdr", (data) => {
data.mapping = THREE.EquirectangularReflectionMapping;
// this.scene.background = data;
// this.scene.backgroundBlurriness = 0.6;
this.scene.environment = data;
})
import { RGBELoader } from 'three/examples/jsm/Addons.js' ... new RGBELoader().setPath("./").load("pine_attic_2k.hdr", (data) => { data.mapping = THREE.EquirectangularReflectionMapping; // this.scene.background = data; // this.scene.backgroundBlurriness = 0.6; this.scene.environment = data; })
import { RGBELoader } from 'three/examples/jsm/Addons.js'

...

new RGBELoader().setPath("./").load("pine_attic_2k.hdr", (data) => {
  data.mapping = THREE.EquirectangularReflectionMapping;
  // this.scene.background = data;
  // this.scene.backgroundBlurriness = 0.6;
  this.scene.environment = data;
})

GLTF / GLB 파일 로딩

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { GLTFLoader, OrbitControls } from "three/addons/Addons.js"
...
const loader = new GLTFLoader();
loader.load(
"fileName.glb",
(gltf) => {
this._scene.add( gltf.scene );
},
(xhr) => { console.log( ( xhr.loaded / xhr.total * 100 ) + "% loaded" ); },
(error) => { console.log( "An error happened" ); }
);
import { GLTFLoader, OrbitControls } from "three/addons/Addons.js" ... const loader = new GLTFLoader(); loader.load( "fileName.glb", (gltf) => { this._scene.add( gltf.scene ); }, (xhr) => { console.log( ( xhr.loaded / xhr.total * 100 ) + "% loaded" ); }, (error) => { console.log( "An error happened" ); } );
import { GLTFLoader, OrbitControls } from "three/addons/Addons.js"

...

const loader = new GLTFLoader();
loader.load(
  "fileName.glb",
  (gltf) => {
    this._scene.add( gltf.scene );    
  },
  (xhr) => { console.log( ( xhr.loaded / xhr.total * 100 ) + "% loaded" ); },
  (error) => { console.log( "An error happened" ); }
);

Object3D를 빌보드로 만들기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
this._mesh.quaternion.copy(this._camera.quaternion );
// or
this._mesh.rotation.setFromRotationMatrix( this._camera.matrix );
this._mesh.quaternion.copy(this._camera.quaternion ); // or this._mesh.rotation.setFromRotationMatrix( this._camera.matrix );
this._mesh.quaternion.copy(this._camera.quaternion );
// or
this._mesh.rotation.setFromRotationMatrix( this._camera.matrix );

FPS 제한하기

requestAnimationFrame로 렌더링을 수행하면 최대한 많은 프레임을 생성하기 됨. 아래는 원하는 프레임수로 제한하기 위해 다음 코드로 30 프레임 제한입니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
_elapsedTime = 0;
_fps = 1 / 60
render() {
const delta = this._clock.getDelta();
this.update(delta);
this._elapsedTime += delta;
if (this._elapsedTime >= (this._fps)) {
this._stats.begin();
this._renderer.render(this._scene, this._camera);
this._stats.end();
this._elapsedTime %= this._fps;
}
requestAnimationFrame(this.render.bind(this));
}
_elapsedTime = 0; _fps = 1 / 60 render() { const delta = this._clock.getDelta(); this.update(delta); this._elapsedTime += delta; if (this._elapsedTime >= (this._fps)) { this._stats.begin(); this._renderer.render(this._scene, this._camera); this._stats.end(); this._elapsedTime %= this._fps; } requestAnimationFrame(this.render.bind(this)); }
  _elapsedTime = 0;
  _fps = 1 / 60
  render() {
    const delta = this._clock.getDelta();
    this.update(delta);
    this._elapsedTime += delta;
    
    if (this._elapsedTime >= (this._fps)) {
      this._stats.begin();
      this._renderer.render(this._scene, this._camera);
      this._stats.end();
      this._elapsedTime %= this._fps;
    }

    requestAnimationFrame(this.render.bind(this));
  }

화면 좌표를 월드 좌표로 변환하는 함수

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 스크린 좌표 -> 월드 좌표 변환 함수
screenToWorld(screenX, screenY, ndcZ = 0.5) { // ndcZ=0.5로 설정 (카메라 방향)
// 1. 화면 픽셀 좌표를 정규화된 디바이스 좌표(NDC)로 변환
const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1;
const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1;
// 2. NDC를 3D 공간의 Ray로 변환
const vector = new THREE.Vector3(ndcX, ndcY, ndcZ);
vector.unproject(this._camera);
// 3. Ray를 따라 월드 좌표를 계산
return vector;
}
screenToWorldAtZ(screenX, screenY, targetZ = 0) {
// 1. 화면 픽셀 좌표 -> NDC 변환
const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1;
const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1;
// 2. NDC -> Ray 생성
const raycaster = new THREE.Raycaster();
const vector = new THREE.Vector2(ndcX, ndcY);
raycaster.setFromCamera(vector, this._camera);
// 3. Ray와 z=targetZ 평면의 교차점 계산
const direction = raycaster.ray.direction;
const origin = raycaster.ray.origin;
const t = (targetZ - origin.z) / direction.z;
const worldPoint = origin.clone().add(direction.multiplyScalar(t));
return worldPoint;
}
// 스크린 좌표 -> 월드 좌표 변환 함수 screenToWorld(screenX, screenY, ndcZ = 0.5) { // ndcZ=0.5로 설정 (카메라 방향) // 1. 화면 픽셀 좌표를 정규화된 디바이스 좌표(NDC)로 변환 const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1; const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1; // 2. NDC를 3D 공간의 Ray로 변환 const vector = new THREE.Vector3(ndcX, ndcY, ndcZ); vector.unproject(this._camera); // 3. Ray를 따라 월드 좌표를 계산 return vector; } screenToWorldAtZ(screenX, screenY, targetZ = 0) { // 1. 화면 픽셀 좌표 -> NDC 변환 const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1; const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1; // 2. NDC -> Ray 생성 const raycaster = new THREE.Raycaster(); const vector = new THREE.Vector2(ndcX, ndcY); raycaster.setFromCamera(vector, this._camera); // 3. Ray와 z=targetZ 평면의 교차점 계산 const direction = raycaster.ray.direction; const origin = raycaster.ray.origin; const t = (targetZ - origin.z) / direction.z; const worldPoint = origin.clone().add(direction.multiplyScalar(t)); return worldPoint; }
// 스크린 좌표 -> 월드 좌표 변환 함수
screenToWorld(screenX, screenY, ndcZ = 0.5) { // ndcZ=0.5로 설정 (카메라 방향)
  // 1. 화면 픽셀 좌표를 정규화된 디바이스 좌표(NDC)로 변환
  const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1;
  const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1;

  // 2. NDC를 3D 공간의 Ray로 변환
  const vector = new THREE.Vector3(ndcX, ndcY, ndcZ);
  vector.unproject(this._camera);

  // 3. Ray를 따라 월드 좌표를 계산
  return vector;
}

screenToWorldAtZ(screenX, screenY, targetZ = 0) {
  // 1. 화면 픽셀 좌표 -> NDC 변환
  const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1;
  const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1;

  // 2. NDC -> Ray 생성
  const raycaster = new THREE.Raycaster();
  const vector = new THREE.Vector2(ndcX, ndcY);
  raycaster.setFromCamera(vector, this._camera);

  // 3. Ray와 z=targetZ 평면의 교차점 계산
  const direction = raycaster.ray.direction;
  const origin = raycaster.ray.origin;
  const t = (targetZ - origin.z) / direction.z;

  const worldPoint = origin.clone().add(direction.multiplyScalar(t));
  return worldPoint;
}

모델을 화면 중심에 표시하기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
_zoomFix(model, distanceFactor = 1.5) {
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center);
const size = box.getSize(new THREE.Vector3());
const maxDimension = Math.max(size.x, size.y, size.z);
const cameraDistance = maxDimension * distanceFactor;
this._camera.position.set(0, 0, cameraDistance);
this._camera.lookAt(this._scene.position);
}
_zoomFix(model, distanceFactor = 1.5) { const box = new THREE.Box3().setFromObject(model); const center = box.getCenter(new THREE.Vector3()); model.position.sub(center); const size = box.getSize(new THREE.Vector3()); const maxDimension = Math.max(size.x, size.y, size.z); const cameraDistance = maxDimension * distanceFactor; this._camera.position.set(0, 0, cameraDistance); this._camera.lookAt(this._scene.position); }
_zoomFix(model, distanceFactor = 1.5) {
  const box = new THREE.Box3().setFromObject(model);
  const center = box.getCenter(new THREE.Vector3());

  model.position.sub(center);

  const size = box.getSize(new THREE.Vector3());
  const maxDimension = Math.max(size.x, size.y, size.z);
  const cameraDistance = maxDimension * distanceFactor;
  this._camera.position.set(0, 0, cameraDistance);
  this._camera.lookAt(this._scene.position);
}

모델의 특정 페이스에 대한 법선 벡터 구하기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
_getNormal(mesh, faceIndex) {
const normalAttribute = mesh.geometry.getAttribute("normal");
const index = mesh.geometry.index;
const faceStartIndex = faceIndex * 3;
const vertexIndexA = index.getX(faceStartIndex);
const vertexIndexB = index.getX(faceStartIndex + 1);
const vertexIndexC = index.getX(faceStartIndex + 2);
const normalA = new THREE.Vector3();
const normalB = new THREE.Vector3();
const normalC = new THREE.Vector3();
const faceNormal = new THREE.Vector3();
normalA.fromBufferAttribute(normalAttribute, vertexIndexA);
normalB.fromBufferAttribute(normalAttribute, vertexIndexB);
normalC.fromBufferAttribute(normalAttribute, vertexIndexC);
faceNormal.addVectors(normalA, normalB).add(normalC).divideScalar(3).normalize();
return faceNormal;
}
_getNormal(mesh, faceIndex) { const normalAttribute = mesh.geometry.getAttribute("normal"); const index = mesh.geometry.index; const faceStartIndex = faceIndex * 3; const vertexIndexA = index.getX(faceStartIndex); const vertexIndexB = index.getX(faceStartIndex + 1); const vertexIndexC = index.getX(faceStartIndex + 2); const normalA = new THREE.Vector3(); const normalB = new THREE.Vector3(); const normalC = new THREE.Vector3(); const faceNormal = new THREE.Vector3(); normalA.fromBufferAttribute(normalAttribute, vertexIndexA); normalB.fromBufferAttribute(normalAttribute, vertexIndexB); normalC.fromBufferAttribute(normalAttribute, vertexIndexC); faceNormal.addVectors(normalA, normalB).add(normalC).divideScalar(3).normalize(); return faceNormal; }
_getNormal(mesh, faceIndex) {
  const normalAttribute = mesh.geometry.getAttribute("normal");
  const index = mesh.geometry.index;
  const faceStartIndex = faceIndex * 3;

  const vertexIndexA = index.getX(faceStartIndex);
  const vertexIndexB = index.getX(faceStartIndex + 1);
  const vertexIndexC = index.getX(faceStartIndex + 2);

  const normalA = new THREE.Vector3();
  const normalB = new THREE.Vector3();
  const normalC = new THREE.Vector3();
  const faceNormal = new THREE.Vector3();

  normalA.fromBufferAttribute(normalAttribute, vertexIndexA);
  normalB.fromBufferAttribute(normalAttribute, vertexIndexB);
  normalC.fromBufferAttribute(normalAttribute, vertexIndexC);

  faceNormal.addVectors(normalA, normalB).add(normalC).divideScalar(3).normalize();

  return faceNormal;
}

어떤 위치에서 매시에 가장 가까운 매시 표면의 위치를 얻는 코드

아래 함수의 point 파라메터는 어떤 위치이고 mesh 파라메터가 대상 메시입니다. point에서 가장 가까운 매시 표면의 좌표가 변환됩니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
findClosestPointOnMesh(/* Vector3 */ point, /* Mesh */ mesh) {
const geometry = mesh.geometry;
const vertices = geometry.attributes.position;
const indices = geometry.index ? geometry.index.array : null;
let closestPoint = new THREE.Vector3();
let minDistance = Infinity;
const localPoint = mesh.worldToLocal(point);
if (indices) {
for (let i = 0; i < indices.length; i += 3) {
const a = new THREE.Vector3().fromBufferAttribute(vertices, indices[i]);
const b = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 1]);
const c = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 2]);
const tempPoint = new THREE.Vector3();
const triangle = new THREE.Triangle(a, b, c);
triangle.closestPointToPoint(localPoint, tempPoint);
const distance = localPoint.distanceTo(tempPoint);
if (distance < minDistance) {
minDistance = distance;
closestPoint.copy(tempPoint);
}
}
} else {
for (let i = 0; i < vertices.count; i += 3) {
const a = new THREE.Vector3().fromBufferAttribute(vertices, i);
const b = new THREE.Vector3().fromBufferAttribute(vertices, i + 1);
const c = new THREE.Vector3().fromBufferAttribute(vertices, i + 2);
const tempPoint = new THREE.Vector3();
const triangle = new THREE.Triangle(a, b, c);
triangle.closestPointToPoint(localPoint, tempPoint);
const distance = localPoint.distanceTo(tempPoint);
if (distance < minDistance) {
minDistance = distance;
closestPoint.copy(tempPoint);
}
}
}
return mesh.localToWorld(closestPoint);
}
findClosestPointOnMesh(/* Vector3 */ point, /* Mesh */ mesh) { const geometry = mesh.geometry; const vertices = geometry.attributes.position; const indices = geometry.index ? geometry.index.array : null; let closestPoint = new THREE.Vector3(); let minDistance = Infinity; const localPoint = mesh.worldToLocal(point); if (indices) { for (let i = 0; i < indices.length; i += 3) { const a = new THREE.Vector3().fromBufferAttribute(vertices, indices[i]); const b = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 1]); const c = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 2]); const tempPoint = new THREE.Vector3(); const triangle = new THREE.Triangle(a, b, c); triangle.closestPointToPoint(localPoint, tempPoint); const distance = localPoint.distanceTo(tempPoint); if (distance < minDistance) { minDistance = distance; closestPoint.copy(tempPoint); } } } else { for (let i = 0; i < vertices.count; i += 3) { const a = new THREE.Vector3().fromBufferAttribute(vertices, i); const b = new THREE.Vector3().fromBufferAttribute(vertices, i + 1); const c = new THREE.Vector3().fromBufferAttribute(vertices, i + 2); const tempPoint = new THREE.Vector3(); const triangle = new THREE.Triangle(a, b, c); triangle.closestPointToPoint(localPoint, tempPoint); const distance = localPoint.distanceTo(tempPoint); if (distance < minDistance) { minDistance = distance; closestPoint.copy(tempPoint); } } } return mesh.localToWorld(closestPoint); }
findClosestPointOnMesh(/* Vector3 */ point, /* Mesh */ mesh) {
  const geometry = mesh.geometry;
  const vertices = geometry.attributes.position;
  const indices = geometry.index ? geometry.index.array : null;

  let closestPoint = new THREE.Vector3();
  let minDistance = Infinity;

  const localPoint = mesh.worldToLocal(point);

  if (indices) {
    for (let i = 0; i < indices.length; i += 3) {
      const a = new THREE.Vector3().fromBufferAttribute(vertices, indices[i]);
      const b = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 1]);
      const c = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 2]);

      const tempPoint = new THREE.Vector3();
      const triangle = new THREE.Triangle(a, b, c);
      triangle.closestPointToPoint(localPoint, tempPoint);

      const distance = localPoint.distanceTo(tempPoint);
      if (distance < minDistance) {
        minDistance = distance;
        closestPoint.copy(tempPoint);
      }
    }
  } else {
    for (let i = 0; i < vertices.count; i += 3) {
      const a = new THREE.Vector3().fromBufferAttribute(vertices, i);
      const b = new THREE.Vector3().fromBufferAttribute(vertices, i + 1);
      const c = new THREE.Vector3().fromBufferAttribute(vertices, i + 2);

      const tempPoint = new THREE.Vector3();
      const triangle = new THREE.Triangle(a, b, c);
      triangle.closestPointToPoint(localPoint, tempPoint);

      const distance = localPoint.distanceTo(tempPoint);
      if (distance < minDistance) {
        minDistance = distance;
        closestPoint.copy(tempPoint);
      }
    }
  }

  return mesh.localToWorld(closestPoint);
}

아래는 위의 함수가 적용된 실행 화면인데, 노란색 포인트 위치가 임의의 위치(point 파라메터)이고 빨간색이 함수의 결과 좌표입니다.

DataTexture를 이용한 Raw 데이터로 텍스쳐 생성

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const width = 256;
const height = 256;
const size = width * height;
const data = new Uint8Array(4 * size); // RGBA 데이터
for (let i = 0; i < size; i++) {
const stride = i * 4;
data[stride] = Math.floor(Math.random() * 256); // R
data[stride + 1] = Math.floor(Math.random() * 256); // G
data[stride + 2] = Math.floor(Math.random() * 256); // B
data[stride + 3] = 255; // A
}
const texture = new THREE.DataTexture(data, width, height);
texture.needsUpdate = true;
const material = new THREE.MeshBasicMaterial({ map: texture });
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), material);
this._scene.add(mesh);
const width = 256; const height = 256; const size = width * height; const data = new Uint8Array(4 * size); // RGBA 데이터 for (let i = 0; i < size; i++) { const stride = i * 4; data[stride] = Math.floor(Math.random() * 256); // R data[stride + 1] = Math.floor(Math.random() * 256); // G data[stride + 2] = Math.floor(Math.random() * 256); // B data[stride + 3] = 255; // A } const texture = new THREE.DataTexture(data, width, height); texture.needsUpdate = true; const material = new THREE.MeshBasicMaterial({ map: texture }); const mesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), material); this._scene.add(mesh);
    const width = 256;
    const height = 256;
    const size = width * height;
    const data = new Uint8Array(4 * size); // RGBA 데이터

    for (let i = 0; i < size; i++) {
      const stride = i * 4;

      data[stride] = Math.floor(Math.random() * 256);     // R
      data[stride + 1] = Math.floor(Math.random() * 256); // G
      data[stride + 2] = Math.floor(Math.random() * 256); // B
      data[stride + 3] = 255;                             // A
    }

    const texture = new THREE.DataTexture(data, width, height);
    texture.needsUpdate = true;

    const material = new THREE.MeshBasicMaterial({ map: texture });
    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), material);
    this._scene.add(mesh);

여러 개의 지오메트리들을 한 개의 지오메트리로 병합하기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const mat3 = new THREE.MeshBasicMaterial();
const pumpkin = await this.loadGLTF('./resources/models/pumpkin.glb');
const geos = [];
// 아래의 2줄에 대한 변환 코드는 updateMatrixWorld 매서드가 실행되면 변환 값이 matrixWorld에 반영됨
pumpkin.children[0].position.set(0, 0, 0);
pumpkin.children[0].scale.setScalar(0.01);
pumpkin.children[0].traverse(child => {
child.updateMatrixWorld();
if(child.isMesh) {
if(child.geometry) {
const attribute = child.geometry.getAttribute('position').clone();
const geometry = new THREE.BufferGeometry();
geometry.setIndex(child.geometry.index);
geometry.setAttribute('position', attribute);
geometry.applyMatrix4(child.matrixWorld);
geos.push(geometry);
}
}
});
const combinedGeometry = BufferGeometryUtils.mergeGeometries(geos);
const combinedMesh = new THREE.Mesh(combinedGeometry, mat3);
const mat3 = new THREE.MeshBasicMaterial(); const pumpkin = await this.loadGLTF('./resources/models/pumpkin.glb'); const geos = []; // 아래의 2줄에 대한 변환 코드는 updateMatrixWorld 매서드가 실행되면 변환 값이 matrixWorld에 반영됨 pumpkin.children[0].position.set(0, 0, 0); pumpkin.children[0].scale.setScalar(0.01); pumpkin.children[0].traverse(child => { child.updateMatrixWorld(); if(child.isMesh) { if(child.geometry) { const attribute = child.geometry.getAttribute('position').clone(); const geometry = new THREE.BufferGeometry(); geometry.setIndex(child.geometry.index); geometry.setAttribute('position', attribute); geometry.applyMatrix4(child.matrixWorld); geos.push(geometry); } } }); const combinedGeometry = BufferGeometryUtils.mergeGeometries(geos); const combinedMesh = new THREE.Mesh(combinedGeometry, mat3);
const mat3 = new THREE.MeshBasicMaterial();
const pumpkin = await this.loadGLTF('./resources/models/pumpkin.glb');
const geos = [];

// 아래의 2줄에 대한 변환 코드는 updateMatrixWorld 매서드가 실행되면 변환 값이 matrixWorld에 반영됨
pumpkin.children[0].position.set(0, 0, 0);
pumpkin.children[0].scale.setScalar(0.01);

pumpkin.children[0].traverse(child => {
  child.updateMatrixWorld(); 
  if(child.isMesh) {
    if(child.geometry) {
      const attribute = child.geometry.getAttribute('position').clone();
      const geometry = new THREE.BufferGeometry();
      geometry.setIndex(child.geometry.index);
      geometry.setAttribute('position', attribute);
      geometry.applyMatrix4(child.matrixWorld);
      geos.push(geometry);
    }
  }
});
const combinedGeometry = BufferGeometryUtils.mergeGeometries(geos);
const combinedMesh = new THREE.Mesh(combinedGeometry, mat3);

알고 있으면 너무 좋은 프론트엔드 웹 기술

웹은 인류가 만든 최고의 문화 또는 기술 중 하나입니다. 웹을 통해 이룰 수 있는 그 가능성은 무한하며 그 가능성을 현실로 이루기 위해서는 기술이 필요한데, 이러한 기술에 대해 설명합니다.



위의 영상에 더해 더 많은 영상을 제공하고 지속적으로 내용이 추가되므로 해당 채널에 방문하여 참고하시기 바랍니다.