基础模板
这个模板所使用的网络图片-可能会失效,使用的时候替换为自己的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>