Переглянути джерело

Add EXRExporter2, create render target refactor, fix renderTargetToDataUrl, renderTargetToBuffer, add exportRenderTarget(png/jpeg and exr). Add support for WebGLRenderTarget export in AssetExporter.

master
Palash Bansal 3 роки тому
джерело
коміт
7640603ba7
Аккаунт користувача з таким Email не знайдено

+ 24
- 11
src/assetmanager/AssetExporter.ts Переглянути файл

import {BaseEvent, EventDispatcher} from 'three'
import {BaseEvent, EventDispatcher, WebGLRenderTarget} from 'three'
import {IMaterial, IObject3D, ITexture} from '../core' import {IMaterial, IObject3D, ITexture} from '../core'
import {AnyOptions} from 'ts-browser-helpers' import {AnyOptions} from 'ts-browser-helpers'
import {BlobExt, ExportFileOptions, IAssetExporter, IExporter, IExportParser} from './IExporter' import {BlobExt, ExportFileOptions, IAssetExporter, IExporter, IExportParser} from './IExporter'
import {SimpleTextExporter} from './export/SimpleTextExporter'
import {SimpleJSONExporter} from './export/SimpleJSONExporter'
import {EXRExporter2, SimpleJSONExporter, SimpleTextExporter} from './export'
import {IRenderTarget} from '../rendering'


export class AssetExporter extends EventDispatcher<BaseEvent, 'exporterCreate' | 'exportFile'> implements IAssetExporter { export class AssetExporter extends EventDispatcher<BaseEvent, 'exporterCreate' | 'exportFile'> implements IAssetExporter {
readonly exporters: IExporter[] = [ readonly exporters: IExporter[] = [
{ctor: ()=>new SimpleJSONExporter(), ext: ['json']}, {ctor: ()=>new SimpleJSONExporter(), ext: ['json']},
{ctor: ()=>new SimpleTextExporter(), ext: ['txt', 'text']}, {ctor: ()=>new SimpleTextExporter(), ext: ['txt', 'text']},
{ctor: ()=>new EXRExporter2(), ext: ['exr']},
// {ctor: ()=>new GLTFDracoExporter(), ext: ['gltf', 'glb']}, // {ctor: ()=>new GLTFDracoExporter(), ext: ['gltf', 'glb']},
] ]


super() super()
} }


