Displaying Locally Saved 3D models in Obsidian Notes

Hi Everyone,

When going over this forum and Reddit trying to find a way to display my 3D models in my Notes Without needing to host them online and use an website Embed. I found that there were currently no real ways of doing so.

So I decided to try something myself with the help of the dataviewjs plugin, and a base knowledge of the three.js library. My script currently supports .stl and .glb 3D models, allows you to use simple OrbitControls around the model, such as zoom in and out, or rotation. And allows for some basic manipulation via the code block such as scale and start rotation. I will expand it, and perhaps turn it into a plugin if I feel the need.

Watch out the following information is all case Sensitive!

You can achieve this by having the dataview plugin installed and adding the following codeblock in your note:

await dv.view("threejsinobsidian",{
	el: dv, 
	name: "FloatingWizardTower.glb",
	rotationX: 0,
	rotationY: 0,
	rotationZ: 0,
	scale: 0.5,
});

Make a folder in your vault called “3DModels” in which you place your 3D models. Then in the codeblock shown above change the name variable to the name of the model.

In the plugin settings change the following to true:

Then make a new folder called “Scripts”, and add a js file called threejsinobsidian.js in there with the following code:

// You always receive an input object
console.log("Succesfully loaded script")
console.log("Inputs received: " + input);

// Sets a div to the container that is provided by dataviewjs
const div = input.el.container;

// Gets the correct path to your model file within the vault
const modelPath = app.vault.adapter.getResourcePath("3DModels/" + input.name);
const modelType = input.name.substr(input.name.length - 3);
const inputRotationX = input.rotationX;
const inputRotationY = input.rotationY;
const inputRotationZ = input.rotationZ;
const scale = input.scale;

runThreeJs()

// Function to load external scripts
function loadScript(url, callback) {
    const script = document.createElement('script');
    script.src = url;
    script.onload = callback;
    script.onerror = () => console.error(`Failed to load script: ${url}`);
    document.head.appendChild(script);
}

function runThreeJs() {
    // Load Three.js, GLTFLoader and Orbitcontrols from the correct CDN
    loadScript('https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js', () => {
        console.log('Three.js loaded');

        loadScript('https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js', () => {
            console.log('OrbitControls loaded');

            threeJS(modelType);

        }, undefined, (error) => {
            console.error('An error happened while loading the OrbitControls', error);
        });
    }, undefined, (error) => {
        console.error('An error happened while loading the Three.js package', error);
    });
}

function threeJS(modelExtension) {
    // Set up scene
    const scene = new THREE.Scene();

    scene.background = new THREE.Color( 0xADD8E6 );

    // Camera setup (ensure it's properly looking at the scene)
    const camera = new THREE.PerspectiveCamera(75, div.clientWidth / 300, 0.1, 1000);
    camera.position.z = 10;

    // Create a renderer
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(div.clientWidth, 300); // Set the renderer size to fit the div
    div.appendChild(renderer.domElement);

    // Add a basic light source (e.g., directional light)
    const light = new THREE.DirectionalLight(0xffffff, 1); // White light, full intensity
    light.position.set(5, 10, 5); // Position the light
    scene.add(light); // Add the light to the scene

    // Optionally, add ambient light for more balanced lighting
    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);  // Dim ambient light
    scene.add(ambientLight);

    const controls = new THREE.OrbitControls(camera, renderer.domElement);

    if (modelExtension == "stl") {
        loadScript('https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/STLLoader.js', () => {
            console.log('STLLoader loaded');
            const loader = new THREE.STLLoader();
            loader.load(modelPath, (geometry) => {
                //const material = new THREE.MeshStandardMaterial({ color: 0x606060 });
                const material = new THREE.MeshPhongMaterial({ color: 0x606060, shininess: 100 });
                //const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 
                const model = new THREE.Mesh(geometry, material);

                model.scale.set(scale, scale, scale);  // Adjust the scale if needed
                model.rotation.x = THREE.Math.degToRad(inputRotationX);
                model.rotation.y = THREE.Math.degToRad(inputRotationY);
                model.rotation.z = THREE.Math.degToRad(inputRotationZ);
                scene.add(model);

                function animate() {
                    requestAnimationFrame(animate);

                    renderer.render(scene, camera);  // Render the scene from the camera's perspective
                }

                animate();
            }, undefined, (error) => {
                console.error('An error happened while loading the model', error);
            });
        });
    } else if (modelExtension == "glb") {
        loadScript('https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js', () => {
            console.log('GLTFLoader loaded');
            const loader = new THREE.GLTFLoader();
            loader.load(modelPath, (gltf) => {
                console.log('Model loaded');
                const model = gltf.scene;
                model.scale.set(scale, scale, scale);
                model.rotation.x = THREE.Math.degToRad(inputRotationX);
                model.rotation.y = THREE.Math.degToRad(inputRotationY);
                model.rotation.z = THREE.Math.degToRad(inputRotationZ);

                // Add the model to the scene
                scene.add(model);

                //const controls = new THREE.OrbitControls(camera, renderer.domElement);

                // Animate and render the scene
                function animate() {
                    requestAnimationFrame(animate);

                    renderer.render(scene, camera);  // Render the scene from the camera's perspective
                }

                animate();
            }, undefined, (error) => {
                console.error('An error happened while loading the model', error);
            });
        });
    }

    renderer.render(scene, camera);
}

This should now display an object in your note! If it does not work, try to use the inspect element in obsidian to see if errors are logged. (Ctrl+Shift+i). See if all names of files are correct. And if that still doesnt fix it try it with another 3D model. And if you still have issues please leave a reply.

I hope this helps some people!

8 Likes

I turned it into an actual plugin, so you dont actually have to go through this trouble of setting it up yourself :slight_smile: Here!