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>
相关文章
Threejs实现相机视角切换,平滑过渡,点击模型切换到查看模型视角
Threejs实现相机视角切换,平滑过渡,点击模型切换到查看模型视角
2803 0
Threejs实现相机视角切换,平滑过渡,点击模型切换到查看模型视角
|
JavaScript 前端开发 索引
用Three.js搞个炫酷3D地球
地球人怎么可以不会画地球!从canvas画地球贴图开始,用Three.js手把手教你实现一个炫酷的3D地球!
用Three.js搞个炫酷3D地球
|
JavaScript
Ant Design Vue栅格Grid的使用
Ant Design Vue栅格Grid的使用
Ant Design Vue栅格Grid的使用
|
3月前
|
移动开发 JavaScript API
Uniapp 与原生 App 集成时如何解决兼容性问题?
Uniapp 与原生 App 集成时如何解决兼容性问题?
596 136
|
JavaScript 数据可视化 算法
vue3+threejs可视化项目——搭建vue3+ts+antd路由布局(第一步)
vue3+threejs可视化项目——搭建vue3+ts+antd路由布局(第一步)
351 6
|
人工智能 Java Serverless
【MCP教程系列】搭建基于 Spring AI 的 SSE 模式 MCP 服务并自定义部署至阿里云百炼
本文详细介绍了如何基于Spring AI搭建支持SSE模式的MCP服务,并成功集成至阿里云百炼大模型平台。通过四个步骤实现从零到Agent的构建,包括项目创建、工具开发、服务测试与部署。文章还提供了具体代码示例和操作截图,帮助读者快速上手。最终,将自定义SSE MCP服务集成到百炼平台,完成智能体应用的创建与测试。适合希望了解SSE实时交互及大模型集成的开发者参考。
12984 60
|
3月前
|
存储 搜索推荐 数据库
🚀 RAGFlow Docker 部署全流程教程
RAGFlow是开源的下一代RAG系统,融合向量数据库与大模型,支持全文检索、插件化引擎切换,适用于企业知识库、智能客服等场景。支持Docker一键部署,提供轻量与完整版本,助力高效搭建私有化AI问答平台。
2270 8
|
JavaScript API 图形学
一个案例带你从零入门Three.js,深度好文!
【8月更文挑战第1天】本教程无需任何Threejs知识!本教程以入门为主,带你快速了解Three.js开发
538 2
一个案例带你从零入门Three.js,深度好文!
|
JavaScript 前端开发 数据可视化
Three.js第2篇,加载glb / gltf模型,Vue加载glb / gltf模型(如何在vue中使用three.js,vue使用threejs加载glb模型)
Three.js 是一个用于在 Web 上创建和显示 3D 图形的 JavaScript 库。它提供了丰富的功能和灵活的 API,使开发者可以轻松地在网页中创建各种 3D 场景、模型和动画效果。可以用来展示产品模型、建立交互式场景、游戏开发、数据可视化、教育和培训等等。这里记录一下如何在Vue项目中使用Three.js
4110 4
Three.js第2篇,加载glb / gltf模型,Vue加载glb / gltf模型(如何在vue中使用three.js,vue使用threejs加载glb模型)
threeJs用精灵模型Sprite实现下雨效果
这篇文章介绍了使用Three.js中的Sprite(精灵)模型来实现下雨特效的方法和技术细节。
578 2
threeJs用精灵模型Sprite实现下雨效果