end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

hands-on three.js - display 3d models - gltf形式

hands-on three.js - display 3d models - end0tknr's kipple - web写経開発

先程の上記entryのきです。

今回の場合、

に記載されている gltf 形式の3Dモデルの表示です。

threejsfundamentals.org の説明は分かりやすいのですが、 sketchfab.com のサンプルGLTFが、よくできているおかげだと思います。

その他、次のurlも参考になるかもしれません。

Three.js_Tips_GLTFモデルを表示する – しめじのネタ帳

https://end0tknr.github.io/sandbox/threejs_obj_gltf/test_threejs_gltf.html

f:id:end0tknr:20210822102457p:plain

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Hello Three.js</title>
  <style>
    html, body { margin: 0; height: 100%; }
    #c { width: 100%; height: 100%; display: block; }
  </style>
</head>
<body>
  <canvas id="c"></canvas>
  
  <script type="module">
    import * as THREE from
    'https://threejsfundamentals.org/threejs/resources/threejs/r127/build/three.module.js';
    import {OrbitControls} from
    'https://threejsfundamentals.org/threejs/resources/threejs/r127/examples/jsm/controls/OrbitControls.js';
    import {GLTFLoader} from
    'https://threejsfundamentals.org/threejs/resources/threejs/r127/examples/jsm/loaders/GLTFLoader.js';
    import {GUI} from
    'https://threejsfundamentals.org/threejs/../3rdparty/dat.gui.module.js';
    
    function main() {
        const canvas = document.querySelector('#c');
        const renderer = new THREE.WebGLRenderer({canvas});
        renderer.shadowMap.enabled = true;
        
        const fov = 45;
        const aspect = 2;  // the canvas default
        const near = 0.1;
        const far = 100;
        const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
        camera.position.set(0, 10, 20);
        
        const controls = new OrbitControls(camera, canvas);
        controls.target.set(0, 5, 0);
        controls.update();
        
        const scene = new THREE.Scene();
        scene.background = new THREE.Color('#DEFEFF');

        {
            const planeSize = 40;
            
            const loader = new THREE.TextureLoader();
            const texture = loader.load('https://threejsfundamentals.org/threejs/resources/images/checker.png');
            texture.wrapS = THREE.RepeatWrapping;
            texture.wrapT = THREE.RepeatWrapping;
            texture.magFilter = THREE.NearestFilter;
            const repeats = planeSize / 2;
            texture.repeat.set(repeats, repeats);
            
            const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
            const planeMat = new THREE.MeshPhongMaterial({
                map: texture,
                side: THREE.DoubleSide,
            });
            const mesh = new THREE.Mesh(planeGeo, planeMat);
            mesh.rotation.x = Math.PI * -.5;
            scene.add(mesh);
        }
        
        {
            const skyColor = 0xB1E1FF;  // light blue
            const groundColor = 0xB97A20;  // brownish orange
            const intensity = 1;
            const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
            scene.add(light);
        }
        
        {
            const color = 0xFFFFFF;
            const intensity = 1;
            const light = new THREE.DirectionalLight(color, intensity);
            light.castShadow = true;
            light.position.set(-250, 800, -850);
            light.target.position.set(-550, 40, -450);
            
            light.shadow.bias = -0.004;
            light.shadow.mapSize.width = 2048;
            light.shadow.mapSize.height = 2048;
            
            scene.add(light);
            scene.add(light.target);
            const cam = light.shadow.camera;
            cam.near = 1;
            cam.far = 2000;
            cam.left = -1500;
            cam.right = 1500;
            cam.top = 1500;
            cam.bottom = -1500;
            
            const cameraHelper = new THREE.CameraHelper(cam);
            scene.add(cameraHelper);
            cameraHelper.visible = false;
            const helper = new THREE.DirectionalLightHelper(light, 100);
            scene.add(helper);
            helper.visible = false;
            
            function makeXYZGUI(gui, vector3, name, onChangeFn) {
                const folder = gui.addFolder(name);
                folder.add(vector3, 'x', vector3.x - 500, vector3.x + 500).onChange(onChangeFn);
                folder.add(vector3, 'y', vector3.y - 500, vector3.y + 500).onChange(onChangeFn);
                folder.add(vector3, 'z', vector3.z - 500, vector3.z + 500).onChange(onChangeFn);
                folder.open();
            }
            
            function updateCamera() {
                // update the light target's matrixWorld because it's needed by the helper
                light.updateMatrixWorld();
                light.target.updateMatrixWorld();
                helper.update();
                // update the light's shadow camera's projection matrix
                light.shadow.camera.updateProjectionMatrix();
                // and now update the camera helper we're using to show the light's shadow camera
                cameraHelper.update();
            }
            updateCamera();
            
            class DimensionGUIHelper {
                constructor(obj, minProp, maxProp) {
                    this.obj = obj;
                    this.minProp = minProp;
                    this.maxProp = maxProp;
                }
                get value() {
                    return this.obj[this.maxProp] * 2;
                }
                set value(v) {
                    this.obj[this.maxProp] = v /  2;
                    this.obj[this.minProp] = v / -2;
                }
            }
            
            class MinMaxGUIHelper {
                constructor(obj, minProp, maxProp, minDif) {
                    this.obj = obj;
                    this.minProp = minProp;
                    this.maxProp = maxProp;
                    this.minDif = minDif;
                }
                get min() {
                    return this.obj[this.minProp];
                }
                set min(v) {
                    this.obj[this.minProp] = v;
                    this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
                }
                get max() {
                    return this.obj[this.maxProp];
                }
                set max(v) {
                    this.obj[this.maxProp] = v;
                    this.min = this.min;  // this will call the min setter
                }
            }
            
            class VisibleGUIHelper {
                constructor(...objects) {
                    this.objects = [...objects];
                }
                get value() {
                    return this.objects[0].visible;
                }
                set value(v) {
                    this.objects.forEach((obj) => {
                        obj.visible = v;
                    });
                }
            }
            
            const gui = new GUI();
            gui.close();
            gui.add(new VisibleGUIHelper(helper, cameraHelper), 'value').name('show helpers');
            gui.add(light.shadow, 'bias', -0.1, 0.1, 0.001);
            {
                const folder = gui.addFolder('Shadow Camera');
                folder.open();
                folder.add(new DimensionGUIHelper(light.shadow.camera, 'left', 'right'), 'value', 1, 4000)
                    .name('width')
                    .onChange(updateCamera);
                folder.add(new DimensionGUIHelper(light.shadow.camera, 'bottom', 'top'), 'value', 1, 4000 )
                    .name('height')
                    .onChange(updateCamera);
                const minMaxGUIHelper = new MinMaxGUIHelper(light.shadow.camera, 'near', 'far', 0.1);
                folder.add(minMaxGUIHelper, 'min', 1, 1000, 1).name('near').onChange(updateCamera);
                folder.add(minMaxGUIHelper, 'max', 1, 4000, 1).name('far').onChange(updateCamera);
                folder.add(light.shadow.camera, 'zoom', 0.01, 1.5, 0.01).onChange(updateCamera);
            }
            
            makeXYZGUI(gui, light.position, 'position', updateCamera);
            makeXYZGUI(gui, light.target.position, 'target', updateCamera);
        }
        
        function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
            const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
            const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
            const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
            // compute a unit vector that points in the direction the camera is now
            // in the xz plane from the center of the box
            const direction = (new THREE.Vector3())
                  .subVectors(camera.position, boxCenter)
                  .multiply(new THREE.Vector3(1, 0, 1))
                  .normalize();
            
            // move the camera to a position distance units way from the center
            // in whatever direction the camera was from the center already
            camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
            
            // pick some near and far values for the frustum that
            // will contain the box.
            camera.near = boxSize / 100;
            camera.far = boxSize * 100;
            
            camera.updateProjectionMatrix();
            
            // point the camera to look at the center of the box
            camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
        }
        
        let curve;
        let curveObject;
        {
            const controlPoints = [
                [1.118281, 5.115846, -3.681386],
                [3.948875, 5.115846, -3.641834],
                [3.960072, 5.115846, -0.240352],
                [3.985447, 5.115846, 4.585005],
                [-3.793631, 5.115846, 4.585006],
                [-3.826839, 5.115846, -14.736200],
                [-14.542292, 5.115846, -14.765865],
                [-14.520929, 5.115846, -3.627002],
                [-5.452815, 5.115846, -3.634418],
                [-5.467251, 5.115846, 4.549161],
                [-13.266233, 5.115846, 4.567083],
                [-13.250067, 5.115846, -13.499271],
                [4.081842, 5.115846, -13.435463],
                [4.125436, 5.115846, -5.334928],
                [-14.521364, 5.115846, -5.239871],
                [-14.510466, 5.115846, 5.486727],
                [5.745666, 5.115846, 5.510492],
                [5.787942, 5.115846, -14.728308],
                [-5.423720, 5.115846, -14.761919],
                [-5.373599, 5.115846, -3.704133],
                [1.004861, 5.115846, -3.641834],
            ];
            const p0 = new THREE.Vector3();
            const p1 = new THREE.Vector3();
            curve = new THREE.CatmullRomCurve3(
                controlPoints.map((p, ndx) => {
                    p0.set(...p);
                    p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
                    return [
                        (new THREE.Vector3()).copy(p0),
                        (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
                        (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
                    ];
                }).flat(),
                true,
            );
            {
                const points = curve.getPoints(250);
                const geometry = new THREE.BufferGeometry().setFromPoints(points);
                const material = new THREE.LineBasicMaterial({color: 0xff0000});
                curveObject = new THREE.Line(geometry, material);
                curveObject.scale.set(100, 100, 100);
                curveObject.position.y = -621;
                curveObject.visible = false;
                scene.add(curveObject);
            }
        }
        
        const cars = [];
        {
            const gltfLoader = new GLTFLoader();
            gltfLoader.load(
                'https://threejsfundamentals.org/threejs/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
                (gltf) => {
                    const root = gltf.scene;
                    scene.add(root);
                    
                    root.traverse((obj) => {
                        if (obj.castShadow !== undefined) {
                            obj.castShadow = true;
                            obj.receiveShadow = true;
                        }
                    });
                    
                    const loadedCars = root.getObjectByName('Cars');
                    const fixes = [
                        { prefix: 'Car_08', y: 0,  rot: [Math.PI * .5, 0, Math.PI * .5], },
                        { prefix: 'CAR_03', y: 33, rot: [0, Math.PI, 0], },
                        { prefix: 'Car_04', y: 40, rot: [0, Math.PI, 0], },
                    ];
                    
                    root.updateMatrixWorld();
                    for (const car of loadedCars.children.slice()) {
                        const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
                        const obj = new THREE.Object3D();
                        car.position.set(0, fix.y, 0);
                        car.rotation.set(...fix.rot);
                        obj.add(car);
                        scene.add(obj);
                        cars.push(obj);
                    }
                    
                    // compute the box that contains all the stuff
                    // from root and below
                    const box = new THREE.Box3().setFromObject(root);
                    
                    const boxSize = box.getSize(new THREE.Vector3()).length();
                    const boxCenter = box.getCenter(new THREE.Vector3());
                    
                    // set the camera to frame the box
                    frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
                    
                    // update the Trackball controls to handle the new size
                    controls.maxDistance = boxSize * 10;
                    controls.target.copy(boxCenter);
                    controls.update();
                });
        }
        
        function resizeRendererToDisplaySize(renderer) {
            const canvas = renderer.domElement;
            const width = canvas.clientWidth;
            const height = canvas.clientHeight;
            const needResize = canvas.width !== width || canvas.height !== height;
            if (needResize) {
                renderer.setSize(width, height, false);
            }
            return needResize;
        }
        
        // create 2 Vector3s we can use for path calculations
        const carPosition = new THREE.Vector3();
        const carTarget = new THREE.Vector3();
        
        function render(time) {
            time *= 0.001;  // convert to seconds
            
            if (resizeRendererToDisplaySize(renderer)) {
                const canvas = renderer.domElement;
                camera.aspect = canvas.clientWidth / canvas.clientHeight;
                camera.updateProjectionMatrix();
            }
            
            {
                const pathTime = time * .01;
                const targetOffset = 0.01;
                cars.forEach((car, ndx) => {
                    // a number between 0 and 1 to evenly space the cars
                    const u = pathTime + ndx / cars.length;
                    
                    // get the first point
                    curve.getPointAt(u % 1, carPosition);
                    carPosition.applyMatrix4(curveObject.matrixWorld);
                    
                    // get a second point slightly further down the curve
                    curve.getPointAt((u + targetOffset) % 1, carTarget);
                    carTarget.applyMatrix4(curveObject.matrixWorld);
                    
                    // put the car at the first point (temporarily)
                    car.position.copy(carPosition);
                    // point the car the second point
                    car.lookAt(carTarget);
                    
                    // put the car between the 2 points
                    car.position.lerpVectors(carPosition, carTarget, 0.5);
                });
            }
            
            renderer.render(scene, camera);
            
            requestAnimationFrame(render);
        }
        
        requestAnimationFrame(render);
    }
    
    main();
  </script>
</body>
</html>