cargo new
새 프로젝트를 생성할 수 있습니다.cargo build
프로젝트를 빌드할 수 있습니다.cargo run
한 번에 프로젝트를 빌드하고 실행할 수 있습니다.cargo check
바이너리를 생성하지 않고 프로젝트의 에러를 체크할 수 있습니다.cargo build --release
최종 배포를 위한 릴리즈를 빌드합니다.
top-level await 에러 대응 (WebGPU를 import 하기 위한 코드에서 …)
아래의 코드가 실행되면 서두부터 await가 실행됩니다. 즉, top-level에서 await가 실행되는 거죠. 이게 Vite에서는 문제가 발생합니다.
import WebGPU from "three/addons/capabilities/WebGPU.js"
이를 해결하기 위해서는 먼저 개발환경에서 다음과 같은 패키지를 설치해야 합니다.
npm install --save-dev vite-plugin-top-level-await
그리고 vite.config.js 파일을 만들어 다음처럼 내용을 구성합니다.
import { defineConfig } from "vite"; import topLevelAwait from "vite-plugin-top-level-await"; export default defineConfig({ plugins:[ topLevelAwait({ // promiseExportName: "__tla", // promiseImportName: i => `__tla_${i}` }) ], });
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 .
그림자 적용에 대한 코드
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(mesh.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()
Image 기반 광원(IBL)
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 파일 로딩
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를 빌보드로 만들기
this._mesh.quaternion.copy(this._camera.quaternion ); // or this._mesh.rotation.setFromRotationMatrix( this._camera.matrix );
FPS 제한하기
requestAnimationFrame로 렌더링을 수행하면 최대한 많은 프레임을 생성하기 됨. 아래는 원하는 프레임수로 제한하기 위해 다음 코드로 30 프레임 제한입니다.
_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)); }
WebGPU를 이용한 계산(Computing)
3개의 실수값을 전달하고 이 값에 2를 곱한 결과를 GPU를 통해 병렬로 처리 실행해 그 결과를 얻는 코드입니다. 먼저 GPU에 대한 객체를 얻는 것은 비동기로 처리해야 하므로 다음과 같은 코드가 필요합니다.
async function main() { // 여기에 이후 모든 코드가 작성됩니다. } await main();
자, 이제 GPU에 대한 객체를 얻습니다.
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice();
실행할 쉐이더 코드를 작성합니다. 쉐이더 코드는 WGSL로 작성됩니다.
const module = device.createShaderModule({ label: 'doubling compute module', code: /* wgsl */ ` @group(0) @binding(0) var<storage, read_write> data: array<f32>; @compute @workgroup_size(1) fn computeSomething( @builtin(global_invocation_id) id: vec3u ) { let i = id.x; data[i] = data[i] * 2.0; } `, });
위의 쉐이더 코드를 실행할 파이프라인 객체를 생성합니다.
const pipeline = device.createComputePipeline({ label: 'x2 compute pipeline', layout: 'auto', compute: { module, entryPoint: 'computeSomething', // 실행할 쉐이더 함수 }, });
실행할 쉐이더 함수를 보면 1개의 인자를 받는데, 그 인자를 정의합니다.
const input = new Float32Array([1, 3, 5]);
위의 인자는 CPU 메모리에 있으므로 이를 GPU 메모리에 만들어서 복사해 줘야 합니다.
const workBuffer = device.createBuffer({ label: 'work buffer', size: input.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, }); device.queue.writeBuffer(workBuffer, 0, input);
GPU를 통해 계산된 최종 결과를 읽기 위한 사본 버퍼를 생성합니다.
const resultBuffer = device.createBuffer({ label: 'result buffer', size: input.byteLength, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
계산을 위해 사용될 인자에 대한 버퍼가 무엇인지를 명확히 지정합니다.
const bindGroup = device.createBindGroup({ label: 'bindGroup for work buffer', layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: workBuffer } }, ], });
GPU에서 실행할 명령을 인코딩합니다.
const encoder = device.createCommandEncoder({ label: 'x2 encoder' }); const pass = encoder.beginComputePass({ label: 'x2 compute pass' }); pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroup); pass.dispatchWorkgroups(input.length); pass.end();
계산 결과를 읽어 내기 위해 맵핑용 버퍼에 복사하는 명령을 인코딩합니다.
encoder.copyBufferToBuffer(workBuffer, 0, resultBuffer, 0, resultBuffer.size);
실해 명령의 실행은 다음 코드를 통해 이루어집니다.
const commandBuffer = encoder.finish(); device.queue.submit([commandBuffer]);
실행 결과를 콘솔에 출력하기 위한 코드입니다.
await resultBuffer.mapAsync(GPUMapMode.READ); const result = new Float32Array(resultBuffer.getMappedRange()); console.log('input', input); console.log('result', result); resultBuffer.unmap();
실행 결과를 보면 다음과 같습니다.
실행 결과를 보면 입력값에 2배만큼 곱해졌는데, 이는 GPU를 통해 병렬로 계산된 것입니다.