这个标题名字可能有歧义,只是不知道如何更好的表达,总之就是将图片的像素转换成3D场景的模型,并设置这个模型的颜色,放到像素点对应的位置从而拼接成一个图片,起因是上文中用js分解了音乐,实现了模型跳动效果,既然音频可以分解,那图片应该也可以,所以就有个这篇博客。
大概得实现逻辑是这样的,先找一个图片,像素要小,越小越好,要有花纹,然后用canvas将图片的每个像素拆解出来,拆解后可以获得这个图片每个像素的位置,颜色,用集合保存每个像素的信息,在3D场景中循环,有了位置和颜色后,在循环中创建一个个正方体,将正方体的位置设置为像素的位置,y轴方向为1,创建贴图,并将贴图的颜色改为像素点的颜色,全部循环后就得到一副用正方体拼接出来的图片了。但是如果你的图片分辨率高,那么拆解出来的像素点很多,就需要筛选掉一些,否则浏览器会卡死,所以强调用分辨率低的图片。
这里先找一副图片:
下面开始代码:
首先创建场景,相机,灯光,渲染器等:
initScene(){scene = new THREE.Scene();},initCamera(){this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);this.camera.position.set(200,400,200);},initLight(){//添加两个平行光const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);directionalLight1.position.set(300,300,600)scene.add(directionalLight1);const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.5);directionalLight2.position.set(600,200,600)scene.add(directionalLight2);},
initRenderer(){this.renderer = new THREE.WebGLRenderer({ antialias: true });this.container = document.getElementById("container")this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);this.renderer.setClearColor('#AAAAAA', 1.0);this.container.appendChild(this.renderer.domElement);},initControl(){this.controls = new OrbitControls(this.camera, this.renderer.domElement);this.controls.enableDamping = true;this.controls.maxPolarAngle = Math.PI / 2.2; // // 最大角度},initAnimate() {requestAnimationFrame(this.initAnimate);this.renderer.render(scene, this.camera);},
然后封装一个用canvas分解图片的方法
getImagePixels(image) {return new Promise((resolve) => {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = image.width;canvas.height = image.height;ctx.drawImage(image, 0, 0);const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const data = imageData.data;const pixels = [];for (let i = 0; i < data.length; i += 4) {const x = (i / 4) % canvas.width;const y = Math.floor((i / 4) / canvas.width);const r = data[i];const g = data[i + 1];const b = data[i + 2];const a = data[i + 3];pixels.push({ x, y, r, g, b, a });}resolve(pixels); // 返回所有像素的数据数组});},
然后调用这个方法获取到像素点集合信息,再循环这个集合,这里为了不卡顿,选择每40个像素点才生成一个模型,也就是下面的i%40===0的判断,
initBox(){const img = new Image();img.src = '/static/images/image.jpg';let geometry = new THREE.BoxGeometry(1, 1, 1);img.onload = async () => {let boxModel = []try {const allPixels = await this.getImagePixels(img);for (let i = 0; i < allPixels.length; i++) {if(i%40 === 0) {let r = allPixels[i].r;let g = allPixels[i].g;let b = allPixels[i].b;let x = allPixels[i].x;let y = allPixels[i].y;let cubeMaterial = new THREE.MeshPhysicalMaterial({color: 'rgb(' + r + ', ' + g + ', ' + b + ')'});this.boxMaterial.push(cubeMaterial)let mesh = new THREE.Mesh(geometry.clone(), cubeMaterial);mesh.position.set(x, 1, y);mesh.updateMatrix() // 更新投影矩阵,不更新各mesh位置会不正确boxModel.push(mesh.geometry.applyMatrix4(mesh.matrix));}}const boxGeometry = mergeGeometries(boxModel,true)let result = new THREE.Mesh(boxGeometry, this.boxMaterial)scene.add(result);console.log("執行完畢")} catch (error) {console.error('Error getting image pixels:', error);}};},
最终得到一副比较虚幻的图片
因为每个模型之间距离比较远,所以图片比较阴暗和虚幻,为了提高图片效果,可以将模型的宽和高改为5,
let geometry = new THREE.BoxGeometry(5, 5, 5);
这样就真实点了,可以根据电脑性能来调整去选取的像素点个数,如果电脑足够好,也可以根据上一篇音乐的效果,给这个图片添加音乐效果的跳动。
完整代码如下:
<template><div style="width:100px;height:100px;"><div id="container"></div></div>
</template><script>
import * as THREE from 'three'
import {OrbitControls} from "three/addons/controls/OrbitControls";
import {mergeGeometries} from "three/addons/utils/BufferGeometryUtils";let scene;
export default {name: "agv-single",data() {return{camera:null,cameraCurve:null,renderer:null,container:null,controls:null,imageData:[],boxMaterial:[],}},methods:{initScene(){scene = new THREE.Scene();},initCamera(){this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);this.camera.position.set(200,400,200);},initLight(){//添加两个平行光const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);directionalLight1.position.set(300,300,600)scene.add(directionalLight1);const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.5);directionalLight2.position.set(600,200,600)scene.add(directionalLight2);},initBox(){const img = new Image();img.src = '/static/images/image.jpg';let geometry = new THREE.BoxGeometry(5, 5, 5);img.onload = async () => {let boxModel = []try {const allPixels = await this.getImagePixels(img);for (let i = 0; i < allPixels.length; i++) {if(i%40 === 0) {let r = allPixels[i].r;let g = allPixels[i].g;let b = allPixels[i].b;let x = allPixels[i].x;let y = allPixels[i].y;let cubeMaterial = new THREE.MeshPhysicalMaterial({color: 'rgb(' + r + ', ' + g + ', ' + b + ')'});this.boxMaterial.push(cubeMaterial)let mesh = new THREE.Mesh(geometry.clone(), cubeMaterial);mesh.position.set(x, 1, y);mesh.updateMatrix() // 更新投影矩阵,不更新各mesh位置会不正确boxModel.push(mesh.geometry.applyMatrix4(mesh.matrix));}}const boxGeometry = mergeGeometries(boxModel,true)let result = new THREE.Mesh(boxGeometry, this.boxMaterial)scene.add(result);console.log("執行完畢")} catch (error) {console.error('Error getting image pixels:', error);}};},getImagePixels(image) {return new Promise((resolve) => {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = image.width;canvas.height = image.height;ctx.drawImage(image, 0, 0);const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const data = imageData.data;const pixels = [];for (let i = 0; i < data.length; i += 4) {const x = (i / 4) % canvas.width;const y = Math.floor((i / 4) / canvas.width);const r = data[i];const g = data[i + 1];const b = data[i + 2];const a = data[i + 3];pixels.push({ x, y, r, g, b, a });}resolve(pixels); // 返回所有像素的数据数组});},initRenderer(){this.renderer = new THREE.WebGLRenderer({ antialias: true });this.container = document.getElementById("container")this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);this.renderer.setClearColor('#AAAAAA', 1.0);this.container.appendChild(this.renderer.domElement);},initControl(){this.controls = new OrbitControls(this.camera, this.renderer.domElement);this.controls.enableDamping = true;this.controls.maxPolarAngle = Math.PI / 2.2; // // 最大角度},initAnimate() {requestAnimationFrame(this.initAnimate);this.renderer.render(scene, this.camera);},initPage(){this.initScene();this.initCamera();this.initLight();this.initBox();this.initRenderer();this.initControl();this.initAnimate();}},mounted() {this.initPage()}
}
</script><style scoped>
#container{position: absolute;width:100%;height:100%;overflow: hidden;
}</style>