1. GLTF 파일 최적화 및 드로우콜 감소

3D 웹 애플리케이션에서 가장 중요한 것은 성능 최적화입니다.
GLTF 모델을 최적화하는 핵심 전략들을 살펴보겠습니다.

1.1 메시 병합 최적화기 구현

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

1.2 텍스처 아틀라스 생성기

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: [/* 패킹된 텍스처 정보 */]
        };
    }
}

2. 고급 재질 시스템 구현

2.1 PBR 머티리얼 매니저

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

2.2 고급 쉐이더 효과

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

3. 고급 렌더링 파이프라인

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

4. 실제 사용 예제

// 메인 애플리케이션
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();
profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글