TresCanvas.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <script setup lang="ts">
  2. import {
  3. ACESFilmicToneMapping,
  4. PCFSoftShadowMap,
  5. PerspectiveCamera,
  6. Scene,
  7. WebGLRenderer,
  8. } from 'three'
  9. import type { App, Ref } from 'vue'
  10. import type { TresCamera, TresContextWithClock, TresObject, TresScene } from '../types/'
  11. import type { PointerEvent } from '@pmndrs/pointer-events'
  12. import * as THREE from 'three'
  13. import {
  14. createRenderer,
  15. defineComponent,
  16. Fragment,
  17. getCurrentInstance,
  18. h,
  19. onMounted,
  20. onUnmounted,
  21. provide,
  22. ref,
  23. shallowRef,
  24. toValue,
  25. watch,
  26. watchEffect,
  27. } from 'vue'
  28. import pkg from '../../package.json'
  29. import type { RendererOptions, TresContext } from '../composables'
  30. import { useTresContextProvider } from '../composables'
  31. import { extend } from '../core/catalogue'
  32. import { nodeOps } from '../core/nodeOps'
  33. import { disposeObject3D } from '../utils/'
  34. import { registerTresDevtools } from '../devtools'
  35. import type { TresPointerEventName } from '../utils/pointerEvents'
  36. const props = withDefaults(defineProps<TresCanvasProps>(), {
  37. alpha: undefined,
  38. depth: undefined,
  39. shadows: undefined,
  40. stencil: undefined,
  41. antialias: true,
  42. windowSize: undefined,
  43. useLegacyLights: undefined,
  44. preserveDrawingBuffer: undefined,
  45. logarithmicDepthBuffer: undefined,
  46. failIfMajorPerformanceCaveat: undefined,
  47. renderMode: 'always',
  48. clearColor: '#000000',
  49. clearAlpha: 1,
  50. enableProvideBridge: true,
  51. toneMapping: ACESFilmicToneMapping,
  52. shadowMapType: PCFSoftShadowMap,
  53. })
  54. const emit = defineEmits<{
  55. ready: [context: TresContext]
  56. pointermissed: [event: PointerEvent<MouseEvent>]
  57. render: [context: TresContext]
  58. beforeLoop: [context: TresContextWithClock]
  59. loop: [context: TresContextWithClock]
  60. } & {
  61. // all pointer events are supported because they bubble up
  62. [key in TresPointerEventName]: [event: PointerEvent<MouseEvent>]
  63. }
  64. >()
  65. const slots = defineSlots<{
  66. default: () => any
  67. }>()
  68. const canvasRef = ref<HTMLCanvasElement>()
  69. /*
  70. `scene` is defined here and not in `useTresContextProvider` because the custom
  71. renderer uses it to mount the app nodes. This happens before `useTresContextProvider` is called.
  72. The custom renderer requires `scene` to be editable (not readonly).
  73. */
  74. const scene = shallowRef<TresScene | Scene>(new Scene())
  75. const instance = getCurrentInstance()
  76. extend(THREE)
  77. const createInternalComponent = (context: TresContext, empty = false) =>
  78. defineComponent({
  79. setup() {
  80. const ctx = getCurrentInstance()?.appContext
  81. if (ctx) { ctx.app = instance?.appContext.app as App }
  82. const provides: { [key: string | symbol]: unknown } = {}
  83. // Helper function to recursively merge provides from parents
  84. function mergeProvides(currentInstance: any) {
  85. if (!currentInstance) { return }
  86. // Recursively process the parent instance
  87. if (currentInstance.parent) {
  88. mergeProvides(currentInstance.parent)
  89. }
  90. // Extract provides from the current instance and merge them
  91. if (currentInstance.provides) {
  92. Object.assign(provides, currentInstance.provides)
  93. }
  94. }
  95. // Start the recursion from the initial instance
  96. if (instance?.parent && props.enableProvideBridge) {
  97. mergeProvides(instance.parent)
  98. Reflect.ownKeys(provides)
  99. .forEach((key) => {
  100. provide(key, provides[key])
  101. })
  102. }
  103. provide('useTres', context)
  104. provide('extend', extend)
  105. if (typeof window !== 'undefined' && ctx?.app) {
  106. registerTresDevtools(ctx?.app, context)
  107. }
  108. return () => h(Fragment, null, !empty ? slots.default() : [])
  109. },
  110. })
  111. const mountCustomRenderer = (context: TresContext, empty = false) => {
  112. const InternalComponent = createInternalComponent(context, empty)
  113. const { render } = createRenderer(nodeOps(context))
  114. render(h(InternalComponent), scene.value as unknown as TresObject)
  115. }
  116. const dispose = (context: TresContext, force = false) => {
  117. disposeObject3D(context.scene.value as unknown as TresObject)
  118. if (force) {
  119. context.renderer.instance.dispose()
  120. if (context.renderer.instance instanceof WebGLRenderer) {
  121. context.renderer.instance.renderLists.dispose()
  122. context.renderer.instance.forceContextLoss()
  123. }
  124. }
  125. (scene.value as TresScene).__tres = {
  126. root: context,
  127. }
  128. }
  129. const context = shallowRef<TresContext | null>(null)
  130. defineExpose({ context, dispose: () => dispose(context.value as TresContext, true) })
  131. const handleHMR = (context: TresContext) => {
  132. dispose(context)
  133. mountCustomRenderer(context)
  134. }
  135. const unmountCanvas = () => {
  136. dispose(context.value as TresContext)
  137. mountCustomRenderer(context.value as TresContext, true)
  138. }
  139. onMounted(() => {
  140. const existingCanvas = canvasRef as Ref<HTMLCanvasElement>
  141. context.value = useTresContextProvider({
  142. scene: scene.value as TresScene,
  143. canvas: existingCanvas,
  144. windowSize: props.windowSize ?? false,
  145. rendererOptions: props,
  146. })
  147. const { camera, renderer } = context.value
  148. const { registerCamera, cameras, activeCamera, deregisterCamera } = camera
  149. mountCustomRenderer(context.value)
  150. const addDefaultCamera = () => {
  151. const camera = new PerspectiveCamera(
  152. 45,
  153. window.innerWidth / window.innerHeight,
  154. 0.1,
  155. 1000,
  156. )
  157. camera.position.set(3, 3, 3)
  158. camera.lookAt(0, 0, 0)
  159. registerCamera(camera)
  160. const unwatch = watchEffect(() => {
  161. if (cameras.value.length >= 2) {
  162. camera.removeFromParent()
  163. deregisterCamera(camera)
  164. unwatch?.()
  165. }
  166. })
  167. }
  168. context.value.events.onPointerMissed((event) => {
  169. emit('pointermissed', event)
  170. })
  171. watch(
  172. () => props.camera,
  173. (newCamera, oldCamera) => {
  174. if (newCamera) {
  175. registerCamera(toValue(newCamera), true)
  176. }
  177. if (oldCamera) {
  178. toValue(oldCamera).removeFromParent()
  179. deregisterCamera(toValue(oldCamera))
  180. }
  181. },
  182. {
  183. immediate: true,
  184. },
  185. )
  186. if (!activeCamera.value) {
  187. addDefaultCamera()
  188. }
  189. renderer.onRender(() => {
  190. if (context.value) {
  191. emit('render', context.value)
  192. }
  193. })
  194. renderer.loop.onLoop((loopContext) => {
  195. if (context.value) {
  196. emit('loop', { ...context.value, ...loopContext })
  197. }
  198. })
  199. renderer.loop.onBeforeLoop((loopContext) => {
  200. if (context.value) {
  201. emit('beforeLoop', { ...context.value, ...loopContext })
  202. }
  203. })
  204. renderer.onReady(() => {
  205. emit('ready', context.value!)
  206. })
  207. // HMR support
  208. if (import.meta.hot && context.value) { import.meta.hot.on('vite:afterUpdate', () => handleHMR(context.value as TresContext)) }
  209. })
  210. onUnmounted(unmountCanvas)
  211. </script>
  212. <script lang="ts">
  213. export interface TresCanvasProps extends RendererOptions {
  214. /**
  215. * Custom camera instance to use as main camera
  216. * If not provided, a default PerspectiveCamera will be created
  217. */
  218. camera?: TresCamera
  219. /**
  220. * Whether the canvas should be sized to the window
  221. * When true, canvas will be fixed positioned and full viewport size
  222. * @default false
  223. */
  224. windowSize?: boolean
  225. /**
  226. * Whether to enable the provide/inject bridge between Vue and TresJS
  227. * When true, Vue's provide/inject will work across the TresJS boundary
  228. * @default true
  229. */
  230. enableProvideBridge?: boolean
  231. }
  232. </script>
  233. <template>
  234. <canvas
  235. ref="canvasRef"
  236. :data-scene="scene.uuid"
  237. :class="$attrs.class"
  238. :data-tres="`tresjs ${pkg.version}`"
  239. :style="{
  240. display: 'block',
  241. width: '100%',
  242. height: '100%',
  243. position: windowSize ? 'fixed' : 'relative',
  244. top: 0,
  245. left: 0,
  246. pointerEvents: 'auto',
  247. touchAction: 'none',
  248. ...$attrs.style as Object,
  249. }"
  250. ></canvas>
  251. </template>