13

I'm upgrading my app to VueJS 3. I read that you could keep the same components. But I have an error in the console now, although I didn't change anything. Here is my component:

<template>
  <v-container>
    <div
      @click="onClick"
      @mousemove="onMouseMove"
      id="menu3D"
      style="background-color: transparent; position: fixed; left: 20px; width:15%; height:100%;">
    </div>
    <v-row class="text-center">

      <v-col
        class="mb-5"
        cols="12"
      >
        <h2 class="headline font-weight-bold mb-3">
          Accueil
        </h2>

        <v-row justify="center">

          <p>
            Client: {{ JSON.stringify(client)}}
          </p>
          <p>
            Mouse: {{ JSON.stringify(mouse)}}
          </p>
          <p>
            Container: {{ JSON.stringify(container)}}
          </p>
        </v-row>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>

import * as Three from 'three';

export default {
  name: 'Accueil',
  mounted() {
    this.init();
  },
  methods: {
    init() {
      this.createScene();
      this.createCamera();
      this.userData.formes.forEach((x) => this.createShape(x));
      this.addSpotlight(16777215);
      this.addAmbientLight();
      this.animate();
      window.addEventListener('resize', this.onResize);
    },
    onResize() {
      const container = document.getElementById('menu3D');
      this.renderer.setSize(container.clientWidth, container.clientHeight);
      this.camera.aspect = container.clientWidth / container.clientHeight;
      this.camera.updateProjectionMatrix();
    },
    createScene() {
      this.renderer = new Three.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      const container = document.getElementById('menu3D');
      this.renderer.setSize(container.clientWidth, container.clientHeight);
      this.renderer.setPixelRatio(window.devicePixelRatio);
      this.renderer.setClearColor(0xffffff, 0);
      container.appendChild(this.renderer.domElement);
    },

    createCamera() {
      const container = document.getElementById('menu3D');
      this.camera = new Three.PerspectiveCamera(50,
        container.clientWidth / container.clientHeight, 0.01, 1000);
      this.camera.position.set(0, 5, 20);
      this.camera.zoom = 1;
    },

    createShape(shape) {
      const material = new Three.MeshStandardMaterial({
        color: '#0000ff',
        roughness: 1,
        metalness: 0.5,
        emissive: 0,
        depthFunc: 3,
        depthTest: true,
        depthWrite: true,
        stencilWrite: false,
        stencilWriteMask: 255,
        stencilFunc: 519,
        stencilRef: 0,
        stencilFuncMask: 255,
        stencilFail: 7680,
        stencilZFail: 7680,
        stencilZPass: 7680,
      });
      switch (shape.nom) {
        case 'Box': {
          this.geometry = new Three.BoxBufferGeometry(1.8, 1.8, 1.8);
          break;
        }
        case 'Sphere': {
          this.geometry = new Three.SphereBufferGeometry(1, 8, 6, 0, 6.283185, 0, 3.141593);
          break;
        }
        case 'Dodecahedron': {
          this.geometry = new Three.DodecahedronBufferGeometry(1.2, 0);
          break;
        }
        case 'Icosahedron': {
          this.geometry = new Three.IcosahedronBufferGeometry(1.5, 0);
          break;
        }
        default: {
          return false;
        }
      }
      this.mesh = new Three.Mesh(this.geometry, material);
      this.mesh.name = shape.nom;
      this.mesh.userData = shape.userData;
      this.mesh.receiveShadow = true;
      this.mesh.castShadow = true;
      this.mesh.position.set(0, shape.userData.position.y, 0);
      this.scene.add(this.mesh);
      return true;
    },

    addSpotlight(color) {
      const light = new Three.SpotLight(color, 2, 1000);
      light.position.set(0, 0, 30);
      this.scene.add(light);
    },

    addAmbientLight() {
      const light = new Three.AmbientLight('#fff', 0.5);
      this.scene.add(light);
    },

    verifForme(e) {
      const t = this;
      const elt = t.scene.getObjectByName(e);
      t.intersects = t.raycaster.intersectObject(elt);
      if (t.intersects.length !== 0) {
        // s'il ne figure pas dans le tableau, on le met en premier
        if (t.userData.souris.indexOf(e) < 0) {
          t.userData.souris.unshift(e);
          console.log(`${t.userData.souris[0]} survolé!`);
        }
        if (t.userData.souris[0] === e) {
          const obj = t.intersects[0].object;
          obj.material.color.set(`#${elt.userData.couleurs[1]}`);
          obj.scale.set(obj.scale.x < 1.4
            ? obj.scale.x + t.VITESSE_ZOOM
            : obj.scale.x, obj.scale.y < 1.4
            ? obj.scale.y + t.VITESSE_ZOOM
            : obj.scale.y, obj.scale.z < 1.4
            ? obj.scale.z + t.VITESSE_ZOOM
            : obj.scale.z);
          obj.rotation.y += t.VITESSE_ROTATION / t.RALENTISSEMENT;
          t.replacer(obj, obj.userData.position.y + obj.userData.decalage);
        } else {
          t.retrecir(e, elt);
        }
      } else {
        if (t.userData.souris.indexOf(e) >= 0) {
          t.userData.souris = t.userData.souris.filter((forme) => forme !== e);
        }
        t.retrecir(e, elt);
      }
    },

    onClick(event) {
      event.preventDefault();
      if (this.userData.souris.length > 0) {
        console.log(`${this.userData.souris[0]} cliqué!`);
      } else {
        console.log('clic dans le vide!');
      }
    },

    onMouseMove(event) {
      const container = document.getElementById('menu3D');
      this.mouse.x = (event.offsetX / container.clientWidth) * 2 - 1;
      this.mouse.y = -(event.offsetY / container.clientHeight) * 2 + 1;
      this.client.clientX = event.clientX;
      this.client.clientY = event.clientY;
      this.container.width = container.clientWidth;
      this.container.height = container.clientHeight;
      // console.log(JSON.stringify(this.mouse))
    },

    replacer(e, py) {
      // la ligne suivante est pour éviter les tremblements
      if (Math.abs(e.position.y - py) < 0.05) { return true; }
      let rhesus = 10 * this.VITESSE_DEPLACEMENT;
      if (this.userData.souris[0] !== e.name) { rhesus *= 3; }
      // console.log(e.name+': '+this.userData.souris[0]+' - '+rhesus)
      if (e.position.y > py) { rhesus = -1; }
      e.position.set(0, Math.trunc(10 * e.position.y + rhesus) / 10, 0);
      return true;
    },

    retrecir(n, e) {
      // on vérifie si le truc cliqué est dessus
      let dec = 0;
      const elt = this;
      if ((elt.userData.souris.length > 0)
        && (elt.userData.formes.map((x) => x.nom).indexOf(n)
        < elt.userData.formes.map((x) => x.nom).indexOf(elt.userData.souris[0]))) {
        dec = Math.trunc(10
          * e.parent.getObjectByName(elt.userData.souris[0]).userData.decalage
          * 2.1) / 10;
      }
      e.material.color.set(`#${e.userData.couleurs[0]}`);
      e.rotation.y += elt.VITESSE_ROTATION;
      e.scale.set(e.scale.x > 1
        ? e.scale.x - elt.VITESSE_ZOOM : e.scale.x,
      e.scale.y > 1
        ? e.scale.y - elt.VITESSE_ZOOM : e.scale.y,
      e.scale.z > 1
        ? e.scale.z - elt.VITESSE_ZOOM : e.scale.z);
      const newY = e.userData.position.y + dec;
      if (e.position.y !== newY) {
        elt.replacer(e, newY);
      }
    },

    animate() {
      const elt = this;
      requestAnimationFrame(this.animate);
      this.raycaster.setFromCamera(this.mouse, this.camera);
      this.userData.formes.map((x) => x.nom).forEach((x) => elt.verifForme(x));
      if (this.userData.souris.length > 0) {
        document.body.style.cursor = 'pointer';
      } else { document.body.style.cursor = 'default'; }
      this.camera.updateProjectionMatrix();
      this.renderer.render(this.scene, this.camera);
    },
  },
  data: () => ({
    container: { height: 0, width: 0 },
    client: { clientX: 0, clientY: 0 },

    scene: new Three.Scene(),
    camera: null,
    renderer: Three.WebGLRenderer,
    mesh: new Three.Mesh(),
    factor: 0,
    mouse: new Three.Vector2(1, 1),
    raycaster: new Three.Raycaster(),
    intersects: [],
    VITESSE_ROTATION: 0.05,
    VITESSE_DEPLACEMENT: 0.1,
    VITESSE_ZOOM: 0.05,
    RALENTISSEMENT: 3,
    userData: {
      souris: [],
      formes: [
        {
          nom: 'Box',
          userData: {
            position: {
              x: 0,
              y: 7.8,
              z: 0,
            },
            couleurs: [
              'aaaaaa',
              '095256',
            ],
            decalage: 0.5,
          },
        },
        {
          nom: 'Icosahedron',
          userData: {
            position: {
              x: 0,
              y: 5.5,
              z: 0,
            },
            couleurs: [
              'aaaaaa',
              '087F8C',
            ],
            decalage: 0.5,
          },
        },
        {
          nom: 'Dodecahedron',
          userData: {
            position: {
              x: 0,
              y: 3.1,
              z: 0,
            },
            couleurs: [
              'aaaaaa',
              '5AAA95',
            ],
            decalage: 0.4,
          },
        },
        {
          nom: 'Sphere',
          userData: {
            position: {
              x: 0,
              y: 1,
              z: 0,
            },
            couleurs: [
              'aaaaaa',
              '86A873',
            ],
            decalage: 0.2,
          },
        },
      ],
    },
  }),
};
</script>

