Home > Enterprise >  Three JS object does not load in Nuxt 3 when navigating between pages
Three JS object does not load in Nuxt 3 when navigating between pages

Time:09-27

Let me start by saying I have a lot to learn. I hate asking for help, but I have went through pages and pages looking for an answer with no luck.

I am sure the issue is simple, but can someone please explain to me what I am doing wrong? My Three JS atom loads just fine. I dont have any issue until i navigate to a different page (NuxtLink) and then return to this page.

It throws this error:

Uncaught (in promise) TypeError: Cannot read properties of null (reading 'width') at new WebGLRenderer (three.module.js:26639:23) at Atom.vue:64:1 at hook.__wdc.hook.__wdc (runtime-core.esm-bundler.js:2626:20) at callWithErrorHandling (runtime-core.esm-bundler.js:155:22) at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:164:21) at hook.__weh.hook.__weh (runtime-core.esm-bundler.js:2684:29) at flushPostFlushCbs (runtime-core.esm-bundler.js:341:32) at flushJobs (runtime-core.esm-bundler.js:395:9)

Which to my understanding means that the canvas isnt loading ? I even tried using the options API instead of the composition API, but I get the same issue. Ive tried using onActivated & KeepAlive as you can see and neither of those work too.

        <script setup>
    import * as THREE from 'three'

    onActivated(() => {
    // Canvas
    const canvas = document.querySelector('canvas.webgl')
    // Sizes
    const sizes = {
        width: 300,
        height: 300
    }
    const pixelRatio = window.devicePixelRatio

    /* Base */
    // Scene
    const scene = new THREE.Scene()

    /* Props */
    // Plane Background
    const backgroundPlane = new THREE.PlaneGeometry(50, 50, 4, 4)
    const backgroundMaterial = new THREE.MeshStandardMaterial({ emissive: "#000505", roughness:0.1, metalness: 1 })
    const background = new THREE.Mesh(backgroundPlane, backgroundMaterial)
    background.position.z = -5
    scene.add(background)

    // Atom Group
    const atom = new THREE.Group()
    scene.add(atom)
    atom.position.z = -2

    const protonSphere = new THREE.SphereGeometry(.3, 16, 16)
    const protonMaterial = new THREE.MeshStandardMaterial({ emissive: "#080E54", roughness:0.4, metalness: 0.9 })
    const proton = new THREE.Mesh(protonSphere, protonMaterial)
    atom.add(proton)

    const electronCloudSphere = new THREE.SphereGeometry(2, 32, 32)
    const electronCloudMaterial = new THREE.MeshPhysicalMaterial({ opacity: .2, transparent: true, reflectivity:0, side:THREE.DoubleSide, roughness:0.5, metalness: 0, ior: 2, thickness: 2, transmission: 1 })
    const electronCloud = new THREE.Mesh(electronCloudSphere, electronCloudMaterial)
    atom.add(electronCloud)

    const electronSphere = new THREE.SphereGeometry(.1, 16, 16)
    const electronMaterial = new THREE.MeshStandardMaterial({ emissive: "#2FFFFF", roughness:0, metalness: 0.9 })
    const electron = new THREE.Mesh(electronSphere, electronMaterial)
    const radius = 1.5
    let theta = Math.random() * Math.PI * 2
    let phi = Math.random() * Math.PI
    const x = radius * Math.sin(theta)
    const z = radius * Math.cos(theta)
    const y = radius * Math.cos(phi)
    electron.position.set(x, y, z)
    atom.add(electron)

    const pointLight = new THREE.PointLight("#B7DBFF", 1, 5)
    pointLight.position.z = 1
    scene.add(pointLight)

    /* Camera & Controls */
    // Base camera
    const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
    camera.position.z = 3
    scene.add(camera)

    /* Renderer */
    const renderer = new THREE.WebGLRenderer({
        canvas,
        antialias: true,
    })
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(pixelRatio, 2))
    
    /* Animate */
    const clock = new THREE.Clock()
    let time = 0

    const tick = () =>
    {
        const elapsedTime = clock.getElapsedTime()
        const deltaTime = elapsedTime - time
        time = elapsedTime

        // Animations
        let shake = (Math.random() * 0.015)
        proton.rotateX(.5)
        proton.position.set(shake, shake, 0)
        atom.rotateX(.01 * Math.random())
        atom.rotateY(.02 * Math.random())
        atom.rotateZ(.04 * Math.random())

        // Render
        renderer.render(scene, camera)

        // Call tick again on the next frame
        window.requestAnimationFrame(tick)
    }
    tick()
    })

</script>

<template>
    <div >
        <canvas id="atom" ></canvas>
    </div>
</template>

CodePudding user response:

Your issue is happening because you do indeed have a querySelector, while the recommended way is to go with template refs: https://vuejs.org/guide/essentials/template-refs.html#template-refs

The reason being is that when you are in an SPA context, the document stays the same but stacking querySelectors and trying to use them will not behave properly with Components lifecycles.

Even worse with Nuxt, because you don't actually have document on the server, so it may cause even more bugs or even memory leaks.

mounted is called only on the client-side, so that's why it's working great down there. But the recommended way is still to use template refs, to avoid any issue (and remove the need to remove the listeners).

CodePudding user response:

Finally figure it out.

const webgl = ref(null)

onMounted(() => {
// Canvas
const canvas = webgl.value

Using template refs seems to fix my issue.

<template>
    <div >
        <canvas ref="webgl" id="atom" ></canvas>
    </div>
</template>

Not sure if this is the best solution... but it seems to work!

  • Related