three.js制作3d饼图

410 阅读5分钟

背景

大屏项目中需要使用3d饼图,但是echarts的3d饼图的配置文档太少了,于是决定自己手搓一个3d饼图

技术栈

vue3+threejs

代码

  1. 首先创建一个3d场景
import * as THREE from 'three';
const source=[ //测试数据
  {
    name:'haha',
    value:10,
  },
  {
    name:'haha1',
    value:25,
  },
  {
    name:'haha2',
    value:12,
  },
]
const option={ //配置json
  camera:'0,-20,20',//相机的位置
  height:1,饼图的高度 //我这边是设置固定高度,可以根据自己情况来修改
  innerWidth:1,//饼图内径
  outWidth:2,//饼图外径
  opacity:0.8,//透明度
}
onMounted(()=>{
  const scene = new THREE.Scene();
  boxList.scene = scene
  const camera = new THREE.PerspectiveCamera(10, window.innerWidth / window.innerHeight, 0.1, 10000);
  boxList.camera = camera
  const camera_pos=option.camera.split(',')
  camera.position.set(...camera_pos);//添加相机并设置位置
  camera.lookAt(0,0,0)//我这边是设置的鼠标不操作柱状图 需要操作可以自己创建轨道控制器
  const renderer = new THREE.WebGLRenderer({antialias:true});//{antialias:true}开启抗锯齿
  boxList.render = renderer
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor(0xa5cfd5, 0.7)//设置场景的背景色和透明度
  let view = document.getElementById('three')
  view.appendChild(renderer.domElement);
  const light1 = new THREE.AmbientLight(0xffffff,2); //添加一个环境光
  scene.add( light1 );
  function animate() {渲染场景
    renderer.render(scene, camera);
  }
  animate();
}

2.创建扇形侧边讲解

微信图片_20240528103307.png 侧边由多个三角面成 我们需要求出每一个点的位置,图形的圆心位置是0,,通过三角函数和内边,外边的半径来求出每个顶点的位置。BufferGeometry是通过三个顶点组成的三角形来形成的网格(这边可以查看threejs的文档),所以组成的列表就是[[1,2,3],[2,3,4]......]这种的

3.处理数据和渲染模型

const color16=()=>{//十六进制颜色随机
  var r = Math.floor(Math.random()*256);
  var g = Math.floor(Math.random()*256);
  var b = Math.floor(Math.random()*256);

  //var color = '#'+r.toString(16)+g.toString(16)+b.toString(16);
  var rs = r.toString(16);
  var gs = g.toString(16);
  var bs = b.toString(16);
  if(rs.length<2) rs = "0"+rs;
            if(gs.length<2) gs = "0"+gs;
            if(bs.length<2) bs = "0"+bs;

  return parseInt(('#' + rs + gs + bs).slice(1),16)
}
const get_pie_vertex=(deg1,offset_deg,group,material)=>{//创建扇形的侧边
  const geometry = new THREE.BufferGeometry();
  const list=[]
  let list1=[]
  for(let i=0;i<=deg1/3;i++){//这个除3是因为我们每个系列的扇形角度都是3的倍数 下面进行过四舍五入到3的操作,
    const deg=Math.PI/180*i*3
    const x=Math.cos(deg)//获取每个顶点的x轴y轴,z轴就是高度,直接写就好
    const y=Math.sin(deg)
    list.push([x,y,0])
    list.push([x,y,option.height])
  }
  for(let i=deg1/3;i>=0;i--){//创建外径
    const deg=Math.PI/180*i*3
    const x=Math.cos(deg)*2
    const y=Math.sin(deg)*2
    list.push([x,y,0])
    list.push([x,y,option.height])
  }
  list.push(list[0])
  list.push(list[1])//将边围住
  list.forEach((item,index)=>{
    if(index>=list.length-2) return 
    list1=[...list1,...item,...list[index+1],...list[index+2]]//将数据整合成网格所需的数据格式
  })
  const vertices = new Float32Array(list1)
  
  geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
  geometry.computeVertexNormals();//这个必须加 要不然侧边会没有颜色
  const mesh = new THREE.Mesh( geometry, material );
  group.add(mesh)
  group.position.set(0,0,0)
  group.rotateZ(offset_deg)旋转偏差角度
  boxList.scene.add(group)
}
const add_pie=(deg,offset_deg)=>{//这个方法创建扇形的上下两个面
  let color=color16()//获取随机颜色
  const material = new THREE.MeshMatcapMaterial( { color: color,side:THREE.DoubleSide,transparent:true,
    opacity:0.8, } );添加扇形的材质
  const group=new THREE.Group()创建此系列数据的组,最后旋转时用到
  const geometry = new THREE.RingGeometry( option.innerWidth, option.outWidth, deg/3 ,1,0,Math.PI/180*deg);创建底部扇形网格
  const one_model=new THREE.Mesh(geometry,material)
  group.add(one_model)
  const geometry1 = new THREE.RingGeometry( option.innerWidth, option.outWidth, deg/3 ,1,0,Math.PI/180*deg);
  const one_model1=new THREE.Mesh(geometry1,material)
  one_model1.position.set(0,0,option.height)
  group.add(one_model1)创建顶部扇形网格
  boxList.scene.add(group)
  get_pie_vertex(deg,offset_deg,group,material)
}
const sum=source.reduce((before,next)=>{//先算出所有值的和
    return before+next.value
},0)
let offset_deg=0//饼图的偏移角度
  source.forEach((item,index)=>{
    let deg=0
    if(index==source.length-1){
      deg=360-offset_deg//当遍历到最后一项时,将剩余的角度都分配給他
    }else{
      deg=Math.round(item.value/sum*360/3)*3每一个面的宽度是3 除下的数据四舍五入到3
    }
    
    console.log(deg,offset_deg)
    add_pie(deg,Math.PI/180*offset_deg)
    offset_deg+=deg
    add_pie(deg,Math.PI/180*offset_deg)
    offset_deg+=deg
  })

所有代码

<template>
  <div id="three">

  </div>
</template>
<script setup>
import { reactive, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { my_three, load_file } from '../option/three/my_three'
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass'
import Three from './three.vue';
const data=reactive({
  mesh:null,
  renderer:null,
})
const source=[
  {
    name:'haha',
    value:10,
  },
  {
    name:'haha1',
    value:25,
  },
  {
    name:'haha2',
    value:12,
  },
]
let composer, renderPass, outlinePass, mesh
const addColor = ( color, config) => {
  composer = new EffectComposer(config.renderer)
      // 新建一个场景通道  为了覆盖到原理来的场景上
      renderPass = new RenderPass(config.scene, config.camera)
      composer.addPass(renderPass);
      // 物体边缘发光通道
      outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), config.scene, config.camera, [mesh])
      outlinePass.selectedObjects = [mesh]
      outlinePass.edgeStrength = 10.0 // 边框的亮度
      outlinePass.edgeGlow = 1// 光晕[0,1]
      outlinePass.usePatternTexture = false // 是否使用父级的材质
      outlinePass.edgeThickness = 1.0 // 边框宽度
      outlinePass.downSampleRatio = 1 // 边框弯曲度
      outlinePass.pulsePeriod = 5 // 呼吸闪烁的速度
      outlinePass.visibleEdgeColor.set(parseInt(0x00ff00)) // 呼吸显示的颜色
      outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0) // 呼吸消失的颜色
      outlinePass.clear = true
      composer.addPass(outlinePass)
      // 自定义的着色器通道 作为参数
      var effectFXAA = new ShaderPass(FXAAShader)
      effectFXAA.uniforms.resolution.value.set(1 / window.innerWidth, 1 / window.innerHeight)
      effectFXAA.renderToScreen = true
      composer.addPass(effectFXAA)
}