public async exportObject(obj?: IObject3D|IMaterial|ITexture, options: ExportFileOptions = {}): Promise<BlobExt|undefined> {
public async exportObject(obj?: IObject3D|IMaterial|ITexture|IRenderTarget, options: ExportFileOptions = {}): Promise<BlobExt|undefined> {
if (!obj?.assetType) { if (!obj?.assetType) {
console.error('Object has no asset type') console.error('Object has no asset type')
return undefined return undefined
} }


// export to blob // export to blob
private async _exportFile(obj: IObject3D|IMaterial|ITexture, options: ExportFileOptions = {}): Promise<BlobExt|undefined> {
private async _exportFile(obj: IObject3D|IMaterial|ITexture|IRenderTarget, options: ExportFileOptions = {}): Promise<BlobExt|undefined> {
// if ((file as any)?.__imported) return (file as any).__imported // todo: cache exports? // if ((file as any)?.__imported) return (file as any).__imported // todo: cache exports?


let res: BlobExt let res: BlobExt
const processed = await this.processBeforeExport(obj, options) const processed = await this.processBeforeExport(obj, options)
const ext = options.exportExt ?? processed?.typeExt ?? processed?.ext const ext = options.exportExt ?? processed?.typeExt ?? processed?.ext
if (!processed || !ext) throw new Error(`Unable to preprocess before export ${ext}`) if (!processed || !ext) throw new Error(`Unable to preprocess before export ${ext}`)
const parser = this._getParser(ext)
if (processed.blob) res = processed.blob
else {
const parser = this._getParser(ext)


this.dispatchEvent({type: 'exportFile', obj, state:'exporting'})
const blob = await parser.parseAsync(processed.obj, {exportExt: processed.ext ?? ext, ...options}) as BlobExt
blob.ext = processed.ext
res = blob
this.dispatchEvent({type: 'exportFile', obj, state:'exporting'})
res = await parser.parseAsync(processed.obj, {exportExt: processed.ext ?? ext, ...options}) as BlobExt
res.ext = processed.ext
}


this.dispatchEvent({type: 'exportFile', obj, state: 'done'}) this.dispatchEvent({type: 'exportFile', obj, state: 'done'})


return this._cachedParsers.find(e => e.ext.includes(ext))?.parser ?? this._createParser(ext) return this._cachedParsers.find(e => e.ext.includes(ext))?.parser ?? this._createParser(ext)
} }


public async processBeforeExport(obj: IObject3D|IMaterial|ITexture, _: AnyOptions = {}): Promise<{obj:any, ext:string, typeExt?:string}|undefined> {
public async processBeforeExport(obj: IObject3D|IMaterial|ITexture|IRenderTarget, _: AnyOptions = {}): Promise<{obj:any, ext:string, typeExt?:string, blob?: BlobExt}|undefined> {
// if (obj.assetExporterProcessed && !options.forceExporterReprocess) return obj //todo;;; // if (obj.assetExporterProcessed && !options.forceExporterReprocess) return obj //todo;;;


switch (obj.assetType) { switch (obj.assetType) {
return {obj: (obj as IMaterial).toJSON(), ext: (obj as IMaterial).constructor?.TypeSlug || 'json', typeExt: 'json'} return {obj: (obj as IMaterial).toJSON(), ext: (obj as IMaterial).constructor?.TypeSlug || 'json', typeExt: 'json'}
case 'texture': case 'texture':
return {obj: (obj as ITexture).toJSON(), ext: 'json'} return {obj: (obj as ITexture).toJSON(), ext: 'json'}
case 'renderTarget':
if (obj.isWebGLMultipleRenderTargets) console.error('AssetExporter: WebGLMultipleRenderTargets export not supported')
else if (!obj.renderManager) return {obj, ext: 'exr'}
else {
const blob = obj.renderManager.exportRenderTarget(obj as WebGLRenderTarget, 'auto')
return {
obj, ext: blob.ext, blob,
}
}
break
default: default:
console.error('AssetExporter: unknown asset type', obj.assetType) console.error('AssetExporter: unknown asset type', obj.assetType)
} }

+ 136
- 53
src/rendering/RenderManager.ts Переглянути файл

import { import {
Blending, Blending,
Color, Color,
FloatType,
HalfFloatType, HalfFloatType,
IUniform, IUniform,
NoBlending, NoBlending,
Vector4, Vector4,
WebGLRenderer, WebGLRenderer,
WebGLRenderTarget, WebGLRenderTarget,
WebGLRenderTargetOptions,
} from 'three' } from 'three'
import {EffectComposer2, IPassID, IPipelinePass, sortPasses} from '../postprocessing' import {EffectComposer2, IPassID, IPipelinePass, sortPasses} from '../postprocessing'
import {IRenderTarget} from './RenderTarget' import {IRenderTarget} from './RenderTarget'
IWebGLRenderer, IWebGLRenderer,
upgradeWebGLRenderer, upgradeWebGLRenderer,
} from '../core' } from '../core'
import {onChange2, serializable, serialize} from 'ts-browser-helpers'
import {base64ToArrayBuffer, Class, onChange2, serializable, serialize, ValOrArr} from 'ts-browser-helpers'
import {uiConfig, uiFolderContainer, uiMonitor, uiSlider, uiToggle} from 'uiconfig.js' import {uiConfig, uiFolderContainer, uiMonitor, uiSlider, uiToggle} from 'uiconfig.js'
import {generateUUID} from '../three'
import {textureDataToImageData} from '../three/utils/texture'
import {EXRExporter2} from '../assetmanager/export/EXRExporter2'
import {BlobExt} from '../assetmanager'


@serializable('RenderManager') @serializable('RenderManager')
@uiFolderContainer('Render Manager') @uiFolderContainer('Render Manager')
// if (material.uniforms.currentFrameCount) material.uniforms.currentFrameCount.value = this.frameCount // if (material.uniforms.currentFrameCount) material.uniforms.currentFrameCount.value = this.frameCount
if (!this.stableNoise) { if (!this.stableNoise) {
if (material.uniforms.frameCount) material.uniforms.frameCount.value = this._totalFrameCount if (material.uniforms.frameCount) material.uniforms.frameCount.value = this._totalFrameCount
else console.warn('BaseRenderer: no uniform: frameCount')
else console.warn('RenderManager: no uniform: frameCount')
} else { } else {
if (material.uniforms.frameCount) material.uniforms.frameCount.value = this.frameCount if (material.uniforms.frameCount) material.uniforms.frameCount.value = this.frameCount
else console.warn('BaseRenderer: no uniform: frameCount')
else console.warn('RenderManager: no uniform: frameCount')
} }
return this return this
} }
} }
} }


