javascript

[js] Three.js를 이용한 웹 환경에서 glb 파일 렌더링 / 상호작용

TTTGGG 2024. 9. 12. 16:10
728x90
반응형
SMALL
// 모달 내에 div를 만들어 sample.glb 파일을 렌더링
// 렌더링한 3D 모델을 마우스 드래그를 통해 회전
// 렌더링한 3D 모델에 더블 클릭을 통해 클릭한 위치 표시 / 표시 제거

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>

<script>
    let renderer; // 전역 변수로 렌더러를 선언
    let scene, camera, model; // 전역 변수로 씬, 카메라, 모델을 선언

    const circles = [];
    const circleRadius = 0.1; // 원의 반지름 설정

    fnTestRenderReady = async function (e) {
        $('#TestRender').on('show.bs.modal', function (e) {
            $('#TestRender_form')[0].reset(); // 폼 리셋
        });

        // 모달이 완전히 열린 후 렌더링 초기화
        $('#TestRender').on('shown.bs.modal', function () {
            initGLBRenderer();
        });

        // 모달이 닫힐 때 렌더러 제거
        $('#TestRender').on('hidden.bs.modal', function () {
            if (renderer) {
                renderer.dispose(); // 렌더러 자원 해제
                renderer.domElement.remove(); // 렌더러 DOM 요소 제거
                renderer = null; // 렌더러를 null로 설정
            }
        });
    }

    function initGLBRenderer() {
        const container = document.getElementById('glbRenderer');

        if (renderer) {
            console.log('Renderer already exists, skipping initialization.');
            return;
        }

        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(container.clientWidth, container.clientHeight);
        container.appendChild(renderer.domElement);

        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
        camera.position.set(0, 0, 5);
        camera.lookAt(0, 0, 0); // 카메라가 모델의 중심을 바라보도록 설정

        const light = new THREE.DirectionalLight(0xffffff, 1);
        light.position.set(5, 5, 5).normalize();
        scene.add(light);

        const loader = new THREE.GLTFLoader();
        loader.load('/Content/img/sample.glb', function (gltf) {
            model = gltf.scene;
            model.scale.set(3, 3, 3);
            model.position.set(0, 0, 0); // 모델 위치 설정

            const meshList = [];
            model.traverse((child) => {
                if (child.type === 'Mesh') {
                    child.geometry.computeBoundingBox();
                    child.geometry.computeBoundingSphere();
                    meshList.push(child); // Mesh만 교차 검사 대상에 추가
                }
            });
            scene.add(model);

            console.log("Model Loaded: ", model);
            console.log("Mesh List for Raycaster: ", meshList);

            let isDragging = false;
            let previousMousePosition = { x: 0, y: 0 };
            let angleX = 0, angleY = 0, angleZ = 0;

            container.addEventListener('mousedown', function (event) {
                isDragging = true;
                previousMousePosition = { x: event.clientX, y: event.clientY };
            });

            container.addEventListener('mousemove', function (event) {
                if (isDragging) {
                    const deltaX = event.clientX - previousMousePosition.x;
                    const deltaY = event.clientY - previousMousePosition.y;

                    angleX += deltaY * 0.5;
                    angleY += deltaX * 0.5;
                    angleZ += deltaX * 0.25;

                    model.rotation.x = THREE.MathUtils.degToRad(angleX);
                    model.rotation.y = THREE.MathUtils.degToRad(angleY);
                    model.rotation.z = THREE.MathUtils.degToRad(angleZ);

                    previousMousePosition = { x: event.clientX, y: event.clientY };
                }
            });

            container.addEventListener('mouseup', function () { isDragging = false; });
            container.addEventListener('mouseleave', function () { isDragging = false; });

            container.addEventListener('dblclick', function (event) {
                // 컨테이너의 위치 및 크기 얻기
                const rect = container.getBoundingClientRect();

                // 마우스 위치를 화면 좌표에서 WebGL 좌표로 변환
                const mouse = new THREE.Vector2();
                mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
                mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

                // Raycaster 생성 및 설정
                const raycaster = new THREE.Raycaster();
                raycaster.setFromCamera(mouse, camera); // 마우스 위치에 따라 광선 설정

                // 모든 Mesh 객체들과 circles 배열의 객체들에 대해 교차 검사 수행
                const intersects = raycaster.intersectObjects([...meshList, ...circles], true);

                if (intersects.length > 0) {
                    const intersect = intersects[0];
                    const point = intersect.point;

                    // 클릭된 위치에 기존 원이 있는지 확인
                    const existingCircleIndex = circles.findIndex(circle =>
                        circle.position.distanceTo(point) <= (0.1 + 0.05) // 원의 반지름이 0.1이므로 허용 오차를 반지름 + 여유 범위로 설정
                    );

                    if (existingCircleIndex !== -1) {
                        console.log('Removing existing circle at:', circles[existingCircleIndex].position);
                        scene.remove(circles[existingCircleIndex]);
                        circles.splice(existingCircleIndex, 1);
                    } else {
                        console.log('Adding new circle at:', point);
                        const circleGeometry = new THREE.CircleGeometry(0.1, 64); // 원의 반지름을 0.1로 설정
                        const circleMaterial = new THREE.MeshBasicMaterial({
                            color: 0xff0000,
                            depthTest: false, // 다른 객체와의 깊이 검사 비활성화
                            depthWrite: true  // 깊이 쓰기 활성화
                        });

                        const circle = new THREE.Mesh(circleGeometry, circleMaterial);
                        circle.position.copy(point);
                        circle.position.z += 1.0; // Z 좌표를 모델 앞쪽으로 설정
                        circle.renderOrder = 10000; // 매우 높은 렌더 순서 설정

                        scene.add(circle);
                        circles.push(circle);
                    }
                } else {
                    console.log('No intersections found.');
                }
            });

            requestAnimationFrame(animate);
        }, undefined, function (error) {
            console.error('An error occurred while loading the GLB file:', error);
        });

        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        }

        window.addEventListener('resize', function () {
            const width = container.clientWidth;
            const height = container.clientHeight;
            renderer.setSize(width, height);
            camera.aspect = width / height;
            camera.updateProjectionMatrix();
        });
    }
</script>

<div class="modal-body">
    <!-- 3D 모델을 렌더링할 div 추가 -->
    <div id="glbRenderer" style="width: 100%; height: 300px;"></div>
</div>

 

 

결과

728x90
반응형
LIST