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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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): 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. if (!uncompressed) throw new Error('GLTFDracoExporter: gltf is null')
  77. let gltf = uncompressed
  78. const bytes = (gltf as ArrayBuffer).byteLength || Infinity
  79. const iDocument = await (typeof gltf === 'object' && !(gltf as any).byteLength ? this._io.readJSON({
  80. json: gltf as GLTF.IGLTF,
  81. resources: {},
  82. }) : this._io.readBinary(new Uint8Array(gltf as ArrayBuffer)))
  83. // iDocument.createExtension(GLTFViewerConfigExtensionGP)
  84. iDocument.createExtension(KHRDracoMeshCompression)
  85. .setRequired(true)
  86. .setEncoderOptions({...this._encoderOptions, ...dracoOptions ?? {}})
  87. if (ops.exportExt === 'glb') {
  88. gltf = await this._io.writeBinary(iDocument)
  89. if (isFinite(bytes)) {
  90. console.log('DRACO Compression ratio: ' + ((gltf as ArrayBuffer).byteLength / bytes).toFixed(5))
  91. }
  92. } else {
  93. const jDoc = await this._io.writeJSON(iDocument)
  94. gltf = jDoc.json
  95. if (Object.values(jDoc.resources).filter(v => v).length > 0) {
  96. console.warn('DRACOExporter: extra resources in resources not supported properly')
  97. ;(gltf as any).resources = jDoc.resources
  98. }
  99. }
  100. gltf.__isGLTFOutput = true
  101. const blob = await super.parseAsync(gltf, ops) as any // this will just convert it to blob because __isGLTFOutput is set (checked in GLTFExporter2)
  102. if (!blob) throw new Error('GLTFDracoExporter: blob is null')
  103. blob.ext = 'glb'
  104. ;(blob as any).__uncompressed = uncompressedBlob
  105. return blob
  106. }
  107. addExtension(extension: typeof Extension): this {
  108. this._io.registerExtensions([extension])
  109. return this
  110. }
  111. createAndAddExtension(name: string, textures?: Record<string, string|number>): this {
  112. return this.addExtension(createGenericExtensionClass(name, textures))
  113. }
  114. }
  115. declare module 'threepipe'{
  116. interface GLTFExporter2Options {
  117. compress?: boolean
  118. dracoOptions?: EncoderOptions
  119. }
  120. }
  121. // for @gltf-transform/core
  122. class ViewerJSONExtensionProperty extends ExtensionProperty {
  123. readonly extensionName: string = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
  124. readonly parentTypes: string[] = [PropertyType.SCENE]
  125. readonly propertyType: string = 'ViewerJSON'
  126. // eslint-disable-next-line @typescript-eslint/naming-convention
  127. protected init(): void {return}
  128. }
  129. class GLTFViewerConfigExtensionGP extends Extension {
  130. public readonly extensionName = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
  131. public static readonly EXTENSION_NAME = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
  132. private _viewerConfig: any = {}
  133. // private _texturesRef: [any, Texture][] = []
  134. read(context: ReaderContext): this {
  135. this._viewerConfig = {}
  136. context.jsonDoc.json.scenes?.forEach((sceneDef, sceneIndex)=>{
  137. if (sceneDef.extensions && sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension]) {
  138. const prop = new ViewerJSONExtensionProperty(this.document.getGraph())
  139. context.scenes[sceneIndex].setExtension(GLTFViewerConfigExtension.ViewerConfigGLTFExtension, prop)
  140. this._viewerConfig = sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension] as any
  141. // prop.setExtras()
  142. /*
  143. const buffers = [] as any[]
  144. Object.values(viewerConfig.resources).forEach((res: any) => {
  145. Object.values(res).forEach((item: any) => {
  146. if (!item.url) return
  147. if (item.url.data?.image !== null) {
  148. buffers.push(item.url)
  149. }
  150. })
  151. })
  152. const jsonDoc = context.jsonDoc
  153. console.log(buffers)
  154. for (const buffer of buffers) {
  155. const img = buffer.data.image as number
  156. const imageDef = jsonDoc.json.images![img]
  157. const bufferViewDef = jsonDoc.json.bufferViews![imageDef.bufferView!]
  158. const bufferDef = jsonDoc.json.buffers![bufferViewDef.buffer]
  159. const bufferData = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER]
  160. const byteOffset = bufferViewDef.byteOffset || 0
  161. const byteLength = bufferViewDef.byteLength
  162. const imageData = bufferData.slice(byteOffset, byteOffset + byteLength)
  163. const texture = this.document.createTexture(imageDef.name)
  164. texture.setImage(imageData)
  165. this._texturesRef.push([buffer, texture])
  166. }
  167. */
  168. }
  169. })
  170. return this
  171. }
  172. write(context: WriterContext): this {
  173. this.document.getRoot().listScenes().forEach((scene)=>{
  174. const prop = scene.getExtension(GLTFViewerConfigExtension.ViewerConfigGLTFExtension)
  175. if (prop) {
  176. const sceneDef = context.jsonDoc.json.scenes?.[context.jsonDoc.json.scene || 0] // todo: get proper scene index, if working with multiple scenes
  177. if (sceneDef && Object.keys(this._viewerConfig).length > 0) {
  178. sceneDef.extensions = sceneDef.extensions || {}
  179. /*
  180. console.log(context.jsonDoc.json.images)
  181. for (const [buffer, texture] of this._texturesRef) {
  182. const imageDef = context.createPropertyDef(texture) as GLTF.IImage
  183. context.createImageData(imageDef, texture.getImage()!, texture)
  184. buffer.data.image = context.jsonDoc.json.images!.push(imageDef) - 1
  185. context.imageIndexMap.set(texture, buffer.data.image)
  186. }
  187. console.log(context.jsonDoc.json)
  188. */
  189. sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension] = this._viewerConfig
  190. // this._texturesRef = []
  191. this._viewerConfig = {}
  192. }
  193. }
  194. })
  195. return this
  196. }
  197. required = true
  198. }
  199. class GenericExtensionProperty extends ExtensionProperty<any> {
  200. readonly extensionName: string
  201. readonly parentTypes: string[] = [PropertyType.MATERIAL, PropertyType.MESH, PropertyType.NODE, PropertyType.SCENE]
  202. readonly propertyType: string = 'GenericExtension'
  203. textures: Record<string, [TextureInfo, Texture|null]> = {}
  204. addTexture(key: string, texInfo: TextureInfo, texture: Texture | null, channels = 0x1111) {
  205. this.setRef(key, texture, {channels})
  206. this.textures[key] = [texInfo, texture]
  207. }
  208. constructor(graph: Graph<Property>, name: string, extensionName: string) {
  209. super(graph, name)
  210. this.extensionName = extensionName
  211. }
  212. // eslint-disable-next-line @typescript-eslint/naming-convention
  213. protected init(): void {return}
  214. }
  215. // see transmission extension for reference
  216. abstract class GenericExtension extends Extension {
  217. abstract readonly extensionName: string
  218. textureChannels: Record<string, number> = {}
  219. read(context: ReaderContext): this {
  220. const jsonDoc = context.jsonDoc
  221. // console.log(jsonDoc)
  222. const materialDefs = jsonDoc.json.materials || []
  223. const textureDefs = jsonDoc.json.textures || []
  224. materialDefs.forEach((materialDef, materialIndex) => {
  225. if (materialDef.extensions && materialDef.extensions[this.extensionName]) {
  226. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  227. context.materials[materialIndex].setExtension(this.extensionName, paramsExt)
  228. const paramsExtDef = materialDef.extensions[this.extensionName] as Record<string, any>
  229. const paramsExtDef2 = {...paramsExtDef}
  230. for (const [key, value] of Object.entries(paramsExtDef2)) {
  231. if (typeof value?.index === 'number') { // this is a texture...
  232. const textureInfoDef = value
  233. const source = textureDefs[textureInfoDef.index]?.source
  234. if (typeof source !== 'number') {
  235. console.warn('GLTF Pipeline: source texture not found for texture info', textureInfoDef)
  236. continue
  237. }
  238. const texture = context.textures[source]
  239. const texInfo = new TextureInfo(this.document.getGraph())
  240. const channels = this.textureChannels[key] ?? 0x1111
  241. paramsExt.addTexture(key, texInfo, texture, channels)
  242. context.setTextureInfo(texInfo, textureInfoDef)
  243. delete paramsExtDef2[key]
  244. }
  245. }
  246. paramsExt.setExtras(paramsExtDef2)
  247. // console.log({...paramsExtDef})
  248. }
  249. })
  250. const meshDefs = jsonDoc.json.meshes || []
  251. meshDefs.forEach((meshDef, meshIndex) => {
  252. if (meshDef.extensions && meshDef.extensions[this.extensionName]) {
  253. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  254. context.meshes[meshIndex].setExtension(this.extensionName, paramsExt)
  255. const paramsExtDef = meshDef.extensions[this.extensionName] as Record<string, any>
  256. paramsExt.setExtras(paramsExtDef)
  257. }
  258. })
  259. const nodeDefs = jsonDoc.json.nodes || []
  260. nodeDefs.forEach((nodeDef, nodeIndex) => {
  261. if (nodeDef.extensions && nodeDef.extensions[this.extensionName]) {
  262. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  263. context.nodes[nodeIndex].setExtension(this.extensionName, paramsExt)
  264. const paramsExtDef = nodeDef.extensions[this.extensionName] as Record<string, any>
  265. paramsExt.setExtras(paramsExtDef)
  266. // console.log(paramsExtDef)
  267. }
  268. })
  269. const sceneDefs = jsonDoc.json.scenes || []
  270. sceneDefs.forEach((sceneDef, sceneIndex) => {
  271. if (sceneDef.extensions && sceneDef.extensions[this.extensionName]) {
  272. const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
  273. context.scenes[sceneIndex].setExtension(this.extensionName, paramsExt)
  274. const paramsExtDef = sceneDef.extensions[this.extensionName] as Record<string, any>
  275. paramsExt.setExtras(paramsExtDef)
  276. // console.log(paramsExtDef)
  277. }
  278. })
  279. return this
  280. }
  281. write(context: WriterContext): this {
  282. const jsonDoc = context.jsonDoc
  283. this.document.getRoot()
  284. .listMaterials()
  285. .forEach((material) => {
  286. const paramsExt = material.getExtension<GenericExtensionProperty>(this.extensionName)
  287. // console.log(paramsExt)
  288. if (paramsExt) {
  289. const materialIndex = context.materialIndexMap.get(material)!
  290. const materialDef = jsonDoc.json.materials![materialIndex]
  291. materialDef.extensions = materialDef.extensions || {}
  292. const extensionDef = paramsExt.getExtras()
  293. const extensionDef2 = {...extensionDef}
  294. // console.log(paramsExt.textures)
  295. for (const [key, value] of Object.entries(paramsExt.textures)) {
  296. const textureInfo = value[0]
  297. const textureLink = value[1]
  298. const texture = textureLink
  299. if (texture)
  300. extensionDef2[key] = context.createTextureInfoDef(texture, textureInfo)
  301. // console.log(texture)
  302. }
  303. // console.log(extensionDef2)
  304. materialDef.extensions[this.extensionName] = extensionDef2
  305. }
  306. })
  307. this.document.getRoot()
  308. .listMeshes()
  309. .forEach((mesh) => {
  310. const paramsExt = mesh.getExtension<GenericExtensionProperty>(this.extensionName)
  311. if (paramsExt) {
  312. const meshIndex = context.meshIndexMap.get(mesh)!
  313. const meshDef = jsonDoc.json.meshes![meshIndex]
  314. meshDef.extensions = meshDef.extensions || {}
  315. meshDef.extensions[this.extensionName] = paramsExt.getExtras()
  316. }
  317. })
  318. this.document.getRoot()
  319. .listNodes()
  320. .forEach((node) => {
  321. const paramsExt = node.getExtension<GenericExtensionProperty>(this.extensionName)
  322. if (paramsExt) {
  323. const nodeIndex = context.nodeIndexMap.get(node)!
  324. const nodeDef = jsonDoc.json.nodes![nodeIndex]
  325. nodeDef.extensions = nodeDef.extensions || {}
  326. nodeDef.extensions[this.extensionName] = paramsExt.getExtras()
  327. }
  328. })
  329. this.document.getRoot()
  330. .listScenes()
  331. .forEach((scene) => {
  332. const paramsExt = scene.getExtension<GenericExtensionProperty>(this.extensionName)
  333. if (paramsExt) {
  334. const sceneIndex = context.jsonDoc.json.scene || 0 // todo: get proper scene index, if working with multiple scenes, this will do the default one.
  335. const sceneDef = jsonDoc.json.scenes![sceneIndex]
  336. if (!sceneDef) return
  337. sceneDef.extensions = sceneDef.extensions || {}
  338. sceneDef.extensions[this.extensionName] = paramsExt.getExtras()
  339. }
  340. })
  341. return this
  342. }
  343. }
  344. function stringToChannel(s: string) {
  345. let r = 0
  346. if (s.includes('R')) r |= TextureChannel.R
  347. if (s.includes('G')) r |= TextureChannel.G
  348. if (s.includes('B')) r |= TextureChannel.B
  349. if (s.includes('A')) r |= TextureChannel.A
  350. return r
  351. }
  352. export function createGenericExtensionClass(name: string, textures?: Record<string, string|number>): typeof GenericExtension {
  353. return class extends GenericExtension {
  354. public static readonly EXTENSION_NAME = name
  355. readonly extensionName = name
  356. textureChannels: Record<string, number> = !textures ? {} : Object.fromEntries(
  357. Object.entries(textures)
  358. .map(([k, v])=>
  359. [k, typeof v === 'number' ? v : stringToChannel(v)])
  360. )
  361. }
  362. }