Cameras
2024-01-19 本文已影响0人
不决书
代码分析说明:
import { mat4, vec3 } from 'wgpu-matrix';
import { makeSample, SampleInit } from '../../components/SampleLayout';
import {
cubeVertexArray,
cubeVertexSize,
cubeUVOffset,
cubePositionOffset,
cubeVertexCount,
} from '../../meshes/cube';
import cubeWGSL from './cube.wgsl';
import { ArcballCamera, WASDCamera, cameraSourceInfo } from './camera';
import { createInputHandler, inputSourceInfo } from './input';
const init: SampleInit = async ({ canvas, pageState, gui }) => {
if (!pageState.active) {
return;
}
// The input handler
// 监控鼠标, 键盘按下弹起滚动的操作,返回对于的状态值
const inputHandler = createInputHandler(window, canvas);
// The camera types
// 初始化相机的位置
const initialCameraPosition = vec3.create(3, 2, 5);
// 相机操作的类型,键盘控制, 轨道控制
const cameras = {
// 实例化轨道相机控制器
arcball: new ArcballCamera({ position: initialCameraPosition }),
// 实例化键盘相机控制器
WASD: new WASDCamera({ position: initialCameraPosition }),
};
// GUI parameters
const params: { type: 'arcball' | 'WASD' } = {
type: 'arcball',
};
// Callback handler for camera mode
let oldCameraType = params.type;
gui.add(params, 'type', ['arcball', 'WASD']).onChange(() => {
// Copy the camera matrix from old to new
const newCameraType = params.type;
cameras[newCameraType].matrix = cameras[oldCameraType].matrix;
oldCameraType = newCameraType;
});
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu') as GPUCanvasContext;
const devicePixelRatio = window.devicePixelRatio;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
alphaMode: 'premultiplied',
});
// Create a vertex buffer from the cube data.
const verticesBuffer = device.createBuffer({
size: cubeVertexArray.byteLength,
usage: GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
verticesBuffer.unmap();
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: device.createShaderModule({
code: cubeWGSL,
}),
entryPoint: 'vertex_main',
buffers: [
{
arrayStride: cubeVertexSize,
attributes: [
{
// position
shaderLocation: 0,
offset: cubePositionOffset,
format: 'float32x4',
},
{
// uv
shaderLocation: 1,
offset: cubeUVOffset,
format: 'float32x2',
},
],
},
],
},
fragment: {
module: device.createShaderModule({
code: cubeWGSL,
}),
entryPoint: 'fragment_main',
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',
cullMode: 'back',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
});
const depthTexture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const uniformBufferSize = 4 * 16; // 4x4 matrix
const uniformBuffer = device.createBuffer({
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// Fetch the image and upload it into a GPUTexture.
let cubeTexture: GPUTexture;
{
const response = await fetch('../assets/img/Di-3d.png');
const imageBitmap = await createImageBitmap(await response.blob());
cubeTexture = device.createTexture({
size: [imageBitmap.width, imageBitmap.height, 1],
format: 'rgba8unorm',
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
device.queue.copyExternalImageToTexture(
{ source: imageBitmap },
{ texture: cubeTexture },
[imageBitmap.width, imageBitmap.height]
);
}
// Create a sampler with linear filtering for smooth interpolation.
const sampler = device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
});
const uniformBindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer,
},
},
{
binding: 1,
resource: sampler,
},
{
binding: 2,
resource: cubeTexture.createView(),
},
],
});
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: undefined, // Assigned later
clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
};
const aspect = canvas.width / canvas.height;
const projectionMatrix = mat4.perspective(
(2 * Math.PI) / 5,
aspect,
1,
100.0
);
const modelViewProjectionMatrix = mat4.create();
function getModelViewProjectionMatrix(deltaTime: number) {
const camera = cameras[params.type];
// 通过输入更新相机类,返回viewMatrix
const viewMatrix = camera.update(deltaTime, inputHandler());
mat4.multiply(projectionMatrix, viewMatrix, modelViewProjectionMatrix);
return modelViewProjectionMatrix as Float32Array;
}
let lastFrameMS = Date.now();
function frame() {
const now = Date.now();
const deltaTime = (now - lastFrameMS) / 1000;
lastFrameMS = now;
if (!pageState.active) {
// Sample is no longer the active page.
return;
}
const modelViewProjection = getModelViewProjectionMatrix(deltaTime);
device.queue.writeBuffer(
uniformBuffer,
0,
modelViewProjection.buffer,
modelViewProjection.byteOffset,
modelViewProjection.byteLength
);
renderPassDescriptor.colorAttachments[0].view = context
.getCurrentTexture()
.createView();
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, uniformBindGroup);
passEncoder.setVertexBuffer(0, verticesBuffer);
passEncoder.draw(cubeVertexCount);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
};
相机 camera 代码
// Note: The code in this file does not use the 'dst' output parameter of functions in the
// 'wgpu-matrix' library, so produces many temporary vectors and matrices.
// This is intentional, as this sample prefers readability over performance.
import { Mat4, Vec3, Vec4, mat4, vec3 } from 'wgpu-matrix';
import Input from './input';
// Common interface for camera implementations
export default interface Camera {
// update updates the camera using the user-input and returns the view matrix.
update(delta_time: number, input: Input): Mat4;
// The camera matrix.
// This is the inverse of the view matrix.
matrix: Mat4;
// Alias to column vector 0 of the camera matrix.
right: Vec4;
// Alias to column vector 1 of the camera matrix.
up: Vec4;
// Alias to column vector 2 of the camera matrix.
back: Vec4;
// Alias to column vector 3 of the camera matrix.
position: Vec4;
}
// The common functionality between camera implementations
class CameraBase {
// The camera matrix
private matrix_ = new Float32Array([
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
]);
// The calculated view matrix
private readonly view_ = mat4.create();
// Aliases to column vectors of the matrix
private right_ = new Float32Array(this.matrix_.buffer, 4 * 0, 4);
private up_ = new Float32Array(this.matrix_.buffer, 4 * 4, 4);
private back_ = new Float32Array(this.matrix_.buffer, 4 * 8, 4);
private position_ = new Float32Array(this.matrix_.buffer, 4 * 12, 4);
// Returns the camera matrix
get matrix() {
return this.matrix_;
}
// Assigns `mat` to the camera matrix
set matrix(mat: Mat4) {
mat4.copy(mat, this.matrix_);
}
// Returns the camera view matrix
get view() {
return this.view_;
}
// Assigns `mat` to the camera view
set view(mat: Mat4) {
mat4.copy(mat, this.view_);
}
// Returns column vector 0 of the camera matrix
get right() {
return this.right_;
}
// Assigns `vec` to the first 3 elements of column vector 0 of the camera matrix
set right(vec: Vec3) {
vec3.copy(vec, this.right_);
}
// Returns column vector 1 of the camera matrix
get up() {
return this.up_;
}
// Assigns `vec` to the first 3 elements of column vector 1 of the camera matrix
set up(vec: Vec3) {
vec3.copy(vec, this.up_);
}
// Returns column vector 2 of the camera matrix
get back() {
return this.back_;
}
// Assigns `vec` to the first 3 elements of column vector 2 of the camera matrix
set back(vec: Vec3) {
vec3.copy(vec, this.back_);
}
// Returns column vector 3 of the camera matrix
get position() {
return this.position_;
}
// Assigns `vec` to the first 3 elements of column vector 3 of the camera matrix
set position(vec: Vec3) {
vec3.copy(vec, this.position_);
}
}
// WASDCamera is a camera implementation that behaves similar to first-person-shooter PC games.
export class WASDCamera extends CameraBase implements Camera {
// The camera absolute pitch angle
private pitch = 0;
// The camera absolute yaw angle
private yaw = 0;
// The movement veloicty
private readonly velocity_ = vec3.create();
// Speed multiplier for camera movement
movementSpeed = 10;
// Speed multiplier for camera rotation
rotationSpeed = 1;
// Movement velocity drag coeffient [0 .. 1]
// 0: Continues forever
// 1: Instantly stops moving
frictionCoefficient = 0.99;
// Returns velocity vector
get velocity() {
return this.velocity_;
}
// Assigns `vec` to the velocity vector
set velocity(vec: Vec3) {
vec3.copy(vec, this.velocity_);
}
// Construtor
constructor(options?: {
// The initial position of the camera
position?: Vec3;
// The initial target of the camera
target?: Vec3;
}) {
super();
if (options && (options.position || options.target)) {
const position = options.position ?? vec3.create(0, 0, -5);
const target = options.target ?? vec3.create(0, 0, 0);
const forward = vec3.normalize(vec3.sub(target, position));
this.recalculateAngles(forward);
this.position = position;
}
}
// Returns the camera matrix
get matrix() {
return super.matrix;
}
// Assigns `mat` to the camera matrix, and recalcuates the camera angles
set matrix(mat: Mat4) {
super.matrix = mat;
this.recalculateAngles(this.back);
}
update(deltaTime: number, input: Input): Mat4 {
const sign = (positive: boolean, negative: boolean) =>
(positive ? 1 : 0) - (negative ? 1 : 0);
// 根据鼠标的x, y 计算绕y轴,与绕x轴
// Apply the delta rotation to the pitch and yaw angles
this.yaw -= input.analog.x * deltaTime * this.rotationSpeed;
this.pitch -= input.analog.y * deltaTime * this.rotationSpeed;
// 限定范围
// Wrap yaw between [0° .. 360°], just to prevent large accumulation.
this.yaw = mod(this.yaw, Math.PI * 2);
// Clamp pitch between [-90° .. +90°] to prevent somersaults.
this.pitch = clamp(this.pitch, -Math.PI / 2, Math.PI / 2);
// Save the current position, as we're about to rebuild the camera matrix.
const position = vec3.copy(this.position);
// Reconstruct the camera's rotation, and store into the camera matrix.
super.matrix = mat4.rotateX(mat4.rotationY(this.yaw), this.pitch);
// Calculate the new target velocity
const digital = input.digital;
const deltaRight = sign(digital.right, digital.left);
const deltaUp = sign(digital.up, digital.down);
const targetVelocity = vec3.create();
const deltaBack = sign(digital.backward, digital.forward);
vec3.addScaled(targetVelocity, this.right, deltaRight, targetVelocity);
vec3.addScaled(targetVelocity, this.up, deltaUp, targetVelocity);
vec3.addScaled(targetVelocity, this.back, deltaBack, targetVelocity);
vec3.normalize(targetVelocity, targetVelocity);
vec3.mulScalar(targetVelocity, this.movementSpeed, targetVelocity);
// Mix new target velocity
this.velocity = lerp(
targetVelocity,
this.velocity,
Math.pow(1 - this.frictionCoefficient, deltaTime)
);
// Integrate velocity to calculate new position
this.position = vec3.addScaled(position, this.velocity, deltaTime);
// Invert the camera matrix to build the view matrix
this.view = mat4.invert(this.matrix);
return this.view;
}
// Recalculates the yaw and pitch values from a directional vector
recalculateAngles(dir: Vec3) {
this.yaw = Math.atan2(dir[0], dir[2]);
this.pitch = -Math.asin(dir[1]);
}
}
// ArcballCamera implements a basic orbiting camera around the world origin
export class ArcballCamera extends CameraBase implements Camera {
// The camera distance from the target
private distance = 0;
// The current angular velocity
private angularVelocity = 0;
// The current rotation axis
private axis_ = vec3.create();
// Returns the rotation axis
get axis() {
return this.axis_;
}
// Assigns `vec` to the rotation axis
set axis(vec: Vec3) {
vec3.copy(vec, this.axis_);
}
// Speed multiplier for camera rotation
rotationSpeed = 1;
// Speed multiplier for camera zoom
zoomSpeed = 0.1;
// Rotation velocity drag coeffient [0 .. 1]
// 0: Spins forever
// 1: Instantly stops spinning
frictionCoefficient = 0.999;
// Construtor
constructor(options?: {
// The initial position of the camera
position?: Vec3;
}) {
super();
if (options && options.position) {
this.position = options.position;
this.distance = vec3.len(this.position);
this.back = vec3.normalize(this.position);
this.recalcuateRight();
this.recalcuateUp();
}
}
// Returns the camera matrix
get matrix() {
return super.matrix;
}
// Assigns `mat` to the camera matrix, and recalcuates the distance
set matrix(mat: Mat4) {
super.matrix = mat;
this.distance = vec3.len(this.position);
}
update(deltaTime: number, input: Input): Mat4 {
const epsilon = 0.0000001;
if (input.analog.touching) {
// Currently being dragged.
this.angularVelocity = 0;
} else {
// Dampen any existing angular velocity
this.angularVelocity *= Math.pow(1 - this.frictionCoefficient, deltaTime);
}
// Calculate the movement vector
const movement = vec3.create();
vec3.addScaled(movement, this.right, input.analog.x, movement);
vec3.addScaled(movement, this.up, -input.analog.y, movement);
// Cross the movement vector with the view direction to calculate the rotation axis x magnitude
const crossProduct = vec3.cross(movement, this.back);
// Calculate the magnitude of the drag
const magnitude = vec3.len(crossProduct);
if (magnitude > epsilon) {
// Normalize the crossProduct to get the rotation axis
this.axis = vec3.scale(crossProduct, 1 / magnitude);
// Remember the current angular velocity. This is used when the touch is released for a fling.
this.angularVelocity = magnitude * this.rotationSpeed;
}
// The rotation around this.axis to apply to the camera matrix this update
const rotationAngle = this.angularVelocity * deltaTime;
if (rotationAngle > epsilon) {
// Rotate the matrix around axis
// Note: The rotation is not done as a matrix-matrix multiply as the repeated multiplications
// will quickly introduce substantial error into the matrix.
this.back = vec3.normalize(rotate(this.back, this.axis, rotationAngle));
this.recalcuateRight();
this.recalcuateUp();
}
// recalculate `this.position` from `this.back` considering zoom
if (input.analog.zoom !== 0) {
this.distance *= 1 + input.analog.zoom * this.zoomSpeed;
}
this.position = vec3.scale(this.back, this.distance);
// Invert the camera matrix to build the view matrix
this.view = mat4.invert(this.matrix);
return this.view;
}
// Assigns `this.right` with the cross product of `this.up` and `this.back`
recalcuateRight() {
this.right = vec3.normalize(vec3.cross(this.up, this.back));
}
// Assigns `this.up` with the cross product of `this.back` and `this.right`
recalcuateUp() {
this.up = vec3.normalize(vec3.cross(this.back, this.right));
}
}
// Returns `x` clamped between [`min` .. `max`]
function clamp(x: number, min: number, max: number): number {
return Math.min(Math.max(x, min), max);
}
// Returns `x` float-modulo `div`
function mod(x: number, div: number): number {
return x - Math.floor(Math.abs(x) / div) * div * Math.sign(x);
}
// Returns `vec` rotated `angle` radians around `axis`
function rotate(vec: Vec3, axis: Vec3, angle: number): Vec3 {
return vec3.transformMat4Upper3x3(vec, mat4.rotation(axis, angle));
}
// Returns the linear interpolation between 'a' and 'b' using 's'
function lerp(a: Vec3, b: Vec3, s: number): Vec3 {
return vec3.addScaled(a, vec3.sub(b, a), s);
}
input 代码
// Input holds as snapshot of input state
export default interface Input {
// Digital input (e.g keyboard state)
readonly digital: {
readonly forward: boolean;
readonly backward: boolean;
readonly left: boolean;
readonly right: boolean;
readonly up: boolean;
readonly down: boolean;
};
// Analog input (e.g mouse, touchscreen)
readonly analog: {
readonly x: number;
readonly y: number;
readonly zoom: number;
readonly touching: boolean;
};
}
// InputHandler is a function that when called, returns the current Input state.
export type InputHandler = () => Input;
// createInputHandler returns an InputHandler by attaching event handlers to the window and canvas.
export function createInputHandler(
window: Window,
canvas: HTMLCanvasElement
): InputHandler {
const digital = {
forward: false,
backward: false,
left: false,
right: false,
up: false,
down: false,
};
const analog = {
x: 0,
y: 0,
zoom: 0,
};
let mouseDown = false;
// 根据按键值,设置digital对于行为的true 和 false
const setDigital = (e: KeyboardEvent, value: boolean) => {
switch (e.code) {
case 'KeyW':
digital.forward = value;
e.preventDefault();
e.stopPropagation();
break;
case 'KeyS':
digital.backward = value;
e.preventDefault();
e.stopPropagation();
break;
case 'KeyA':
digital.left = value;
e.preventDefault();
e.stopPropagation();
break;
case 'KeyD':
digital.right = value;
e.preventDefault();
e.stopPropagation();
break;
case 'Space':
digital.up = value;
e.preventDefault();
e.stopPropagation();
break;
case 'ShiftLeft':
case 'ControlLeft':
case 'KeyC':
digital.down = value;
e.preventDefault();
e.stopPropagation();
break;
}
};
// 监听键盘按下
window.addEventListener('keydown', (e) => setDigital(e, true));
// 键盘键盘弹起
window.addEventListener('keyup', (e) => setDigital(e, false));
// 鼠标按下
canvas.style.touchAction = 'pinch-zoom';
canvas.addEventListener('pointerdown', () => {
mouseDown = true;
});
// 鼠标弹起
canvas.addEventListener('pointerup', () => {
mouseDown = false;
});
// 鼠标移动
canvas.addEventListener('pointermove', (e) => {
mouseDown = e.pointerType == 'mouse' ? (e.buttons & 1) !== 0 : true;
if (mouseDown) {
analog.x += e.movementX;
analog.y += e.movementY;
}
});
// 鼠标滚轮
canvas.addEventListener(
'wheel',
(e) => {
mouseDown = (e.buttons & 1) !== 0;
if (mouseDown) {
// The scroll value varies substantially between user agents / browsers.
// Just use the sign.
analog.zoom += Math.sign(e.deltaY);
e.preventDefault();
e.stopPropagation();
}
},
{ passive: false }
);
return () => {
const out = {
digital,
analog: {
x: analog.x,
y: analog.y,
zoom: analog.zoom,
touching: mouseDown,
},
};
// Clear the analog values, as these accumulate.
analog.x = 0;
analog.y = 0;
analog.zoom = 0;
return out;
};
}
顶点 片元着色器
struct Uniforms {
modelViewProjectionMatrix : mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture: texture_2d<f32>;
struct VertexOutput {
@builtin(position) Position : vec4f,
@location(0) fragUV : vec2f,
}
@vertex
fn vertex_main(
@location(0) position : vec4f,
@location(1) uv : vec2f
) -> VertexOutput {
return VertexOutput(uniforms.modelViewProjectionMatrix * position, uv);
}
@fragment
fn fragment_main(@location(0) fragUV: vec2f) -> @location(0) vec4f {
return textureSample(myTexture, mySampler, fragUV);
}
总结步骤:
- createInputHandler 方法监视了鼠标的操作及键盘的 A S D W SPACE等键的按下与弹起
- 相机控制类定义了两种,一种是WASD的控制方式,一种是轨道相机的控制方式,根据键盘鼠标的输入,转换为CameraMatrix矩阵的旋转、缩放、平移,最后求逆矩阵就是viewMatrix
- 每一个帧更新viewMatrix来改变相机的显示