在Hugo生成的页面中渲染3D模型

使用Three.js渲染GLTF格式3D模型

发布于 2024年2月27日星期二

我使用了Hugo来创建我的这个博客网站。为了能够在Hugo生成的页面中渲染3D模型,我使用了three.js。为了方便使用,我把代码封装成了Hugo的shortcode(保存为layouts\shortcodes\model3d.html)。

“model3d"这个shortcode有四个参数:

  • src:GLTF模型路径
  • ratio:视图宽高比,默认1.7778
  • grid:是否显示网格,默认false
  • back_light:是否显示背面的光源,默认false
{{ $_hugo_config := `{ "version": 1 }` }}

{{ $modelPath := .Get "src" }}
{{ $aspectRatio := .Get "ratio" | default 1.7778 }}
{{ $withGrid := .Get "grid" | default false }}
{{ $withBackLight := .Get "back_light" | default false }}

<div id="container_model3d">
</div>

<script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.161.0/build/three.module.js",
            "three/addons/": "https://unpkg.com/three@0.161.0/examples/jsm/"
        }
    }
</script>

<script type="module">

    import * as THREE from 'three';

    import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

    import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
    import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
    import { DRACOLoader } from 'three/addons/loaders/DRACOLoader'
    import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';
    import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';

    let camera, scene, renderer, containerParent;

    init();
    render();

    function init() {
        const container = document.getElementById('container_model3d');
        container.onload = () => {
            onWindowResize();
        }

        // Get aspect ratio of the canvas
        const aspectRatio = {{ $aspectRatio }}
        // Get path of the 3d model
        const model_path = {{ $modelPath }};

        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(container.clientWidth, container.clientWidth / aspectRatio);
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1;
        container.appendChild(renderer.domElement);

        camera = new THREE.PerspectiveCamera(45, aspectRatio, 2, 100);
        camera.position.set(-4, 3, 6);

        const environment = new RoomEnvironment(renderer);
        const pmremGenerator = new THREE.PMREMGenerator(renderer);

        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xbbbbbb);
        scene.environment = pmremGenerator.fromScene(environment).texture;
        environment.dispose();

        const withGrid = {{ $withGrid }};
        if (withGrid) {
            const grid = new THREE.GridHelper(10, 10, 0xffffff, 0xffffff);
            grid.material.opacity = 0.4;
            grid.material.depthWrite = false;
            grid.material.transparent = true;
            scene.add(grid);
        }

        const withBackLight = {{ $withBackLight }};
        if (withBackLight) {
            RectAreaLightUniformsLib.init();

            const rectLight = new THREE.RectAreaLight(0xffffff, 25, 5, 5);
            rectLight.position.set(-6, 6, -10);
            rectLight.lookAt( 0, 0, 0 );
            scene.add( rectLight );
            scene.add( new RectAreaLightHelper( rectLight ) );
        }

        const ktx2Loader = new KTX2Loader()
            .setTranscoderPath('https://unpkg.com/three@0.161.0/examples/jsm/libs/basis/')
            .detectSupport(renderer);

        const loader = new GLTFLoader();
        loader.setKTX2Loader(ktx2Loader);
        loader.setMeshoptDecoder(MeshoptDecoder);
        loader.setDRACOLoader(DRACOLoader)

        loader.load(model_path, function (gltf) {
            gltf.scene.position.y = 0;
            scene.add(gltf.scene);
            render();
        });

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.addEventListener('change', render); // use if there is no animation loop
        controls.minDistance = 4;
        controls.maxDistance = 15;
        controls.target.set(0, 0, 0);
        controls.update();

        window.addEventListener('resize', onWindowResize);
    }

    function onWindowResize() {
        camera.aspect = aspectRatio;
        camera.updateProjectionMatrix();
        renderer.setSize(container.clientWidth, container.clientWidth / aspectRatio);
        render();
    }

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

</script>

在Markdown中使用这个shortcode的例子:

 {{< model3d  src="/path/to/model.glb" back_light="true" ratio="1.3333" grid="true" >}}

效果就如同在文章Blender习作集7中一样:

经过整理测试之后,我会把这个shortcode在github上开源。