ThreeJs制作模型图片

简介: 这篇文章介绍了如何使用Three.js将一张图片转化为3D场景中的像素化模型,通过提取图片的像素颜色并将它们应用到3D立方体上,形成一种特殊的图像展示效果。

这个标题名字可能有歧义,只是不知道如何更好的表达,总之就是将图片的像素转换成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>
相关文章
|
JavaScript 应用服务中间件 图形学
基于Threejs实现glb三维模型的预览
文章将重点介绍如何基于Threejs进行三维模型glb进行预览,可以旋转控制。
1394 0
基于Threejs实现glb三维模型的预览
|
2月前
Threejs制作窗户透亮效果
这篇文章讲解了如何在Three.js中制作窗户的透亮效果,包括设置透明材质和光照以实现逼真的窗户渲染效果的技术细节。
77 1
|
2月前
Threejs制作骨骼模型
这篇文章详细介绍了在Three.js中创建骨骼动画的过程,包括骨骼节点的创建、权重设置以及控制骨骼关节实现动态效果的步骤,并通过一个具体的圆柱体模型演示了如何添加和控制骨骼动画。
36 2
|
2月前
Threejs制作海面效果
这篇文章详细介绍了利用Three.js制作逼真海面效果的过程,包括设置Water材质、调整光照及实现波动动画的步骤。
52 0
Threejs制作海面效果
|
2月前
ThreeJs设置模型的边线
这篇文章介绍了如何在Three.js中为3D模型添加边线效果,并提供了实现这一功能的代码示例。
52 2
|
人工智能 算法 vr&ar
在线GLTF模型材质编辑工具
模型材质贴图的作用是为三维模型赋予外观表面的纹理和颜色。它可以增加模型的细节、真实感和视觉效果,使得模型更具有逼真和吸引力。通过贴图,模型可以呈现出不同的材质,如金属、木材、布料等,并且能够模拟反射、阴影和光照效果,使模型在渲染过程中更加真实。贴图还可以用来描绘模型的细节纹理,例如皮肤的纹理、衣物的图案等。总之,模型材质贴图对于创造逼真的三维模型非常重要。
155 1
|
图形学
【Unity实战系列】如何把你的二次元老婆/老公导入Unity进行二创并且进行二次元渲染?(附模型网站分享)
【Unity实战系列】如何把你的二次元老婆/老公导入Unity进行二创并且进行二次元渲染?(附模型网站分享)
552 0
|
计算机视觉
平面设计实验二 相册的制作与图层
平面设计实验二 相册的制作与图层
84 0
|
iOS开发 MacOS Windows
Unity2D像素游戏开发——Aseprite简单人物绘画+动画制作导出精灵表示例
Unity2D像素游戏开发——Aseprite简单人物绘画+动画制作导出精灵表示例
687 0
Unity2D像素游戏开发——Aseprite简单人物绘画+动画制作导出精灵表示例
|
图形学
Unity 导入原神人物模型
Unity 导入原神人物模型
4422 2
Unity 导入原神人物模型