/**
* Only to be used for testing. To do it properly, render the target to the main canvas(with proper encoding and type conversion) and call canvas.toDataURL()
* @param target
* @param mimeType
* @param quality
*/
renderTargetToDataUrl(target: WebGLRenderTarget, mimeType = 'image/png', quality = 90): string {
const canvas = document.createElement('canvas')
canvas.width = target.width
canvas.height = target.height
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Unable to get 2d context')
const imageData = ctx.createImageData(target.width, target.height, {colorSpace: ['display-p3', 'srgb'].includes(target.texture.colorSpace) ? <PredefinedColorSpace>target.texture.colorSpace : undefined})
if (target.texture.type === HalfFloatType) {
const buffer = new Uint16Array(target.width * target.height * 4)
this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buffer)
for (let i = 0; i < buffer.length; i++) {
imageData.data[i] = buffer[i] / 15360 * 255 // todo check packing
}
} else {
// todo: handle rgbm to srgb conversion?
this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, imageData.data)
}
ctx.putImageData(imageData, 0, 0)
const string = canvas.toDataURL(mimeType, quality)
canvas.remove()
return string
}

renderTargetToBuffer(target: WebGLRenderTarget): Uint8Array|Uint16Array {
const buffer = target.texture.type === HalfFloatType ?
new Uint16Array(target.width * target.height * 4) :
new Uint8Array(target.width * target.height * 4)
this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buffer)
return buffer
}

// endregion // endregion


// region Getters and Setters // region Getters and Setters
set pipeline(value: IPassID[]) { set pipeline(value: IPassID[]) {
this._pipeline = value this._pipeline = value
if (this.autoBuildPipeline) { if (this.autoBuildPipeline) {
console.warn('BaseRenderer: pipeline changed, but autoBuildPipeline is true. This will not have any effect.')
console.warn('RenderManager: pipeline changed, but autoBuildPipeline is true. This will not have any effect.')
} }
this.rebuildPipeline() this.rebuildPipeline()
} }


// endregion // endregion


// region Events Dispatch

private _updated(data?: Partial<IRenderManagerUpdateEvent>) {
this.dispatchEvent({...data, type: 'update'})
}

// endregion



// / TODO

// region Utils


/** /**
* *
} }




/**
* Converts a render target to a png/jpeg data url string.
* @param target
* @param mimeType
* @param quality
*/
renderTargetToDataUrl(target: WebGLRenderTarget, mimeType = 'image/png', quality = 90): string {
const canvas = document.createElement('canvas')
canvas.width = target.width
canvas.height = target.height
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Unable to get 2d context')
const imageData = ctx.createImageData(target.width, target.height, {colorSpace: ['display-p3', 'srgb'].includes(target.texture.colorSpace) ? <PredefinedColorSpace>target.texture.colorSpace : undefined})
if (target.texture.type === HalfFloatType || target.texture.type === FloatType) {
const buffer = this.renderTargetToBuffer(target)
textureDataToImageData({data: buffer, width: target.width, height: target.height}, target.texture.colorSpace, imageData) // this handles converting to srgb
} else {
// todo: handle rgbm to srgb conversion?
this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, imageData.data)
}

ctx.putImageData(imageData, 0, 0)
const string = canvas.toDataURL(mimeType, quality)
canvas.remove()
return string
}

renderTargetToBuffer(target: WebGLRenderTarget): Uint8Array|Uint16Array|Float32Array {
const buffer =
target.texture.type === HalfFloatType ?
new Uint16Array(target.width * target.height * 4) :
target.texture.type === FloatType ?
new Float32Array(target.width * target.height * 4) :
new Uint8Array(target.width * target.height * 4)
this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buffer)
return buffer
}

