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.

GLTFDracoExporter.ts 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import {
  2. Extension,
  3. ExtensionProperty,
  4. GLTF,
  5. Graph,
  6. Property,
  7. PropertyType,
  8. ReaderContext,
  9. Texture,
  10. TextureChannel,
  11. TextureInfo,
  12. WebIO,
  13. WriterContext,
  14. } from '@gltf-transform/core'
  15. import {EncoderOptions} from '@gltf-transform/extensions/dist/khr-draco-mesh-compression/encoder'
  16. import {ALL_EXTENSIONS, KHRDracoMeshCompression} from '@gltf-transform/extensions'
  17. import {DRACOLoader2, GLTFExporter2, GLTFExporter2Options, GLTFViewerConfigExtension, IExportParser} from 'threepipe'
  18. /**
  19. * GLTF Draco Exporter
  20. *
  21. * Extension of GLTFExporter2 that runs the output through gltf-transform for draco compression.
  22. */
  23. export class GLTFDracoExporter extends GLTFExporter2 implements IExportParser {
  24. public loader?: DRACOLoader2 // required for loading draco libs.
  25. private _io: WebIO
  26. private _loadedLibs = false
  27. private _encoderOptions: EncoderOptions
  28. constructor(encoderOptions?: EncoderOptions, loader?: DRACOLoader2) {
  29. super()
  30. encoderOptions = encoderOptions || {
  31. method: KHRDracoMeshCompression.EncoderMethod.EDGEBREAKER,
  32. encodeSpeed: 5,
  33. }
  34. this._io = new WebIO().registerExtensions(ALL_EXTENSIONS)
  35. .registerExtensions([
  36. GLTFViewerConfigExtensionGP,
  37. ])
  38. this._encoderOptions = encoderOptions
  39. if (loader) {
  40. this.loader = loader
  41. this.loader.setDecoderConfig({type: 'js'}) // todo: hack for now.
  42. this.loader.preload(true, true)
  43. }
  44. }
  45. preload(): this {
  46. this._loadLibs()
  47. return this
  48. }
  49. private async _loadLibs() {
  50. if (this._loadedLibs || !this.loader) return
  51. const libs = await Promise.all([
  52. this.loader.initEncoder(),
  53. this.loader.initDecoder(),
  54. ])
  55. this._io.registerDependencies({
  56. ['draco3d.encoder']: libs[0],
  57. ['draco3d.decoder']: libs[1], // only required if we are loading a draco compressed gltf
  58. })
  59. this._loadedLibs = true
  60. }
  61. async parseAsync(obj: any, {compress = false, dracoOptions, ...options}: {compress: boolean, dracoOptions?: EncoderOptions} & GLTFExporter2Options, throwOnError = false): Promise<Blob> {
  62. if (!this.loader) {
  63. console.error('GLTFDracoExporter: No DRACOLoader2 instance provided')
  64. return super.parseAsync(obj, options)
  65. }
  66. await this._loadLibs()
  67. const ops = {...options}
  68. if (compress) {
  69. // externalImagesInExtras: this is required because gltf-transform doesn't support external images in glb
  70. // see https://github.com/donmccurdy/glTF-Transform/discussions/644
  71. ops.externalImagesInExtras = true
  72. }
  73. const uncompressed = await new Promise((resolve, reject) => this.parse(obj, resolve, reject, ops)) as any
  74. const uncompressedBlob = await super.parseAsync(uncompressed, ops)
  75. if (!compress) return uncompressedBlob
  76. try {
  77. if (!uncompressed) throw new Error('GLTFDracoExporter: gltf is null')
  78. let gltf = uncompressed
  79. const bytes = (gltf as ArrayBuffer).byteLength || Infinity
  80. const iDocument = await (typeof gltf === 'object' && !(gltf as any).byteLength ? this._io.readJSON({
  81. json: gltf as GLTF.IGLTF,
  82. resources: {},
  83. }) : this._io.readBinary(new Uint8Array(gltf as ArrayBuffer)))
  84. // iDocument.createExtension(GLTFViewerConfigExtensionGP)
  85. iDocument.createExtension(KHRDracoMeshCompression)
  86. .setRequired(true)
  87. .setEncoderOptions({...this._encoderOptions, ...dracoOptions ?? {}})
  88. if (ops.exportExt === 'glb') {
  89. gltf = await this._io.writeBinary(iDocument)
  90. if (isFinite(bytes)) {
  91. console.log('DRACO Compression ratio: ' + ((gltf as ArrayBuffer).byteLength / bytes).toFixed(5))
  92. }
  93. } else {
  94. const jDoc = await this._io.writeJSON(iDocument)
  95. gltf = jDoc.json
  96. if (Object.values(jDoc.resources).filter(v => v).length > 0) {
  97. console.warn('DRACOExporter: extra resources in resources not supported properly')
  98. ;(gltf as any).resources = jDoc.resources
  99. }
  100. }
  101. gltf.__isGLTFOutput = true
  102. const blob = await super.parseAsync(gltf, ops) as any // this will just convert it to blob because __isGLTFOutput is set (checked in GLTFExporter2)
  103. if (!blob) throw new Error('GLTFDracoExporter: blob is null')
  104. blob.ext = 'glb'
  105. ;(blob as any).__uncompressed = uncompressedBlob
  106. return blob
  107. } catch (e) {
  108. if (throwOnError) throw e
  109. console.error('Unable to compress glb with DRACO extension, fallback to uncompressed')
  110. console.error(e)
  111. return uncompressedBlob
  112. }
  113. }
  114. addExtension(extension: typeof Extension): this {
  115. this._io.registerExtensions([extension])
  116. return this
  117. }
  118. createAndAddExtension(name: string, textures?: Record<string, string|number>): this {
  119. return this.addExtension(createGenericExtensionClass(name, textures))
  120. }
  121. }
  122. declare module 'threepipe'{
  123. interface GLTFExporter2Options {
  124. compress?: boolean
  125. dracoOptions?: EncoderOptions
  126. }
  127. }
  128. // for @gltf-transform/core
  129. class ViewerJSONExtensionProperty extends ExtensionProperty {
  130. readonly extensionName: string = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
  131. readonly parentTypes: string[] = [PropertyType.SCENE]
  132. readonly propertyType: string = 'ViewerJSON'
  133. // eslint-disable-next-line @typescript-eslint/naming-convention
  134. protected init(): void {return}
  135. }
  136. class GLTFViewerConfigExtensionGP extends Extension {
  137. public readonly extensionName = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
  138. public static readonly EXTENSION_NAME = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
  139. private _viewerConfig: any = {}
  140. // private _texturesRef: [any, Texture][] = []
  141. read(context: ReaderContext): this {
  142. this._viewerConfig = {}
  143. context.jsonDoc.json.scenes?.forEach((sceneDef, sceneIndex)=>{
  144. if (sceneDef.extensions && sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension]) {
  145. const prop = new ViewerJSONExtensionProperty(this.document.getGraph())
  146. context.scenes[sceneIndex].setExtension(GLTFViewerConfigExtension.ViewerConfigGLTFExtension, prop)
  147. this._viewerConfig = sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension] as any
  148. // prop.setExtras()
  149. /*
  150. const buffers = [] as any[]
  151. Object.values(viewerConfig.resources).forEach((res: any) => {
  152. Object.values(res).forEach((item: any) => {
  153. if (!item.url) return
  154. if (item.url.data?.image !== null) {
  155. buffers.push(item.url)
  156. }
  157. })
  158. })
  159. const jsonDoc = context.jsonDoc
  160. console.log(buffers)
  161. for (const buffer of buffers) {
  162. const img = buffer.data.image as number
  163. const imageDef = jsonDoc.json.images![img]
  164. const bufferViewDef = jsonDoc.json.bufferViews![imageDef.bufferView!]
  165. const bufferDef = jsonDoc.json.buffers![bufferViewDef.buffer]
  166. const bufferData = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER]
  167. const byteOffset = bufferViewDef.byteOffset || 0
  168. const byteLength = bufferViewDef.byteLength
  169. const imageData = bufferData.slice(byteOffset, byteOffset + byteLength)
  170. const texture = this.document.createTexture(imageDef.name)
  171. texture.setImage(imageData)
  172. this._texturesRef.push([buffer, texture])
  173. }
  174. */
  175. }
  176. })
  177. return this
  178. }
  179. write(context: WriterContext): this {
  180. this.document.getRoot().listScenes().forEach((scene)=>{
  181. const prop = scene.getExtension(GLTFViewerConfigExtension.ViewerConfigGLTFExtension)
  182. if (prop) {
  183. const sceneDef = context.jsonDoc.json.scenes?.[context.jsonDoc.json.scene || 0] // todo: get proper scene index, if working with multiple scenes
  184. if (sceneDef && Object.keys(this._viewerConfig).length > 0) {
  185. sceneDef.extensions = sceneDef.extensions || {}
  186. /*
  187. console.log(context.jsonDoc.json.images)
  188. for (const [buffer, texture] of this._texturesRef) {
  189. const imageDef = context.createPropertyDef(texture) as GLTF.IImage
  190. context.createImageData(imageDef, texture.getImage()!, texture)
  191. buffer.data.image = context.jsonDoc.json.images!.push(imageDef) - 1
  192. context.imageIndexMap.set(texture, buffer.data.image)
  193. }
  194. console.log(context.jsonDoc.json)
  195. */
  196. sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension] = this._viewerConfig
  197. // this._texturesRef = []
  198. this._viewerConfig = {}
  199. }
  200. }
  201. })
  202. return this
  203. }
  204. required = true
  205. }
  206. class GenericExtensionProperty extends ExtensionProperty<any> {
  207. readonly extensionName: string
  208. readonly parentTypes: string[] = [PropertyType.MATERIAL, PropertyType.MESH, PropertyType.NODE, PropertyType.SCENE]
  209. readonly propertyType: string = 'GenericExtension'
  210. textures: Record<string, [TextureInfo, Texture|null]> = {}
  211. addTexture(key: string, texInfo: TextureInfo, texture: Texture | null, channels = 0x1111) {
  212. this.setRef(key, texture, {channels})
  213. this.textures[key] = [texInfo, texture]
  214. }
  215. constructor(graph: Graph<Property>, name: string, extensionName: string) {
  216. super(graph, name)
  217. this.extensionName = extensionName
  218. }
  219. // eslint-disable-next-line @typescript-eslint/naming-convention
  220. protected init(): void {return}
  221. }
  222. // see transmission extension for reference
  223. abstract class GenericExtension extends Extension {
  224. abstract readonly extensionName: string
  225. textureChannels: Record<string, number> = {}
  226. read(context: ReaderContext): this {
  227. const jsonDoc = context.jsonDoc
  228. // console.log(jsonDoc)
  229. const materialDefs = jsonDoc.json.materials || []
  230. const textureDefs = jsonDoc.json.textures || []
  231. materialDefs.forEach((materialDef, materialIndex) => {
  232. if (materialDef.extensions && materialDef.extensions[this.extensionName]) {
  233. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  234. context.materials[materialIndex].setExtension(this.extensionName, paramsExt)
  235. const paramsExtDef = materialDef.extensions[this.extensionName] as Record<string, any>
  236. const paramsExtDef2 = {...paramsExtDef}
  237. for (const [key, value] of Object.entries(paramsExtDef2)) {
  238. if (typeof value?.index === 'number') { // this is a texture...
  239. const textureInfoDef = value
  240. const source = textureDefs[textureInfoDef.index]?.source
  241. if (typeof source !== 'number') {
  242. console.warn('GLTF Pipeline: source texture not found for texture info', textureInfoDef)
  243. continue
  244. }
  245. const texture = context.textures[source]
  246. const texInfo = new TextureInfo(this.document.getGraph())
  247. const channels = this.textureChannels[key] ?? 0x1111
  248. paramsExt.addTexture(key, texInfo, texture, channels)
  249. context.setTextureInfo(texInfo, textureInfoDef)
  250. delete paramsExtDef2[key]
  251. }
  252. }
  253. paramsExt.setExtras(paramsExtDef2)
  254. // console.log({...paramsExtDef})
  255. }
  256. })
  257. const meshDefs = jsonDoc.json.meshes || []
  258. meshDefs.forEach((meshDef, meshIndex) => {
  259. if (meshDef.extensions && meshDef.extensions[this.extensionName]) {
  260. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  261. context.meshes[meshIndex].setExtension(this.extensionName, paramsExt)
  262. const paramsExtDef = meshDef.extensions[this.extensionName] as Record<string, any>
  263. paramsExt.setExtras(paramsExtDef)
  264. }
  265. })
  266. const nodeDefs = jsonDoc.json.nodes || []
  267. nodeDefs.forEach((nodeDef, nodeIndex) => {
  268. if (nodeDef.extensions && nodeDef.extensions[this.extensionName]) {
  269. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  270. context.nodes[nodeIndex].setExtension(this.extensionName, paramsExt)
  271. const paramsExtDef = nodeDef.extensions[this.extensionName] as Record<string, any>
  272. paramsExt.setExtras(paramsExtDef)
  273. // console.log(paramsExtDef)
  274. }
  275. })
  276. const sceneDefs = jsonDoc.json.scenes || []
  277. sceneDefs.forEach((sceneDef, sceneIndex) => {
  278. if (sceneDef.extensions && sceneDef.extensions[this.extensionName]) {
  279. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  280. context.scenes[sceneIndex].setExtension(this.extensionName, paramsExt)
  281. const paramsExtDef = sceneDef.extensions[this.extensionName] as Record<string, any>
  282. paramsExt.setExtras(paramsExtDef)
  283. // console.log(paramsExtDef)
  284. }
  285. })
  286. return this
  287. }
  288. write(context: WriterContext): this {
  289. const jsonDoc = context.jsonDoc
  290. this.document.getRoot()
  291. .listMaterials()
  292. .forEach((material) => {
  293. const paramsExt = material.getExtension<GenericExtensionProperty>(this.extensionName)
  294. // console.log(paramsExt)
  295. if (paramsExt) {
  296. const materialIndex = context.materialIndexMap.get(material)!
  297. const materialDef = jsonDoc.json.materials![materialIndex]
  298. materialDef.extensions = materialDef.extensions || {}
  299. const extensionDef = paramsExt.getExtras()
  300. const extensionDef2 = {...extensionDef}
  301. // console.log(paramsExt.textures)
  302. for (const [key, value] of Object.entries(paramsExt.textures)) {
  303. const textureInfo = value[0]
  304. const textureLink = value[1]
  305. const texture = textureLink
  306. if (texture)
  307. extensionDef2[key] = context.createTextureInfoDef(texture, textureInfo)
  308. // console.log(texture)
  309. }
  310. // console.log(extensionDef2)
  311. materialDef.extensions[this.extensionName] = extensionDef2
  312. }
  313. })
  314. this.document.getRoot()
  315. .listMeshes()
  316. .forEach((mesh) => {
  317. const paramsExt = mesh.getExtension<GenericExtensionProperty>(this.extensionName)
  318. if (paramsExt) {
  319. const meshIndex = context.meshIndexMap.get(mesh)!
  320. const meshDef = jsonDoc.json.meshes![meshIndex]
  321. meshDef.extensions = meshDef.extensions || {}
  322. meshDef.extensions[this.extensionName] = paramsExt.getExtras()
  323. }
  324. })
  325. this.document.getRoot()
  326. .listNodes()
  327. .forEach((node) => {
  328. const paramsExt = node.getExtension<GenericExtensionProperty>(this.extensionName)
  329. if (paramsExt) {
  330. const nodeIndex = context.nodeIndexMap.get(node)!
  331. const nodeDef = jsonDoc.json.nodes![nodeIndex]
  332. nodeDef.extensions = nodeDef.extensions || {}
  333. nodeDef.extensions[this.extensionName] = paramsExt.getExtras()
  334. }
  335. })
  336. this.document.getRoot()
  337. .listScenes()
  338. .forEach((scene) => {
  339. const paramsExt = scene.getExtension<GenericExtensionProperty>(this.extensionName)
  340. if (paramsExt) {
  341. const sceneIndex = context.jsonDoc.json.scene || 0 // todo: get proper scene index, if working with multiple scenes, this will do the default one.
  342. const sceneDef = jsonDoc.json.scenes![sceneIndex]
  343. if (!sceneDef) return
  344. sceneDef.extensions = sceneDef.extensions || {}
  345. sceneDef.extensions[this.extensionName] = paramsExt.getExtras()
  346. }
  347. })
  348. return this
  349. }
  350. }
  351. function stringToChannel(s: string) {
  352. let r = 0
  353. if (s.includes('R')) r |= TextureChannel.R
  354. if (s.includes('G')) r |= TextureChannel.G
  355. if (s.includes('B')) r |= TextureChannel.B
  356. if (s.includes('A')) r |= TextureChannel.A
  357. return r
  358. }
  359. export function createGenericExtensionClass(name: string, textures?: Record<string, string|number>): typeof GenericExtension {
  360. return class extends GenericExtension {
  361. public static readonly EXTENSION_NAME = name
  362. readonly extensionName = name
  363. textureChannels: Record<string, number> = !textures ? {} : Object.fromEntries(
  364. Object.entries(textures)
  365. .map(([k, v])=>
  366. [k, typeof v === 'number' ? v : stringToChannel(v)])
  367. )
  368. }
  369. }