And here is the error I have in the console with VueJS 3:

three.module.js?5a89:24471 Uncaught TypeError: 
'get' on proxy: property 'modelViewMatrix' is a read-only and 
non-configurable data property on the proxy target but the proxy did not 
return its actual value (expected '#<Matrix4>' but got '[object Object]')
at renderObject (three.module.js?5a89:24471)
at renderObjects (three.module.js?5a89:24458)
at Proxy.WebGLRenderer.render (three.module.js?5a89:24258)
at animate (HelloWorld.vue?fdab:192)

If anyone has got a clue, thanks in advance...

0

3 Answers 3

32
+200

It worked with Vue 2

Reason it worked fine with Vue 2 lies in the fact Vue 2 is using different reactivity system based on Object.defineProperty API.

The same API is used by THREE.js a lot to add some non-writable and non-configurable properties to it's data structures

When object with such property was passed to Vue (by declaring it inside data for example), Vue just skipped such property resulting in stored value/object being non-reactive (as Vue could not detect property access while rendering the component template)

Vue 3 proxies

Vue 3 is using new reactivity system base on ES6 proxies.

This is pretty new and even that a lot of effort has been put into developing and testing it, issues like this will arise as people start migrating (And I completely agree with @Serg - Vue 3 is still new and unless you have skill and time to "live on the edge" you should wait a bit before migrating from Vue 2)