exportRenderTarget(target: WebGLRenderTarget, mimeType = 'auto'): BlobExt {
const hdrFormats = ['image/x-exr']
let hdr = target.texture.type === HalfFloatType || target.texture.type === FloatType
if (mimeType === 'auto') {
mimeType = hdr ? 'image/x-exr' : 'image/png'
}
if (!hdrFormats.includes(mimeType)) hdr = false
let buffer: ArrayBufferLike
if (!hdr) {
const url = this.renderTargetToDataUrl(target, mimeType === 'auto' ? undefined : mimeType)
buffer = base64ToArrayBuffer(url.split(',')[1])
mimeType = url.split(';')[0].split(':')[1]
} else {
if (mimeType !== 'image/x-exr') {
console.warn('RenderManager: mimeType ', mimeType, ' is not supported for HDR. Using EXR instead')
mimeType = 'image/x-exr'
}
const exporter = new EXRExporter2()
buffer = exporter.parse(this._renderer, target)
}
const b = new Blob([buffer], {type: mimeType}) as BlobExt
b.ext = mimeType === 'image/x-exr' ? 'exr' : mimeType.split('/')[1]
return b
}

// endregion


// region Events Dispatch

private _updated(data?: Partial<IRenderManagerUpdateEvent>) {
this.dispatchEvent({...data, type: 'update'})
}

// endregion

protected _createTargetClass(clazz: Class<WebGLRenderTarget>, size: number[], options: WebGLRenderTargetOptions): IRenderTarget {
const processNewTarget = this._processNewTarget
return new class RenderTarget extends clazz implements IRenderTarget {
isTemporary?: boolean
sizeMultiplier?: number
uuid: string
readonly assetType = 'renderTarget'
name = 'RenderTarget'
// @ts-expect-error because WebGLRenderTarget does not have texture as array
texture: ValOrArr<Texture&{_target: IRenderTarget}>

constructor(public readonly renderManager: IRenderManager, ...ps: any[]) {
super(...ps)
this.uuid = generateUUID()
const ops = ps[ps.length - 1] as WebGLRenderTargetOptions
if (Array.isArray(this.texture)) {
this.texture.forEach(t => {
if (ops.colorSpace !== undefined) t.colorSpace = ops.colorSpace
t._target = this
t.toJSON = () => {
console.warn('Multiple render target texture.toJSON not supported yet.')
return {}
}
})
} else {
this.texture._target = this
this.texture.toJSON = () => ({ // todo use readRenderTargetPixels as data url or data buffer.
isRenderTargetTexture: true,
}) // so that it doesn't get serialized
}
}

setSize(w: number, h: number, depth?: number) {
super.setSize(Math.floor(w), Math.floor(h), depth)
// console.log('setSize', w, h, depth)
return this
}

clone(trackTarget = true): any {
if (this.isTemporary) throw 'Cloning temporary render targets not supported'
if (Array.isArray(this.texture)) throw 'Cloning multiple render targets not supported'
// Note: todo: webgl render target.clone messes up the texture, by not copying isRenderTargetTexture prop and maybe some other stuff. So its better to just create a new one
const cloned = super.clone() as IRenderTarget
const tex = cloned.texture
if (Array.isArray(tex)) tex.forEach(t => t.isRenderTargetTexture = true)
else tex.isRenderTargetTexture = true
return processNewTarget(cloned, this.sizeMultiplier || 1, trackTarget)
}
}(this, ...size, options)
}

/** /**
* @deprecated use renderScale instead * @deprecated use renderScale instead
*/ */

+ 6
- 1
src/rendering/RenderTarget.ts Переглянути файл

} from 'three' } from 'three'
import {Vector4} from 'three/src/math/Vector4' import {Vector4} from 'three/src/math/Vector4'
import {DepthTexture} from 'three/src/textures/DepthTexture' import {DepthTexture} from 'three/src/textures/DepthTexture'
import type {IRenderManager} from '../core'
import {ValOrArr} from 'ts-browser-helpers'


export interface IRenderTarget extends EventDispatcher { export interface IRenderTarget extends EventDispatcher {
isWebGLRenderTarget: boolean isWebGLRenderTarget: boolean
width: number width: number
height: number height: number
depth: number depth: number
assetType?: 'renderTarget'
name?: string


texture: Texture | Texture[]
texture: ValOrArr<Texture&{_target?: IRenderTarget}>
uuid?: string uuid?: string
sizeMultiplier?: number sizeMultiplier?: number
isTemporary?: boolean isTemporary?: boolean
isWebGLCubeRenderTarget?: boolean isWebGLCubeRenderTarget?: boolean
isWebGLMultipleRenderTargets?: boolean isWebGLMultipleRenderTargets?: boolean


readonly renderManager?: IRenderManager
} }


