背景
大屏项目中需要使用3d饼图,但是echarts的3d饼图的配置文档太少了,于是决定自己手搓一个3d饼图
技术栈
vue3+threejs
代码
- 首先创建一个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.创建扇形侧边讲解
侧边由多个三角面成 我们需要求出每一个点的位置,图形的圆心位置是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>