From 6369197e606d3a5925474d52d9afd4d104dd2e55 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Sat, 13 Apr 2024 14:52:17 +0200 Subject: [PATCH 01/50] feat: createRenderLoop unique to context --- playground/src/components/TheSphere.vue | 24 +++++- playground/src/pages/basic/index.vue | 11 +-- playground/src/pages/perf/OnDemand.vue | 1 + src/components/TresCanvas.vue | 8 +- src/composables/index.ts | 1 + src/composables/useLoop/index.ts | 7 ++ src/composables/useRenderLoop/index.ts | 21 +++-- src/composables/useRenderer/index.ts | 6 +- .../useTresContextProvider/index.ts | 29 +++++++ src/core/loop.ts | 80 +++++++++++++++++++ 10 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 src/composables/useLoop/index.ts create mode 100644 src/core/loop.ts diff --git a/playground/src/components/TheSphere.vue b/playground/src/components/TheSphere.vue index 9c1761fcc..b8690195a 100644 --- a/playground/src/components/TheSphere.vue +++ b/playground/src/components/TheSphere.vue @@ -1,7 +1,29 @@ - + + diff --git a/src/components/TresCanvas.vue b/src/components/TresCanvas.vue index b7b576bfd..a63f4c929 100644 --- a/src/components/TresCanvas.vue +++ b/src/components/TresCanvas.vue @@ -28,7 +28,7 @@ import { type TresContext, useLogger, usePointerEventHandler, - useRenderLoop, + /* useRenderLoop, */ useTresContextProvider, } from '../composables' import { extend } from '../core/catalogue' @@ -88,7 +88,7 @@ const canvas = ref() */ const scene = shallowRef(new Scene()) -const { resume } = useRenderLoop() +/* const { resume } = useRenderLoop() */ const instance = getCurrentInstance()?.appContext.app extend(THREE) @@ -124,7 +124,7 @@ const dispose = (context: TresContext, force = false) => { context.renderer.value.forceContextLoss() } mountCustomRenderer(context) - resume() +/* resume() */ } const disableRender = computed(() => props.disableRender) @@ -144,6 +144,8 @@ onMounted(() => { emit, }) + instance?.provide('useTres', context) + usePointerEventHandler(context.value) const { registerCamera, camera, cameras, deregisterCamera } = context.value diff --git a/src/composables/index.ts b/src/composables/index.ts index 0551ac1e2..6c5c862bf 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -8,3 +8,4 @@ export * from './useLogger' export * from './useSeek' export * from './usePointerEventHandler' export * from './useTresContextProvider' +export * from './useLoop/' diff --git a/src/composables/useLoop/index.ts b/src/composables/useLoop/index.ts new file mode 100644 index 000000000..6b00176f0 --- /dev/null +++ b/src/composables/useLoop/index.ts @@ -0,0 +1,7 @@ +import { useTresContext } from '../useTresContextProvider' + +export function useLoop(cb) { + const ctx = useTresContext() + + return ctx.loop.onLoop(cb) +} diff --git a/src/composables/useRenderLoop/index.ts b/src/composables/useRenderLoop/index.ts index bf1a4ef21..b44629d42 100644 --- a/src/composables/useRenderLoop/index.ts +++ b/src/composables/useRenderLoop/index.ts @@ -2,6 +2,7 @@ import type { EventHookOn, Fn } from '@vueuse/core' import { createEventHook, useRafFn } from '@vueuse/core' import type { Ref } from 'vue' import { Clock } from 'three' +import { useLogger } from '../useLogger' export interface RenderLoop { delta: number @@ -40,11 +41,15 @@ onAfterLoop.on(() => { elapsed = clock.getElapsedTime() }) -export const useRenderLoop = (): UseRenderLoopReturn => ({ - onBeforeLoop: onBeforeLoop.on, - onLoop: onLoop.on, - onAfterLoop: onAfterLoop.on, - pause, - resume, - isActive, -}) +export const useRenderLoop = (): UseRenderLoopReturn => { + const { logError } = useLogger() + logError('useRenderLoop is deprecated in v4, use `useLoop` instead. Check the migration guide for more information.') + return { + onBeforeLoop: onBeforeLoop.on, + onLoop: onLoop.on, + onAfterLoop: onAfterLoop.on, + pause, + resume, + isActive, + } +} diff --git a/src/composables/useRenderer/index.ts b/src/composables/useRenderer/index.ts index 2abc2f2ba..49b228f9f 100644 --- a/src/composables/useRenderer/index.ts +++ b/src/composables/useRenderer/index.ts @@ -163,7 +163,7 @@ export function useRenderer( // TheLoop - const { resume, onLoop } = useRenderLoop() + /* const { resume, onLoop } = useRenderLoop() onLoop(() => { if (camera.value && !toValue(disableRender) && render.frames.value > 0) { @@ -182,7 +182,7 @@ export function useRenderer( } }) - resume() + resume() */ const getThreeRendererDefaults = () => { const plainRenderer = new WebGLRenderer() @@ -279,7 +279,7 @@ export function useRenderer( renderer.value.forceContextLoss() }) - if (import.meta.hot) { import.meta.hot.on('vite:afterUpdate', resume) } + /* if (import.meta.hot) { import.meta.hot.on('vite:afterUpdate', resume) } */ return { renderer, diff --git a/src/composables/useTresContextProvider/index.ts b/src/composables/useTresContextProvider/index.ts index 4d1e12ca1..e080e2cd4 100644 --- a/src/composables/useTresContextProvider/index.ts +++ b/src/composables/useTresContextProvider/index.ts @@ -12,6 +12,8 @@ import { useLogger } from '../useLogger' import type { TresScene } from '../../types' import type { EventProps } from '../usePointerEventHandler' import useSizes, { type SizesType } from '../useSizes' +import type { RendererLoop } from '../../core/loop' +import { createRenderLoop } from '../../core/loop' export interface InternalState { priority: Ref @@ -56,6 +58,8 @@ export interface TresContext { raycaster: ShallowRef perf: PerformanceState render: RenderState + // Loop + loop: RendererLoop /** * Invalidates the current frame when renderMode === 'on-demand' */ @@ -173,6 +177,7 @@ export function useTresContextProvider({ registerCamera, setCameraActive, deregisterCamera, + loop: createRenderLoop(), } provide('useTres', ctx) @@ -182,6 +187,30 @@ export function useTresContextProvider({ root: ctx, } + // The loop + + ctx.loop.onLoop(() => { + if (camera.value && render.frames.value > 0) { + renderer.value.render(scene, camera.value) + emit('render', ctx.renderer.value) + } + + // Reset priority + render.priority.value = 0 + + if (render.mode.value === 'always') { + render.frames.value = 1 + } + else { + render.frames.value = Math.max(0, render.frames.value - 1) + } + }) + ctx.loop.start() + + onUnmounted(() => { + ctx.loop.stop() + }) + // Performance const updateInterval = 100 // Update interval in milliseconds const fps = useFps({ every: updateInterval }) diff --git a/src/core/loop.ts b/src/core/loop.ts new file mode 100644 index 000000000..25824742a --- /dev/null +++ b/src/core/loop.ts @@ -0,0 +1,80 @@ +import type { Ref } from 'vue' +import { ref } from 'vue' +import { Clock, MathUtils } from 'three' +import type { Fn } from '@vueuse/core' + +export interface RendererLoop { + subscribers: Map + loopId: string + onLoop: (callback: Fn) => void + start: () => void + stop: () => void + isActive: Ref +} + +export function createRenderLoop(): RendererLoop { + const clock = new Clock(false) + const isActive = ref(false) + + let animationFrameId: number + const loopId = MathUtils.generateUUID() + + const subscribers = new Map() + + type LoopCallback = (params: { + delta: number + elapsed: number + clock: Clock + }) => void + + function registerCallback(callback: LoopCallback, index = 0) { + if (!subscribers.has(index)) { + subscribers.set(index, []) + } + subscribers.get(index).push(callback) + } + + function start() { + if (!isActive.value) { + clock.start() + isActive.value = true + loop() + } + } + + function stop() { + if (isActive.value) { + clock.stop() + cancelAnimationFrame(animationFrameId) + isActive.value = false + } + } + + function loop() { + const delta = clock.getDelta() + const elapsed = clock.getElapsedTime() + /* console.log('loop', animationFrameId) */ + + // Sort and execute callbacks based on index + Array.from(subscribers.keys()) + .sort((a, b) => a - b) // Ensure numerical order + .forEach((index) => { + subscribers.get(index).forEach((callback: LoopCallback) => { + callback({ delta, elapsed, clock }) + }) + }) + + if (isActive.value) { + animationFrameId = requestAnimationFrame(loop) + } + } + + return { + subscribers, + loopId, + start, + stop, + onLoop: (callback: LoopCallback, index = 0) => registerCallback(callback, index), + isActive, + } +} From 23be834bb234985746ede3f8384f6859338a2b9c Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Sat, 13 Apr 2024 15:44:02 +0200 Subject: [PATCH 02/50] feat: onLoop returns current state --- docs/api/composables.md | 201 +++++++++++++++--------- playground/src/components/TheSphere.vue | 8 +- src/composables/useLoop/index.ts | 9 +- 3 files changed, 137 insertions(+), 81 deletions(-) diff --git a/docs/api/composables.md b/docs/api/composables.md index f86551be3..5b16df570 100644 --- a/docs/api/composables.md +++ b/docs/api/composables.md @@ -4,75 +4,101 @@ Vue 3 [Composition API](https://vuejs.org/guide/extras/composition-api-faq.html# **TresJS** takes huge advantage of this API to create a set of composable functions that can be used to create animations, interact with the scene and more. It also allows you to create more complex scenes that might not be possible using just the Vue Components (Textures, Loaders, etc.). -The core of **TresJS** uses these composables internally, so you would be using the same API that the core uses. For instance, components that need to updated on the internal render loop use the `useRenderLoop` composable to register a callback that will be called every time the renderer updates the scene. +The core of **TresJS** uses these composables internally, so you would be using the same API that the core uses. -## useRenderLoop - -The `useRenderLoop` composable is the core of **TresJS** animations. It allows you to register a callback that will be called on native refresh rate. This is the most important composable in **TresJS**. +## useTresContext +This composable aims to provide access to the state model which contains multiple useful properties. ```ts -const { onLoop, resume } = useRenderLoop() - -onLoop(({ delta, elapsed, clock }) => { - // I will run at every frame ~60FPS (depending of your monitor) -}) +const { camera, renderer, camera, cameras } = useTresContext() ``` ::: warning -Be mindful of the performance implications of using this composable. It will run at every frame, so if you have a lot of logic in your callback, it might impact the performance of your app. Specially if you are updating reactive states or references. +`useTresContext` can be only be used inside of a `TresCanvas` since this component acts as the provider for the context data. ::: -The `onLoop` callback receives an object with the following properties based on the [THREE clock](https://threejs.org/docs/?q=clock#api/en/core/Clock): - -- `delta`: The delta time between the current and the last frame. This is the time in seconds since the last frame. -- `elapsed`: The elapsed time since the start of the render loop. - -This composable is based on `useRafFn` from [vueuse](https://vueuse.org/core/useRafFn/). Thanks to [@wheatjs](https://github.com/wheatjs) for the amazing contribution. +```vue + + + +``` -### Before and after render +```vue +// MyModel.vue -You can also register a callback that will be called before and after the renderer updates the scene. This is useful if you add a profiler to measure the FPS for example. + +``` -onBeforeLoop(({ delta, elapsed }) => { - // I will run before the renderer updates the scene - fps.begin() -}) +### Properties of context +| Property | Description | +| --- | --- | +| **camera** | The currently active camera | +| **cameras** | The cameras that exist in the scene | +| **controls** | The controls of your scene | +| **deregisterCamera** | A method to deregister a camera. This is only required if you manually create a camera. Cameras in the template are deregistered automatically. | +| **extend** | Extends the component catalogue. See [extending](/advanced/extending) | +| **raycaster** | the global raycaster used for pointer events | +| **registerCamera** | a method to register a camera. This is only required if you manually create a camera. Cameras in the template are registered automatically. | +| **renderer** | the [WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) of your scene | +| **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`. | +| **loop** | the renderer loop | -onAfterLoop(({ delta, elapsed }) => { - // I will run after the renderer updates the scene - fps.end() -}) -``` +### useLoop -### Pause and resume +This composable allows you to execute a callback on every rendered frame, similar to `useRenderLoop` but unique to each `TresCanvas` instance. However, this composable can only be used inside a `TresCanvas` component since it relies on the context provided by `TresCanvas`. -You can pause and resume the render loop using the exposed `pause` and `resume` methods. +::: warning +`useLoop` can be only be used inside of a `TresCanvas` since this component acts as the provider for the context data. +::: -```ts -const { pause, resume } = useRenderLoop() +::: code-group -// Pause the render loop -pause() +```vue [App.vue] + -// Resume the render loop -resume() + ``` -Also you can get the active state of the render loop using the `isActive` property. +```vue [AnimatedBox.vue] + -console.log(isActive) // true + ``` +::: + +Your callback function will be triggered just before a frame is rendered and it will be unmounted automatically when the component is destroyed. + ## useLoader The `useLoader` composable allows you to load assets using the [THREE.js loaders](https://threejs.org/docs/#manual/en/introduction/Loading-3D-models). It returns a promise with loaded asset. @@ -197,46 +223,73 @@ watch(character, ({ model }) => { }) ``` -## useTresContext -This composable aims to provide access to the state model which contains multiple useful properties. +## useRenderLoop + +::: warning +This composable will be deprecated on V5. Use `useLoop` instead. [Read why](#useloop) +::: + +The `useRenderLoop` composable is the core of **TresJS** animations. It allows you to register a callback that will be called on native refresh rate. ```ts -const { camera, renderer, camera, cameras } = useTresContext() +const { onLoop, resume } = useRenderLoop() + +onLoop(({ delta, elapsed, clock }) => { + // I will run at every frame ~60FPS (depending of your monitor) +}) ``` ::: warning -`useTresContext` can be only be used inside of a `TresCanvas` since `TresCanvas` acts as the provider for the context data. Use [the context exposed by TresCanvas](tres-canvas#exposed-public-properties) if you find yourself needing it in parent components of TresCanvas. +Be mindful of the performance implications of using this composable. It will run at every frame, so if you have a lot of logic in your callback, it might impact the performance of your app. Specially if you are updating reactive states or references. ::: -```vue - - - +The `onLoop` callback receives an object with the following properties based on the [THREE clock](https://threejs.org/docs/?q=clock#api/en/core/Clock): + +- `delta`: The delta time between the current and the last frame. This is the time in seconds since the last frame. +- `elapsed`: The elapsed time since the start of the render loop. + +This composable is based on `useRafFn` from [vueuse](https://vueuse.org/core/useRafFn/). Thanks to [@wheatjs](https://github.com/wheatjs) for the amazing contribution. + +### Before and after render + +You can also register a callback that will be called before and after the renderer updates the scene. This is useful if you add a profiler to measure the FPS for example. + +```ts +const { onBeforeLoop, onAfterLoop } = useRenderLoop() + +onBeforeLoop(({ delta, elapsed }) => { + // I will run before the renderer updates the scene + fps.begin() +}) + +onAfterLoop(({ delta, elapsed }) => { + // I will run after the renderer updates the scene + fps.end() +}) ``` -```vue -// MyModel.vue +### Pause and resume - +```ts +const { pause, resume } = useRenderLoop() + +// Pause the render loop +pause() + +// Resume the render loop +resume() ``` -### Properties of context -| Property | Description | -| --- | --- | -| **camera** | The currently active camera | -| **cameras** | The cameras that exist in the scene | -| **controls** | The controls of your scene | -| **deregisterCamera** | A method to deregister a camera. This is only required if you manually create a camera. Cameras in the template are deregistered automatically. | -| **extend** | Extends the component catalogue. See [extending](/advanced/extending) | -| **raycaster** | the global raycaster used for pointer events | -| **registerCamera** | a method to register a camera. This is only required if you manually create a camera. Cameras in the template are registered automatically. | -| **renderer** | the [WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) of your scene | -| **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`. | +Also you can get the active state of the render loop using the `isActive` property. + +```ts +const { resume, isActive } = useRenderLoop() + +console.log(isActive) // false + +resume() + +console.log(isActive) // true +``` diff --git a/playground/src/components/TheSphere.vue b/playground/src/components/TheSphere.vue index b8690195a..6eac68ed4 100644 --- a/playground/src/components/TheSphere.vue +++ b/playground/src/components/TheSphere.vue @@ -1,6 +1,6 @@ diff --git a/src/composables/useLoop/index.ts b/src/composables/useLoop/index.ts index 6b00176f0..7babaa4ad 100644 --- a/src/composables/useLoop/index.ts +++ b/src/composables/useLoop/index.ts @@ -1,7 +1,10 @@ +import type { Fn } from '@vueuse/core' import { useTresContext } from '../useTresContextProvider' -export function useLoop(cb) { +export function useLoop(cb: (arg0: any) => void) { const ctx = useTresContext() - - return ctx.loop.onLoop(cb) + const wrappedCallback = (params: any) => { + cb({ ...params, ctx }) + } + return ctx.loop.onLoop(wrappedCallback as Fn) } From d01fdab84efb73ea7062a4899cec5513eb544cf6 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Sat, 13 Apr 2024 16:13:59 +0200 Subject: [PATCH 03/50] feat: ensuring callback excecution with index order --- playground/src/components/TheSphere.vue | 6 +++--- src/composables/useLoop/index.ts | 4 ++-- src/composables/useTresContextProvider/index.ts | 3 ++- src/core/loop.ts | 4 +++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/playground/src/components/TheSphere.vue b/playground/src/components/TheSphere.vue index 6eac68ed4..d2544609c 100644 --- a/playground/src/components/TheSphere.vue +++ b/playground/src/components/TheSphere.vue @@ -6,17 +6,17 @@ import { useLoop } from '@tresjs/core' const sphereRef = ref() -/* useLoop(() => { +useLoop(() => { console.log('before renderer') }, -1) useLoop(() => { console.log('after renderer') -}, 1) */ +}, 2) useLoop((state) => { if (!sphereRef.value) { return } - console.log('state', state) + console.log('this should be before render', state) sphereRef.value.position.y += Math.sin(state.elapsed) * 0.01 }) diff --git a/src/composables/useLoop/index.ts b/src/composables/useLoop/index.ts index 7babaa4ad..983805910 100644 --- a/src/composables/useLoop/index.ts +++ b/src/composables/useLoop/index.ts @@ -1,10 +1,10 @@ import type { Fn } from '@vueuse/core' import { useTresContext } from '../useTresContextProvider' -export function useLoop(cb: (arg0: any) => void) { +export function useLoop(cb: (arg0: any) => void, index = 0) { const ctx = useTresContext() const wrappedCallback = (params: any) => { cb({ ...params, ctx }) } - return ctx.loop.onLoop(wrappedCallback as Fn) + return ctx.loop.onLoop(wrappedCallback as Fn, index) } diff --git a/src/composables/useTresContextProvider/index.ts b/src/composables/useTresContextProvider/index.ts index e080e2cd4..47e7c6862 100644 --- a/src/composables/useTresContextProvider/index.ts +++ b/src/composables/useTresContextProvider/index.ts @@ -190,6 +190,7 @@ export function useTresContextProvider({ // The loop ctx.loop.onLoop(() => { + console.log('the render 1') if (camera.value && render.frames.value > 0) { renderer.value.render(scene, camera.value) emit('render', ctx.renderer.value) @@ -204,7 +205,7 @@ export function useTresContextProvider({ else { render.frames.value = Math.max(0, render.frames.value - 1) } - }) + }, 1) ctx.loop.start() onUnmounted(() => { diff --git a/src/core/loop.ts b/src/core/loop.ts index 25824742a..1acd13cea 100644 --- a/src/core/loop.ts +++ b/src/core/loop.ts @@ -25,9 +25,10 @@ export function createRenderLoop(): RendererLoop { delta: number elapsed: number clock: Clock - }) => void + }, index: number) => void function registerCallback(callback: LoopCallback, index = 0) { + console.log('registerCallback', { index, callback }) if (!subscribers.has(index)) { subscribers.set(index, []) } @@ -59,6 +60,7 @@ export function createRenderLoop(): RendererLoop { Array.from(subscribers.keys()) .sort((a, b) => a - b) // Ensure numerical order .forEach((index) => { + console.log('Processing index:', index) // Debug: Check order of processing subscribers.get(index).forEach((callback: LoopCallback) => { callback({ delta, elapsed, clock }) }) From 2ff1c15910dc2b306cf06ea2b1ef6a7142742198 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Sat, 13 Apr 2024 16:33:50 +0200 Subject: [PATCH 04/50] feat: take control of render loop logic --- playground/src/components/TheSphere.vue | 18 ++++++++--- src/composables/useRenderer/index.ts | 31 +------------------ .../useTresContextProvider/index.ts | 1 - src/core/loop.ts | 19 +++++++----- 4 files changed, 26 insertions(+), 43 deletions(-) diff --git a/playground/src/components/TheSphere.vue b/playground/src/components/TheSphere.vue index d2544609c..c69ea78b5 100644 --- a/playground/src/components/TheSphere.vue +++ b/playground/src/components/TheSphere.vue @@ -5,20 +5,28 @@ import { useLoop } from '@tresjs/core' /* const { invalidate } = useTres() */ const sphereRef = ref() - -useLoop(() => { - console.log('before renderer') +useLoop((state) => { + if (!sphereRef.value) { return } + sphereRef.value.position.y += Math.sin(state.elapsed) * 0.01 +}) +/* useLoop(() => { + console.count('before renderer') }, -1) useLoop(() => { - console.log('after renderer') + console.count('after renderer') }, 2) useLoop((state) => { if (!sphereRef.value) { return } - console.log('this should be before render', state) + console.log('this should be just before render') sphereRef.value.position.y += Math.sin(state.elapsed) * 0.01 }) + +useLoop(({ ctx }) => { + console.log('this should replace the renderer', ctx) + ctx.renderer.value.render(ctx.scene.value, ctx.camera.value) +}, 1) */