threepipe
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

tpImageInputGenerator.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import {
  2. CustomContextMenu,
  3. DataTexture,
  4. EXRExporter2,
  5. FloatType,
  6. generateUUID,
  7. getOrCall,
  8. HalfFloatType,
  9. imageBitmapToBase64,
  10. ImportResultExtras,
  11. ITexture,
  12. LinearSRGBColorSpace,
  13. makeTextSvg,
  14. RepeatWrapping,
  15. SRGBColorSpace,
  16. Texture,
  17. textureToDataUrl,
  18. ThreeViewer,
  19. upgradeTexture,
  20. WebGLRenderTarget,
  21. } from 'threepipe'
  22. import type {UiObjectConfig} from 'uiconfig.js'
  23. import {TweakpaneUiPlugin} from './TweakpaneUiPlugin'
  24. const staticData = {
  25. placeholderVal: 'placeholder',
  26. renderTarImage: makeTextSvg('Render Target'),
  27. dataTexImage: makeTextSvg('Data Texture'),
  28. lutCubeTexImage: makeTextSvg('CUBE Texture'),
  29. compressedTexImage: makeTextSvg('Compressed Texture'),
  30. textureMap: {} as any,
  31. imageMap: {} as any,
  32. tempMap: {} as any,
  33. }
  34. function proxyGetValue(cc: any, viewer: ThreeViewer) {
  35. if (cc?.get) cc = cc.get()
  36. let ret: any = undefined
  37. if (!cc) return staticData.placeholderVal
  38. if (cc.isCompressedTexture && !cc.image.tp_src) {
  39. cc.image.tp_src = staticData.compressedTexImage
  40. }
  41. // todo: video is not playing
  42. // if (cc.isVideoTexture && !cc.image.tp_src) {
  43. // cc.image.tp_src = dataTexImage
  44. // }
  45. if (cc.isTexture) {
  46. // console.warn('here')
  47. // todo: use textureToCanvas for data texture
  48. if (cc.image && !cc.image.tp_src) {
  49. if (cc.image instanceof ImageBitmap || cc.image instanceof HTMLImageElement || cc.image instanceof HTMLVideoElement) { // todo: support playback in video
  50. cc.image.tp_src = imageBitmapToBase64(cc.image, 160)
  51. } else if (cc.isRenderTargetTexture) {
  52. if (cc._target) {
  53. cc.image.tp_src = viewer.renderManager.renderTargetToDataUrl(cc._target)
  54. setTimeout(()=>cc.image.tp_src && delete cc.image.tp_src, 1000) // clear after 1 second so it refreshes on next render
  55. }
  56. } else {
  57. cc.image.tp_src = textureToDataUrl(cc, 160, false, 'image/png', 90) // this supports DataTexture also
  58. }
  59. if (!cc.image.tp_src) {
  60. if (cc.isRenderTargetTexture) cc.image.tp_src = staticData.renderTarImage
  61. else if (cc.isDataTexture) cc.image.tp_src = staticData.dataTexImage
  62. }
  63. }
  64. if (cc.image) {
  65. ret = cc.image.tp_src_uuid
  66. ret = ret ? staticData.imageMap[ret] : undefined
  67. if (!ret) ret = cc.image.tp_src || cc.image.src
  68. }
  69. } else if (typeof cc === 'string') {
  70. ret = cc
  71. } else if (cc.domainMin) { // for lut CUBE files.
  72. ret = cc.texture
  73. if (cc.texture.image && !cc.texture.image.tp_src) {
  74. cc.texture.image.tp_src = staticData.lutCubeTexImage
  75. }
  76. if (cc.texture.image) {
  77. ret = cc.texture.image.tp_src_uuid
  78. ret = ret ? staticData.imageMap[ret] : undefined
  79. if (!ret) ret = cc.texture.image.tp_src || cc.texture.image.src
  80. }
  81. } else if (cc) {
  82. console.error('unknown value', cc)
  83. }
  84. if (!ret) ret = staticData.placeholderVal
  85. if (cc.image && !cc.image.tp_src_uuid) {
  86. const uuid = generateUUID()
  87. cc.image.tp_src_uuid = uuid
  88. staticData.tempMap[ret] = uuid
  89. }
  90. // console.log(ret, cc, tar, key)
  91. if (typeof ret === 'string')
  92. ret = staticData.imageMap[ret] ?? ret // Note: this will be a bottleneck if the length of src is too long.
  93. return ret
  94. }
  95. const setterTex = (v1: any, config: UiObjectConfig, renderer: TweakpaneUiPlugin)=>{
  96. if (v1 && v1.isTexture) {
  97. if (!v1.isDataTexture) {
  98. const key = renderer.methods.getBinding(config)[1] + ''
  99. const isLinear = ['normalMap', 'aoMap', 'emissiveMap', 'roughnessMap', 'metalnessMap', 'displacementMap', 'bumpMap', 'alphaMap'].includes(key)
  100. v1.colorSpace = isLinear ? LinearSRGBColorSpace : SRGBColorSpace
  101. v1.wrapS = RepeatWrapping
  102. v1.wrapT = RepeatWrapping
  103. v1.flipY = config.__proxy.value_?.flipY ?? true // todo: figure out flipY
  104. } else {
  105. v1.needsUpdate = true
  106. }
  107. if (v1.image) {
  108. if (!v1.image.id?.length) v1.image.id = generateUUID()
  109. if (!staticData.textureMap[v1.image.id]) staticData.textureMap[v1.image.id] = v1
  110. }
  111. }
  112. config.__proxy.value_ = v1
  113. renderer.methods.setValue(config, v1, {last: true}, false)
  114. config.uiRefresh?.(false, 'postFrame')
  115. }
  116. function proxySetValue(v: any, cc: any, config: UiObjectConfig, viewer: ThreeViewer, renderer: TweakpaneUiPlugin) {
  117. if (typeof v === 'string') {
  118. if (typeof cc === 'string') setterTex(v, config, renderer)
  119. return
  120. }
  121. v = v || staticData.placeholderVal
  122. if ((v as any).isPlaceholder || v === staticData.placeholderVal) {
  123. if (cc) setterTex(typeof cc === 'string' ? '' : null, config, renderer)
  124. return
  125. }
  126. let iMapKey = v.tp_src_uuid
  127. if (!iMapKey) {
  128. iMapKey = v.src ?? v.tp_src
  129. iMapKey = staticData.tempMap[iMapKey] ?? iMapKey
  130. delete staticData.tempMap[iMapKey]
  131. v.tp_src_uuid = iMapKey
  132. }
  133. if (iMapKey)
  134. staticData.imageMap[iMapKey] = v
  135. // todo: dispose textures if not used.
  136. if (typeof cc === 'string') {
  137. setterTex(iMapKey, config, renderer)
  138. return
  139. }
  140. if (cc === v || cc && (
  141. cc.image === v
  142. || cc.image?.src === v.src
  143. || cc.image?.tp_src === v.tp_src && v.tp_src != null
  144. || cc.image?.tp_src === v.src && v.src != null
  145. || cc.image?.src === v.tp_src && v.tp_src != null
  146. )) return
  147. if (v instanceof File) { // v.src must be from createObjectURL.
  148. viewer.assetManager.importer.importSingle<ITexture>({file: v, path: (v as any).src}).then(texture => {
  149. if (!texture) return
  150. if (texture.isDataTexture) texture.needsUpdate = true
  151. const ext = (v as any).src?.split('?')?.[0]?.split('.').pop()
  152. if ((texture as any).userData) {
  153. if (!(texture as any).userData.mimeType)
  154. (texture as any).userData.mimeType = 'image/' + (['jpg', 'jpeg'].includes(ext) ? 'jpeg' : 'png')
  155. }
  156. setterTex(texture, config, renderer)
  157. })
  158. } else if (v.isTexture) {
  159. setterTex(v, config, renderer)
  160. } else { // HTMLImageElement, ImageBitmap, HTMLVideoElement
  161. let tex: ITexture = staticData.textureMap[v.id] || staticData.textureMap[v.src] || staticData.textureMap[v.tp_src]
  162. if (tex) {
  163. setterTex(tex, config, renderer)
  164. return
  165. }
  166. tex = new Texture(v)
  167. upgradeTexture.call(tex)
  168. tex.assetType = 'texture'
  169. tex.needsUpdate = true
  170. // set userData.mimeType for GLTFExporter
  171. const ext = v.src?.split('?')?.[0]?.split('.').pop()
  172. if (!tex.userData.mimeType)
  173. tex.userData.mimeType = 'image/' + (['jpg', 'jpeg'].includes(ext) ? 'jpeg' : 'png')
  174. setterTex(tex, config, renderer)
  175. // todo: make normal maps jpeg always? jpg is lossy
  176. }
  177. }
  178. function removeImage(config: UiObjectConfig, renderer: TweakpaneUiPlugin) {
  179. const vc = config.uiRef.controller_.valueController as any
  180. vc.value.setRawValue('')
  181. const isStr = typeof config.__proxy.value_ === 'string'
  182. setterTex(isStr ? '' : null, config, renderer)
  183. }
  184. function downloadImage(config: UiObjectConfig, _: TweakpaneUiPlugin, viewer: ThreeViewer) {
  185. CustomContextMenu.Remove()
  186. const tex: ITexture&Partial<ImportResultExtras> = config.__proxy.value_
  187. if (!tex) return
  188. let vcv = tex.image ?? config.uiRef.controller_.valueController.value.rawValue
  189. if (tex.__rootBlob && !tex.__rootBlob.objectUrl) tex.__rootBlob.objectUrl = URL.createObjectURL(tex.__rootBlob)
  190. let src = tex.__rootBlob ? tex.__rootBlob.objectUrl : tex.userData.rootPath || vcv?.src
  191. let revokeSrc = false
  192. // HTML image/video/bitmap
  193. if (vcv && (vcv instanceof ImageBitmap || vcv instanceof HTMLImageElement || vcv instanceof HTMLVideoElement) && !src)
  194. vcv = imageBitmapToBase64(vcv)
  195. let name = tex.__rootBlob ? tex.__rootBlob.name || 'image.' + (tex.__rootBlob.ext || 'png') : null
  196. // Render target texture
  197. if (!src && tex.isRenderTargetTexture) {
  198. const target1 = tex._target
  199. if (target1?.isWebGLRenderTarget) {
  200. const val = viewer.renderManager.exportRenderTarget(target1 as WebGLRenderTarget)
  201. if (!val) {
  202. console.error('cannot export render target', vcv, tex, target1, config)
  203. return
  204. }
  205. name = 'renderTarget.' + (val.ext || 'png')
  206. src = URL.createObjectURL(val)
  207. revokeSrc = true
  208. } else {
  209. console.error('Render target not supported', vcv, tex, target1, config)
  210. return
  211. }
  212. }
  213. // data texture
  214. if (!src && tex.isDataTexture) {
  215. if (tex.type !== HalfFloatType && tex.type !== FloatType) {
  216. // todo: use textureToCanvas for data texture
  217. console.error('Only Float and HalfFloat Data texture export is supported', vcv, tex, config)
  218. return
  219. }
  220. const buffer = new EXRExporter2().parse(undefined as any, tex as DataTexture&ITexture)
  221. const val: Blob|undefined = new Blob([buffer], {type: 'image/x-exr'})
  222. if (!val) {
  223. console.error('cannot export data texture', vcv, tex, config)
  224. return
  225. }
  226. name = 'dataTexture.exr'
  227. src = URL.createObjectURL(val)
  228. }
  229. if (!src) {
  230. console.error('cannot export image', vcv, tex, config)
  231. return
  232. }
  233. const link = document.createElement('a')
  234. document.body.appendChild(link)
  235. link.style.display = 'none'
  236. link.href = src
  237. link.download = name || (src.startsWith('data:') ? 'image.png' : src.split('/').pop() ?? 'image.png')
  238. link.target = '_blank'
  239. link.click()
  240. if (revokeSrc) setTimeout(()=>{
  241. document.body.removeChild(link)
  242. URL.revokeObjectURL(src)
  243. }, 1000)
  244. }
  245. async function imageFromUrl(renderer: TweakpaneUiPlugin, config: UiObjectConfig, viewer: ThreeViewer) {
  246. // let url: string|null = navigator.clipboard ? await navigator.clipboard.readText() : ''
  247. let url: string | null = ''
  248. // if (!url || !url.startsWith('http') && !url.startsWith('data:image')) {
  249. // url = ''
  250. // }
  251. url = await renderer.prompt('Load texture: Enter Image/Texture URL', url, true)
  252. if (!url || !url.startsWith('http') && !url.startsWith('data:image')) {
  253. if (url !== null) await renderer.alert('Loading Image: Invalid URL')
  254. return
  255. } else {
  256. url = url.trim()
  257. }
  258. const cc = config.__proxy.value_
  259. const isStr = typeof cc === 'string'
  260. if (isStr) {
  261. setterTex(url, config, renderer)
  262. } else { // texture
  263. viewer.assetManager.importer.importSingle<ITexture>(url).then(texture => {
  264. if (!texture) {
  265. console.warn('Failed to load texture', url)
  266. return
  267. }
  268. setterTex(texture, config, renderer)
  269. })
  270. }
  271. }
  272. export const tpImageInputGenerator = (viewer: ThreeViewer) => (parent: any /* FolderApi */, config: UiObjectConfig, renderer: TweakpaneUiPlugin, params?: any) => {
  273. // if (config.value !== undefined) throw 'Not supported yet'
  274. if (!config.__proxy) {
  275. config.__proxy = {
  276. listedOnChange: false,
  277. }
  278. Object.defineProperty(config.__proxy, 'value', {
  279. get: () => {
  280. config.__proxy.value_ = renderer.methods.getValue(config)
  281. const ret = proxyGetValue(config.__proxy.value_, viewer)
  282. if (typeof ret !== 'string' && !ret.id?.length) ret.id = generateUUID()
  283. const id = typeof ret === 'string' ? ret : ret.id ?? ret
  284. if (!staticData.textureMap[id]) staticData.textureMap[id] = config.__proxy.value_
  285. return ret
  286. },
  287. set: (v: any) => {
  288. if (getOrCall(config.readOnly)) return
  289. config.__proxy.value_ = renderer.methods.getValue(config) // current value
  290. proxySetValue(v, config.__proxy.value_, config, viewer, renderer)
  291. },
  292. })
  293. }
  294. config.__proxy.value_ = renderer.methods.getValue(config)
  295. params = params ?? {}
  296. params.extensions = ['.jpg', '.png', '.svg', '.hdr',
  297. '.exr', /* '.mp4', '.ogg', '.mov',*/ '.jpeg',
  298. '.bmp', '.gif', '.webp', '.cube']
  299. if (typeof params.imageFit === 'undefined') params.imageFit = 'contain'
  300. if (typeof params.clickCallback === 'undefined') params.clickCallback = (ev: MouseEvent, inp: HTMLInputElement) => {
  301. const target = ev?.target as HTMLElement
  302. const rect = target?.getBoundingClientRect()
  303. if (!rect) {
  304. inp.click()
  305. return
  306. }
  307. const cv = config.uiRef.controller_.valueController.value.rawValue
  308. const isPlaceholder = cv === staticData.placeholderVal || cv?.isPlaceholder
  309. const items: any = isPlaceholder ? {} : {
  310. ['download image']: () => downloadImage(config, renderer, viewer),
  311. }
  312. const readOnly = getOrCall(config.readOnly)
  313. if (!isPlaceholder && !readOnly) Object.assign(items, {
  314. ['remove image']: () => removeImage(config, renderer),
  315. })
  316. if (!readOnly) Object.assign(items, {
  317. ['set/replace image']: () => inp.click(),
  318. ['from url']: async() => imageFromUrl(renderer, config, viewer),
  319. })
  320. const menu = CustomContextMenu.Create({
  321. ...items,
  322. 'cancel': () => {return},
  323. }, 2, rect.height + 8, false, true)
  324. target.parentElement?.appendChild(menu)
  325. if (rect.y > document.body.clientHeight * 0.7) {
  326. menu.style.top = 'auto'
  327. menu.style.bottom = rect.height + 8 + 'px'
  328. }
  329. }
  330. params.view = 'input-image'
  331. return renderer.typeGenerators.input(parent, config, renderer, params)
  332. }