export interface CreateRenderTargetOptions { export interface CreateRenderTargetOptions {

+ 5
- 47
src/rendering/RenderTargetManager.ts Переглянути файл

WebGLRenderTarget, WebGLRenderTarget,
WebGLRenderTargetOptions, WebGLRenderTargetOptions,
} from 'three' } from 'three'
import {generateUUID} from '../three'


export abstract class RenderTargetManager<E extends BaseEvent = BaseEvent, ET extends string = string> extends EventDispatcher<E, ET> { export abstract class RenderTargetManager<E extends BaseEvent = BaseEvent, ET extends string = string> extends EventDispatcher<E, ET> {
abstract isWebGL2: boolean abstract isWebGL2: boolean
height, height,
count, count,
}: {width: number, height: number, count?: number}, options: WebGLRenderTargetOptions = {}, clazz?: Class<T>): T { }: {width: number, height: number, count?: number}, options: WebGLRenderTargetOptions = {}, clazz?: Class<T>): T {
const processNewTarget = this._processNewTarget
let size = [width, height] let size = [width, height]
if (count && count > 1) size.push(count) if (count && count > 1) size.push(count)


if (width !== height) throw 'Width and height of cube render target must be equal' if (width !== height) throw 'Width and height of cube render target must be equal'
size = [width] size = [width]
} }
const params = [...size, {
return this._createTargetClass((clazz as any) ?? WebGLRenderTarget, size, {
format: RGBAFormat, format: RGBAFormat,
minFilter: LinearFilter, minFilter: LinearFilter,
magFilter: LinearFilter, magFilter: LinearFilter,
type: UnsignedByteType, type: UnsignedByteType,
colorSpace: NoColorSpace, colorSpace: NoColorSpace,
...options, ...options,
}]
return new class RenderTarget extends ((clazz as any as Class<WebGLRenderTarget>) ?? WebGLRenderTarget) implements IRenderTarget {
isTemporary?: boolean
sizeMultiplier?: number
uuid: string

constructor(...ps: any[]) {
super(...ps)
this.uuid = generateUUID()
const ops = ps[ps.length - 1] as WebGLRenderTargetOptions
if (Array.isArray(this.texture)) {
this.texture.forEach(t => {
t.colorSpace = ops.colorSpace
t.toJSON = () => {
console.warn('Multiple render target texture.toJSON not supported yet.')
return {}
}
})
} else {
this.texture.toJSON = () => ({ // todo use readRenderTargetPixels as data url or data buffer.
isRenderTargetTexture: true,
}) // so that it doesn't get serialized
}
}

setSize(w: number, h: number, depth?: number) {
super.setSize(Math.floor(w), Math.floor(h), depth)
// console.log('setSize', w, h, depth)
return this
}

clone(trackTarget = true): any {
if (this.isTemporary) throw 'Cloning temporary render targets not supported'
if (Array.isArray(this.texture)) throw 'Cloning multiple render targets not supported'
// Note: todo: webgl render target.clone messes up the texture, by not copying isRenderTargetTexture prop and maybe some other stuff. So its better to just create a new one
const cloned = super.clone() as IRenderTarget
const tex = cloned.texture
if (Array.isArray(tex)) tex.forEach(t => t.isRenderTargetTexture = true)
else tex.isRenderTargetTexture = true
return processNewTarget(cloned, this.sizeMultiplier || 1, trackTarget)
}

}(...params) as any as T
}) as T
} }


protected abstract _createTargetClass(clazz: Class<WebGLRenderTarget>, size: number[], options: WebGLRenderTargetOptions): IRenderTarget

dispose() { dispose() {
this._trackedTargets.forEach(t=>t.dispose()) this._trackedTargets.forEach(t=>t.dispose())
Object.values(this._trackedTempTargets).forEach(t=>t.dispose()) Object.values(this._trackedTempTargets).forEach(t=>t.dispose())
texture.minFilter = LinearFilter texture.minFilter = LinearFilter
} }


private _processNewTarget(target: IRenderTarget, sizeMultiplier: number | undefined, trackTarget: boolean): IRenderTarget {
protected _processNewTarget(target: IRenderTarget, sizeMultiplier: number | undefined, trackTarget: boolean): IRenderTarget {
if (sizeMultiplier !== undefined) target.sizeMultiplier = sizeMultiplier if (sizeMultiplier !== undefined) target.sizeMultiplier = sizeMultiplier
if (trackTarget) this.trackTarget(target) if (trackTarget) this.trackTarget(target)
return target return target

Завантаження…
Відмінити
Зберегти