uniapp - Vr3d全景720°预览

149 阅读3分钟

基础模板

这个模板所使用的网络图片-可能会失效,使用的时候替换为自己的720°全景图

<template>

  <view class="container">

    <canvas type="webgl" id="webglCanvas" canvas-id="webglCanvas"

      style="width: 100%; height: 100vh;"

      @touchstart="handleTouchStart"

      @touchmove="handleTouchMove"

      @touchend="handleTouchEnd">

    </canvas>

  </view>

</template>

  


<script>

export default {

  data() {

    return {

      gl: null,

      program: null,

      sphereVertices: null,

      rotation: {

        x: 0,

        y: Math.PI, // 初始旋转180度,使视角朝前

      },

      lastTouch: {

        x: 0,

        y: 0

      },

      isTouching: false,

      texture: null,

      touchSensitivity: 0.002,

      maxVerticalRotation: Math.PI / 2.1,

      imageAspectRatio: 1

    }

  },

  mounted() {

    this.$nextTick(() => {

      this.initWebGL();

    });

  },

  beforeDestroy() {

    // 清理资源

    if (this.gl) {

      this.gl.deleteTexture(this.texture);

      this.gl.deleteProgram(this.program);

    }

  },

  methods: {

    initWebGL() {

      const query = uni.createSelectorQuery().in(this);

      query.select('#webglCanvas')

        .fields({ node: true, size: true })

        .exec((res) => {

          if (!res[0] || !res[0].node) {

            console.error('Canvas not found');

            return;

          }

  


          const canvas = res[0].node;

          const dpr = uni.getSystemInfoSync().pixelRatio;

          canvas.width = res[0].width * dpr;

          canvas.height = res[0].height * dpr;

  


          this.gl = canvas.getContext('webgl', {

            antialias: true,

            preserveDrawingBuffer: true

          });

  


          if (!this.gl) {

            console.error('WebGL not supported');

            return;

          }

  


          this.gl.viewport(0, 0, canvas.width, canvas.height);

          this.initShaders();

          this.initSphere();

          this.loadTexture();

        });

    },

  


    initShaders() {

      const gl = this.gl;

     

      // 顶点着色器

      const vertexShader = gl.createShader(gl.VERTEX_SHADER);

      gl.shaderSource(vertexShader, `

        attribute vec4 aVertexPosition;

        attribute vec2 aTextureCoord;

        uniform mat4 uModelViewMatrix;

        uniform mat4 uProjectionMatrix;

        varying vec2 vTextureCoord;

        void main(void) {

          gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;

          vTextureCoord = aTextureCoord;

        }

      `);

      gl.compileShader(vertexShader);

  


      // 片段着色器

      const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

      gl.shaderSource(fragmentShader, `

        precision mediump float;

        varying vec2 vTextureCoord;

        uniform sampler2D uSampler;

        void main(void) {

          gl_FragColor = texture2D(uSampler, vTextureCoord);

        }

      `);

      gl.compileShader(fragmentShader);

  


      // 创建着色器程序

      this.program = gl.createProgram();

      gl.attachShader(this.program, vertexShader);

      gl.attachShader(this.program, fragmentShader);

      gl.linkProgram(this.program);

  


      if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {

        console.error('Unable to initialize shader program:', gl.getProgramInfoLog(this.program));

        return;

      }

    },

  


    initSphere() {

      const gl = this.gl;

      const radius = 500;

      const segments = 32; // 减少分段数以提高性能

      const vertices = [];

      const textureCoords = [];

      const indices = [];

  


      // 生成球体顶点

      for (let lat = 0; lat <= segments; lat++) {

        const theta = lat * Math.PI / segments;

        const sinTheta = Math.sin(theta);

        const cosTheta = Math.cos(theta);

  


        for (let long = 0; long <= segments; long++) {

          const phi = long * 2 * Math.PI / segments;

          const sinPhi = Math.sin(phi);

          const cosPhi = Math.cos(phi);

  


          const x = sinTheta * cosPhi;

          const y = cosTheta;

          const z = sinTheta * sinPhi;

  


          vertices.push(x * radius, y * radius, z * radius);

  


          // 纹理坐标

          const u = long / segments;

          const v = lat / segments;

          textureCoords.push(u, v);

        }

      }

  


      // 生成索引

      for (let lat = 0; lat < segments; lat++) {

        for (let long = 0; long < segments; long++) {

          const first = lat * (segments + 1) + long;

          const second = first + segments + 1;

  


          indices.push(first, second, first + 1);

          indices.push(second, second + 1, first + 1);

        }

      }

  


      // 创建缓冲区

      const vertexBuffer = gl.createBuffer();

      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

  


      const textureCoordBuffer = gl.createBuffer();

      gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);

      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);

  


      const indexBuffer = gl.createBuffer();

      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

  


      this.sphereVertices = {

        position: vertexBuffer,

        textureCoord: textureCoordBuffer,

        indices: indexBuffer,

        vertexCount: indices.length

      };

    },

  


    loadTexture() {

      const gl = this.gl;

     

      // 创建纹理

      this.texture = gl.createTexture();

      gl.bindTexture(gl.TEXTURE_2D, this.texture);

  


      // 设置临时纹理

      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));

  


      // 使用网络图片URL

      const imageUrl = 'https://mp-aa479501-d3ca-4827-a348-0eaaa39a92ab.cdn.bspapp.com/swiper/A.jpg';

      console.log('Loading image from:', imageUrl);

  


      // 使用base64下载图片

      uni.request({

        url: imageUrl,

        responseType: 'arraybuffer',

        success: (res) => {

          if (res.statusCode === 200) {

            // 将arraybuffer转换为base64

            const base64 = uni.arrayBufferToBase64(res.data);

            const base64Url = 'data:image/jpeg;base64,' + base64;

            console.log('Image converted to base64');

  


            // 创建离屏画布

            const offscreenCanvas = uni.createOffscreenCanvas({

              type: '2d',

              width: 4096, // 设置最大分辨率为4096x2048

              height: 2048

            });

           

            const ctx = offscreenCanvas.getContext('2d');

            const img = offscreenCanvas.createImage();

           

            img.onload = () => {

              console.log('Original image dimensions:', img.width, 'x', img.height);

             

              // 计算压缩后的尺寸,保持宽高比

              const maxDimension = 4096; // 最大尺寸

              let targetWidth = img.width;

              let targetHeight = img.height;

             

              if (img.width > maxDimension || img.height > maxDimension) {

                if (img.width > img.height) {

                  targetWidth = maxDimension;

                  targetHeight = Math.round((img.height * maxDimension) / img.width);

                } else {

                  targetHeight = maxDimension;

                  targetWidth = Math.round((img.width * maxDimension) / img.height);

                }

              }

             

              console.log('Compressed dimensions:', targetWidth, 'x', targetHeight);

             

              // 调整画布大小

              offscreenCanvas.width = targetWidth;

              offscreenCanvas.height = targetHeight;

             

              // 使用双线性插值进行缩放

              ctx.imageSmoothingEnabled = true;

              ctx.imageSmoothingQuality = 'high';

             

              // 绘制图片到离屏画布

              ctx.drawImage(img, 0, 0, targetWidth, targetHeight);

             

              // 获取图片数据

              const imageData = ctx.getImageData(0, 0, targetWidth, targetHeight);

             

              // 更新纹理

              gl.bindTexture(gl.TEXTURE_2D, this.texture);

              gl.texImage2D(

                gl.TEXTURE_2D,

                0,

                gl.RGBA,

                targetWidth,

                targetHeight,

                0,

                gl.RGBA,

                gl.UNSIGNED_BYTE,

                imageData.data

              );

  


              // 设置纹理参数

              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);

              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); // 使用mipmap

              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

             

              // 生成mipmap

              gl.generateMipmap(gl.TEXTURE_2D);

  


              // 保存图片宽高比

              this.imageAspectRatio = targetWidth / targetHeight;

             

              // 开始渲染

              this.drawScene();

            };

  


            img.onerror = (error) => {

              console.error('Failed to load image:', error);

            };

  


            img.src = base64Url;

          } else {

            console.error('Download failed with status:', res.statusCode);

          }

        },

        fail: (error) => {

          console.error('Failed to download image:', error);

        }

      });

    },

  


    handleTouchStart(event) {

      const touch = event.touches[0];

      this.lastTouch = {

        x: touch.pageX,

        y: touch.pageY

      };

      this.isTouching = true;

    },

  


    handleTouchMove(event) {

      if (!this.isTouching) return;

     

      const touch = event.touches[0];

      const deltaX = touch.pageX - this.lastTouch.x;

      const deltaY = (touch.pageY - this.lastTouch.y);

  


      this.rotation.y += deltaX * this.touchSensitivity;

      const newRotationX = this.rotation.x + deltaY * this.touchSensitivity;

     

      if (Math.abs(newRotationX) <= this.maxVerticalRotation) {

        this.rotation.x = newRotationX;

      }

  


      this.lastTouch = {

        x: touch.pageX,

        y: touch.pageY

      };

  


      this.drawScene();

    },

  


    handleTouchEnd() {

      this.isTouching = false;

    },

  


    drawScene() {

      const gl = this.gl;

      if (!gl || !this.program || !this.texture) return;

  


      gl.clearColor(0.0, 0.0, 0.0, 1.0);

      gl.clearDepth(1.0);

      gl.enable(gl.DEPTH_TEST);

      gl.depthFunc(gl.LEQUAL);

      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  


      const fieldOfView = 75 * Math.PI / 180;

      const aspect = gl.canvas.width / gl.canvas.height;

      const zNear = 0.1;

      const zFar = 2000.0;

      const projectionMatrix = this.createPerspectiveMatrix(fieldOfView, aspect, zNear, zFar);

  


      const modelViewMatrix = this.createModelViewMatrix();

  


      gl.useProgram(this.program);

  


      const positionLocation = gl.getAttribLocation(this.program, 'aVertexPosition');

      const textureCoordLocation = gl.getAttribLocation(this.program, 'aTextureCoord');

  


      gl.bindBuffer(gl.ARRAY_BUFFER, this.sphereVertices.position);

      gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

      gl.enableVertexAttribArray(positionLocation);

  


      gl.bindBuffer(gl.ARRAY_BUFFER, this.sphereVertices.textureCoord);

      gl.vertexAttribPointer(textureCoordLocation, 2, gl.FLOAT, false, 0, 0);

      gl.enableVertexAttribArray(textureCoordLocation);

  


      const projectionMatrixLocation = gl.getUniformLocation(this.program, 'uProjectionMatrix');

      const modelViewMatrixLocation = gl.getUniformLocation(this.program, 'uModelViewMatrix');

      const samplerLocation = gl.getUniformLocation(this.program, 'uSampler');

  


      gl.uniformMatrix4fv(projectionMatrixLocation, false, projectionMatrix);

      gl.uniformMatrix4fv(modelViewMatrixLocation, false, modelViewMatrix);

  


      gl.activeTexture(gl.TEXTURE0);

      gl.bindTexture(gl.TEXTURE_2D, this.texture);

      gl.uniform1i(samplerLocation, 0);

  


      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.sphereVertices.indices);

      gl.drawElements(gl.TRIANGLES, this.sphereVertices.vertexCount, gl.UNSIGNED_SHORT, 0);

    },

  


    createPerspectiveMatrix(fovy, aspect, near, far) {

      const f = 1.0 / Math.tan(fovy / 2);

      const nf = 1 / (near - far);

  


      return [

        f / aspect, 0, 0, 0,

        0, f, 0, 0,

        0, 0, (far + near) * nf, -1,

        0, 0, 2 * far * near * nf, 0

      ];

    },

  


    createModelViewMatrix() {

      const matrix = [

        1, 0, 0, 0,

        0, 1, 0, 0,

        0, 0, 1, 0,

        0, 0, 0, 1

      ];

  


      // 先应用水平旋转

      const cosY = Math.cos(this.rotation.y);

      const sinY = Math.sin(this.rotation.y);

      const rotationMatrixY = [

        cosY, 0, sinY, 0,

        0, 1, 0, 0,

        -sinY, 0, cosY, 0,

        0, 0, 0, 1

      ];

  


      // 再应用垂直旋转

      const cosX = Math.cos(this.rotation.x);

      const sinX = Math.sin(this.rotation.x);

      const rotationMatrixX = [

        1, 0, 0, 0,

        0, cosX, -sinX, 0,

        0, sinX, cosX, 0,

        0, 0, 0, 1

      ];

  


      // 先应用水平旋转,再应用垂直旋转

      const rotationMatrix = this.multiplyMatrices(rotationMatrixY, rotationMatrixX);

      return this.multiplyMatrices(matrix, rotationMatrix);

    },

  


    multiplyMatrices(a, b) {

      const result = new Array(16).fill(0);

     

      for (let i = 0; i < 4; i++) {

        for (let j = 0; j < 4; j++) {

          for (let k = 0; k < 4; k++) {

            result[i * 4 + j] += a[i * 4 + k] * b[k * 4 + j];

          }

        }

      }

     

      return result;

    }

  }

};

</script>

  


<style>

.container {

  width: 100%;

  height: 100vh;

  position: relative;

  overflow: hidden;

}

</style>