瀏覽代碼

docs: performance page, on-demand rendering

alvarosabu 1 年之前
父節點
當前提交
d5efdf48c9

+ 1 - 0
docs/.vitepress/config.ts

@@ -81,6 +81,7 @@ export default defineConfig({
         items: [
           { text: 'Extending', link: '/advanced/extending' },
           { text: 'primitive', link: '/advanced/primitive' },
+          { text: 'Performance', link: '/advanced/performance' },
           {
             text: 'Caveats',
             link: '/advanced/caveats',

+ 17 - 0
docs/.vitepress/theme/components/BlenderCube.vue

@@ -0,0 +1,17 @@
+<script setup lang="ts">
+import { useTresContext } from '@tresjs/core'
+import { useGLTF } from '@tresjs/cientos'
+
+const { nodes } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
+const model = nodes.Cube
+
+model.position.set(0, 1, 0)
+
+const state = useTresContext()
+
+state.invalidate()
+</script>
+
+<template>
+  <primitive :object="model" />
+</template>

+ 87 - 0
docs/.vitepress/theme/components/GraphPane.vue

@@ -0,0 +1,87 @@
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { useRafFn } from '@vueuse/core'
+import { useState } from '../composables/state'
+
+const width = 160
+const height = 40
+const strokeWidth = 2
+const updateInterval = 100 // Update interval in milliseconds
+const topOffset = 0 // Offset from the top
+
+const points = ref('')
+const frameTimes = ref([])
+const maxFrames = ref(width / strokeWidth)
+
+let lastUpdateTime = performance.now()
+
+const { renderingTimes } = useState()
+
+useRafFn(({ timestamp }) => {
+  if (timestamp - lastUpdateTime >= updateInterval) {
+    lastUpdateTime = timestamp
+
+    frameTimes.value.push(renderingTimes?.value)
+    renderingTimes.value = 0
+
+    if (frameTimes.value.length > maxFrames.value) {
+      frameTimes.value.shift()
+    }
+
+    points.value = frameTimes.value
+      .map(
+        (value, index) =>
+          `${index * strokeWidth},${
+            height + topOffset - strokeWidth / 2 - (value * (height + topOffset - strokeWidth)) / 2
+          }`,
+      )
+      .join(' ')
+  }
+})
+</script>
+
+<template>
+  <div
+    class="absolute right-2 top-2 flex px-4 py-1 justify-between gap-4 items-center mb-2 z-10 bg-white shadow-xl
+      rounded 
+      border-4 
+      border-solid 
+      bg-primary 
+      border-primary 
+      pointer-events-none
+      overflow-hidden"
+  >
+    <label class="text-secondary text-xs w-1/3">Rendering Activity</label>
+
+    <div
+      class="
+        bg-gray-100
+        relative
+        w-2/3
+        p-1
+        rounded
+        text-right
+        text-xs
+        focus:border-gray-200
+        outline-none
+        border-none
+        font-sans
+      "
+    >
+      <svg
+        :width="width"
+        :height="height"
+        xmlns="http://www.w3.org/2000/svg"
+        fill="none"
+      >
+        <polyline
+          :points="points"
+          stroke="lightgray"
+          :stroke-width="strokeWidth"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+        />
+      </svg>
+    </div>
+  </div>
+</template>

+ 40 - 0
docs/.vitepress/theme/components/OnDemandRendering.vue

@@ -0,0 +1,40 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three'
+import { OrbitControls } from '@tresjs/cientos'
+import { useState } from '../composables/state'
+import BlenderCube from './BlenderCube.vue'
+import GraphPane from './GraphPane.vue'
+import RenderingLogger from './RenderingLogger.vue'
+
+const { renderingTimes } = useState()
+
+function onRender() {
+  renderingTimes.value = 1
+
+}
+</script>
+
+<template>
+  <GraphPane />
+  <TresCanvas
+    render-mode="on-demand"
+    clear-color="#82DBC5"
+    @render="onRender"
+  >
+    <TresPerspectiveCamera
+      :position="[5, 5, 5]"
+      :look-at="[0, 0, 0]"
+    />
+    <Suspense>
+      <BlenderCube />
+    </Suspense>
+    <TresGridHelper />
+    <RenderingLogger />
+    <TresAmbientLight :intensity="1" />
+    <TresDirectionalLight
+      :position="[0, 8, 4]"
+      :intensity="0.7"
+    />
+  </TresCanvas>
+</template>

+ 24 - 0
docs/.vitepress/theme/components/RenderingLogger.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import { useRenderLoop, useTresContext } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+import { onMounted } from 'vue'
+import { useState } from '../composables/state'
+
+const { renderingTimes } = useState()
+
+const state = useTresContext()
+
+function manualInvalidate() {
+  state.invalidate()
+}
+
+onMounted(() => {
+  manualInvalidate()
+})
+</script>
+
+<template>
+  <OrbitControls
+    @change="manualInvalidate"
+  />
+</template>

+ 11 - 0
docs/.vitepress/theme/composables/state.ts

@@ -0,0 +1,11 @@
+import { reactive, toRefs } from 'vue'
+
+const state = reactive({
+  renderingTimes: 0,
+})
+export function useState() {
+  return {
+    ...toRefs(state),
+    
+  }
+}

+ 34 - 0
docs/advanced/performance.md

@@ -0,0 +1,34 @@
+# Scaling performance 🚀
+
+> Quick guide with tips to improve performance of your Tres.js application.
+
+We are running WebGL on the browser, which can be quite expensive and it will depend on how powerful the user's device is. To make 3D accessible to everyone, we need to make sure our applications are optimized to run also on low-end devices. This guide will provide some tips to improve the performance of your Tres.js application.
+
+## On-demand rendering
+
+By default, Tres.js will render your scene on every frame. This is great for most applications, but if you are building a game or a complex application, you might want to control when the scene is rendered. 
+
+Otherwise it might drain your device battery 🔋 🔜 🪫 and make your computer sound like an airplane 🛫.
+
+Ideally, you only want to **render the scene when necessary**, for example when the user interacts with the scene and the camera moves, or when objects in the scene are animated.
+
+You can do that by setting the `renderMode` prop to `on-demand` or `manual`:
+
+
+### Mode `on-demand`
+
+<ClientOnly>
+  <div style="position: relative; aspect-ratio: 16/9; height: auto; margin: 2rem 0; border-radius: 8px; overflow:hidden;">
+    <onDemandRendering />
+  </div>
+</ClientOnly>
+
+
+```vue
+<TresCanvas render-mode="on-demand">
+  <!-- Your scene goes here -->
+</TresCanvas>
+```
+
+
+

+ 3 - 2
docs/api/tres-canvas.md

@@ -77,12 +77,13 @@ renderer.shadowMap.type = PCFSoftShadowMap
 | **clearColor** | The color the renderer will use to clear the canvas. | `#000000` |
 | **context** | This can be used to attach the renderer to an existing [RenderingContext](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) | |
 | **depth** | Whether the drawing buffer has a [depth buffer](https://en.wikipedia.org/wiki/Z-buffering) of at least 16 bits. | `true` |
+| **renderMode** | Render mode, can be `always`, `on-demand` or `manual`. See [Performance](../advanced/performance)  | `always` |
 | **disableRender** | Disable render on requestAnimationFrame, useful for PostProcessing | `false` |
 | **failIfMajorPerformanceCaveat** | Whether the renderer creation will fail upon low performance is detected. See [WebGL spec](https://registry.khronos.org/webgl/specs/latest/1.0/#5.2) for details. | `false` |
 | **logarithmicDepthBuffer** | Whether to use a logarithmic depth buffer. It may be necessary to use this if dealing with huge differences in scale in a single scene. Note that this setting uses gl_FragDepth if available which disables the [Early Fragment Test](https://www.khronos.org/opengl/wiki/Early_Fragment_Test) optimization and can cause a decrease in performance. | `false` |
 | **outputColorSpace** | Defines the output encoding | `LinearEncoding` |
-| **powerPreference** | Provides a hint to the user agent indicating what configuration of GPU is suitable for this WebGL context. Can be "high-performance", "low-power" or "default". | `default` |
-| **precision** | Shader precision. Can be "highp", "mediump" or "lowp". | "highp" if supported by the device |
+| **powerPreference** | Provides a hint to the user agent indicating what configuration of GPU is suitable for this WebGL context. Can be `high-performance`, `low-power` or `default`. | `default` |
+| **precision** | Shader precision. Can be `highp`, `mediump` or `lowp`. | "highp" if supported by the device |
 | **premultipliedAlpha** | Whether the renderer will assume that colors have [premultiplied alpha](https://en.wikipedia.org/wiki/Glossary_of_computer_graphics#premultiplied_alpha). | `true` |
 | **preserveDrawingBuffer** | Whether to preserve the buffers until manually cleared or overwritten.. | `false` |
 | **shadows** | Enable shadows in the renderer | `false` |

+ 4 - 0
docs/components.d.ts

@@ -7,14 +7,18 @@ export {}
 
 declare module 'vue' {
   export interface GlobalComponents {
+    BlenderCube: typeof import('./.vitepress/theme/components/BlenderCube.vue')['default']
     DonutExample: typeof import('./.vitepress/theme/components/DonutExample.vue')['default']
     EmbedExperiment: typeof import('./.vitepress/theme/components/EmbedExperiment.vue')['default']
     ExtendExample: typeof import('./.vitepress/theme/components/ExtendExample.vue')['default']
     FirstScene: typeof import('./.vitepress/theme/components/FirstScene.vue')['default']
     FirstSceneLightToon: typeof import('./.vitepress/theme/components/FirstSceneLightToon.vue')['default']
+    GraphPane: typeof import('./.vitepress/theme/components/GraphPane.vue')['default']
     HomeSponsors: typeof import('./.vitepress/theme/components/HomeSponsors.vue')['default']
     LocalOrbitControls: typeof import('./.vitepress/theme/components/LocalOrbitControls.vue')['default']
     LoveVueThreeJS: typeof import('./.vitepress/theme/components/LoveVueThreeJS.vue')['default']
+    OnDemandRendering: typeof import('./.vitepress/theme/components/OnDemandRendering.vue')['default']
+    RenderingLogger: typeof import('./.vitepress/theme/components/RenderingLogger.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SandboxDemo: typeof import('./.vitepress/theme/components/SandboxDemo.vue')['default']

+ 1 - 1
docs/package.json

@@ -9,7 +9,7 @@
     "preview": "vitepress preview"
   },
   "dependencies": {
-    "@tresjs/core": "workspace:*"
+    "@tresjs/core": "workspace:^"
   },
   "devDependencies": {
     "unocss": "^0.58.3",

+ 5 - 0
playground/src/pages/rendering-modes/index.vue

@@ -4,12 +4,17 @@ import { TresCanvas } from '@tresjs/core'
 import Scene from './scene.vue'
 
 const clearColor = ref('#82DBC5')
+
+setTimeout(() => {
+  clearColor.value = '#000000'
+}, 3000)
 </script>
 
 <template>
   <TresCanvas
     :clear-color="clearColor"
     render-mode="on-demand"
+    @render="() => console.log('onRender')"
   >
     <Scene />
   </TresCanvas>

+ 1 - 1
playground/src/pages/rendering-modes/scene.vue

@@ -13,7 +13,7 @@ const showMesh = ref(true)
 
 setTimeout(() => {
   /*  positionX.value = 1 */
-  showMesh.value = false
+/*   showMesh.value = false */
 }, 3000)
 
 /* invalidate() */

+ 1 - 1
pnpm-lock.yaml

@@ -130,7 +130,7 @@ importers:
   docs:
     dependencies:
       '@tresjs/core':
-        specifier: workspace:*
+        specifier: workspace:^
         version: link:..
     devDependencies:
       unocss:

+ 3 - 1
src/components/TresCanvas.vue

@@ -69,6 +69,8 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
   renderMode: 'always',
 })
 
+const emit = defineEmits(['render'])
+
 const { logWarning } = useLogger()
 
 const canvas = ref<HTMLCanvasElement>()
@@ -124,7 +126,6 @@ const disableRender = computed(() => props.disableRender)
 const context = shallowRef<TresContext | null>(null)
 
 defineExpose({ context, dispose: () => dispose(context.value as TresContext, true) })
-
 onMounted(() => {
   const existingCanvas = canvas as Ref<HTMLCanvasElement>
 
@@ -134,6 +135,7 @@ onMounted(() => {
     windowSize: props.windowSize ?? true,
     disableRender: disableRender.value ?? false,
     rendererOptions: props,
+    emit,
   })
 
   usePointerEventHandler({ scene: scene.value, contextParts: context.value })

+ 21 - 6
src/composables/useRenderer/index.ts

@@ -98,7 +98,6 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
   preset?: MaybeRefOrGetter<RendererPresetsType>
   renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
 }
-
 /**
  * Reactive three.js WebGLRenderer instance
  *
@@ -111,6 +110,7 @@ export function useRenderer(
     canvas,
     options,
     disableRender,
+    emit,
     contextParts: { sizes, camera, internal, invalidate },
   }:
   {
@@ -146,12 +146,22 @@ export function useRenderer(
   watch(webGLRendererConstructorParameters, () => {
     renderer.value.dispose()
     renderer.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
+
+    if (options.renderMode === 'on-demand') {
+      invalidate()
+    }
   })
 
   watchEffect(() => {
     renderer.value.setSize(sizes.width.value, sizes.height.value)
   })
 
+  watch(() => options.clearColor, () => {
+    if (options.renderMode === 'on-demand') {
+      invalidate()
+    }
+  })
+
   const { pixelRatio } = useDevicePixelRatio()
 
   watchEffect(() => {
@@ -165,8 +175,10 @@ export function useRenderer(
   const { resume, onLoop } = useRenderLoop()
 
   onLoop(() => {
-    if (camera.value && !toValue(disableRender) && internal.frames.value > 0)
+    if (camera.value && !toValue(disableRender) && internal.frames.value > 0) {
       renderer.value.render(scene, camera.value)
+      emit('render', renderer.value)
+    }
 
     // Reset priority
     internal.priority.value = 0
@@ -201,6 +213,13 @@ export function useRenderer(
 
   const threeDefaults = getThreeRendererDefaults()
 
+  const renderMode = toValue(options.renderMode)
+
+  if (renderMode !== 'always') { 
+    // Invalidate for the first time
+    invalidate()
+  }
+
   watchEffect(() => {
     const rendererPreset = toValue(options.preset)
 
@@ -219,10 +238,6 @@ export function useRenderer(
       // If the render mode is 'always', ensure there's always a frame pending
       internal.frames.value = Math.max(1, internal.frames.value)
     }
-    else {
-      // Invalidate for the first time
-      invalidate()
-    }
 
     const getValue = <T>(option: MaybeRefOrGetter<T>, pathInThree: string): T | undefined => {
       const value = toValue(option)

+ 3 - 0
src/composables/useTresContextProvider/index.ts

@@ -52,12 +52,14 @@ export function useTresContextProvider({
   windowSize,
   disableRender,
   rendererOptions,
+  emit,
 }: {
   scene: Scene
   canvas: MaybeRef<HTMLCanvasElement>
   windowSize: MaybeRefOrGetter<boolean>
   disableRender: MaybeRefOrGetter<boolean>
   rendererOptions: UseRendererOptions
+  emit: (event: string, ...args: any[]) => void
 }): TresContext {
 
   const elementSize = computed(() =>
@@ -97,6 +99,7 @@ export function useTresContextProvider({
       scene,
       canvas,
       options: rendererOptions,
+      emit,
       contextParts: { sizes, camera, internal, invalidate },
       disableRender,
     })