Bladeren bron

added camera helper showing orientation axes

Alexander Rose 5 jaren geleden
bovenliggende
commit
0e77369fdb

+ 16 - 7
src/mol-canvas3d/canvas3d.ts

@@ -34,9 +34,13 @@ import { PickPass } from './passes/pick';
 import { ImagePass, ImageProps } from './passes/image';
 import { Sphere3D } from '../mol-math/geometry';
 import { isDebugMode } from '../mol-util/debug';
+import { CameraHelper, CameraHelperParams } from './helper/camera-helper';
 
 export const Canvas3DParams = {
-    cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']] as const),
+    camera: PD.Group({
+        mode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']] as const, { label: 'Camera' }),
+        helper: PD.Group(CameraHelperParams, { isFlat: true })
+    }, { pivot: 'mode' }),
     cameraFog: PD.MappedStatic('on', {
         on: PD.Group({
             intensity: PD.Numeric(50, { min: 1, max: 100, step: 1 }),
@@ -176,7 +180,7 @@ namespace Canvas3D {
 
         const camera = new Camera({
             position: Vec3.create(0, 0, 100),
-            mode: p.cameraMode,
+            mode: p.camera.mode,
             fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
             clipFar: p.cameraClipping.far
         })
@@ -185,8 +189,9 @@ namespace Canvas3D {
         const renderer = Renderer.create(webgl, p.renderer)
         const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
         const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
+        const cameraHelper = new CameraHelper(webgl, p.camera.helper);
 
-        const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper)
+        const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper, cameraHelper)
         const pickPass = new PickPass(webgl, renderer, scene, camera, 0.5)
         const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
         const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
@@ -468,8 +473,8 @@ namespace Canvas3D {
             reprCount,
             setProps: (props: Partial<Canvas3DProps>) => {
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null)
-                if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
-                    cameraState.mode = props.cameraMode
+                if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
+                    cameraState.mode = props.camera.mode
                 }
                 if (props.cameraFog !== undefined) {
                     const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0
@@ -489,6 +494,7 @@ namespace Canvas3D {
                 }
                 if (Object.keys(cameraState).length > 0) camera.setState(cameraState)
 
+                if (props.camera?.helper) cameraHelper.setProps(props.camera.helper)
                 if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs
                 if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground
 
@@ -501,7 +507,7 @@ namespace Canvas3D {
                 requestDraw(true)
             },
             getImagePass: (props: Partial<ImageProps> = {}) => {
-                return new ImagePass(webgl, renderer, scene, camera, debugHelper, props)
+                return new ImagePass(webgl, renderer, scene, camera, debugHelper, cameraHelper, props)
             },
 
             get props() {
@@ -510,7 +516,10 @@ namespace Canvas3D {
                     : 0
 
                 return {
-                    cameraMode: camera.state.mode,
+                    camera: {
+                        mode: camera.state.mode,
+                        helper: { ...cameraHelper.props }
+                    },
                     cameraFog: camera.state.fog > 0
                         ? { name: 'on' as const, params: { intensity: camera.state.fog } }
                         : { name: 'off' as const, params: {} },

+ 178 - 0
src/mol-canvas3d/helper/camera-helper.ts

@@ -0,0 +1,178 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import Scene from '../../mol-gl/scene';
+import { Camera } from '../camera';
+import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
+import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
+import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
+import { GraphicsRenderObject } from '../../mol-gl/render-object';
+import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
+import { ColorNames } from '../../mol-util/color/names';
+import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
+import { Viewport } from '../camera/util';
+import { ValueCell } from '../../mol-util';
+import { Sphere3D } from '../../mol-math/geometry';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import produce from 'immer';
+import { Shape } from '../../mol-model/shape';
+import { ShapeRepresentation } from '../../mol-repr/shape/representation';
+import { RuntimeContext } from '../../mol-task';
+
+// TODO add scale line/grid
+
+const AxesParams = {
+    ...Mesh.Params,
+    colorX: PD.Color(ColorNames.red),
+    colorY: PD.Color(ColorNames.green),
+    colorZ: PD.Color(ColorNames.blue),
+    scale: PD.Numeric(1, { min: 0.1, max: 2, step: 0.1 }),
+}
+type AxesParams = typeof AxesParams
+type AxesProps = PD.Values<AxesParams>
+
+export const CameraHelperParams = {
+    axes: PD.MappedStatic('on', {
+        on: PD.Group(AxesParams),
+        off: PD.Group({})
+    }, { cycle: true, description: 'Show camera orientation axes' }),
+}
+export type CameraHelperParams = typeof CameraHelperParams
+export type CameraHelperProps = PD.Values<CameraHelperParams>
+
+export class CameraHelper {
+    scene: Scene
+    camera: Camera
+    props: CameraHelperProps = {
+        axes: { name: 'off', params: {} }
+    }
+
+    private renderObject: GraphicsRenderObject | undefined
+
+    constructor(webgl: WebGLContext, props: Partial<CameraHelperProps> = {}) {
+        this.scene = Scene.create(webgl)
+
+        this.camera = new Camera()
+        Vec3.set(this.camera.up, 0, 1, 0)
+        Vec3.set(this.camera.target, 0, 0, 0)
+
+        this.setProps(props)
+    }
+
+    setProps(props: Partial<CameraHelperProps>) {
+        this.props = produce(this.props, p => {
+            if (props.axes !== undefined) {
+                p.axes.name = props.axes.name
+                if (props.axes.name === 'on') {
+                    this.scene.clear()
+                    this.renderObject = undefined
+                    createAxesRenderObject(props.axes.params).then(renderObject => {
+                        this.renderObject = renderObject
+                        this.scene.add(this.renderObject)
+                        this.scene.commit()
+                    })
+
+                    Vec3.set(this.camera.position, 0, 0, props.axes.params.scale * 200)
+                    Mat4.lookAt(this.camera.view, this.camera.position, this.camera.target, this.camera.up)
+
+                    p.axes.params = { ...props.axes.params }
+                }
+            }
+        })
+    }
+
+    get isEnabled() {
+        return this.props.axes.name === 'on'
+    }
+
+    update(camera: Camera) {
+        if (!this.renderObject) return
+
+        updateCamera(this.camera, camera.viewport)
+
+        const m = this.renderObject.values.aTransform.ref.value as unknown as Mat4
+        Mat4.extractRotation(m, camera.view)
+
+        const r = this.renderObject.values.boundingSphere.ref.value.radius
+        Mat4.setTranslation(m, Vec3.create(
+            -camera.viewport.width / 2 + r,
+            -camera.viewport.height / 2 + r,
+            0
+        ))
+
+        ValueCell.update(this.renderObject.values.aTransform, this.renderObject.values.aTransform.ref.value)
+        this.scene.update([this.renderObject], true)
+    }
+}
+
+function updateCamera(camera: Camera, viewport: Viewport) {
+    const { near, far } = camera
+
+    const fullLeft = -(viewport.width - viewport.x) / 2
+    const fullRight = (viewport.width - viewport.x) / 2
+    const fullTop = (viewport.height - viewport.y) / 2
+    const fullBottom = -(viewport.height - viewport.y) / 2
+
+    const dx = (fullRight - fullLeft) / 2
+    const dy = (fullTop - fullBottom) / 2
+    const cx = (fullRight + fullLeft) / 2
+    const cy = (fullTop + fullBottom) / 2
+
+    const left = cx - dx
+    const right = cx + dx
+    const top = cy + dy
+    const bottom = cy - dy
+
+    Mat4.ortho(camera.projection, left, right, top, bottom, near, far)
+}
+
+function createAxesMesh(scale: number, mesh?: Mesh) {
+    const state = MeshBuilder.createState(512, 256, mesh)
+    const radius = 0.05 * scale
+    const x = Vec3.scale(Vec3(), Vec3.unitX, scale)
+    const y = Vec3.scale(Vec3(), Vec3.unitY, scale)
+    const z = Vec3.scale(Vec3(), Vec3.unitZ, scale)
+    const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 }
+
+    state.currentGroup = 0
+    addSphere(state, Vec3.origin, radius, 2)
+
+    state.currentGroup = 1
+    addSphere(state, x, radius, 2)
+    addCylinder(state, Vec3.origin, x, 1, cylinderProps)
+
+    state.currentGroup = 2
+    addSphere(state, y, radius, 2)
+    addCylinder(state, Vec3.origin, y, 1, cylinderProps)
+
+    state.currentGroup = 3
+    addSphere(state, z, radius, 2)
+    addCylinder(state, Vec3.origin, z, 1, cylinderProps)
+
+    return MeshBuilder.getMesh(state)
+}
+
+function getAxesShape(ctx: RuntimeContext, data: {}, props: AxesProps, shape?: Shape<Mesh>) {
+    const scale = 100 * props.scale
+    const mesh = createAxesMesh(scale, shape?.geometry)
+    mesh.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale + scale / 4))
+    const getColor = (groupId: number) => {
+        switch (groupId) {
+            case 1: return props.colorX
+            case 2: return props.colorY
+            case 3: return props.colorZ
+            default: return ColorNames.grey
+        }
+    }
+    return Shape.create('axes', {}, mesh, getColor, () => 1, () => '')
+}
+
+async function createAxesRenderObject(props: AxesProps) {
+    const repr = ShapeRepresentation(getAxesShape, Mesh.Utils)
+    await repr.createOrUpdate(props, {}).run()
+    return repr.renderObjects[0]
+}

+ 19 - 13
src/mol-canvas3d/passes/draw.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -11,6 +11,7 @@ import Scene from '../../mol-gl/scene';
 import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
 import { Texture } from '../../mol-gl/webgl/texture';
 import { Camera } from '../camera';
+import { CameraHelper } from '../helper/camera-helper';
 
 export class DrawPass {
     colorTarget: RenderTarget
@@ -19,7 +20,7 @@ export class DrawPass {
 
     private depthTarget: RenderTarget | null
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper) {
+    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper, private cameraHelper: CameraHelper) {
         const { gl, extensions, resources } = webgl
         const width = gl.drawingBufferWidth
         const height = gl.drawingBufferHeight
@@ -43,7 +44,7 @@ export class DrawPass {
     }
 
     render(toDrawingBuffer: boolean, transparentBackground: boolean) {
-        const { webgl, renderer, scene, camera, debugHelper, colorTarget, depthTarget } = this
+        const { webgl, renderer, colorTarget, depthTarget } = this
         if (toDrawingBuffer) {
             webgl.unbindFramebuffer()
         } else {
@@ -55,21 +56,26 @@ export class DrawPass {
         }
 
         renderer.setViewport(0, 0, colorTarget.getWidth(), colorTarget.getHeight())
-        renderer.render(scene, camera, 'color', true, transparentBackground)
-        if (debugHelper.isEnabled) {
-            debugHelper.syncVisibility()
-            renderer.render(debugHelper.scene, camera, 'color', false, transparentBackground)
-        }
+        this.renderInternal('color', transparentBackground)
 
         // do a depth pass if not rendering to drawing buffer and
         // extensions.depthTexture is unsupported (i.e. depthTarget is set)
         if (!toDrawingBuffer && depthTarget) {
             depthTarget.bind()
-            renderer.render(scene, camera, 'depth', true, transparentBackground)
-            if (debugHelper.isEnabled) {
-                debugHelper.syncVisibility()
-                renderer.render(debugHelper.scene, camera, 'depth', false, transparentBackground)
-            }
+            this.renderInternal('depth', transparentBackground)
+        }
+    }
+
+    private renderInternal(variant: 'color' | 'depth', transparentBackground: boolean) {
+        const { renderer, scene, camera, debugHelper, cameraHelper } = this
+        renderer.render(scene, camera, variant, true, transparentBackground)
+        if (debugHelper.isEnabled) {
+            debugHelper.syncVisibility()
+            renderer.render(debugHelper.scene, camera, variant, false, transparentBackground)
+        }
+        if (cameraHelper.isEnabled) {
+            cameraHelper.update(camera)
+            renderer.render(cameraHelper.scene, cameraHelper.camera, variant, false, transparentBackground)
         }
     }
 }

+ 3 - 2
src/mol-canvas3d/passes/image.ts

@@ -15,6 +15,7 @@ import { PostprocessingPass, PostprocessingParams } from './postprocessing'
 import { MultiSamplePass, MultiSampleParams } from './multi-sample'
 import { Camera } from '../camera';
 import { Viewport } from '../camera/util';
+import { CameraHelper } from '../helper/camera-helper';
 
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
@@ -39,12 +40,12 @@ export class ImagePass {
     get width() { return this._width }
     get height() { return this._height }
 
-    constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, props: Partial<ImageProps>) {
+    constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, cameraHelper: CameraHelper, props: Partial<ImageProps>) {
         const p = { ...PD.getDefaultValues(ImageParams), ...props }
 
         this._transparentBackground = p.transparentBackground
 
-        this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper)
+        this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper, cameraHelper)
         this.postprocessing = new PostprocessingPass(webgl, this._camera, this.drawPass, p.postprocessing)
         this.multiSample = new MultiSamplePass(webgl, this._camera, this.drawPass, this.postprocessing, p.multiSample)
 

+ 25 - 0
src/mol-math/linear-algebra/3d/mat4.ts

@@ -254,6 +254,31 @@ namespace Mat4 {
         return out;
     }
 
+    export function extractRotation(out: Mat4, mat: Mat4) {
+        const scaleX = 1 / Math.sqrt(mat[0] * mat[0] + mat[1] * mat[1] + mat[2] * mat[2]);
+        const scaleY = 1 / Math.sqrt(mat[4] * mat[4] + mat[5] * mat[5] + mat[6] * mat[6]);
+        const scaleZ = 1 / Math.sqrt(mat[8] * mat[8] + mat[9] * mat[9] + mat[10] * mat[10]);
+
+        out[0] = mat[0] * scaleX;
+        out[1] = mat[1] * scaleX;
+        out[2] = mat[2] * scaleX;
+        out[3] = 0;
+        out[4] = mat[4] * scaleY;
+        out[5] = mat[5] * scaleY;
+        out[6] = mat[6] * scaleY;
+        out[7] = 0;
+        out[8] = mat[8] * scaleZ;
+        out[9] = mat[9] * scaleZ;
+        out[10] = mat[10] * scaleZ;
+        out[11] = 0;
+        out[12] = 0;
+        out[13] = 0;
+        out[14] = 0;
+        out[15] = 1;
+
+        return out;
+    }
+
     export function transpose(out: Mat4, a: Mat4) {
         // If we are transposing ourselves we can skip a few steps but have to cache some values
         if (out === a) {

+ 2 - 0
src/mol-math/linear-algebra/3d/vec3.ts

@@ -546,6 +546,8 @@ namespace Vec3 {
         return `[${a[0].toPrecision(precision)} ${a[1].toPrecision(precision)} ${a[2].toPrecision(precision)}]`;
     }
 
+    export const origin: ReadonlyVec3 = Vec3.create(0, 0, 0)
+
     export const unit: ReadonlyVec3 = Vec3.create(1, 1, 1)
     export const negUnit: ReadonlyVec3 = Vec3.create(-1, -1, -1)
 

+ 5 - 6
src/tests/browser/marching-cubes.ts

@@ -1,13 +1,12 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import './index.html'
 import { resizeCanvas } from '../../mol-canvas3d/util';
-import { Canvas3D } from '../../mol-canvas3d/canvas3d';
-import { RendererParams } from '../../mol-gl/renderer';
+import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
 import { ColorNames } from '../../mol-util/color/names';
 import { PositionData, Box3D, Sphere3D } from '../../mol-math/geometry';
 import { OrderedSet } from '../../mol-data/int';
@@ -32,9 +31,9 @@ const canvas = document.createElement('canvas')
 parent.appendChild(canvas)
 resizeCanvas(canvas, parent)
 
-const canvas3d = Canvas3D.fromCanvas(canvas, {
-    renderer: { ...PD.getDefaultValues(RendererParams), backgroundColor: ColorNames.white },
-    cameraMode: 'orthographic'
+const canvas3d = PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
+    renderer: { backgroundColor: ColorNames.white },
+    camera: { mode: 'orthographic' }
 })
 canvas3d.animate()