Cameras

作者: 不决书 | 来源:发表于2024-01-19 07:24 被阅读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);
    }
    
    

    总结步骤:

    1. createInputHandler 方法监视了鼠标的操作及键盘的 A S D W SPACE等键的按下与弹起
    2. 相机控制类定义了两种,一种是WASD的控制方式,一种是轨道相机的控制方式,根据键盘鼠标的输入,转换为CameraMatrix矩阵的旋转、缩放、平移,最后求逆矩阵就是viewMatrix
    3. 每一个帧更新viewMatrix来改变相机的显示

    相关文章

      网友评论

          本文标题:Cameras

          本文链接:https://www.haomeiwen.com/subject/oaecodtx.html