// const material = new THREE.MeshMatcapMaterial( { color: 0xff0000,side:THREE.DoubleSide,transparent:true,
//     opacity:0.8, } );
const boxList={
  scene:null
}
const option={
  camera:'0,-20,20',
  height:1,
  innerWidth:1,
  outWidth:2,
  opacity:0.8,
}
const color16=()=>{//十六进制颜色随机
  var r = Math.floor(Math.random()*256);
  var g = Math.floor(Math.random()*256);
  var b = Math.floor(Math.random()*256);

  //var color = '#'+r.toString(16)+g.toString(16)+b.toString(16);
  var rs = r.toString(16);
  var gs = g.toString(16);
  var bs = b.toString(16);
  if(rs.length<2) rs = "0"+rs;
            if(gs.length<2) gs = "0"+gs;
            if(bs.length<2) bs = "0"+bs;

  return parseInt(('#' + rs + gs + bs).slice(1),16)
}
const get_pie_vertex=(deg1,offset_deg,group,material)=>{
  const geometry = new THREE.BufferGeometry();
  const list=[]
  let list1=[]
  for(let i=0;i<=deg1/3;i++){
    const deg=Math.PI/180*i*3
    const x=Math.cos(deg)
    const y=Math.sin(deg)
    list.push([x,y,0])
    list.push([x,y,option.height])
  }
  for(let i=deg1/3;i>=0;i--){
    const deg=Math.PI/180*i*3
    const x=Math.cos(deg)*2
    const y=Math.sin(deg)*2
    list.push([x,y,0])
    list.push([x,y,option.height])
  }
  list.push(list[0])
  list.push(list[1])
  list.forEach((item,index)=>{
    if(index>=list.length-2) return 
    list1=[...list1,...item,...list[index+1],...list[index+2]]
  })
  const vertices = new Float32Array(list1)
  
  geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
  geometry.computeVertexNormals();
  const mesh = new THREE.Mesh( geometry, material );
  group.add(mesh)
  group.position.set(0,0,0)
  group.rotateZ(offset_deg)
  boxList.scene.add(group)
}
const add_pie=(deg,offset_deg)=>{
  let color=color16()
  const material = new THREE.MeshMatcapMaterial( { color: color,side:THREE.DoubleSide,transparent:true,
    opacity:0.8, } );
  const group=new THREE.Group()
  const geometry = new THREE.RingGeometry( option.innerWidth, option.outWidth, deg/3 ,1,0,Math.PI/180*deg);
  const one_model=new THREE.Mesh(geometry,material)
  group.add(one_model)
  const geometry1 = new THREE.RingGeometry( option.innerWidth, option.outWidth, deg/3 ,1,0,Math.PI/180*deg);
  const one_model1=new THREE.Mesh(geometry1,material)
  one_model1.position.set(0,0,option.height)
  group.add(one_model1)
  boxList.scene.add(group)
  get_pie_vertex(deg,offset_deg,group,material)
}

