threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {
  2. _testFinish,
  3. BaseGroundPlugin,
  4. BasicShadowMap,
  5. Color,
  6. DataUtils,
  7. DirectionalLight,
  8. IObject3D,
  9. LoadingScreenPlugin,
  10. MaterialExtension,
  11. ProgressivePlugin,
  12. ShaderChunk,
  13. shaderReplaceString,
  14. SSAAPlugin,
  15. ThreeViewer,
  16. Vector3,
  17. } from 'threepipe'
  18. import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'
  19. const hdris = [
  20. 'https://threejs.org/examples/textures/equirectangular/quarry_01_1k.hdr',
  21. 'https://threejs.org/examples/textures/equirectangular/spot1Lux.hdr',
  22. 'https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr',
  23. 'https://dist.pixotronics.com/webgi/assets/hdr/gem_2.hdr',
  24. 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_04_1k.hdr',
  25. 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_03_1k.hdr',
  26. 'https://threejs.org/examples/textures/equirectangular/pedestrian_overpass_1k.hdr',
  27. 'https://threejs.org/examples/textures/equirectangular/blouberg_sunrise_2_1k.hdr',
  28. 'https://threejs.org/examples/textures/equirectangular/royal_esplanade_1k.hdr',
  29. 'https://threejs.org/examples/textures/equirectangular/moonless_golf_1k.hdr',
  30. 'https://threejs.org/examples/textures/equirectangular/san_giuseppe_bridge_2k.hdr',
  31. 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_06_1k.hdr',
  32. 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_05_1k.hdr',
  33. 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_02_1k.hdr',
  34. 'https://hdrihaven.r2cache.com/hdr/1k/studio_small_01_1k.hdr',
  35. 'https://hdrihaven.r2cache.com/hdr/1k/empty_warehouse_01_1k.hdr',
  36. ]
  37. async function init() {
  38. const viewer = new ThreeViewer({
  39. canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
  40. msaa: false,
  41. rgbm: false,
  42. plugins: [new ProgressivePlugin((window as any).TESTING ? 20 : 200), SSAAPlugin, LoadingScreenPlugin],
  43. dropzone: {
  44. addOptions: {
  45. disposeSceneObjects: true,
  46. autoSetEnvironment: true,
  47. autoSetBackground: true,
  48. },
  49. },
  50. })
  51. const directionalLight = createDirLight(viewer)
  52. viewer.materialManager.registerMaterialExtension(extension)
  53. viewer.renderManager.renderer.shadowMap.type = BasicShadowMap
  54. // extra check to ignore the sampling of shadow if intensity is 0
  55. ShaderChunk.lights_fragment_begin = shaderReplaceString(
  56. ShaderChunk.lights_fragment_begin,
  57. 'directLight.color *= ( directLight.visible && receiveShadow )',
  58. 'directLight.color *= ( directLight.visible && receiveShadow && length(directLight.color) > 0.001)',
  59. {replaceAll: true})
  60. const ground = viewer.addPluginSync(BaseGroundPlugin)
  61. ground.mesh!.castShadow = false
  62. ground.material!.roughness = 1
  63. ground.material!.metalness = 0
  64. const ui = viewer.addPluginSync(new TweakpaneUiPlugin(false))
  65. await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
  66. autoCenter: true,
  67. autoScale: true,
  68. })
  69. viewer.scene.envMapIntensity = 1
  70. await viewer.setEnvironmentMap(hdris[0], {
  71. setBackground: true,
  72. })
  73. ui.appendChild({
  74. type: 'dropdown',
  75. label: 'Environment Map',
  76. children: hdris.map((url)=>({
  77. label: url.split('/').pop()!.split('.').shift()!,
  78. value: url,
  79. })),
  80. value: hdris[0],
  81. onChange: async(ev)=>{
  82. console.log(ev.value)
  83. await viewer.setEnvironmentMap(ev.value, {
  84. setBackground: true,
  85. })
  86. refreshHist()
  87. },
  88. })
  89. let histogram2 = createHistogramFromImage(viewer.scene.environment?.image)
  90. function refreshHist() {
  91. histogram2 = createHistogramFromImage(viewer.scene.environment?.image)
  92. }
  93. viewer.addEventListener('postFrame', ()=>updateLight(viewer, directionalLight, histogram2))
  94. ui.setupPluginUi(BaseGroundPlugin)
  95. // const targetPreview = viewer.addPluginSync(new RenderTargetPreviewPlugin())
  96. // targetPreview.addTarget(()=>directionalLight.shadow.map, 'shadow')
  97. }
  98. const extension: MaterialExtension = {
  99. isCompatible: ()=> true,
  100. computeCacheKey: ()=> 'aomap1',
  101. shaderExtender(shader) {
  102. shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#include <aomap_fragment>', `
  103. #ifdef USE_AOMAP
  104. // reads channel R, compatible with a combined OcclusionRoughnessMetallic (RGB) texture
  105. float ambientOcclusion = ( texture2D( aoMap, vAoMapUv ).r - 1.0 ) * aoMapIntensity + 1.0;
  106. #else
  107. const int ii = 0;
  108. DirectionalLightShadow edls = directionalLightShadows[ ii ];
  109. float ambientOcclusion = getShadow( directionalShadowMap[ ii ], edls.shadowMapSize, edls.shadowBias, edls.shadowRadius, vDirectionalShadowCoord[ ii ] );
  110. #endif
  111. reflectedLight.indirectDiffuse *= ambientOcclusion;
  112. #if defined( USE_ENVMAP ) && defined( STANDARD )
  113. float dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );
  114. reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
  115. #endif
  116. `)
  117. // shader.defines.USE_UV = ''
  118. },
  119. }
  120. function createDirLight(viewer: ThreeViewer) {
  121. const directionalLight = new DirectionalLight(0xffffff, 4)
  122. directionalLight.position.set(-2, -2, 2)
  123. directionalLight.lookAt(0, 0, 0)
  124. directionalLight.color.set(0xffffff)
  125. directionalLight.intensity = 0
  126. directionalLight.castShadow = true
  127. directionalLight.shadow.mapSize.setScalar(1024)
  128. directionalLight.shadow.camera.near = 0.1
  129. directionalLight.shadow.camera.far = 10
  130. directionalLight.shadow.camera.top = 2
  131. directionalLight.shadow.camera.bottom = -2
  132. directionalLight.shadow.camera.left = -2
  133. directionalLight.shadow.camera.right = 2
  134. viewer.scene.addObject(directionalLight, {addToRoot: true})
  135. // move to index 0 in parent.children, so that directionalLight always has index 0 in shader. required for material extension
  136. const parent = directionalLight.parent!
  137. const index = parent.children.indexOf(directionalLight)
  138. if (index > 0) {
  139. parent.children.splice(index, 1)
  140. parent.children.unshift(directionalLight)
  141. }
  142. return directionalLight
  143. }
  144. function updateLight(viewer: ThreeViewer, directionalLight: DirectionalLight, histogram: ReturnType<typeof createHistogramFromImage>) {
  145. if (viewer.renderManager.frameCount < 1) return
  146. // if (viewer.renderManager.frameCount > 2) return
  147. const bounds = viewer.scene.getBounds(false)
  148. const size = bounds.getSize(new Vector3()).length()
  149. const center = bounds.getCenter(new Vector3())
  150. const i = viewer.renderManager.frameCount <= 1 ? histogram.brightestI : histogram.sampleIndex()
  151. histogram.indexToColor(i, directionalLight)
  152. directionalLight.intensity = 0 // so it doesnt show in the scene
  153. histogram.indexToPosition(i, directionalLight.position).multiplyScalar(0.5 + size).add(center)
  154. directionalLight.lookAt(center)
  155. directionalLight.shadow.camera.near = Math.max(size / 100, 0.1)
  156. directionalLight.shadow.camera.far = size * 2.5
  157. directionalLight.shadow.camera.updateProjectionMatrix()
  158. viewer.renderManager.resetShadows()
  159. }
  160. function sampleRandom2(pow = 2) {
  161. return Math.max(0, Math.pow(Math.random(), pow) - 0.001)
  162. }
  163. function sampleRandom() {
  164. return Math.max(0, Math.random() - 0.001)
  165. }
  166. const maxIntensityClamp = 50
  167. const ignoreBottomBins = 1 // should be at-least 1 to ignore black pixels.
  168. const numBins = 100 // Number of bins in the histogram (configurable)
  169. const sampleRandPower = 1.25 // increase this to give more focus to higher intensity pixels. between 1 and 2
  170. const topHalf = true // todo if this is true, half the shadow in shader?
  171. function createHistogramFromImage(image: {data: Uint16Array, width: number, height: number}) {
  172. const histogram: number[][] = []
  173. let maxIntensity = -1
  174. let brightestI = 0
  175. // const maxIntensity1 = 65504
  176. for (let i = 0; i < image.data.length / 4; i++) {
  177. const r = DataUtils.fromHalfFloat(image.data[i * 4])
  178. const g = DataUtils.fromHalfFloat(image.data[i * 4 + 1])
  179. const b = DataUtils.fromHalfFloat(image.data[i * 4 + 2])
  180. const a = DataUtils.fromHalfFloat(image.data[i * 4 + 3])
  181. const intensity = a * Math.max(r, g, b) // Calculate intensity
  182. const binIndex = Math.floor(numBins * Math.max(0, Math.min(1 - 0.001, intensity / maxIntensityClamp))) // Calculate the bin index
  183. histogram[binIndex] ||= []
  184. histogram[binIndex].push(i)
  185. if (maxIntensity < intensity) {
  186. maxIntensity = intensity
  187. brightestI = i
  188. }
  189. if (topHalf && i > image.data.length / 8) break
  190. }
  191. histogram.reverse()
  192. const cdf = histogram.map((bin) => bin ? bin.length : 0)
  193. const maxW = numBins - 1 - ignoreBottomBins + 1
  194. cdf[0] = cdf[0] * maxW
  195. for (let i = 1; i < numBins; i++) {
  196. cdf[i] = cdf[i - 1] + (cdf[i] || 0) * (maxW - i) // *i for intensity of that bin
  197. }
  198. console.log(cdf)
  199. return {
  200. histogram, cdf,
  201. brightestI,
  202. maxIntensity,
  203. sampleIndex: ()=>{
  204. const max = cdf[cdf.length - 1]
  205. const r = sampleRandom2(sampleRandPower) * max
  206. const binIndex = cdf.findIndex((value) => value >= r)
  207. const bin = histogram[binIndex]
  208. const index = Math.floor(bin.length * sampleRandom())
  209. return bin[index]
  210. },
  211. indexToPosition: (i: number, position: Vector3)=>{
  212. // todo handle envMapRotation
  213. const {width, height} = image
  214. const x = i % width / width
  215. const y = 1 - Math.floor(i / width) / height
  216. const phi = Math.PI * (x * 2 - 1)
  217. const theta = Math.PI * 0.5 * (y * 2 - 1)
  218. return position.set(
  219. Math.cos(theta) * Math.cos(phi),
  220. Math.sin(theta),
  221. Math.cos(theta) * Math.sin(phi),
  222. )
  223. },
  224. indexToColor: (i: number, light: {color: Color, intensity: number})=>{
  225. // todo handle envMapIntensity
  226. const r = DataUtils.fromHalfFloat(image.data[i * 4])
  227. const g = DataUtils.fromHalfFloat(image.data[i * 4 + 1])
  228. const b = DataUtils.fromHalfFloat(image.data[i * 4 + 2])
  229. const a = DataUtils.fromHalfFloat(image.data[i * 4 + 3])
  230. light.color.setRGB(Math.min(1, r * a), Math.min(1, g * a), Math.min(1, b * a))
  231. light.intensity = Math.min(a * Math.max(r, g, b), maxIntensityClamp)
  232. },
  233. }
  234. }
  235. init().finally(_testFinish)