Skip to content

Commit

Permalink
feat: 140 on demand rendering (#497)
Browse files Browse the repository at this point in the history
* feat: conditional rendering

* chore: remove subscribe system

* feat: on-demand automatic invalidation with prop changes

* feat: invalidate once first when is `renderMode !== 'always'`

* docs: performance page, on-demand rendering

* chore: fix windowsize issue

* chore(lint): fix maximum line length issues

* feat: invalidate on-demand on window resize

* feat: add advance method for manual mode

* feat: fix manual first render with advance

* docs: performance manual mode

* docs: add badge with version

* chore: correct typos and PR suggestions

* chore: tell dont ask fix

* feat: render state instead of internal
  • Loading branch information
alvarosabu authored Jan 23, 2024
1 parent 83f0e06 commit f688c64
Show file tree
Hide file tree
Showing 20 changed files with 594 additions and 54 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 18 additions & 0 deletions docs/.vitepress/theme/components/BlenderCube.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<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>
101 changes: 101 additions & 0 deletions docs/.vitepress/theme/components/GraphPane.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<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
dark:bg-dark
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
dark:bg-gray-600
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 changes: 40 additions & 0 deletions docs/.vitepress/theme/components/OnDemandRendering.vue
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions docs/.vitepress/theme/components/RenderingLogger.vue
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions docs/.vitepress/theme/composables/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { reactive, toRefs } from 'vue'

const state = reactive({
renderingTimes: 0,
})
export function useState() {
return {
...toRefs(state),

}
}
131 changes: 131 additions & 0 deletions docs/advanced/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# 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 <Badge type="tip" text="^4.0.0" />

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>
```

#### Automatic Invalidation

When using `render-mode="on-demand"`, Tres.js will automatically invalidate the current frame by observing component props and lifecycle hooks like `onMounted` and `onUnmounted`. It will also invalidate the frame when resizing the window or changing any prop from the `<TresCanvas>` component like `clearColor` or `antialias`.

The code below updates TresMesh's position-x prop every second, triggering a new render.

```vue
<script setup>
import { ref } from 'vue'
const positionX = ref(0)
setTimeout(() => {
positionX.value = 1
}, 1000)
</script>
<template>
<TresCanvas render-mode="on-demand">
<TresMesh :position-x="positionX">
<TresBoxGeometry />
<TresMeshBasicMaterial color="teal" />
</TresMesh>
</TresCanvas>
</template>
```

#### Manual Invalidation

Since it is not really possible to observe all the possible changes in your application, you can also manually invalidate the frame by calling the `invalidate()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext):


::: code-group

```vue [App.vue]
<script setup>
import { TresCanvas } from '@tresjs/core'
import Scene from './Scene.vue'
</script>
<template>
<TresCanvas
render-mode="manual"
>
<Scene />
</TresCanvas>
</template>
```

```vue [Scene.vue]
<script setup>
import { useTres } from '@tresjs/core'
const boxRef = ref()
const { invalidate } = useTres()
watch(boxRef.value, () => {
boxRef.value.position.x = 1
invalidate()
})
</script>
<template>
<TresMesh ref="boxRef">
<TresBoxGeometry />
<TresMeshBasicMaterial color="teal" />
</TresMesh>
</template>
```

:::

### Mode `always`

In this rendering mode, Tres will continously render the scene on every frame. This is the default mode and the easiest to use, but it's also the most resource expensive one.


### Mode `manual`

If you want to have full control of when the scene is rendered, you can set the `render-mode` prop to `manual`:

```vue
<TresCanvas render-mode="manual">
<!-- Your scene goes here -->
</TresCanvas>
```

In this mode, Tres will not render the scene automatically. You will need to call the `advance()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext) to render the scene:

```vue
<script setup>
import { useTres } from '@tresjs/core'
const { advance } = useTres()
advance()
</script>
```

3 changes: 3 additions & 0 deletions docs/api/composables.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,7 @@ const context = useTresContext()
| **scene** | the [scene](https://threejs.org/docs/?q=sce#api/en/scenes/Scene). |
| **setCameraActive** | a method to set a camera active |
| **sizes** | contains width, height and aspect ratio of your canvas |
| **invalidate** | a method to invalidate the render loop. This is only required if you set the `render-mode` prop to `on-demand`. |
| **advance** | a method to advance the render loop. This is only required if you set the `render-mode` prop to `manual`. |


5 changes: 3 additions & 2 deletions docs/api/tres-canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
4 changes: 4 additions & 0 deletions docs/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"preview": "vitepress preview"
},
"dependencies": {
"@tresjs/core": "workspace:*"
"@tresjs/core": "workspace:^"
},
"devDependencies": {
"unocss": "^0.58.3",
Expand Down
Loading

0 comments on commit f688c64

Please sign in to comment.