onMounted(()=>{
  window.onresize=()=>{
    console.log('更改窗口大小')
  }
  const scene = new THREE.Scene();
  boxList.scene = scene
  const camera = new THREE.PerspectiveCamera(10, window.innerWidth / window.innerHeight, 0.1, 10000);
  boxList.camera = camera
  const camera_pos=option.camera.split(',')
  camera.position.set(...camera_pos);
  const renderer = new THREE.WebGLRenderer({antialias:true});//{antialias:true}开启抗锯齿
  boxList.render = renderer
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor(0xa5cfd5, 0.7)
  // const axesHelper = new THREE.AxesHelper(5);//添加坐标箭头
  // axesHelper.position.set(0, 0, 0)
  // scene.add(axesHelper);
  let view = document.getElementById('three')
  view.appendChild(renderer.domElement);
  //总组
  const group = new THREE.Group();
  
  const light1 = new THREE.AmbientLight(0xffffff,2); // soft white light
  scene.add( light1 );
  //创建轨道控制器
  //const orbitControls = new OrbitControls(camera, renderer.domElement);
  //orbitControls.target = new THREE.Vector3(0, 0, 0);//控制焦点
  camera.lookAt(0,0,0)
 
  const clock = new THREE.Clock();//用于更新轨道控制器
 
  scene.add(group)
  
  const sum=source.reduce((before,next)=>{
    return before+next.value
  },0)
  console.log(sum)
  let offset_deg=0
  source.forEach((item,index)=>{
    let deg=0
    if(index==source.length-1){
      deg=360-offset_deg
    }else{
      deg=Math.round(item.value/sum*360/3)*3
    }
    
    console.log(deg,offset_deg)
    add_pie(deg,Math.PI/180*offset_deg)
    offset_deg+=deg
  })
  
// const light = new THREE.AmbientLight( 0x404040 ); // soft white light
// scene.add( light );	
////-----------------------------------------

  //addColor(0xff0000,{renderer,camera,scene})
  function animate() {
    renderer.render(scene, camera);
    //const delta = clock.getDelta();
    //orbitControls.update(delta);
    
    
    //requestAnimationFrame(animate);

    
    if (composer) {
      composer.render()
    } 
  }
  animate();
})
</script>
<style lang='less' scoped>

</style>