index.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import { ACESFilmicToneMapping, Color, WebGLRenderer } from 'three'
  2. import { type MaybeRef, computed, onUnmounted, shallowRef, watch, watchEffect } from 'vue'
  3. import {
  4. type MaybeRefOrGetter,
  5. toValue,
  6. unrefElement,
  7. useDevicePixelRatio,
  8. } from '@vueuse/core'
  9. import type { ColorSpace, Scene, ShadowMapType, ToneMapping, WebGLRendererParameters } from 'three'
  10. import { useLogger } from '../useLogger'
  11. import type { TresColor } from '../../types'
  12. import { useRenderLoop } from '../useRenderLoop'
  13. import { normalizeColor } from '../../utils/normalize'
  14. import type { TresContext } from '../useTresContextProvider'
  15. import { get, merge, set } from '../../utils'
  16. // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
  17. import { revision } from '../../core/revision'
  18. import { rendererPresets } from './const'
  19. import type { RendererPresetsType } from './const'
  20. type TransformToMaybeRefOrGetter<T> = {
  21. [K in keyof T]: MaybeRefOrGetter<T[K]> | MaybeRefOrGetter<T[K]>;
  22. }
  23. export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRendererParameters> {
  24. /**
  25. * Enable shadows in the Renderer
  26. *
  27. * @default false
  28. */
  29. shadows?: MaybeRefOrGetter<boolean>
  30. /**
  31. * Set the shadow map type
  32. * Can be PCFShadowMap, PCFSoftShadowMap, BasicShadowMap, VSMShadowMap
  33. * [see](https://threejs.org/docs/?q=we#api/en/constants/Renderer)
  34. *
  35. * @default PCFSoftShadowMap
  36. */
  37. shadowMapType?: MaybeRefOrGetter<ShadowMapType>
  38. /**
  39. * Whether to use physically correct lighting mode.
  40. * See the [lights / physical example](https://threejs.org/examples/#webgl_lights_physical).
  41. *
  42. * @default false
  43. * @deprecated Use {@link WebGLRenderer.useLegacyLights useLegacyLights} instead.
  44. */
  45. physicallyCorrectLights?: MaybeRefOrGetter<boolean>
  46. /**
  47. * Whether to use legacy lighting mode.
  48. *
  49. * @type {MaybeRefOrGetter<boolean>}
  50. * @memberof UseRendererOptions
  51. */
  52. useLegacyLights?: MaybeRefOrGetter<boolean>
  53. /**
  54. * Defines the output encoding of the renderer.
  55. * Can be LinearSRGBColorSpace, SRGBColorSpace
  56. *
  57. * @default LinearSRGBColorSpace
  58. */
  59. outputColorSpace?: MaybeRefOrGetter<ColorSpace>
  60. /**
  61. * Defines the tone mapping used by the renderer.
  62. * Can be NoToneMapping, LinearToneMapping,
  63. * ReinhardToneMapping, Uncharted2ToneMapping,
  64. * CineonToneMapping, ACESFilmicToneMapping,
  65. * CustomToneMapping
  66. *
  67. * @default ACESFilmicToneMapping
  68. */
  69. toneMapping?: MaybeRefOrGetter<ToneMapping>
  70. /**
  71. * Defines the tone mapping exposure used by the renderer.
  72. *
  73. * @default 1
  74. */
  75. toneMappingExposure?: MaybeRefOrGetter<number>
  76. /**
  77. * The color value to use when clearing the canvas.
  78. *
  79. * @default 0x000000
  80. */
  81. clearColor?: MaybeRefOrGetter<TresColor>
  82. windowSize?: MaybeRefOrGetter<boolean | string>
  83. preset?: MaybeRefOrGetter<RendererPresetsType>
  84. renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
  85. }
  86. export function useRenderer(
  87. {
  88. scene,
  89. canvas,
  90. options,
  91. disableRender,
  92. emit,
  93. contextParts: { sizes, camera, render, invalidate, advance },
  94. }:
  95. {
  96. canvas: MaybeRef<HTMLCanvasElement>
  97. scene: Scene
  98. options: UseRendererOptions
  99. emit: (event: string, ...args: any[]) => void
  100. contextParts: Pick<TresContext, 'sizes' | 'camera' | 'render'> & { invalidate: () => void, advance: () => void }
  101. disableRender: MaybeRefOrGetter<boolean>
  102. },
  103. ) {
  104. const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
  105. alpha: toValue(options.alpha) ?? true,
  106. depth: toValue(options.depth),
  107. canvas: unrefElement(canvas),
  108. context: toValue(options.context),
  109. stencil: toValue(options.stencil),
  110. antialias: toValue(options.antialias) ?? true,
  111. precision: toValue(options.precision),
  112. powerPreference: toValue(options.powerPreference),
  113. premultipliedAlpha: toValue(options.premultipliedAlpha),
  114. preserveDrawingBuffer: toValue(options.preserveDrawingBuffer),
  115. logarithmicDepthBuffer: toValue(options.logarithmicDepthBuffer),
  116. failIfMajorPerformanceCaveat: toValue(options.failIfMajorPerformanceCaveat),
  117. }))
  118. const renderer = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
  119. function invalidateOnDemand() {
  120. if (options.renderMode === 'on-demand') {
  121. invalidate()
  122. }
  123. }
  124. // since the properties set via the constructor can't be updated dynamically,
  125. // the renderer is recreated once they change
  126. watch(webGLRendererConstructorParameters, () => {
  127. renderer.value.dispose()
  128. renderer.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
  129. invalidateOnDemand()
  130. })
  131. watch([sizes.width, sizes.height], () => {
  132. renderer.value.setSize(sizes.width.value, sizes.height.value)
  133. invalidateOnDemand()
  134. }, {
  135. immediate: true,
  136. })
  137. watch(() => options.clearColor, invalidateOnDemand)
  138. const { pixelRatio } = useDevicePixelRatio()
  139. watch(pixelRatio, () => {
  140. renderer.value.setPixelRatio(pixelRatio.value)
  141. })
  142. const { logError } = useLogger()
  143. // TheLoop
  144. const { resume, onLoop } = useRenderLoop()
  145. onLoop(() => {
  146. if (camera.value && !toValue(disableRender) && render.frames.value > 0) {
  147. renderer.value.render(scene, camera.value)
  148. emit('render', renderer.value)
  149. }
  150. // Reset priority
  151. render.priority.value = 0
  152. if (toValue(options.renderMode) === 'always') {
  153. render.frames.value = 1
  154. }
  155. else {
  156. render.frames.value = Math.max(0, render.frames.value - 1)
  157. }
  158. })
  159. resume()
  160. const getThreeRendererDefaults = () => {
  161. const plainRenderer = new WebGLRenderer()
  162. const defaults = {
  163. shadowMap: {
  164. enabled: plainRenderer.shadowMap.enabled,
  165. type: plainRenderer.shadowMap.type,
  166. },
  167. toneMapping: plainRenderer.toneMapping,
  168. toneMappingExposure: plainRenderer.toneMappingExposure,
  169. outputColorSpace: plainRenderer.outputColorSpace,
  170. }
  171. plainRenderer.dispose()
  172. return defaults
  173. }
  174. const threeDefaults = getThreeRendererDefaults()
  175. const renderMode = toValue(options.renderMode)
  176. if (renderMode === 'on-demand') {
  177. // Invalidate for the first time
  178. invalidate()
  179. }
  180. if (renderMode === 'manual') {
  181. // Advance for the first time, setTimeout to make sure there is something to render
  182. setTimeout(() => {
  183. advance()
  184. }, 1)
  185. }
  186. watchEffect(() => {
  187. const rendererPreset = toValue(options.preset)
  188. if (rendererPreset) {
  189. if (!(rendererPreset in rendererPresets)) { logError(`Renderer Preset must be one of these: ${Object.keys(rendererPresets).join(', ')}`) }
  190. merge(renderer.value, rendererPresets[rendererPreset])
  191. }
  192. // Render mode
  193. if (renderMode === 'always') {
  194. // If the render mode is 'always', ensure there's always a frame pending
  195. render.frames.value = Math.max(1, render.frames.value)
  196. }
  197. const getValue = <T>(option: MaybeRefOrGetter<T>, pathInThree: string): T | undefined => {
  198. const value = toValue(option)
  199. const getValueFromPreset = () => {
  200. if (!rendererPreset) { return }
  201. return get(rendererPresets[rendererPreset], pathInThree)
  202. }
  203. if (value !== undefined) { return value }
  204. const valueInPreset = getValueFromPreset() as T
  205. if (valueInPreset !== undefined) { return valueInPreset }
  206. return get(threeDefaults, pathInThree)
  207. }
  208. const setValueOrDefault = <T>(option: MaybeRefOrGetter<T>, pathInThree: string) =>
  209. set(renderer.value, pathInThree, getValue(option, pathInThree))
  210. setValueOrDefault(options.shadows, 'shadowMap.enabled')
  211. setValueOrDefault(options.toneMapping ?? ACESFilmicToneMapping, 'toneMapping')
  212. setValueOrDefault(options.shadowMapType, 'shadowMap.type')
  213. if (revision < 150) { setValueOrDefault(!options.useLegacyLights, 'physicallyCorrectLights') }
  214. setValueOrDefault(options.outputColorSpace, 'outputColorSpace')
  215. setValueOrDefault(options.toneMappingExposure, 'toneMappingExposure')
  216. const clearColor = getValue(options.clearColor, 'clearColor')
  217. if (clearColor) {
  218. renderer.value.setClearColor(
  219. clearColor
  220. ? normalizeColor(clearColor)
  221. : new Color(0x000000), // default clear color is not easily/efficiently retrievable from three
  222. )
  223. }
  224. })
  225. onUnmounted(() => {
  226. renderer.value.dispose()
  227. renderer.value.forceContextLoss()
  228. })
  229. if (import.meta.hot) { import.meta.hot.on('vite:afterUpdate', resume) }
  230. return {
  231. renderer,
  232. }
  233. }
  234. export type UseRendererReturn = ReturnType<typeof useRenderer>