[오프라인강좌 소개] three.js와 blender를 이용한 3D 인터렉티브 웹 개발

안녕하세요, GIS Developer 김형준입니다. 오는 10월 24일에서 26일 판교에서 3D 그래픽 웹 개발을 위한 three.js 라이브러리와 3차원 모델링 제작툴인 Blender를 활용한 3차원 인터렉티브 웹 개발에 대한 오프라인 강좌를 진행합니다. 소개 영상은 아래와 같습니다.

이번 강좌는 한국메타버스산업협회에서 주관하는 강좌로 교육비는 무료이며 수강신청은 아래 URL을 통해 가능합니다.

https://www.metaverse-campus.kr/lecture/viewAll.do?pageIndex=1&menu_idx=50&lecIdx=17&proIdx=147

참여 인원수에 제한이 있으므로 빠른 신청 부탁드리겠습니다. 감사합니다.

three.js로 웹에서 멋진 3D 장면 연출하기

three.js를 이용하여 웹에서.. 몽환적인 장면을 만들어 보는 코드를 작성해 보았습니다. 결과는 다음 동영상과 같습니다.

복잡한 Shader를 사용하지 않았습니다. three.js의 기본적인 API만을 사용했습니다.

또 다른 한가지 예입니다. 피닉스 한마리가 날아오르는 장면을 연출한 것인데요. 처음엔 생기가 없지만 조금씩 높이 날아 오를수록 몸이 빛나기 시작합니다.

조만간 제 Youtube 채널(GIS DEVELOPER)에 위 2가지에 예제를 three.js로 어떻게 만드는지 그 내용을 업로드할 예정입니다.

물리(Physics) 계수

반발계수(coefficient of restitution, COR)

반발 계수는 물체의 충돌 전후 속도의 비율을 나타내는 분수값이다. 반발 계수가 1인 물체는 탄성 충돌을 하며, 반발 계수가 1보다 작은 물체는 비탄성 충돌을 한다. 반발 계수가 0이면 완전 비탄성 충돌을 하며, 충돌한 물체와 붙어서 튀지 않는다. (출처: 위키백과)

마찰력(friction)

마찰력은 두 물체의 접촉면 사이에서 물체의 운동을 방해하는 힘이다. 마찰력의 계수는 물질의 고유 성질이 아닌, 어떤 물체의 어떤 표면 상태에서 어떤 물질과 접촉하는가 등에 관계있으며 실험을 통해 결정되는 현상론적인 양이다. 교과서는 일반적으로 마찰력은 접촉면의 넓이에는 무관하다고 서술하나 이것은 접촉면이 이상적으로 매끄러운 경우에만 성립한다. 실제로는 접촉면의 매끄러운 정도 등에 따라 영향을 받는다. (출처: 위키백과)

질량(mass)

질량은 물리학에서 물질이 가지고 있는 고유한 양을 일컫는 말이다. 질량의 SI 단위는 킬로그램(kg)이다. (출처: 위키백과)

THREE.JS 퀵 레퍼런스 코드

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

그림자 적용에 대한 코드

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;

지오메트리의 좌표 수정

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 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);
}

안개 설정 코드

scene.fog = new THREE.Fog("rgba(54,219,214,1)", 1000, 1400);

OrbitControls 관련 코드

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 얻기

const board = this._scene.getObjectByName("Board");
const box = new THREE.Box3().setFromObject(board);
console.log(box);

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

mesh.updateMatrixWorld();

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

Faked Shadow

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

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 );

텍스쳐 이미지 품질 올리기

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

texture.anisotropy = renderer.capabilities.getMaxAnisotropy();

async 리소스 로딩

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로 후다닥 만들기

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 파일 로딩

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

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()