瀏覽代碼

feat: 503 conditional rendering of primitives (#514)

* feat(nodeOps): switch instance logic for reactive `object` prop

* chore: playground primitives with models

* chore: fix linter

* chore: fix tests and linters, primitive object is now reactive

* chore: refactor instance swaping logic to overwrite set and copy properties

* chore: tests

* chore: remove console.log

* chore: remove unused import watch

* feat: add primitive conditional to patch object prop
Alvaro Saburido 1 年之前
父節點
當前提交
79d8a762da

+ 1 - 0
playground/components.d.ts

@@ -14,6 +14,7 @@ declare module 'vue' {
     DanielTest: typeof import('./src/components/DanielTest.vue')['default']
     DebugUI: typeof import('./src/components/DebugUI.vue')['default']
     DeleteMe: typeof import('./src/components/DeleteMe.vue')['default']
+    DynamicModel: typeof import('./src/components/DynamicModel.vue')['default']
     FBXModels: typeof import('./src/components/FBXModels.vue')['default']
     Gltf: typeof import('./src/components/gltf/index.vue')['default']
     LocalOrbitControls: typeof import('./src/components/LocalOrbitControls.vue')['default']

+ 21 - 0
playground/src/components/DynamicModel.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import { useControls } from '@tresjs/leches'
+import { useGLTF } from '@tresjs/cientos'
+
+const { nodes } 
+  = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', 
+    { draco: true })
+
+const { scene: AkuAku, nodes: akukuNodes } = await useGLTF(
+  'https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/aku-aku/AkuAku.gltf',
+  { draco: true },
+)
+
+const { isCube } = useControls({
+  isCube: false,
+})
+</script>
+
+<template>
+  <primitive :object="isCube ? nodes.Cube : AkuAku" />
+</template>

+ 15 - 16
playground/src/components/TheExperience.vue

@@ -3,8 +3,8 @@ import { ref, watchEffect } from 'vue'
 import { TresCanvas } from '@tresjs/core'
 import { OrbitControls } from '@tresjs/cientos'
 import { TresLeches, useControls } from '@tresjs/leches'
-import '@tresjs/leches/styles'
 import TheSphere from './TheSphere.vue'
+import '@tresjs/leches/styles'
 
 const gl = {
   clearColor: '#82DBC5',
@@ -12,17 +12,14 @@ const gl = {
 }
 
 const wireframe = ref(true)
-
-const canvas = ref()
-const meshRef = ref()
-
 const { isVisible } = useControls({
   isVisible: true,
 })
+const canvas = ref()
 
 watchEffect(() => {
-  if (meshRef.value) {
-    console.log(meshRef.value)
+  if (canvas.value) {
+    console.log(canvas.value.context)
   }
 })
 </script>
@@ -39,16 +36,21 @@ watchEffect(() => {
       :look-at="[0, 4, 0]"
     />
     <OrbitControls />
+    <TresFog
+      :color="gl.clearColor"
+      :near="5"
+      :far="15"
+    />
     <TresMesh
       :position="[-2, 6, 0]"
       :rotation="[0, Math.PI, 0]"
-      name="cone"
       cast-shadow
     >
       <TresConeGeometry :args="[1, 1.5, 3]" />
       <TresMeshToonMaterial color="#82DBC5" />
     </TresMesh>
     <TresMesh
+      v-if="isVisible"
       :position="[0, 4, 0]"
       cast-shadow
     >
@@ -59,23 +61,20 @@ watchEffect(() => {
       />
     </TresMesh>
     <TresMesh
-      ref="meshRef"
-      :rotation="[-Math.PI / 2, 0, Math.PI / 2]"
-      name="floor"
+      :rotation="[-Math.PI / 2, 0, 0]"
       receive-shadow
       @click="wireframe = !wireframe"
     >
-      <TresPlaneGeometry :args="[20, 20, 20]" />
-      <TresMeshToonMaterial
-        color="#D3FC8A"
-      />
+      <TresPlaneGeometry :args="[10, 10, 10, 10]" />
+      <TresMeshToonMaterial color="#D3FC8A" />
     </TresMesh>
-    <TheSphere v-if="isVisible" />
+    <TheSphere />
     <TresAxesHelper :args="[1]" />
     <TresDirectionalLight
       :position="[0, 2, 4]"
       :intensity="2"
       cast-shadow
     />
+    <TresOrthographicCamera />
   </TresCanvas>
 </template>

+ 135 - 0
playground/src/pages/primitives.vue

@@ -0,0 +1,135 @@
+<script setup lang="ts">
+import { ref, watchEffect } from 'vue'
+import { 
+  BasicShadowMap,
+  SRGBColorSpace,
+  NoToneMapping,
+  Mesh,
+  TorusGeometry,
+  MeshToonMaterial,
+  TorusKnotGeometry,
+  PlaneGeometry,
+  Group,
+  SphereGeometry, 
+} from 'three'
+import { TresCanvas, useRenderLoop } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+import { TresLeches, useControls } from '@tresjs/leches'
+import '@tresjs/leches/styles'
+
+const gl = {
+  clearColor: '#82DBC5',
+  shadows: true,
+  alpha: false,
+  shadowMapType: BasicShadowMap,
+  outputColorSpace: SRGBColorSpace,
+  toneMapping: NoToneMapping,
+}
+const canvas = ref()
+const meshRef = ref()
+
+const { knot } = useControls({
+  knot: true,
+})
+
+const { isVisible } = useControls({
+  isVisible: true,
+})
+
+watchEffect(() => {
+  if (meshRef.value) {
+    console.log(meshRef.value)
+  }
+})
+
+const torus = new Mesh(
+  new TorusGeometry(1, 0.5, 16, 100),
+  new MeshToonMaterial({
+    color: '#82DBC5',
+  }),
+)
+
+const torusKnot = new Mesh(
+  new TorusKnotGeometry(1, 0.5, 100, 16),
+  new MeshToonMaterial({
+    color: '#ff00ff',
+  }),
+)
+
+const sphere = new Mesh(
+  new SphereGeometry(1, 32, 32),
+  new MeshToonMaterial({
+    color: '#82DBC5',
+  }),
+)
+
+sphere.position.set(2, -2, 0)
+
+const firstGroup = new Group()
+firstGroup.add(torus)
+firstGroup.add(torusKnot)
+
+const secondGroup = new Group()
+secondGroup.add(sphere)
+
+const primitiveRef = ref()
+
+useRenderLoop().onLoop(() => {  
+  if (primitiveRef.value) {
+    primitiveRef.value.rotation.x += 0.01
+    primitiveRef.value.rotation.y += 0.01
+  }
+})
+
+watchEffect(() => {
+  console.log('primitiveRef.value', primitiveRef.value)
+})
+
+const reactivePrimitiveRef = ref(new Mesh(
+  new TorusKnotGeometry(1, 0.5, 100, 16),
+  new MeshToonMaterial({
+    color: 'orange',
+  }),
+))
+
+const modelArray = ref([torus, torusKnot, sphere])
+</script>
+
+<template>
+  <TresLeches />
+  <TresCanvas
+    v-bind="gl"
+    ref="canvas"
+    window-size
+    class="awiwi"
+    :style="{ background: '#008080' }"
+  >
+    <TresPerspectiveCamera
+      :position="[7, 7, 7]"
+    />
+    <OrbitControls />
+    <!--  <primitive
+      :object="reactivePrimitiveRef"
+    /> -->
+    <!--    <primitive
+      v-for="(model, index) of modelArray"
+      :key="index"
+      :object="model"
+      :position="[index * 2, index * 2, 0]"
+    /> -->
+    <primitive
+      v-if="isVisible"
+      ref="primitiveRef"
+      :object="knot ? torusKnot : torus"
+    />
+    <!--    <Suspense>
+      <DynamicModel />
+    </Suspense> -->
+    <TresAxesHelper :args="[1]" />
+    <TresDirectionalLight
+      :position="[0, 2, 4]"
+      :intensity="2"
+      cast-shadow
+    />
+  </TresCanvas>
+</template>

+ 5 - 0
playground/src/router.ts

@@ -81,6 +81,11 @@ const routes = [
     name: 'Perf',
     component: () => import('./pages/perf/index.vue'),
   },
+  {
+    path: '/primitives',
+    name: 'Primitives',
+    component: () => import('./pages/primitives.vue'),
+  },
   {
     path: '/rendering-modes',
     name: 'Rendering Modes',

+ 14 - 14
pnpm-lock.yaml

@@ -89,7 +89,7 @@ importers:
         version: 0.160.1
       unocss:
         specifier: ^0.58.3
-        version: 0.58.4(postcss@8.4.32)(vite@5.0.10)
+        version: 0.58.4(postcss@8.4.33)(vite@5.0.10)
       unplugin:
         specifier: ^1.6.0
         version: 1.6.0
@@ -116,7 +116,7 @@ importers:
         version: 5.1.0(vue@3.4.15)
       vitepress:
         specifier: 1.0.0-rc.34
-        version: 1.0.0-rc.34(@algolia/client-search@4.22.0)(postcss@8.4.32)(search-insights@2.13.0)(typescript@5.3.3)
+        version: 1.0.0-rc.34(@algolia/client-search@4.22.0)(postcss@8.4.33)(search-insights@2.13.0)(typescript@5.3.3)
       vitest:
         specifier: ^1.1.1
         version: 1.2.2(@vitest/ui@1.2.2)(jsdom@23.0.1)
@@ -135,7 +135,7 @@ importers:
     devDependencies:
       unocss:
         specifier: ^0.58.3
-        version: 0.58.4(postcss@8.4.32)(vite@5.0.10)
+        version: 0.58.4(postcss@8.4.33)(vite@5.0.10)
       vite-svg-loader:
         specifier: ^5.1.0
         version: 5.1.0(vue@3.4.15)
@@ -2119,7 +2119,7 @@ packages:
       sirv: 2.0.4
     dev: true
 
-  /@unocss/postcss@0.58.4(postcss@8.4.32):
+  /@unocss/postcss@0.58.4(postcss@8.4.33):
     resolution: {integrity: sha512-pg2qCGakV1TyMApPdvuvqqmPDhgogPWF14J97BT5zIfGYITAJSmBsm7d3+06w6EuqIS+vcYRw+qCV3oX6qTeiA==}
     engines: {node: '>=14'}
     peerDependencies:
@@ -2131,7 +2131,7 @@ packages:
       css-tree: 2.3.1
       fast-glob: 3.3.2
       magic-string: 0.30.5
-      postcss: 8.4.32
+      postcss: 8.4.33
     dev: true
 
   /@unocss/preset-attributify@0.58.4:
@@ -2927,7 +2927,7 @@ packages:
     resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==}
     engines: {node: '>= 10.0.0'}
     dependencies:
-      '@babel/types': 7.23.6
+      '@babel/types': 7.23.9
     dev: true
 
   /balanced-match@1.0.2:
@@ -3395,8 +3395,8 @@ packages:
   /constantinople@4.0.1:
     resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
     dependencies:
-      '@babel/parser': 7.23.6
-      '@babel/types': 7.23.6
+      '@babel/parser': 7.23.9
+      '@babel/types': 7.23.9
     dev: true
 
   /conventional-changelog-angular@7.0.0:
@@ -7919,7 +7919,7 @@ packages:
     engines: {node: '>= 10.0.0'}
     dev: true
 
-  /unocss@0.58.4(postcss@8.4.32)(vite@5.0.10):
+  /unocss@0.58.4(postcss@8.4.33)(vite@5.0.10):
     resolution: {integrity: sha512-JYeQddAIObJPr6nuxahOgku0MIzjIaQ2P73KtJr0zSuzx6kiq20jf67FgDIOP1Ks6s7iJd7Ga3yuY2h49XjDjg==}
     engines: {node: '>=14'}
     peerDependencies:
@@ -7935,7 +7935,7 @@ packages:
       '@unocss/cli': 0.58.4
       '@unocss/core': 0.58.4
       '@unocss/extractor-arbitrary-variants': 0.58.4
-      '@unocss/postcss': 0.58.4(postcss@8.4.32)
+      '@unocss/postcss': 0.58.4(postcss@8.4.33)
       '@unocss/preset-attributify': 0.58.4
       '@unocss/preset-icons': 0.58.4
       '@unocss/preset-mini': 0.58.4
@@ -8337,7 +8337,7 @@ packages:
       fsevents: 2.3.3
     dev: true
 
-  /vitepress@1.0.0-rc.34(@algolia/client-search@4.22.0)(postcss@8.4.32)(search-insights@2.13.0)(typescript@5.3.3):
+  /vitepress@1.0.0-rc.34(@algolia/client-search@4.22.0)(postcss@8.4.33)(search-insights@2.13.0)(typescript@5.3.3):
     resolution: {integrity: sha512-TUbTiSdAZFni2XlHlpx61KikgkQ5uG4Wtmw2R0SXhIOG6qGqzDJczAFjkMc4i45I9c3KyatwOYe8oEfCnzVYwQ==}
     hasBin: true
     peerDependencies:
@@ -8360,7 +8360,7 @@ packages:
       mark.js: 8.11.1
       minisearch: 6.3.0
       mrmime: 2.0.0
-      postcss: 8.4.32
+      postcss: 8.4.33
       shikiji: 0.9.19
       shikiji-core: 0.9.19
       shikiji-transformers: 0.9.19
@@ -8660,8 +8660,8 @@ packages:
     resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
     engines: {node: '>= 10.0.0'}
     dependencies:
-      '@babel/parser': 7.23.6
-      '@babel/types': 7.23.6
+      '@babel/parser': 7.23.9
+      '@babel/types': 7.23.9
       assert-never: 1.2.1
       babel-walk: 3.0.0-canary-5
     dev: true

+ 34 - 6
src/core/nodeOps.ts

@@ -1,4 +1,4 @@
-import type { RendererOptions } from 'vue'
+import { type RendererOptions } from 'vue'
 import { BufferAttribute } from 'three'
 import { isFunction } from '@alvarosabu/utils'
 import type { Object3D, Camera } from 'three'
@@ -49,7 +49,7 @@ export const nodeOps: RendererOptions<TresObject, TresObject | null> = {
       if (props?.object === undefined) logError('Tres primitives need a prop \'object\'')
       const object = props.object as TresObject
       name = object.type
-      instance = Object.assign(object, { type: name, attach: props.attach })
+      instance = Object.assign(object.clone(), { type: name }) as TresObject
     }
     else {
       const target = catalogue.value[name]
@@ -136,6 +136,8 @@ export const nodeOps: RendererOptions<TresObject, TresObject | null> = {
     if (!node) return
     const ctx = node.__tres
     // remove is only called on the node being removed and not on child nodes.
+    node.parent = node.parent || scene
+    
     const { 
       deregisterObjectAtPointerEventHandler,
       deregisterBlockingObjectAtPointerEventHandler, 
@@ -180,15 +182,41 @@ export const nodeOps: RendererOptions<TresObject, TresObject | null> = {
       disposeMaterialsAndGeometries(node)
       deregisterCameraIfRequired(node as Object3D)
       deregisterAtPointerEventHandlerIfRequired?.(node as TresObject)
+      invalidateInstance(node as TresObject)
+      node.dispose?.()
+      
     }
 
-    invalidateInstance(node as TresObject)
-    node.dispose?.()
   },
-  patchProp(node, prop, _prevValue, nextValue) {
+  patchProp(node, prop, prevValue, nextValue) {
     if (node) {
       let root = node
       let key = prop
+      if (node.__tres.primitive && key === 'object' && prevValue !== null) {
+        // If the prop 'object' is changed, we need to re-instance the object and swap the old one with the new one
+        const newInstance = nodeOps.createElement('primitive', undefined, undefined, { 
+          object: nextValue, 
+        })
+        for (const subkey in newInstance) {
+          if (subkey === 'uuid') continue
+          const target = node[subkey]
+          const value = newInstance[subkey]
+          if (!target?.set && !isFunction(target)) node[subkey] = value
+          else if (target.constructor === value.constructor && target?.copy) target?.copy(value)
+          else if (Array.isArray(value)) target.set(...value)
+          else if (!target.isColor && target.setScalar) target.setScalar(value)
+          else target.set(value)
+        }
+        newInstance.__tres.root = scene?.__tres.root
+        // This code is needed to handle the case where the prop 'object' type change from a group to a mesh or vice versa, otherwise the object will not be rendered correctly (models will be invisible)
+        if (newInstance.isGroup) {
+          node.geometry = undefined
+          node.material = undefined
+        }
+        else {
+          delete node.isGroup
+        }
+      }
 
       if (node.__tres.root) {
         const { 
@@ -211,7 +239,7 @@ export const nodeOps: RendererOptions<TresObject, TresObject | null> = {
 
       if (key === 'args') {
         const prevNode = node as TresObject3D
-        const prevArgs = _prevValue ?? []
+        const prevArgs = prevValue ?? []
         const args = nextValue ?? []
         const instanceName = node.__tres.type || node.type