This new reactivity system doesn't play well with non-writable non-configurable properties on objects - you can find minimal reproducible example in this sandbox

  1. Imho it is a bug and is reported to Vue@next repo
  2. sandbox uses composition API but that doesn't matter as using reactive() is the same as declaring your variables inside data() function (Vue just do it automatically for you)

Workarounds

As said before, problem is in reactivity system. I'm not an expert on THREE.js but from what I know it doesn't make much sense to put the THREE data structures into Vue reactivity system - all point of reactivity is to detect data changes and re-render template when needed. THREE has its own rendering system and is usually using single <canvas> HTML element so it makes no sense to trigger Vue re-render on THREE data structures change...

There are multiple ways to opt-out from Vue reactivity:

  1. Use Object.freeze() on your objects. Not very useful in this case but good to know
  2. Do not declare your variables in data() and assign the values in created()/mounted() hook (example bellow). You can assign them into component itself (this) if you need to access them in multiple methods or as a local variables (const/let) whenf you don't need it
  3. When using Composition API, do not use reactive() on THREE data structures

NOTE: Even if they admit it is a bug, only way of fixing it is to leave such property and object it holds non-reactive (not putting Proxy around that object) so result will be same as opting-out of reactivity completely. But using this workaround also gives you faster and less memory hungry app as all the reactivity is not really that cheap

Example - creating non-reactive component properties

export default {
  data() {
    return {
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      this.scene = new THREE.Scene();
      this.camera = new THREE.OrthographicCamera(...);

      this.renderer = new THREE.WebGLRenderer({ ... })
      this.geometry = new THREE.PlaneBufferGeometry(  );
      const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });

      this.plane = new THREE.Mesh(this.geometry, material);

      this.scene.add(this.plane);

      this.renderer.render(this.scene, this.camera);
    },
}
Sign up to request clarification or add additional context in comments.

Comments

8

toRaw(vue3) - At this time, you can feel his strength !

You can use this method, to solve a series of these problems

If mesh/xxx is a ref variable

  • scene.add(toRaw(mesh.value))
  • renderer.value.render(toRaw(scene.value), camera.value);

3 Comments

This doesn't answer the question
This solved my problem simply adding toRaw around my scene. I did not use the ".value" property anywhere. The reason this works is that the "this.scene" is reactive and doesn't return the actual value, but instead something like a reference.
If you want an object be reactive except for specific properties, you also have the option to "markRaw" so those properties aren't converted to proxies. vuejs.org/api/reactivity-advanced.html#markraw
1

I am using threejs + vue3 + pinia. Pinia was wrapping objects in Proxy too, but I need to pass 3d object to it sometimes (inside if other model). So I had a model like:

class SomeModel {
  otherProp: 'some value',
  graphicObject: new THREE.Object3D(),
}

The way I fixed this issue is by changing graphicObject prop to a function, that return 3d object, that was saved in other variable. It looks like this:

class SomeModel {
  otherProp: 'some value',
 
  constructor(graphicObject) {
    this.graphicObject = () => graphicObject,   
  }
}

new SomeModel(new THREE.Object3D());

This way 3d object is hidden from Vue at all time, if you dont pass this object directly to any reactive variable. And the way you access it in other methods is by just calling this function like in example:

<script setup>
import { ref } from 'vue';

// You may just call constructor inside new SomeModel() if you want.
const graphicObject = new THREE.Object3D();

const someModel = ref(new SomeModel(graphicObject));

function changePosition(x, y, z) {
  // should not emit errors, because 3d object is not reactive -
  // it's just a value, returned from function.
  someModel.value.graphicObject().position.set(x, y, z);
}
</script>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.