Bladeren bron

feat: webgpu native support (#1029)

Alvaro Saburido 2 weken geleden
bovenliggende
commit
6f3d41d1c7

+ 58 - 0
playground/vue/src/pages/advanced/webGPU/HologramCube.vue

@@ -0,0 +1,58 @@
+<script setup lang="ts">
+import { useGLTF } from '@tresjs/cientos'
+import { add, cameraProjectionMatrix, cameraViewMatrix, color, Fn, hash, mix, normalView, positionWorld, sin, timerGlobal, uniform, varying, vec3, vec4 } from 'three/tsl'
+import { AdditiveBlending, DoubleSide, MeshBasicNodeMaterial } from 'three/webgpu'
+
+const { nodes } = useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
+
+const model = computed(() => nodes.value.BlenderCube)
+/**
+ * Material
+ */
+const material = new MeshBasicNodeMaterial({
+  transparent: true,
+  side: DoubleSide,
+  depthWrite: false,
+  blending: AdditiveBlending,
+})
+// Position
+const glitchStrength = varying(0)
+material.vertexNode = Fn(() => {
+  const glitchTime = timerGlobal().sub(positionWorld.y.mul(0.5))
+  glitchStrength.assign(add(
+    sin(glitchTime),
+    sin(glitchTime.mul(3.45)),
+    sin(glitchTime.mul(8.76)),
+  ).div(3).smoothstep(0.3, 1))
+  const glitch = vec3(
+    hash(positionWorld.xz.abs().mul(9999)).sub(0.5),
+    0,
+    hash(positionWorld.yx.abs().mul(9999)).sub(0.5),
+  )
+  positionWorld.xyz.addAssign(glitch.mul(glitchStrength.mul(0.5)))
+  return cameraProjectionMatrix.mul(cameraViewMatrix).mul(positionWorld)
+})()
+// Color
+const colorInside = uniform(color('#ff6088'))
+const colorOutside = uniform(color('#4d55ff'))
+material.colorNode = Fn(() => {
+  const stripes = positionWorld.y.sub(timerGlobal(0.02)).mul(20).mod(1).pow(3)
+  const fresnel = normalView.dot(vec3(0, 0, 1)).abs().oneMinus()
+  const falloff = fresnel.smoothstep(0.8, 0.2)
+  const alpha = stripes.mul(fresnel).add(fresnel.mul(1.25)).mul(falloff)
+  const finalColor = mix(colorInside, colorOutside, fresnel.add(glitchStrength.mul(0.6)))
+  return vec4(finalColor, alpha)
+})()
+
+watch(model, (newModel) => {
+  newModel.traverse((child) => {
+    if (child.isMesh) {
+      child.material = material
+    }
+  })
+})
+</script>
+
+<template>
+  <primitive v-if="model" :object="model" />
+</template>

+ 79 - 0
playground/vue/src/pages/advanced/webGPU/index.vue

@@ -0,0 +1,79 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { WebGPURenderer } from 'three/webgpu'
+import type { ShadowMapType, ToneMapping } from 'three'
+import type { TresRendererSetupContext } from '@tresjs/core'
+import { ACESFilmicToneMapping, AgXToneMapping, BasicShadowMap, CineonToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, ReinhardToneMapping, VSMShadowMap } from 'three'
+// import { OrbitControls } from '@tresjs/cientos'
+import { TresLeches, useControls } from '@tresjs/leches'
+import '@tresjs/leches/styles'
+
+import HologramCube from './HologramCube.vue'
+
+const createWebGPURenderer = (ctx: TresRendererSetupContext) => {
+  const renderer = new WebGPURenderer({
+    canvas: toValue(ctx.canvas),
+    // WebGPU specific configuration
+    alpha: true,
+    antialias: true,
+  })
+  return renderer
+}
+
+const { clearColor, clearAlpha, toneMapping, shadows, shadowMapType } = useControls({
+  clearColor: '#000000',
+  clearAlpha: {
+    value: 1,
+    min: 0,
+    max: 1,
+    step: 0.01,
+    label: 'Clear Alpha',
+  },
+  toneMapping: {
+    value: ACESFilmicToneMapping,
+    options: [
+      { text: 'No Tone Mapping', value: NoToneMapping },
+      { text: 'Linear', value: LinearToneMapping },
+      { text: 'Reinhard', value: ReinhardToneMapping },
+      { text: 'Cineon', value: CineonToneMapping },
+      { text: 'ACES Filmic', value: ACESFilmicToneMapping },
+      { text: 'AgX', value: AgXToneMapping }, // New in Three.js r155
+      { text: 'Neutral', value: NeutralToneMapping },
+    ],
+  },
+  shadows: true,
+  shadowMapType: {
+    value: PCFSoftShadowMap,
+    options: [
+      { text: 'Basic', value: BasicShadowMap },
+      { text: 'PCF', value: PCFShadowMap },
+      { text: 'PCF Soft', value: PCFSoftShadowMap },
+      { text: 'VSM', value: VSMShadowMap },
+    ],
+  },
+})
+
+const formattedToneMapping = computed(() => {
+  return Number(toneMapping.value) as ToneMapping
+})
+
+const formattedShadowMapType = computed(() => {
+  return Number(shadowMapType.value) as ShadowMapType
+})
+</script>
+
+<template>
+  <TresLeches />
+
+  <TresCanvas :renderer="createWebGPURenderer" :clear-color="clearColor" :clear-alpha="clearAlpha" :tone-mapping="formattedToneMapping" :shadows="shadows" :shadow-map-type="formattedShadowMapType">
+    <TresPerspectiveCamera
+      :position="[3, 3, 3]"
+      :look-at="[0, 0, 0]"
+    />
+    <Suspense>
+      <HologramCube />
+    </Suspense>
+    <!-- <OrbitControls /> -->
+    <TresAmbientLight :intensity="1" />
+  </TresCanvas>
+</template>

+ 5 - 0
playground/vue/src/router/routes/advanced.ts

@@ -54,4 +54,9 @@ export const advancedRoutes = [
     name: 'Memory Test: Tres Objects',
     name: 'Memory Test: Tres Objects',
     component: () => import('../../pages/advanced/MemoryTresObjects.vue'),
     component: () => import('../../pages/advanced/MemoryTresObjects.vue'),
   },
   },
