Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/viewer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export {
SNAPSHOT_MAX_EDGE,
SNAPSHOT_MIME,
SNAPSHOT_QUALITY,
type SnapshotCaptureChannel,
type SnapshotCaptureMode,
type SnapshotCaptureResult,
type SnapshotCropRegion,
Expand Down
15 changes: 15 additions & 0 deletions packages/viewer/src/lib/snapshot-pipeline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from 'bun:test'
import { OrthographicCamera, PerspectiveCamera } from 'three'
import { snapshotCameraDepthNode } from './snapshot-pipeline'

test('keeps orthographic depth linear without perspective conversion', () => {
const rawDepth = {}
const linearDepth = {}
const scenePass = {
getLinearDepthNode: () => linearDepth,
getTextureNode: () => ({ r: rawDepth }),
}

expect(snapshotCameraDepthNode(scenePass as never, new OrthographicCamera())).toBe(rawDepth)
expect(snapshotCameraDepthNode(scenePass as never, new PerspectiveCamera())).toBe(linearDepth)
})
25 changes: 20 additions & 5 deletions packages/viewer/src/lib/snapshot-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ function clampSnapshotSize(width: number, height: number): { w: number; h: numbe
}

export type SnapshotCaptureMode = 'standard' | 'viewport' | 'area'
/** Depth captures encode near as white and far or background as black. */
export type SnapshotCaptureChannel = 'rgb' | 'depth'

export type SnapshotCropRegion = {
x: number
Expand Down Expand Up @@ -84,10 +86,12 @@ export type SnapshotPipeline = {
camera: Camera
}) => void
capture: ({
channel,
captureMode,
cropRegion,
standardSize,
}: {
channel?: SnapshotCaptureChannel
captureMode?: SnapshotCaptureMode
cropRegion?: SnapshotCropRegion
standardSize?: SnapshotSize
Expand Down Expand Up @@ -215,8 +219,12 @@ export async function createSnapshotPipeline({
// into an intermediate RT so FXAA can sample it with neighbour UV offsets.
const aaOutput = fxaa(convertToTexture(finalOutput))

const pipeline = new RenderPipeline(renderer)
pipeline.outputNode = aaOutput
const colorPipeline = new RenderPipeline(renderer)
colorPipeline.outputNode = aaOutput
const depth = snapshotCameraDepthNode(scenePass, camera).oneMinus()
const depthPipeline = new RenderPipeline(renderer)
depthPipeline.outputNode = vec4(vec3(depth), float(1))
depthPipeline.outputColorTransform = false

// Dedicated render target — pipeline outputs here instead of the canvas,
// so R3F's main render loop can never overwrite our capture.
Expand Down Expand Up @@ -249,7 +257,7 @@ export async function createSnapshotPipeline({
bgProjInvUniform.value.copy(captureCamera.projectionMatrixInverse)
bgCamWorldUniform.value.copy(captureCamera.matrixWorld)
},
capture: async ({ captureMode, cropRegion, standardSize }) => {
capture: async ({ channel = 'rgb', captureMode, cropRegion, standardSize }) => {
const standardW = standardSize?.w ?? THUMBNAIL_WIDTH
const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT
const { width: captureWidth, height: captureHeight } = renderer.domElement
Expand All @@ -262,7 +270,7 @@ export async function createSnapshotPipeline({
try {
;(renderer as any).setClearAlpha(0)
renderer.setRenderTarget(renderTarget)
pipeline.render()
;(channel === 'depth' ? depthPipeline : colorPipeline).render()
} finally {
renderer.setRenderTarget(null)
}
Expand Down Expand Up @@ -388,7 +396,8 @@ export async function createSnapshotPipeline({
return { blob, outW, outH }
},
dispose: () => {
pipeline.dispose()
colorPipeline.dispose()
depthPipeline.dispose()
renderTarget.dispose()
},
}
Expand All @@ -400,3 +409,9 @@ export async function createSnapshotPipeline({
return null
}
}

export function snapshotCameraDepthNode(scenePass: ReturnType<typeof pass>, camera: Camera) {
return camera.type === 'OrthographicCamera'
? scenePass.getTextureNode('depth').r
: scenePass.getLinearDepthNode()
}