Render 3D models in the web page generated by Hugo

Render GLTF model with Three.js

Posted on Tuesday, February 27, 2024

I used Hugo to create my blog website. To render 3D models in the pages generated by Hugo, I used three.js. To make it easy to use, I wrapped the code into a shortcode (saved as layouts\shortcodes\model3d.html).

The short code “model3d" has four parameters:

  • src:GLTF model path
  • ratio:Aspect ratio of the view port, default to 1.7778
  • grid:Whether to show the grid or not, default to false
  • back_light:Whether to add the back light, default to 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>

Example to use the shortcode in the markdown file:

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

Just like the model in article Blender Exercises Set 7:

I will open source this shortcode in github after I test it.