+  {
+    path: '/advanced/webgpu',
+    name: 'WebGPU',
+    component: () => import('../../pages/advanced/webGPU/index.vue'),
+  },
 ]
 ]

+ 11 - 6
src/components/TresCanvas.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import type {
+import {
+  ACESFilmicToneMapping,
+  PCFSoftShadowMap,
+  PerspectiveCamera,
+  Scene,
   WebGLRenderer,
   WebGLRenderer,
 } from 'three'
 } from 'three'
 import type { App, Ref } from 'vue'
 import type { App, Ref } from 'vue'
 import type { TresCamera, TresObject, TresScene } from '../types/'
 import type { TresCamera, TresObject, TresScene } from '../types/'
-import { ACESFilmicToneMapping, PCFSoftShadowMap, PerspectiveCamera, Scene } from 'three'
 import type { PointerEvent } from '@pmndrs/pointer-events'
 import type { PointerEvent } from '@pmndrs/pointer-events'
 import * as THREE from 'three'
 import * as THREE from 'three'
 
 
@@ -24,7 +27,7 @@ import {
   watchEffect,
   watchEffect,
 } from 'vue'
 } from 'vue'
 import pkg from '../../package.json'
 import pkg from '../../package.json'
-import type { RendererOptions, TresContext } from '../composables'
+import type { RendererOptions, TresContext, TresRenderer } from '../composables'
 import { useTresContextProvider } from '../composables'
 import { useTresContextProvider } from '../composables'
 import { extend } from '../core/catalogue'
 import { extend } from '../core/catalogue'
 import { nodeOps } from '../core/nodeOps'
 import { nodeOps } from '../core/nodeOps'
