backup/renderers/renderer-lite/src/LiteRenderer.ts (571 lines of code) (raw):
/**
* Copyright (C) 2021 Alibaba Group Holding Limited
* All rights reserved.
*/
import * as IR from '@gs.i/schema-scene'
import {
Object3D,
Vector3,
WebGLRenderer,
Raycaster as ThreeRaycaster,
WebGLRenderTarget,
Clock,
Scene,
PerspectiveCamera,
Color,
LinearFilter,
RGBAFormat,
DepthTexture,
Vector2,
AmbientLight,
DirectionalLight,
PointLight,
} from 'three-lite'
import { Renderer, colorToString, PolarisProps, PickResult } from '@polaris.gl/base'
import { DefaultConfig as ConvConfig, ThreeLiteConverter } from '@gs.i/backend-threelite'
import { CameraProxy } from 'camera-proxy'
import * as SDK from '@gs.i/frontend-sdk'
import { GSIView } from '@polaris.gl/view-gsi'
// import * as postprocessing from 'postprocessing'
// const { EffectComposer, ShaderPass } = postprocessing
import { calcCamNearFar } from './utils'
import { Raycaster, RaycastInfo } from '@gs.i/processor-raycast'
/**
*
*
* @export
* @interface RendererProps
* @extends {PolarisProps}
*/
export interface RendererProps
extends Required<
Pick<
PolarisProps,
| 'width'
| 'ratio'
| 'height'
| 'antialias'
| 'background'
| 'fov'
| 'viewOffset'
| 'renderToFBO'
| 'lights'
| 'cameraNear'
| 'cameraFar'
| 'postprocessing'
>
> {
enableReflection?: boolean
reflectionRatio?: number
castShadow?: boolean
}
export const defaultProps = {
enableReflection: false,
reflectionRatio: 1.0,
castShadow: false,
}
// export interface PolarisPass extends postprocessing.Pass {
// onViewChange?: (cam: CameraProxy) => void
// }
// Temp vars
const _vec3 = new Vector3()
/**
*
*
* @export
* @class LiteRenderer
* @extends {Renderer}
*/
export class LiteRenderer extends Renderer {
props: RendererProps
/**
* GSI - threelite 转换器
*/
conv: ThreeLiteConverter
/**
* GL2 渲染器
*/
renderer: WebGLRenderer
canvas: HTMLCanvasElement
context: WebGLRenderingContext | null
/**
* 外部进行后处理使用的 fbo
*/
frame: WebGLRenderTarget
/**
* GSI - Scene 保留引用,应当不允许更改
* 每帧根据cameraProxy修改scene的偏移
*/
private polarisViewWrapper: IR.BaseNode
/**
* 转换后的 group
*/
private threeRoot: Object3D
/**
* used to do camera transformation for whole scene
*/
private rootWrapper: SDK.Mesh
/**
* 顶层 scene,由 scene props 生成,在 polaris-renderer 中管理
*/
private scene: Scene
/**
* 场景光源
*/
private lights: Object3D
/**
* 场景相机
*/
private camera: PerspectiveCamera
/**
* Postprocessing
*/
effectComposer: any // postprocessing.EffectComposer
passes: any // { [name: string]: PolarisPass }
/**
* Picking
*/
raycaster: Raycaster
private _internalThreeRaycaster: ThreeRaycaster
/**
* capabilities object
*/
private _capabilities: {
pointSizeRange: [number, number]
lineWidthRange: [number, number]
maxVertexAttributes: number
maxVaryingVectors: number
[name: string]: any
}
/**
* 反射相关FBO、Pass
*/
// private reflectionTexture: WebGLRenderTarget
// private reflectionTextureFXAA: WebGLRenderTarget
// private reflectionTextureRough: WebGLRenderTarget
// private reflectionDrawPass: Pass
// private reflectionFxaaPass: Pass
// private reflectionBlurPass: Pass
// private reflector: Mesh
// private reflectionTexMatrix: Matrix4
constructor(props: RendererProps) {
super()
this.props = {
...defaultProps,
...props,
}
/**
* @stage_0 MPV
*/
const canvasWidth = this.props.width * (this.props.ratio ?? 1.0)
const canvasHeight = this.props.height * (this.props.ratio ?? 1.0)
const canvas = document.createElement('canvas')
canvas.width = canvasWidth
canvas.height = canvasHeight
canvas.style.position = 'absolute'
canvas.style.left = '0px'
canvas.style.top = '0px'
canvas.style.width = this.props.width + 'px'
canvas.style.height = this.props.height + 'px'
this.canvas = canvas
// Converter
this.conv = new ThreeLiteConverter(ConvConfig)
/**
* init renderer
*/
const attributes: WebGLContextAttributes = {
alpha: true,
antialias: this.props.antialias === 'msaa',
stencil: false,
}
this.context = canvas.getContext('webgl', attributes)
if (!this.context) {
throw new Error('GSILiteRenderer - Cannot get WebGLRenderingContext. ')
}
this.renderer = new WebGLRenderer({
canvas: this.canvas,
context: this.context,
alpha: true,
antialias: this.props.antialias === 'msaa',
stencil: false,
})
this.renderer.setClearAlpha(1.0)
/** @FIXME gamma correction未生效 */
// this.renderer.gammaOutput = true
// this.renderer.gammaFactor = 2.2
/**
* init webgl capabilities
*/
const gl = this.renderer.getContext()
this._capabilities = {
pointSizeRange: gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE),
lineWidthRange: gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE),
maxVertexAttributes: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
maxVaryingVectors: gl.getParameter(gl.MAX_VARYING_VECTORS),
}
/**
* init scene
*/
this.scene = new Scene()
if (this.props.background === 'transparent') {
this.scene.background = null
this.renderer.setClearAlpha(0.0)
} else {
this.scene.background = new Color(this.props.background as string)
}
/**
* gsi three conv 不开启 decomposeMatrix ,不然性能很差。
* 这样做的问题是,conv 得到的 object3d 无法再 transform,即使加在 three.group 中,
* 也不会被父的 transform 影响。
*/
this.scene.autoUpdate = false
this.scene.matrixAutoUpdate = false
this.rootWrapper = new SDK.Mesh()
this.rootWrapper.name = 'rootWrapper'
/**
* init lights
*/
this.lights = new Object3D()
this.lights.name = 'LightsWrapper'
this.scene.add(this.lights)
this.initLights()
/**
* init Camera
* @note 整个 camera 的转换都放进 renderer 里,这里只提供 camera proxy
* 反正在哪里转都是一样的
*/
const { cameraNear, cameraFar } = this.props
this.camera = new PerspectiveCamera(
this.props.fov,
this.props.width / this.props.height,
cameraNear,
cameraFar
)
this.camera.matrixAutoUpdate = false
if (this.props.viewOffset) {
this.camera.setViewOffset(
this.props.width,
this.props.height,
this.props.viewOffset[0],
this.props.viewOffset[1],
this.props.viewOffset[2] ?? this.props.width,
this.props.viewOffset[3] ?? this.props.height
)
}
/**
* @stage_1 BI 需要
*/
/**
* init picking
* 出于兼容性、实现简洁性,应该使用 CPU picking (raycasting)
* 但是出于 GL2 和 WebGPU 的优势考虑,应该使用 GPU picking (color buffer)
* - GPU picking
* WebGL
* 使用一个单独的 RT
* 使用一个PickingMaterial 绘制layer masked物体,同时编ID,映射layer
* WebGL2
* 使用一个独立的 color buffer,同步绘制
*/
this.raycaster = new Raycaster({
boundingProcessor: ConvConfig.boundingProcessor,
matrixProcessor: ConvConfig.matrixProcessor,
})
this._internalThreeRaycaster = new ThreeRaycaster()
/**
* @stage_2 只兼容桌面端,考虑是否放到 GL2 renderer 里,IE 是否需要?
*/
// init shadow
// this.initReflection()
// init pp
this.frame = new WebGLRenderTarget(canvasWidth, canvasHeight, {
minFilter: LinearFilter,
magFilter: LinearFilter,
format: RGBAFormat,
stencilBuffer: false,
depthBuffer: true,
})
this.frame.depthTexture = new (DepthTexture as any)()
// this.initPostprocessing()
}
render(view: GSIView) {
if (this.polarisViewWrapper === undefined) {
this.polarisViewWrapper = view.alignmentWrapper
this.rootWrapper.add(this.polarisViewWrapper)
} else if (this.polarisViewWrapper !== view.alignmentWrapper) {
throw new Error('Lite Renderer: Sharing renderer between polaris instances is not supported')
}
if (!this.threeRoot) {
this.threeRoot = this.conv.convert(this.rootWrapper)
this.scene.add(this.threeRoot)
} else {
this.conv.convert(this.rootWrapper)
}
if (this.props.renderToFBO) {
// Postprocessing rendering
this.renderer.setRenderTarget(this.frame)
this.renderer.render(this.scene, this.camera)
// this.effectComposer.render(this._clock.getDelta())
} else {
// Normal rendering
this.renderer.render(this.scene, this.camera)
}
}
resize(width, height, ratio = 1.0) {
// 物理像素
this.canvas.style.width = width + 'px'
this.canvas.style.height = height + 'px'
// 逻辑像素
const canvasWidth = width * ratio
const canvasHeight = height * ratio
this.canvas.width = canvasWidth
this.canvas.height = canvasHeight
this.props.width = width
this.props.height = height
this.renderer.setDrawingBufferSize(width, height, ratio)
this.camera.aspect = canvasWidth / canvasHeight
this.camera.updateProjectionMatrix()
// 在PolarisGSI中触发updateCamera()
}
capture() {
// https://stackoverflow.com/questions/32556939/saving-canvas-to-image-via-canvas-todataurl-results-in-black-rectangle/32641456#32641456
this.renderer.context.flush()
this.renderer.context.finish()
return this.canvas.toDataURL('image/png')
}
dispose() {
this.renderer.dispose()
this.conv.dispose()
if (this.effectComposer) {
this.effectComposer.dispose()
}
for (const key in this.passes) {
this.passes[key].dispose()
}
this.frame && this.frame.dispose()
if (this.canvas.parentElement) {
this.canvas.parentElement.removeChild(this.canvas)
}
}
updateCamera(cam: CameraProxy): void {
this.camera.position.set(
cam.position[0] - cam.center[0],
cam.position[1] - cam.center[1],
cam.position[2] - cam.center[2]
)
this.camera.rotation.fromArray(cam.rotationEuler)
this.camera.updateMatrix()
this.camera.updateMatrixWorld(true)
this.rootWrapper.transform.position.set(-cam.center[0], -cam.center[1], -cam.center[2])
// Update point lights position
const pLights = this.props.lights?.pointLights
if (pLights) {
this.lights.children.forEach((light) => {
const type = light.name.split('.')[0]
const i = light.name.split('.')[1]
if (i === undefined) return
const index = parseInt(i)
if (isNaN(index)) return
if (type === 'PointLight') {
light.position.set(
pLights[index].position.x - cam.center[0],
pLights[index].position.y - cam.center[1],
pLights[index].position.z - cam.center[2]
)
light.updateMatrix()
light.updateMatrixWorld(true)
}
})
}
// Update cameraNear/cameraFar
const [near, far] = calcCamNearFar(cam)
if (near && far && (this.camera.near !== near || this.camera.far !== far)) {
this.camera.near = Math.max(near, this.props.cameraNear as number)
this.camera.far = Math.min(far, this.props.cameraFar as number)
this.camera.updateProjectionMatrix()
}
// Update postprocessing
if (this.effectComposer) {
// The drawing buffer size takes the device pixel ratio into account.
const vec2 = new Vector2()
const { width, height } = this.renderer.getDrawingBufferSize(vec2)
this.frame.setSize(width, height)
this.effectComposer.inputBuffer.setSize(width, height)
this.effectComposer.outputBuffer.setSize(width, height)
for (const pass of this.effectComposer.passes) {
pass.setSize(width, height)
pass.onViewChange && pass.onViewChange(cam)
}
}
/** Update reflectionTexture */
// const canvasSize = this.renderer.getSize(vec2)
// const reflectionWidth = Math.floor(canvasSize.width * (this.props.reflectionRatio ?? 1.0))
// const reflectionHeight = Math.floor(canvasSize.height * (this.props.reflectionRatio ?? 1.0))
// this.reflectionTexture.setSize(reflectionWidth, reflectionHeight)
// this.reflectionTextureFXAA.setSize(reflectionWidth, reflectionHeight)
// this.reflectionTextureRough.setSize(reflectionWidth / 2, reflectionHeight / 2)
// this.reflectionDrawPass.resize(reflectionWidth, reflectionHeight)
// this.reflectionFxaaPass.resize(reflectionWidth, reflectionHeight)
// this.reflectionBlurPass.resize(reflectionWidth, reflectionHeight)
}
updateProps(props) {
this.props = {
...this.props,
...props,
}
if (this.props.background === 'transparent') {
this.scene.background = null
this.renderer.setClearAlpha(0.0)
} else {
this.scene.background = new Color(this.props.background as string)
}
const canvasWidth = this.props.width * (this.props.ratio ?? 1.0)
const canvasHeight = this.props.height * (this.props.ratio ?? 1.0)
this.canvas.style.width = this.props.width + 'px'
this.canvas.style.height = this.props.height + 'px'
this.canvas.width = canvasWidth
this.canvas.height = canvasHeight
this.renderer.setSize(canvasWidth, canvasHeight)
this.renderer.setViewport(0, 0, canvasWidth, canvasHeight)
const { cameraNear, cameraFar, fov } = this.props
this.camera.near = cameraNear as number
this.camera.far = cameraFar as number
this.camera.fov = fov as number
this.camera.aspect = this.props.width / this.props.height
if (this.props.viewOffset && this.props.viewOffset.length === 4) {
this.camera.setViewOffset(
this.props.width,
this.props.height,
this.props.viewOffset[0],
this.props.viewOffset[1],
this.props.viewOffset[2],
this.props.viewOffset[3]
)
}
this.camera.updateProjectionMatrix()
this.initLights()
if (this.props.postprocessing && this.props.postprocessing.length > 0) {
/** @QianXun 目前先采用全部替换pass的方式来更新pp props */
// this.initPostprocessing()
} else {
this.props.renderToFBO = false
this.renderer.autoClear = true
}
}
getNDC(worldPosition: { x: number; y: number; z: number }): number[] {
_vec3.x = worldPosition.x
_vec3.y = worldPosition.y
_vec3.z = worldPosition.z
_vec3.project(this.camera)
return _vec3.toArray()
}
// TODO refactor picking
/**
*
*
* @param {MeshDataType} object
* @param {{ x: number; y: number }} ndcCoords
* @param {{ allInters?: boolean; threshold?: number; backfaceCulling?: boolean }} options allInters: 是否返回所有碰撞点并排序; threshold: lineMesh碰撞测试阈值; backfaceCulling: triangleMesh是否测试背面
* @return {*} {PickResult}
* @memberof GSIGL2Renderer
*/
// pick(
// object: RenderableNode,
// ndcCoords: { x: number; y: number },
// options: { allInters?: boolean; threshold?: number; backfaceCulling?: boolean }
// ): PickResult {
// const result: PickResult = {
// hit: false,
// intersections: [],
// }
// this._internalThreeRaycaster.setFromCamera(ndcCoords, this.camera)
// this.raycaster.set(
// this._internalThreeRaycaster.ray.origin.clone(),
// this._internalThreeRaycaster.ray.direction.clone()
// )
// this.raycaster.near = this.camera.near
// this.raycaster.far = Infinity
// if (!object.geometry) {
// return result
// }
// options = options ?? {}
// const info = this.raycaster.raycast(object, options.allInters ?? false)
// if (info.hit) {
// return info
// }
// return result
// }
getCapabilities(): {
pointSizeRange: [number, number]
lineWidthRange: [number, number]
maxVertexAttributes: number
maxVaryingVectors: number
[name: string]: any
} {
return this._capabilities
}
/**
* 初始化场景光源
*
* @private
* @memberof GSIGL2Renderer
*/
private initLights() {
if (this.props.lights) {
// gltf to three
if (this.props.lights.ambientLight) {
const name = 'AmbientLight'
let aLight: AmbientLight = this.lights.getObjectByName(name) as AmbientLight
if (!aLight) {
aLight = new AmbientLight()
this.lights.add(aLight)
}
if (this.props.lights.ambientLight.color) {
aLight.color = new Color(colorToString(this.props.lights.ambientLight.color))
}
aLight.intensity = this.props.lights.ambientLight.intensity ?? 1.0
aLight.name = name
aLight.matrixAutoUpdate = false
}
if (this.props.lights.directionalLights) {
const dLights = this.props.lights.directionalLights
dLights.forEach((item, index) => {
const name = 'DirectionalLight.' + index
let dlight: DirectionalLight = this.lights.getObjectByName(name) as DirectionalLight
if (!dlight) {
dlight = new DirectionalLight()
this.lights.add(dlight)
}
if (item.color) {
dlight.color = new Color(colorToString(item.color))
}
if (item.position) {
dlight.position.copy(item.position as Vector3)
}
dlight.intensity = item.intensity ?? 1.0
dlight.matrixAutoUpdate = false
dlight.name = name
dlight.updateMatrix()
dlight.updateMatrixWorld(true)
})
// Remove
this.lights.children.forEach((item) => {
const type = item.name.split('.')[0]
const i = item.name.split('.')[1]
if (i === undefined) return
const index = parseInt(i)
if (isNaN(index)) return
if (type === 'DirectionalLight' && index >= dLights.length) {
this.lights.remove(item)
}
})
}
if (this.props.lights.pointLights) {
const pLights = this.props.lights.pointLights
pLights.forEach((item, index) => {
const name = 'PointLight.' + index
let plight: PointLight = this.lights.getObjectByName(name) as PointLight
if (!plight) {
plight = new PointLight()
this.lights.add(plight)
}
if (item.color) {
plight.color = new Color(colorToString(item.color))
}
if (item.position) {
plight.position.copy(item.position as Vector3)
}
plight.intensity = item.intensity ?? 1.0
plight.distance = item.range ?? 0.0
plight.decay = 2 // physical
plight.name = name
plight.matrixAutoUpdate = false
plight.updateMatrix()
plight.updateMatrixWorld(true)
})
// Remove
this.lights.children.forEach((item) => {
const type = item.name.split('.')[0]
const i = item.name.split('.')[1]
if (i === undefined) return
const index = parseInt(i)
if (isNaN(index)) return
if (type === 'PointLight' && index >= pLights.length) {
this.lights.remove(item)
}
})
}
}
}
/**
* 初始化后期处理
*
* @private
* @memberof GSIGL2Renderer
*/
// private initPostprocessing() {
// this.props.renderToFBO = false
// const ppList = this.props.postprocessing
// ? this.props.postprocessing.length > 0
// ? this.props.postprocessing
// : undefined
// : undefined
// const antialiasing = this.props.antialias
// // if no pp & 'msaa', using hardware antialiasing
// if ((!antialiasing || antialiasing === 'msaa') && !ppList) {
// return
// }
// // If use pp aa -> disable webgl2 msaa, GL2
// this.frame['multisample'] = antialiasing === 'msaa' ? 4 : 0
// // dispose prev EffectComposer
// if (this.effectComposer) {
// this.effectComposer.dispose()
// }
// // Preparation
// const passes = {}
// // EffectComposer
// const composer = (this.effectComposer = new EffectComposer(this.renderer, {
// // Bokeh需要depth
// depthBuffer: true,
// depthTexture: true,
// }))
// this.props.renderToFBO = true
// let aaPass
// const depthPasses: PolarisPass[] = [] // Depth passes should be rendered before aa pass
// const normalPasses: PolarisPass[] = [] // Normal passes should be rendered last
// // Render pass
// const frameBuffer = new ReadPass(this.frame)
// composer.addPass(frameBuffer)
// // AA pass
// if (antialiasing === 'smaa') {
// const SMAAPass = postprocessing['SMAAPass']
// const areaImage = new Image()
// areaImage.src = SMAAPass.areaImageDataURL
// const searchImage = new Image()
// searchImage.src = SMAAPass.searchImageDataURL
// aaPass = new SMAAPass(searchImage, areaImage)
// passes['SMAAPass'] = aaPass
// } else if (antialiasing === 'fxaa') {
// aaPass = new ShaderPass(genFXAAMaterial({ THREE }))
// aaPass.name = 'FXAAPass'
// aaPass.onViewChange = (cam) => {
// if (!this.renderer) return
// const drawingBufferSize = this.renderer.getDrawingBufferSize()
// const matr = aaPass.getFullscreenMaterial()
// matr.uniforms.resolution.value.set(drawingBufferSize.width, drawingBufferSize.height)
// }
// passes['FXAAPass'] = aaPass
// }
// if (!aaPass && !ppList) {
// return
// }
// // No more passes needed
// if (!ppList) {
// aaPass.renderToScreen = true
// composer.addPass(aaPass)
// return
// }
// // More passes needed
// if (aaPass) {
// aaPass.renderToScreen = false
// }
// // Process custom passes
// for (let i = 0; i < ppList.length; i++) {
// const { name, props } = ppList[i]
// const passName = name + 'Pass'
// let pass: PolarisPass
// if (passName === 'BokehPass' || passName === 'RealisticBokehPass') {
// // BokehPass needs depthTexture as input
// pass = new BokehPass()
// const uniforms = pass['uniforms']
// uniforms.tDepth.value = this.frame.depthTexture
// pass.onViewChange = (cam) => {
// if (!(props && props.autoFocus === false)) {
// uniforms.focus.value = cam.distance
// }
// if (!(props && props.autoDOF === false)) {
// uniforms.dof.value = cam.distance * 0.15
// uniforms.aperture.value = 1 / cam.distance
// }
// uniforms.cameraNear.value = this.props.cameraNear
// uniforms.cameraFar.value = this.props.cameraFar
// }
// depthPasses.push(pass)
// } else {
// if (!postprocessing[passName]) {
// console.error('[Polaris::Renderer-gsi-gl2] Invalid pass name')
// continue
// }
// if (Array.isArray(props)) {
// providePassArgs(props, {
// scene: this.scene,
// camera: this.camera,
// })
// pass = new postprocessing[passName](...props)
// } else {
// pass = new postprocessing[passName]({
// ...props,
// })
// }
// normalPasses.push(pass)
// }
// // Attemp to set pass material.depthWrite
// if (pass.getFullscreenMaterial()) {
// pass.getFullscreenMaterial().depthWrite = !!props.depthWrite
// } else if (pass['setMaterialsProps']) {
// pass['setMaterialsProps']({
// depthWrite: !!props.depthWrite,
// })
// } else {
// // Do nothing
// }
// pass.enabled = !props.disabled
// passes[passName] = pass
// }
// // Add passes, order: aa -> depth -> normal passes
// if (aaPass) composer.addPass(aaPass)
// depthPasses.forEach((p) => {
// // Attemp to set scene depth, may not work as expected
// if (p.getFullscreenMaterial()) {
// p.getFullscreenMaterial().uniforms.tDepth.value = this.frame.depthTexture
// }
// composer.addPass(p)
// })
// normalPasses.forEach((p) => composer.addPass(p))
// this.passes = passes
// // Set last pass .renderToScreen
// if (composer.passes.length > 0) {
// for (let i = composer.passes.length - 1; i >= 0; i--) {
// const pass = composer.passes[i]
// if (pass.enabled) {
// pass.renderToScreen = true
// break
// }
// }
// }
// }
}