二维相机轨道控制器

325 阅读2分钟

前言

源码

github.com/buglas/canv…

学习目标

  • 创建相机轨道控制器
  • 操控相机变换视图

知识点

  • 位移
  • 缩放

前情回顾

之前我们创建了Scene对象,接下来我们建立OrbitControler对象。

image-20230301224856781

1-OrbitControler对象的功能分析

OrbitControler的功能是操控相机变换视图。

OrbitControler会在鼠标按下的时候,平移视图;在滑动滚轮的时候,缩放视口。

2-OrbitControler对象的代码实现

其整体代码如下:

  • /src/lmm/core/Scene.ts
import { Vector2 } from '../math/Vector2'
import { EventDispatcher } from '../core/EventDispatcher'
import { Camera } from '../core/Camera'

/* change 事件 */
const _changeEvent = { type: 'change' }

/* 暂存数据类型 */
type Stage = {
    cameraZoom: number
    cameraPosition: Vector2
    panStart: Vector2
}

/* 配置项 */
type Option = {
    camera?: Camera
    enableZoom?: boolean
    zoomSpeed?: number
    enablePan?: boolean
    panSpeed?: number
}

/* 相机轨道控制 */
class OrbitControler extends EventDispatcher {
    // 相机
    camera: Camera
    // 允许缩放
    enableZoom = true
    // 缩放速度
    zoomSpeed = 3.0

    // 允许位移
    enablePan = true
    // 位移速度
    panSpeed = 1.0

    // 是否正在拖拽中
    panning = false

    //变换相机前的暂存数据
    stage: Stage = {
        cameraZoom: 1,
        cameraPosition: new Vector2(),
        panStart: new Vector2(),
    }

    constructor(camera: Camera, option: Option = {}) {
        super()
        this.camera = camera
        this.setOption(option)
    }

    /* 设置属性 */
    setOption(option: Option) {
        Object.assign(this, option)
    }

    /* 缩放 */
    doScale(deltaY: number) {
        const { enableZoom, camera, zoomSpeed, stage } = this
        if (!enableZoom) {
            return
        }
        stage.cameraZoom = camera.zoom
        const scale = Math.pow(0.95, zoomSpeed)
        if (deltaY > 0) {
            camera.zoom /= scale
        } else {
            camera.zoom *= scale
        }
        this.dispatchEvent(_changeEvent)
    }

    /* 鼠标按下 */
    pointerdown(cx: number, cy: number) {
        const {
            enablePan,
            stage: { cameraPosition, panStart },
            camera: { position },
        } = this
        if (!enablePan) {
            return
        }
        this.panning = true
        cameraPosition.copy(position)
        panStart.set(cx, cy)
    }

    /* 鼠标抬起 */
    pointerup() {
        this.panning = false
    }

    /* 位移 */
    pointermove(cx: number, cy: number) {
        const {
            enablePan,
            camera: { position },
            stage: {
                panStart: { x, y },
                cameraPosition,
            },
            panning,
        } = this
        if (!enablePan || !panning) {
            return
        }
        position.copy(cameraPosition.clone().add(new Vector2(x - cx, y - cy)))
        this.dispatchEvent(_changeEvent)
    }
}

export { OrbitControler }

OrbitControler对象的属性比较简单,都有注释,我就不再多说。

doScale(deltaY) 方法对应的是滚轮事件,它会根据滚轮数据,设置相机的zoom属性,从而实现视图缩放。

pointerdown(cx: number, cy: number) 方法会在鼠标按下时,暂存鼠标状态。

3-OrbitControler对象的测试

在examples文件夹中建立一个OrbitControler.vue文件,用于测试。

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { OrbitControler } from '../lmm/controler/OrbitControler'
import { Scene } from '../lmm/core/Scene'
import { Vector2 } from '../lmm/math/Vector2'
import { Img } from '../lmm/objects/Img'

// 获取父级属性
defineProps({
    size: { type: Object, default: { width: 0, height: 0 } },
})

// 对应canvas 画布的Ref对象
const canvasRef = ref<HTMLCanvasElement>()

/* 场景 */
const scene = new Scene()

/* 相机轨道控制器 */
const orbitControler = new OrbitControler(scene.camera)

/* 图案 */
const image = new Image()
image.src =
    'https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/1.png'
const pattern = new Img({ image })
scene.add(pattern)

/* 测试 */
function test(canvas: HTMLCanvasElement) {
    const imgSize = new Vector2(image.width, image.height).multiplyScalar(0.6)
    pattern.setOption({
        /* 模型矩阵 */
        rotate: 0.4,
        position: new Vector2(0, -50),
        scale: new Vector2(0.5),

        /* Img属性 */
        size: imgSize.clone(),
        offset: imgSize.clone().multiplyScalar(-0.5),
    })

    

    /* 按需渲染 */
    orbitControler.addEventListener('change', () => {
        scene.render()
    })

    /* 滑动滚轮缩放 */
    canvas.addEventListener('wheel', ({ deltaY }) => {
        orbitControler.doScale(deltaY)
    })

    /* 按住滚轮平移 */
    canvas.addEventListener('pointerdown', (event: PointerEvent) => {
        if (event.button == 1) {
            orbitControler.pointerdown(event.clientX, event.clientY)
        }
    })
    canvas.addEventListener('pointermove', (event: PointerEvent) => {
        orbitControler.pointermove(event.clientX, event.clientY)
    })
    window.addEventListener('pointerup', (event: PointerEvent) => {
        if (event.button == 1) {
            orbitControler.pointerup()
        }
    })

    /* 渲染 */
    scene.render()
}

onMounted(() => {
    const canvas = canvasRef.value
    if (canvas) {
        scene.setOption({ canvas })
        image.onload = function () {
            test(canvas)
        }
    }
})
</script>

<template>
    <canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
</template>

<style scoped></style>

在上面的代码中,我们可以按住鼠标滚轮移动视口,滑动鼠标滚轮缩放视口。

当然,相机的操作,最终还是要作用于模型上的。

总结

对于相机的操作,提前架构好底层的矩阵变换关系是很重要的,这会让我们之后的代码更加顺畅。

下一章我们会说当前课程最重要的部分-ImgControler。