@@ -54,7 +57,7 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
 
 
 const emit = defineEmits<{
 const emit = defineEmits<{
   ready: [context: TresContext]
   ready: [context: TresContext]
-  render: [renderer: WebGLRenderer]
+  render: [renderer: TresRenderer]
   pointermissed: [event: PointerEvent<MouseEvent>]
   pointermissed: [event: PointerEvent<MouseEvent>]
 } & {
 } & {
   // all pointer events are supported because they bubble up
   // all pointer events are supported because they bubble up
@@ -128,8 +131,10 @@ const dispose = (context: TresContext, force = false) => {
   disposeObject3D(context.scene.value as unknown as TresObject)
   disposeObject3D(context.scene.value as unknown as TresObject)
   if (force) {
   if (force) {
     context.renderer.instance.dispose()
     context.renderer.instance.dispose()
-    context.renderer.instance.renderLists.dispose()
-    context.renderer.instance.forceContextLoss()
+    if (context.renderer.instance instanceof WebGLRenderer) {
+      context.renderer.instance.renderLists.dispose()
+      context.renderer.instance.forceContextLoss()
+    }
   }
   }
   (scene.value as TresScene).__tres = {
   (scene.value as TresScene).__tres = {
     root: context,
     root: context,

+ 59 - 27
src/composables/useRenderer/useRendererManager.ts

@@ -1,4 +1,5 @@
-import type { ColorRepresentation, ColorSpace, Object3D, Scene, ShadowMapType, ToneMapping } from 'three'
+import type { RendererLoop } from './../../core/loop'
+import type { ColorRepresentation, ColorSpace, Object3D, ShadowMapType, ToneMapping } from 'three'
 
 
 import type { TresContext } from '../useTresContextProvider'
 import type { TresContext } from '../useTresContextProvider'
 
 
@@ -8,12 +9,18 @@ import {
   useDevicePixelRatio,
   useDevicePixelRatio,
 } from '@vueuse/core'
 } from '@vueuse/core'
 import { Material, Mesh, WebGLRenderer } from 'three'
 import { Material, Mesh, WebGLRenderer } from 'three'
-import { computed, type MaybeRef, onUnmounted, type Reactive, readonly, ref, toValue, watch, watchEffect } from 'vue'
+import { computed, onUnmounted, ref, toValue, watch, watchEffect } from 'vue'
+import type { MaybeRef, ShallowRef } from 'vue'
+import type { Renderer } from 'three/webgpu'
 
 
 // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
 // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
 import { setPixelRatio } from '../../utils'
 import { setPixelRatio } from '../../utils'
 
 
 import { logWarning } from '../../utils/logger'
 import { logWarning } from '../../utils/logger'
+import type { SizesType } from '../useSizes'
+import type { UseCameraReturn } from '../useCamera'
+import type { TresScene } from '../../types'
+import { isFunction, isObject } from '../../utils/is'
 
 
 /**
 /**
  * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
  * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
@@ -22,6 +29,8 @@ import { logWarning } from '../../utils/logger'
  */
  */
 export type RenderMode = 'always' | 'on-demand' | 'manual'
 export type RenderMode = 'always' | 'on-demand' | 'manual'
 
 
+export type TresRenderer = WebGLRenderer | Renderer
+
 export interface RendererOptions {
 export interface RendererOptions {
   /**
   /**
    * WebGL Context options (Readonly because they are passed to the renderer constructor)
    * WebGL Context options (Readonly because they are passed to the renderer constructor)
@@ -172,11 +181,19 @@ export interface RendererOptions {
    * Custom WebGL renderer instance
    * Custom WebGL renderer instance
    * Allows using a pre-configured renderer instead of creating a new one
    * Allows using a pre-configured renderer instead of creating a new one
    */
    */
-  // renderer?: (ctx: TresRendererSetupContext) => Promise<TresRenderer> | TresRenderers
+  renderer?: (ctx: TresRendererSetupContext) => TresRenderer
+}
+
+export interface TresRendererSetupContext {
+  sizes: SizesType
+  scene: ShallowRef<TresScene>
+  camera: UseCameraReturn
+  loop: RendererLoop
+  canvas: MaybeRef<HTMLCanvasElement>
 }
 }
 
 
 export interface UseRendererOptions {
 export interface UseRendererOptions {
-  scene: Scene
+  scene: ShallowRef<TresScene>
   canvas: MaybeRef<HTMLCanvasElement>
   canvas: MaybeRef<HTMLCanvasElement>
   options: RendererOptions
   options: RendererOptions
   contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
   contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
@@ -188,25 +205,33 @@ export function useRendererManager(
     canvas,
     canvas,
     options,
     options,
     contextParts: { sizes, loop, camera },
     contextParts: { sizes, loop, camera },
-  }:
-  {
-    scene: Scene
-    canvas: MaybeRef<HTMLCanvasElement>
-    options: Reactive<RendererOptions>
-    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
-  },
+  }: UseRendererOptions,
 ) {
 ) {
-  const renderer = new WebGLRenderer({
-    ...options,
-    canvas: unrefElement(canvas),
-  })
+  const getRenderer = () => {
+    if (isFunction(options.renderer)) {
+      return options.renderer({
+        sizes,
+        scene,
+        camera,
+        loop,
+        canvas,
+      })
+    }
+
+    return new WebGLRenderer({
+      ...options,
+      canvas: unrefElement(canvas),
+    })
+  }
+
+  const renderer = getRenderer()
 
 
   const frames = ref(0)
   const frames = ref(0)
   const maxFrames = 60
   const maxFrames = 60
   const canBeInvalidated = computed(() => toValue(options.renderMode) === 'on-demand' && frames.value === 0)
   const canBeInvalidated = computed(() => toValue(options.renderMode) === 'on-demand' && frames.value === 0)
 
 
   const forceMaterialUpdate = () =>
   const forceMaterialUpdate = () =>
-    scene.traverse((child: Object3D) => {
+    scene.value.traverse((child: Object3D) => {
       if (child instanceof Mesh && child.material instanceof Material) {
       if (child instanceof Mesh && child.material instanceof Material) {
         child.material.needsUpdate = true
         child.material.needsUpdate = true
       }
       }
@@ -242,11 +267,24 @@ export function useRendererManager(
 
 
   const isModeAlways = computed(() => toValue(options.renderMode) === 'always')
   const isModeAlways = computed(() => toValue(options.renderMode) === 'always')
 
 
-  const renderEventHook = createEventHook<WebGLRenderer>()
+  const renderEventHook = createEventHook<TresRenderer>()
+
+  // be aware that the WebGLRenderer does not extend from Renderer
+  const isRenderer = (value: unknown): value is Renderer =>
+    isObject(value) && 'isRenderer' in value && Boolean(value.isRenderer)
+
+  const readyEventHook = createEventHook<TresRenderer>()
+  let hasTriggeredReady = false
+
+  if (isRenderer(renderer)) {
+    // Initialize the WebGPU context
+    renderer.init()
+    readyEventHook.trigger(renderer)
+  }
 
 
   loop.register(() => {
   loop.register(() => {
     if (camera.activeCamera.value && frames.value) {
     if (camera.activeCamera.value && frames.value) {
-      renderer.render(scene, camera.activeCamera.value)
+      renderer.render(scene.value, camera.activeCamera.value)
 
 
       renderEventHook.trigger(renderer)
       renderEventHook.trigger(renderer)
     }
     }
@@ -256,13 +294,6 @@ export function useRendererManager(
       : Math.max(0, frames.value - 1)
       : Math.max(0, frames.value - 1)
   }, 'render')
   }, 'render')
 
 
-  const isReady = computed(() =>
-    !!(renderer.domElement.width && renderer.domElement.height),
-  )
-
-  const readyEventHook = createEventHook<WebGLRenderer>()
-  let hasTriggeredReady = false
-
   // Watch the sizes and invalidate the renderer when they change
   // Watch the sizes and invalidate the renderer when they change
   watch([sizes.width, sizes.height], () => {
   watch([sizes.width, sizes.height], () => {
     renderer.setSize(sizes.width.value, sizes.height.value)
     renderer.setSize(sizes.width.value, sizes.height.value)
@@ -362,12 +393,13 @@ export function useRendererManager(
 
 
   onUnmounted(() => {
   onUnmounted(() => {
     renderer.dispose()
     renderer.dispose()
-    renderer.forceContextLoss()
+    if ('forceContextLoss' in renderer) {
+      renderer.forceContextLoss()
+    }
   })
   })
 
 
   return {
   return {
     instance: renderer,
     instance: renderer,
-    isReady: readonly(isReady),
     advance,
     advance,
     onRender: renderEventHook.on,
     onRender: renderEventHook.on,
     onReady: readyEventHook.on,
     onReady: readyEventHook.on,

+ 4 - 3
src/composables/useTres/index.ts

@@ -1,16 +1,17 @@
 import type { ComputedRef } from 'vue'
 import type { ComputedRef } from 'vue'
 import type { TresContext } from '../useTresContextProvider'
 import type { TresContext } from '../useTresContextProvider'
 import { useTresContext } from '../useTresContextProvider'
 import { useTresContext } from '../useTresContextProvider'
-import type { Camera, WebGLRenderer } from 'three'
+import type { Camera } from 'three'
+import type { TresRenderer } from '..'
 
 
 export interface TresPartialContext extends Omit<TresContext, 'renderer' | 'camera'> {
 export interface TresPartialContext extends Omit<TresContext, 'renderer' | 'camera'> {
   /**
   /**
    * The renderer instance
    * The renderer instance
    *
    *
-   * @type {WebGLRenderer}
+   * @type {TresRenderer}
    * @memberof TresPartialContext
    * @memberof TresPartialContext
    */
    */
-  renderer: WebGLRenderer
+  renderer: TresRenderer
   /**
   /**
    * The current active camera
    * The current active camera
    *
    *

+ 4 - 5
src/composables/useTresContextProvider/index.ts

@@ -12,7 +12,6 @@ import type { UseCameraReturn } from '../useCamera/'
 import { useCameraManager } from '../useCamera'
 import { useCameraManager } from '../useCamera'
 import { useRendererManager } from '../useRenderer/useRendererManager'
 import { useRendererManager } from '../useRenderer/useRendererManager'
 import useSizes, { type SizesType } from '../useSizes'
 import useSizes, { type SizesType } from '../useSizes'
-import type { TresCanvasProps } from '../../components/TresCanvas.vue'
 import { useEventManager } from '../useEventManager'
 import { useEventManager } from '../useEventManager'
 
 
 export interface TresContext {
 export interface TresContext {
@@ -35,9 +34,9 @@ export function useTresContextProvider({
   scene: TresScene
   scene: TresScene
   canvas: MaybeRef<HTMLCanvasElement>
   canvas: MaybeRef<HTMLCanvasElement>
   windowSize: MaybeRefOrGetter<boolean>
   windowSize: MaybeRefOrGetter<boolean>
-  rendererOptions: TresCanvasProps
+  rendererOptions: RendererOptions
 }): TresContext {
 }): TresContext {
-  const localScene = shallowRef<TresScene>(scene)
+  const localScene = shallowRef(scene)
   const sizes = useSizes(windowSize, canvas)
   const sizes = useSizes(windowSize, canvas)
 
 
   const camera = useCameraManager({ sizes })
   const camera = useCameraManager({ sizes })
@@ -46,9 +45,9 @@ export function useTresContextProvider({
 
 
   const renderer = useRendererManager(
   const renderer = useRendererManager(
     {
     {
-      scene,
+      scene: localScene,
       canvas,
       canvas,
-      options: rendererOptions as RendererOptions,
+      options: rendererOptions,
       contextParts: { sizes, camera, loop },
       contextParts: { sizes, camera, loop },
     },
     },
   )
   )

+ 12 - 8
src/devtools/inspectorHandlers.ts

@@ -271,7 +271,7 @@ export const inspectorStateHandler = (tres: TresContext, { highlightMesh, prevIn
       }
       }
 
 
       if (instance.isScene) {
       if (instance.isScene) {
-        payload.state = {
+        const sceneState = {
           ...payload.state,
           ...payload.state,
           state: [
           state: [
             {
             {
@@ -285,15 +285,19 @@ export const inspectorStateHandler = (tres: TresContext, { highlightMesh, prevIn
                 lines: tres.renderer.instance.info.render.lines,
                 lines: tres.renderer.instance.info.render.lines,
               },
               },
             },
             },
-            {
-              key: 'Programs',
-              value: tres.renderer.instance.info.programs?.map(program => ({
-                ...program,
-                programName: program.name,
-              })) || [],
-            },
           ],
           ],
         }
         }
+
+        if ('programs' in tres.renderer.instance.info) {
+          sceneState.state.push({
+            key: 'Programs',
+            value: tres.renderer.instance.info.programs?.map(program => ({
+              ...program,
+              programName: program.name,
+            })),
+          })
+        }
+        payload.state = sceneState
       }
       }
     }
     }
     else if (payload.nodeId.includes('context')) {
     else if (payload.nodeId.includes('context')) {