최근 threejs를 공부하고 있다.
threejs는 많은 연산을 필요로 하게 되는데, web assembly도 찍먹하고 있다보니, threejs를 wasm으로 포팅하면 좋을 것 같은 생각이 들었다.
threejs의 모든 파일을 web assembly로 포팅할 수 있으면 베스트겠지만...
rust도 잘 모르고, threejs도, wasm도 잘 모르는 상태에서 조금 복잡한 연산 정도를 wasm으로 포팅하여 사용하는 정도만 해보려고 한다.
나중에 threejs를 직접 구현해 볼 생각을 가지고 있는데,
간단한 정도의 개발이라면 wasm으로 포팅해서 개발해보는 것도 좋을 것 같은 생각이 든다.
특히 연산이 많은 3D, 4D 부분을 threejs로 포팅하면 성능이 많이 개선될 것 같다.
연산을 하기 전에 우선 threejs로만 코드를 작성해봤다.
(성능의 체감을 느끼기 위해 좀 복잡한 코드가 필요해서 gpt의 도움을 아주 많이 받았으나... 생각보다 복잡하지 않은게 함정)
import * as THREE from 'three';
// Three.js Scene 설정
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 파티클 초기화
const particleCount = 1000;
const positions = new Float32Array(particleCount * 3); // [x, y, z, x, y, z, ...]
const velocities = new Float32Array(particleCount * 3); // [vx, vy, vz, vx, vy, vz, ...]
for (let i = 0; i < particleCount * 3; i++) {
positions[i] = (Math.random() - 0.5) * 10; // Random position
velocities[i] = (Math.random() - 0.5) * 2; // Random velocity
}
// Three.js 파티클 시스템
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({ color: 0x00ff00, size: 0.1 });
const points = new THREE.Points(geometry, material);
scene.add(points);
camera.position.z = 20;
// 물리 계산 함수
function updatePositions(deltaTime) {
for (let i = 0; i < particleCount; i++) {
const idx = i * 3;
// 속도 기반 위치 업데이트
positions[idx] += velocities[idx] * deltaTime;
positions[idx + 1] += velocities[idx + 1] * deltaTime;
positions[idx + 2] += velocities[idx + 2] * deltaTime;
// 중력 효과 추가
velocities[idx + 1] -= 9.81 * deltaTime;
// 바운더리 체크 (경계 충돌)
if (positions[idx + 1] < -5) {
positions[idx + 1] = -5;
velocities[idx + 1] *= -0.5; // 반사 속도 감소
}
}
}
// 애니메이션 루프
function animate() {
requestAnimationFrame(animate);
// 물리 계산 업데이트
updatePositions(0.016); // delta_time = 16ms
// Three.js에 업데이트된 데이터 반영
geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
파티클을 생성해서 떨어지는 정도의 간단한? 작업이다.
파티클의 개수만큼 반복을 해야하기 때문에 파티클의 수가 많아질 수록 더 많은 연산을 필요로 하게 된다.
같은 코드를 WASM을 사용하여 변경을 해봤다.
(역시 gpt의 도움을 아주 많이 받아서...)
참고로 코드는 러스트를 사용했다.
wasm.js
import init, { ParticleSystem } from './three-lib/pkg/three_lib.js';
import * as THREE from 'three';
async function run() {
const wasm = await init();
const particleCount = 1000;
const deltaTime = 0.016;
// WebAssembly에서 파티클 시스템 생성
const particleSystem = new ParticleSystem(particleCount);
// 포인터로 WebAssembly 메모리 접근
const positionsPtr = particleSystem.get_positions_ptr();
const memory = new Float32Array(
wasm.memory.buffer,
positionsPtr,
particleCount * 3
);
// Three.js 설정
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(memory, 3));
const material = new THREE.PointsMaterial({ color: 0x00ff00, size: 0.1 });
const points = new THREE.Points(geometry, material);
scene.add(points);
camera.position.z = 20;
// 애니메이션 루프
function animate() {
requestAnimationFrame(animate);
// WebAssembly로 파티클 업데이트
particleSystem.update_positions(deltaTime);
// Three.js 렌더링
geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
}
run();
three_lib.rs
use wasm_bindgen::prelude::*;
use rand::Rng;
#[wasm_bindgen]
pub struct ParticleSystem {
positions: Vec<f32>,
velocities: Vec<f32>,
}
#[wasm_bindgen]
impl ParticleSystem {
#[wasm_bindgen(constructor)]
pub fn new(count: usize) -> ParticleSystem {
let mut rng = rand::thread_rng();
let mut positions = Vec::with_capacity(count * 3);
let mut velocities = Vec::with_capacity(count * 3);
for _ in 0..count {
positions.push((rng.gen::<f32>() - 0.5) * 10.0);
positions.push((rng.gen::<f32>() - 0.5) * 10.0);
positions.push((rng.gen::<f32>() - 0.5) * 10.0);
velocities.push((rng.gen::<f32>() - 0.5) * 2.0);
velocities.push((rng.gen::<f32>() - 0.5) * 2.0);
velocities.push((rng.gen::<f32>() - 0.5) * 2.0);
}
ParticleSystem {
positions,
velocities,
}
}
pub fn get_positions_ptr(&self) -> *const f32 {
self.positions.as_ptr()
}
pub fn get_velocities_ptr(&self) -> *const f32 {
self.velocities.as_ptr()
}
pub fn update_positions(&mut self, delta_time: f32) {
let count = self.positions.len() / 3;
for i in 0..count {
let idx = i * 3;
self.positions[idx] += self.velocities[idx] * delta_time;
self.positions[idx + 1] += self.velocities[idx + 1] * delta_time;
self.positions[idx + 2] += self.velocities[idx + 2] * delta_time;
self.velocities[idx + 1] -= 9.81 * delta_time;
if self.positions[idx + 1] < -5.0 {
self.positions[idx + 1] = -5.0;
self.velocities[idx + 1] *= -0.5;
}
}
}
}
파티클이 1000개뿐일 때는 사실상 js로만 만드나 wasm으로 만드나 큰차이가 없다.
좀 더 큰 차이를 보기 위해 파티클 개수를 10000000개로 변경하니 조금 두드러지는 차이가 보이기 시작했다.
오른쪽이 js로만 실행한 영상이며, 왼쪽이 wasm으로 실행한 영상이다.
js를 먼저 새로고침했음에도 불구하고 WASM이 아주 조금 더 부드럽고 빠르게 내려오는 것을 확인할 수 있다.
threejs 동작 원리를 공부하는 도중에 좀 더 성능을 낼 수 있는 방법에 대해 고민하다 보니, 이렇게 WASM을 사용하는 방법도 있을 것 같다는 생각이 들었다.
실제로도 threejs를 전부 포팅하거나, 혹은 일부 포팅을 하여 성능을 개선해 사용하고 있다고도 한다.
언젠가 만들 수 있을지는 잘 모르지만...
threejs를 직접 구현해볼 생각을 가지고 있기 때문에 WASM이랑 같이 녹여서 만들어보기도 해야겠다.
이 글 자체는 굉장히 별거 없다.
단지, WASM의 활용 영역을 이렇게 하나 발견했고, 직접 해보면서 차이를 보고 싶었기 때문이다.
문법을 몰라서 삽질을 조금 하긴 했는데, 역시 삽질마저 개발의 일부이지 않을까 싶다~
개인적으로 WASM은 웹 생태계에 굉장히 큰 변화를 가져올 것으로 생각한다.
이렇게 흥미가 있을 법한 내용들로 조금씩이나마 공부를 하여 기반을 좀 다져둬야 겠다.
특히... rust도 공부를 좀 해둬야겠다...
'자바스크립트' 카테고리의 다른 글
WASM (Web Assembly) 시작하기 (0) | 2024.10.12 |
---|---|
uncaught referenceError: regeneratorRuntime is not defined 해결 (0) | 2022.02.14 |
사파리 new Date('yyyy.MM.dd') 이슈 (0) | 2021.11.01 |