TresCanvas.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <script setup lang="ts">
  2. import { PerspectiveCamera, Scene } from 'three'
  3. import type {
  4. WebGLRendererParameters,
  5. ColorSpace,
  6. ShadowMapType,
  7. ToneMapping,
  8. } from 'three'
  9. import type { Ref,
  10. App } from 'vue'
  11. import {
  12. computed,
  13. onMounted,
  14. provide,
  15. ref,
  16. shallowRef,
  17. watch,
  18. watchEffect,
  19. Fragment,
  20. defineComponent,
  21. h,
  22. getCurrentInstance,
  23. } from 'vue'
  24. import pkg from '../../package.json'
  25. import {
  26. useTresContextProvider,
  27. useLogger,
  28. usePointerEventHandler,
  29. useRenderLoop,
  30. type TresContext,
  31. } from '../composables'
  32. import { extend } from '../core/catalogue'
  33. import { render } from '../core/renderer'
  34. import type { RendererPresetsType } from '../composables/useRenderer/const'
  35. import type { TresCamera, TresObject } from '../types/'
  36. export interface TresCanvasProps
  37. extends Omit<WebGLRendererParameters, 'canvas'> {
  38. // required by for useRenderer
  39. shadows?: boolean
  40. clearColor?: string
  41. toneMapping?: ToneMapping
  42. shadowMapType?: ShadowMapType
  43. useLegacyLights?: boolean
  44. outputColorSpace?: ColorSpace
  45. toneMappingExposure?: number
  46. // required by useTresContextProvider
  47. camera?: TresCamera
  48. preset?: RendererPresetsType
  49. windowSize?: boolean
  50. disableRender?: boolean
  51. }
  52. const props = withDefaults(defineProps<TresCanvasProps>(), {
  53. alpha: undefined,
  54. depth: undefined,
  55. shadows: undefined,
  56. stencil: undefined,
  57. antialias: undefined,
  58. windowSize: undefined,
  59. disableRender: undefined,
  60. useLegacyLights: undefined,
  61. preserveDrawingBuffer: undefined,
  62. logarithmicDepthBuffer: undefined,
  63. failIfMajorPerformanceCaveat: undefined,
  64. })
  65. const { logWarning } = useLogger()
  66. const canvas = ref<HTMLCanvasElement>()
  67. /*
  68. `scene` is defined here and not in `useTresContextProvider` because the custom
  69. renderer uses it to mount the app nodes. This happens before `useTresContextProvider` is called.
  70. The custom renderer requires `scene` to be editable (not readonly).
  71. */
  72. const scene = shallowRef(new Scene())
  73. const { resume } = useRenderLoop()
  74. const slots = defineSlots<{
  75. default(): any
  76. }>()
  77. const instance = getCurrentInstance()?.appContext.app
  78. const createInternalComponent = (context: TresContext) =>
  79. defineComponent({
  80. setup() {
  81. const ctx = getCurrentInstance()?.appContext
  82. if (ctx) ctx.app = instance as App
  83. provide('useTres', context)
  84. provide('extend', extend)
  85. return () => h(Fragment, null, slots?.default ? slots.default() : [])
  86. },
  87. })
  88. const mountCustomRenderer = (context: TresContext) => {
  89. const InternalComponent = createInternalComponent(context)
  90. render(h(InternalComponent), scene.value as unknown as TresObject)
  91. }
  92. const dispose = (context: TresContext, force = false) => {
  93. scene.value.children = []
  94. if (force) {
  95. context.renderer.value.dispose()
  96. context.renderer.value.renderLists.dispose()
  97. context.renderer.value.forceContextLoss()
  98. }
  99. mountCustomRenderer(context)
  100. resume()
  101. }
  102. const disableRender = computed(() => props.disableRender)
  103. const context = shallowRef<TresContext | null>(null)
  104. defineExpose({ context, dispose: () => dispose(context.value as TresContext, true) })
  105. onMounted(() => {
  106. const existingCanvas = canvas as Ref<HTMLCanvasElement>
  107. context.value = useTresContextProvider({
  108. scene: scene.value,
  109. canvas: existingCanvas,
  110. windowSize: props.windowSize,
  111. disableRender,
  112. rendererOptions: props,
  113. })
  114. usePointerEventHandler({ scene: scene.value, contextParts: context.value })
  115. const { registerCamera, camera, cameras, deregisterCamera } = context.value
  116. mountCustomRenderer(context.value)
  117. const addDefaultCamera = () => {
  118. const camera = new PerspectiveCamera(
  119. 45,
  120. window.innerWidth / window.innerHeight,
  121. 0.1,
  122. 1000,
  123. )
  124. camera.position.set(3, 3, 3)
  125. camera.lookAt(0, 0, 0)
  126. registerCamera(camera)
  127. const unwatch = watchEffect(() => {
  128. if (cameras.value.length >= 2) {
  129. camera.removeFromParent()
  130. deregisterCamera(camera)
  131. unwatch?.()
  132. }
  133. })
  134. }
  135. watch(
  136. () => props.camera,
  137. (newCamera, oldCamera) => {
  138. if (newCamera) registerCamera(newCamera)
  139. if (oldCamera) {
  140. oldCamera.removeFromParent()
  141. deregisterCamera(oldCamera)
  142. }
  143. },
  144. {
  145. immediate: true,
  146. },
  147. )
  148. if (!camera.value) {
  149. logWarning(
  150. 'No camera found. Creating a default perspective camera. '
  151. + 'To have full control over a camera, please add one to the scene.',
  152. )
  153. addDefaultCamera()
  154. }
  155. if (import.meta.hot && context.value)
  156. import.meta.hot.on('vite:afterUpdate', () => dispose(context.value as TresContext))
  157. })
  158. </script>
  159. <template>
  160. <canvas
  161. ref="canvas"
  162. :data-scene="scene.uuid"
  163. :class="$attrs.class"
  164. :data-tres="`tresjs ${pkg.version}`"
  165. :style="{
  166. display: 'block',
  167. width: '100%',
  168. height: '100%',
  169. position: windowSize ? 'fixed' : 'relative',
  170. top: 0,
  171. left: 0,
  172. pointerEvents: 'auto',
  173. touchAction: 'none',
  174. ...$attrs.style as Object,
  175. }"
  176. />
  177. </template>