
3D 웹 애플리케이션에서 가장 중요한 것은 성능 최적화입니다.
GLTF 모델을 최적화하는 핵심 전략들을 살펴보겠습니다.
class MeshOptimizer {
private static readonly VERTEX_LIMIT = 65536; // 16비트 인덱스 제한
static optimizeScene(scene: THREE.Scene): void {
const meshes: THREE.Mesh[] = [];
// 동일한 머티리얼을 사용하는 메시들을 그룹화
const materialGroups = new Map<THREE.Material, THREE.Mesh[]>();
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
const material = object.material;
if (!materialGroups.has(material)) {
materialGroups.set(material, []);
}
materialGroups.get(material)!.push(object);
}
});
// 각 머티리얼 그룹별로 메시 병합
materialGroups.forEach((meshGroup, material) => {
if (meshGroup.length > 1) {
const mergedGeometry = this.mergeMeshes(meshGroup);
const mergedMesh = new THREE.Mesh(mergedGeometry, material);
// 원본 메시 제거 및 병합된 메시 추가
meshGroup.forEach(mesh => mesh.parent?.remove(mesh));
scene.add(mergedMesh);
}
});
}
private static mergeMeshes(meshes: THREE.Mesh[]): THREE.BufferGeometry {
const geometries: THREE.BufferGeometry[] = [];
meshes.forEach(mesh => {
// 월드 변환 매트릭스 적용
const geometry = mesh.geometry.clone();
geometry.applyMatrix4(mesh.matrixWorld);
geometries.push(geometry);
});
// BufferGeometryUtils를 사용하여 지오메트리 병합
return THREE.BufferGeometryUtils.mergeBufferGeometries(geometries);
}
}
class TextureAtlasGenerator {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private maxSize: number;
private padding: number;
constructor(maxSize: number = 2048, padding: number = 2) {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d')!;
this.maxSize = maxSize;
this.padding = padding;
}
async generateAtlas(textures: THREE.Texture[]): Promise<THREE.Texture> {
// 텍스처 크기 계산 및 패킹 알고리즘 구현
const layout = this.calculateLayout(textures);
this.canvas.width = layout.width;
this.canvas.height = layout.height;
// 텍스처 배치
for (const item of layout.items) {
const texture = textures[item.index];
this.ctx.drawImage(
texture.image,
item.x + this.padding,
item.y + this.padding,
item.width - this.padding * 2,
item.height - this.padding * 2
);
}
// 새로운 텍스처 생성
const atlasTexture = new THREE.Texture(this.canvas);
atlasTexture.needsUpdate = true;
return atlasTexture;
}
private calculateLayout(textures: THREE.Texture[]): any {
// 여기에 텍스처 패킹 알고리즘 구현
// (예: Skyline Packing Algorithm)
// ...
return {
width: 2048,
height: 2048,
items: [/* 패킹된 텍스처 정보 */]
};
}
}
class PBRMaterialManager {
private materialCache: Map<string, THREE.Material>;
constructor() {
this.materialCache = new Map();
}
async createPBRMaterial(options: {
baseColorMap?: string;
normalMap?: string;
metallicRoughnessMap?: string;
aoMap?: string;
emissiveMap?: string;
}): Promise<THREE.Material> {
const cacheKey = JSON.stringify(options);
if (this.materialCache.has(cacheKey)) {
return this.materialCache.get(cacheKey)!;
}
const material = new THREE.MeshPhysicalMaterial({
envMapIntensity: 1.0,
metalness: 1.0,
roughness: 1.0,
});
// 텍스처 로드 및 적용
const textureLoader = new THREE.TextureLoader();
const loadTexture = async (url: string): Promise<THREE.Texture> => {
return new Promise((resolve) => {
textureLoader.load(url, (texture) => {
texture.encoding = THREE.sRGBEncoding;
resolve(texture);
});
});
};
if (options.baseColorMap) {
material.map = await loadTexture(options.baseColorMap);
}
if (options.normalMap) {
material.normalMap = await loadTexture(options.normalMap);
}
if (options.metallicRoughnessMap) {
material.metalnessMap = await loadTexture(options.metallicRoughnessMap);
material.roughnessMap = material.metalnessMap;
}
if (options.aoMap) {
material.aoMap = await loadTexture(options.aoMap);
}
if (options.emissiveMap) {
material.emissiveMap = await loadTexture(options.emissiveMap);
material.emissive.setHex(0xffffff);
}
this.materialCache.set(cacheKey, material);
return material;
}
}
const customShaderMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
baseTexture: { value: null },
normalMap: { value: null }
},
vertexShader: `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vUv = uv;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform float time;
uniform sampler2D baseTexture;
uniform sampler2D normalMap;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
// 노멀맵 적용
vec3 normalMap = texture2D(normalMap, vUv).xyz * 2.0 - 1.0;
normal = normalize(normal + normalMap);
// 프레넬 효과
float fresnel = pow(1.0 - dot(normal, viewDir), 5.0);
// 기본 텍스처
vec4 baseColor = texture2D(baseTexture, vUv);
// 시간에 따른 효과
float pulse = sin(time * 2.0) * 0.5 + 0.5;
// 최종 색상 계산
vec3 finalColor = mix(baseColor.rgb, vec3(1.0), fresnel * pulse);
gl_FragColor = vec4(finalColor, baseColor.a);
}
`
});
class AdvancedRenderer {
private renderer: THREE.WebGLRenderer;
private composer: EffectComposer;
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
constructor(scene: THREE.Scene, camera: THREE.PerspectiveCamera) {
this.scene = scene;
this.camera = camera;
this.renderer = new THREE.WebGLRenderer({
antialias: true,
stencil: false,
depth: true
});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.0;
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.composer = new EffectComposer(this.renderer);
this.setupPostProcessing();
}
private setupPostProcessing() {
// 기본 렌더 패스
const renderPass = new RenderPass(this.scene, this.camera);
this.composer.addPass(renderPass);
// SSAO
const ssaoPass = new SSAOPass(this.scene, this.camera);
ssaoPass.kernelRadius = 16;
this.composer.addPass(ssaoPass);
// 블룸
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, 0.4, 0.85
);
this.composer.addPass(bloomPass);
// 색보정
const colorCorrectionPass = new ShaderPass(ColorCorrectionShader);
this.composer.addPass(colorCorrectionPass);
}
render() {
this.composer.render();
}
}
// 메인 애플리케이션
class ModelViewer {
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private renderer: AdvancedRenderer;
private modelOptimizer: MeshOptimizer;
private materialManager: PBRMaterialManager;
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new AdvancedRenderer(this.scene, this.camera);
this.modelOptimizer = new MeshOptimizer();
this.materialManager = new PBRMaterialManager();
this.initialize();
}
private async initialize() {
// GLTF 모델 로드
const loader = new GLTFLoader();
const gltf = await loader.loadAsync('model.gltf');
// 메시 최적화
MeshOptimizer.optimizeScene(gltf.scene);
// PBR 머티리얼 적용
const pbrMaterial = await this.materialManager.createPBRMaterial({
baseColorMap: 'textures/basecolor.jpg',
normalMap: 'textures/normal.jpg',
metallicRoughnessMap: 'textures/metallicRoughness.jpg',
aoMap: 'textures/ao.jpg'
});
gltf.scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.material = pbrMaterial;
}
});
this.scene.add(gltf.scene);
}
animate() {
requestAnimationFrame(() => this.animate());
this.renderer.render();
}
}
// 사용
const viewer = new ModelViewer();
viewer.animate();