import { inject , injectable , named } from 'inversify' ; import { ComponentManager } from '../../../ComponentManager' ; import { IDENTIFIER } from '../../../identifier' ; import { decodePickingColor } from '../../../utils/math' ; import { FrameGraphHandle } from '../../framegraph/FrameGraphHandle' ; import { FrameGraphPass } from '../../framegraph/FrameGraphPass' ; import { PassNode } from '../../framegraph/PassNode' ; import { ResourcePool } from '../../framegraph/ResourcePool' ; import { FrameGraphSystem } from '../../framegraph/System' ; import { MaterialComponent } from '../../material/MaterialComponent' ; import { MeshComponent } from '../../mesh/MeshComponent' ; import { IFramebuffer } from '../IFramebuffer' ; import { IRendererService , IView } from '../IRendererService' ; import { IRenderPass } from './IRenderPass' ; import { RenderPass , RenderPassData } from './RenderPass' ; export interface PixelPickingPassData { output : FrameGraphHandle ; } const PickingStage = { NONE : 0.0 , ENCODE : 1.0 , HIGHLIGHT : 2.0 , } ; see https @ injectable ( ) export class PixelPickingPass implements IRenderPass < PixelPickingPassData > { public static IDENTIFIER = 'PixelPicking Pass' ; @ inject ( IDENTIFIER . RenderEngine ) private readonly engine : IRendererService ; @ inject ( IDENTIFIER . ResourcePool ) private readonly resourcePool : ResourcePool ; @ inject ( IDENTIFIER . RenderPassFactory ) private readonly renderPassFactory : < T > ( name : string ) => IRenderPass < T > ; @ inject ( IDENTIFIER . MeshComponentManager ) private readonly mesh : ComponentManager < MeshComponent > ; private pickingFBO : IFramebuffer ; private views : IView [ ] ; private highlightEnabled = true ; private highlightColor = [ 255 , 0 , 0 , 255 ] ; /**
     * 简单的 throttle，防止连续触发 hover 时导致频繁渲染到 picking framebuffer
     */ private alreadyInRendering : boolean = false ; public enableHighlight ( enabled ) { this . highlightEnabled = enabled ; } public setHighlightColor ( color ) { this . highlightColor = color ; } public setup = ( fg , passNode , pass ) : void => { const output = fg . createRenderTarget ( passNode , 'picking fbo' , { width : 1 , height : 1 , } ) ; pass . data = { output : passNode . write ( fg , output ) , } ; passNode . hasSideEffect = true ; } ; public execute = async ( fg , pass , views ) : Promise < void > => { this . views = views ; if ( this . alreadyInRendering ) { return ; } for ( const view of views ) { const { width , height } = view . getViewport ( ) ; this . alreadyInRendering = true ; const resourceNode = fg . getResourceNode ( pass . data . output ) ; this . pickingFBO = this . resourcePool . getOrCreateResource ( resourceNode . resource ) ; this . pickingFBO . resize ( { width , height } ) ; this . engine . useFramebuffer ( this . pickingFBO , ( ) => { this . engine . clear ( { framebuffer : this . pickingFBO , color : [ 0 , 0 , 0 , 0 ] , stencil : 0 , depth : 1 , } ) ; const renderPass = this . renderPassFactory < RenderPassData > ( RenderPass . IDENTIFIER ) ; const meshes = [ ] ; const scene = view . getScene ( ) ; for ( const meshEntity of scene . getEntities ( ) ) { const mesh = this . mesh . getComponentByEntity ( meshEntity ) ! ; const material = mesh . material ; material . setUniform ( 'u_PickingStage' , PickingStage . ENCODE ) ; meshes . push ( mesh ) ; } renderPass . renderView ( view ) ; meshes . forEach ( ( mesh ) => { const material = mesh . material ; material . setUniform ( 'u_PickingStage' , PickingStage . HIGHLIGHT ) ; } ) ; this . alreadyInRendering = false ; } ) ; } } ; public pick = ( { x , y } , view ) => { const { readPixels , useFramebuffer } = this . engine ; const { width , height } = view . getViewport ( ) ; const xInDevicePixel = x * window . devicePixelRatio ; const yInDevicePixel = y * window . devicePixelRatio ; if ( xInDevicePixel > width || xInDevicePixel < 0 || yInDevicePixel > height || yInDevicePixel < 0 ) { return ; } let pickedColors ; let pickedFeatureIdx ; useFramebuffer ( this . pickingFBO , ( ) => { pickedColors = readPixels ( { x : Math . round ( xInDevicePixel ) , y : Math . round ( height - ( y + 1 ) * window . devicePixelRatio ) , width : 1 , height : 1 , data : new Uint8Array ( 1 * 1 * 4 ) , framebuffer : this . pickingFBO , } ) ; if ( pickedColors [ 0 ] !== 0 || pickedColors [ 1 ] !== 0 || pickedColors [ 2 ] !== 0 ) { pickedFeatureIdx = decodePickingColor ( pickedColors ) ; if ( this . highlightEnabled ) { this . highlightPickedFeature ( pickedColors , view ) ; } } } ) ; return pickedFeatureIdx ; } ; /**
     * highlight 如果直接修改选中 feature 的 buffer，存在两个问题：
     * 1. 鼠标移走时无法恢复
     * 2. 无法实现高亮颜色与原始原色的 alpha 混合
     * 因此高亮还是放在 shader 中做比较好
     */ private highlightPickedFeature ( pickedColors , view ) { if ( pickedColors ) { for ( const meshEntity of view . getScene ( ) . getEntities ( ) ) { const mesh = this . mesh . getComponentByEntity ( meshEntity ) ! ; const material = mesh . material ; material . setUniform ( 'u_PickingStage' , PickingStage . HIGHLIGHT ) ; material . setUniform ( 'u_PickingColor' , [ pickedColors [ 0 ] , pickedColors [ 1 ] , pickedColors [ 2 ] , ] ) ; material . setUniform ( 'u_HighlightColor' , this . highlightColor ) ; } } } }