three.js实现3d球体树状结构布局——添加入场、出场、点击放大等动画

目录

    • 系列文章
    • 前言
    • 新增功能
      • 添加背景
      • 灯光旋转动画
      • 数据入场、出场动画
      • 点击放大
    • 实现效果
    • 实现源码
    • 相关资源

系列文章

    three.js实现3d球体树状结构布局——树状结构的实现

前言

    本文建议先看系列文章第一篇树状结构的实现,下文内容也将衔接第一篇内容。

新增功能

添加背景

// 球形背景几何体的材质
const createMeshPhongMaterialTexture = (data) => {
    let materialData = {
        map: new THREE.TextureLoader().load(data.url)
    }
    if(data.side){
        materialData.side = THREE.BackSide
    }
    return new THREE.MeshPhysicalMaterial(materialData);
}

const bjMeshStyle = {
    geometry: {
        radius: 'auto',
        widthSegments: 320,
        heightSegments: 160
    },
    material: {
        url: "./sphere-bg2.jpg",
        side: true
    }
}

const bjGeometry = createSphereGeometry(bjMeshStyle.geometry);
const bjMaterial = createMeshPhongMaterialTexture(bjMeshStyle.material);
const bjMesh = createMesh(bjGeometry, bjMaterial);
bjMesh.position.set(
    cameraStyle.lookAt[0],
    cameraStyle.lookAt[1],
    cameraStyle.lookAt[2]
);
scene.add(bjMesh);

灯光旋转动画

pointLight = createPointLight({ color: "#fff", intensity: 1 });
let pointLightL = R * 10
pointLight.rotateSpeed =  Math.PI / 100;
pointLight.lightAngle = Math.atan(1);
pointLight.arcR = Math.sqrt(pointLightL * pointLightL * 2);
pointLight.init_position = {
    x: pointLightL,
    y: pointLightL,
    z: pointLightL,
}
pointLight.position.set(pointLightL, pointLightL, pointLightL);
scene.add(pointLight);

// 渲染
const render = () => {
    rotate()
    //循环调用
    requestAnimationFrame(render);
    renderer.render(scene, camera);
};
const resetPointLightPosition = () => {
    pointLight.position.set(pointLight.init_position.x, pointLight.init_position.y, pointLight.init_position.z);
    pointLight.lightAngle = Math.atan(1);
}
const rotate = (angle) => {
    if(pointLight.rotateFlag){
        return
    }
    pointLight.lightAngle += angle || pointLight.rotateSpeed
    pointLight.lightAngle = pointLight.lightAngle % (Math.PI * 2)
    pointLight.position.set(Math.sin(pointLight.lightAngle) * pointLight.arcR, pointLight.position.y, Math.cos(pointLight.lightAngle) * pointLight.arcR);
}

数据入场、出场动画

const computedPointPosition = (
    data,
    style,
    ArcRArr,
    startAngle = 0,
    endAngle = Math.PI * 2,
    deep = 0
) => {
    let totalWight = 0;
    for (let i = 0; i < data.length; i++) {
        totalWight += data[i].weight;
    }
    let AngleScope = endAngle - startAngle;
    let curAngle = startAngle;
    let randTranslate = ArcRArr[deep] * 10 || 1000
    for (let i = 0; i < data.length; i++) {
        let item = data[i];
        let ratioAngle = (item.weight / totalWight) * AngleScope;
        item.targetPostion = {
            x: Math.sin(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[0],
            y: style.startPositionY - deep * (style.yr || 0) + style.centerXYZ[1],
            z: Math.cos(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[2]
        }
        item.translatePostion = {
            y: rand(-randTranslate, randTranslate),
            x: rand(-randTranslate, randTranslate),
            z: rand(-randTranslate, ArcRArr[deep])
        }
        if (item?.children?.length) {
            computedPointPosition(
                item?.children,
                style,
                ArcRArr,
                curAngle,
                curAngle + ratioAngle,
                deep + 1
            );
        }
        curAngle += ratioAngle;
    }
};

const render = () => {
    rotate()
    pointAnimation()
    cameraAnimation()
    //循环调用
    requestAnimationFrame(render);
    renderer.render(scene, camera);
};
const pointAnimation = () => {
    if(animationIndex === targetAnimationIndex){
        return
    }
    animationIndex++
    let scale = (targetAnimationIndex - animationIndex) / targetAnimationIndex
    let scale2 = animationIndex / targetAnimationIndex
    sphereGeometrys.forEach((item, index) => {
        let data = {
            x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale,
            y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale,
            z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale
        }
        item.position.set(data.x, data.y, data.z);
        textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
    })
    old_sphereGeometrys.forEach((item, index) => {
        let data = {
            x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale2,
            y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale2,
            z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale2
        }
        item.position.set(data.x, data.y, data.z);
        old_textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
    })

    if(animationIndex === targetAnimationIndex){
        old_sphereGeometrys.forEach((item,index) => {
            scene.remove(item);
            scene.remove(old_textGeometrys[index]);
        })
        old_sphereGeometrys = []
        old_textGeometrys = []

        if(lineGeometrys.length){
            scene.add(...lineGeometrys);
        }
    }
}
const cameraAnimation = () => {
    if(cameraAnimationIndex === targetCameraAnimationIndex){
        return
    }
    cameraAnimationIndex++
    let scale = (targetCameraAnimationIndex - cameraAnimationIndex) / targetCameraAnimationIndex
    camera.position.set(
        camera.camera_position.x + camera.translate_camera_position.x * scale,
        camera.camera_position.y + camera.translate_camera_position.y * scale,
        camera.camera_position.z + camera.translate_camera_position.z * scale,
    );
    camera.lookAt(
        cameraStyle.lookAt[0],
        cameraStyle.lookAt[1],
        cameraStyle.lookAt[2]
    );
}

点击放大

treeDom.value.addEventListener( 'click', meshOnClick );

// 点击回调
const meshOnClick = (event) => {
    const pointer = new THREE.Vector2();
    pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
    pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

    let intersects = [];
    let raycaster = new THREE.Raycaster();
    raycaster.setFromCamera( pointer, camera );
    intersects = raycaster.intersectObjects( sphereGeometrys, true );
    if ( intersects.length > 0 ) {
        meshOnClickCb(intersects[0].object)
    }
}
const meshOnClickCb = (mesh) => {
    let dx = mesh.position.x - cameraStyle.lookAt[0]
    let dy = mesh.position.y - cameraStyle.lookAt[1]
    let dz = mesh.position.z - cameraStyle.lookAt[2]
    if(Math.abs(dx) < 0.001){
        dx = 0
    }
    if(Math.abs(dy) < 0.001){
        dy = 0
    }
    if(Math.abs(dz) < 0.001){
        dz = 0
    }
    let targetPositon = {}
    if(dx === 0 && dy === 0 && dz === 0){
        targetPositon = {
            x: mesh.position.x + treeStyle.pointInterval,
            y: mesh.position.y + treeStyle.pointInterval,
            z: mesh.position.z + treeStyle.pointInterval,
        }
    }else if(dx === 0 && dy === 0){
        let dzPointInterval = dz / Math.abs(dz) * treeStyle.pointInterval * 2
        targetPositon = {
            x: mesh.position.x + dzPointInterval,
            y: mesh.position.y + dzPointInterval,
            z: mesh.position.z + dzPointInterval,
        }
    }else if(dy === 0 && dz === 0){
        let dxPointInterval = dx / Math.abs(dx) * treeStyle.pointInterval * 2
        targetPositon = {
            x: mesh.position.x + dxPointInterval,
            y: mesh.position.y + dxPointInterval,
            z: mesh.position.z + dxPointInterval,
        }
    }else if(dx === 0 && dz === 0){
        let dyPointInterval = dy / Math.abs(dy) * treeStyle.pointInterval * 2
        targetPositon = {
            x: mesh.position.x + dyPointInterval,
            y: mesh.position.y + dyPointInterval,
            z: mesh.position.z + dyPointInterval,
        }
    }else{
        let dd = null
        if(dx !== 0){
            dd = dx
        }else if(dy !== 0){
            dd = dy
        }else if(dz !== 0){
            dd = dz
        }
        let translateL = treeStyle.pointInterval * dd / Math.abs(dd)
        let dxdd = dx / dd
        let dydd = dy / dd
        let dzdd = dz / dd
        let maxdd = dxdd > dydd ? dxdd : dydd
        maxdd = maxdd > dzdd ? maxdd : dzdd
        if(Math.abs(maxdd) > 1){
            let scale = Math.abs(maxdd) / 1
            dxdd = dxdd / scale
            dydd = dydd / scale
            dzdd = dzdd / scale
        }
        targetPositon = {
            x: mesh.position.x + translateL * dxdd,
            y: mesh.position.y + translateL * dydd,
            z: mesh.position.z + translateL * dzdd,
        }
    }
    camera.translate_camera_position = {
        x: camera.position.x - targetPositon.x,
        y: camera.position.y - targetPositon.y,
        z: camera.position.z - targetPositon.z,
    }
    camera.camera_position = JSON.parse(JSON.stringify(targetPositon))
    let length = Math.sqrt(Math.pow(camera.translate_camera_position.x, 2) + Math.pow(camera.translate_camera_position.y, 2) + Math.pow(camera.translate_camera_position.z, 2))
    cameraAnimationIndex = length > treeStyle.pointInterval ? 0 : Math.floor(length / treeStyle.pointInterval * targetCameraAnimationIndex)
}

实现效果

three.js实现3d球体树状结构布局博客演示视频2

实现源码

<script setup>
import { onMounted, ref, onBeforeUnmount, computed, reactive } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import SpriteText from "three-spritetext";
import { MeshLine, MeshLineMaterial } from "three.meshline";
// 图片
import cameraSvg from '@/assets/images/camera.svg'
import openSvg from '@/assets/images/open.svg'
import closeSvg from '@/assets/images/close.svg'
import startSvg from '@/assets/images/start.svg'
import stopSvg from '@/assets/images/stop.svg'
import dataSvg from '@/assets/images/data.svg'

// 渲染dom
let treeDom = ref(null);
// 场景
const createScene = () => {
    return new THREE.Scene();
};
// 相机(透视投影相机)
const createPerspectiveCamera = ({ fov, aspect, near, far }) => {
    /* 
        fov — 摄像机视锥体垂直视野角度
        aspect — 摄像机视锥体长宽比
        near — 摄像机视锥体近端面
        far — 摄像机视锥体远端面
    */
    return new THREE.PerspectiveCamera(fov, aspect, near, far);
};
// 渲染器
const createWebGLRenderer = ({ dom, width, height }) => {
    /* 
        renderDom — dom
        width — 渲染宽度 一般取domclientWidth
        height — 渲染高度 一般取clientHeight
    */
    if (width === undefined) {
        width = dom.clientWidth;
    }
    if (height === undefined) {
        height = dom.clientHeight;
    }
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);
    dom.appendChild(renderer.domElement);

    return renderer;
};
// 辅助线
const createAxesHelper = (length) => {
    return new THREE.AxesHelper(length);
};
// 环境光
const createAmbientLight = ({ color, intensity }) => {
    // color - (可选参数)) 十六进制光照颜色。 缺省值 0xffffff (白色)。
    // intensity - (可选参数) 光照强度。 缺省值 1。
    return new THREE.AmbientLight(color, intensity);
};
// 点光
const createPointLight = ({ color, intensity, distance, decay }) => {
    /*
        color - (可选参数)) 十六进制光照颜色。 缺省值 0xffffff (白色)。
        intensity - (可选参数) 光照强度。 缺省值 1。
        distance - 这个距离表示从光源到光照强度为0的位置。 当设置为0时,光永远不会消失(距离无穷大)。缺省值 0.
        decay - 沿着光照距离的衰退量。缺省值 2。
    */
    return new THREE.PointLight(color, intensity, distance, decay);
};
// 球形几何体
const createSphereGeometry = ({
    radius,
    widthSegments,
    heightSegments,
    phiStart,
    phiLength,
    thetaStart,
    thetaLength,
}) => {
    /*
        radius — 球体半径,默认为1。
        widthSegments — 水平分段数(沿着经线分段),最小值为3,默认值为32。
        heightSegments — 垂直分段数(沿着纬线分段),最小值为2,默认值为16。
        phiStart — 指定水平(经线)起始角度,默认值为0。。
        phiLength — 指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2。
        thetaStart — 指定垂直(纬线)起始角度,默认值为0。
        thetaLength — 指定垂直(纬线)扫描角度大小,默认值为 Math.PI。
    */
    return new THREE.SphereGeometry(
        radius,
        widthSegments,
        heightSegments,
        phiStart,
        phiLength,
        thetaStart,
        thetaLength
    );
};
// 球形几何体的材质
const createMeshLambertMaterial = (data) => {
    return new THREE.MeshLambertMaterial(data);
};
// 球形背景几何体的材质
const createMeshPhongMaterialTexture = (data) => {
    let materialData = {
        map: new THREE.TextureLoader().load(data.url)
    }
    if(data.side){
        materialData.side = THREE.BackSide
    }
    return new THREE.MeshPhysicalMaterial(materialData);
}
// 线几何体
const createLineGeometry = (points) => {
    const pointsVector3 = [];
    for (let i = 0; i < points.length; i++) {
        pointsVector3.push(
            new THREE.Vector3(points[i].x, points[i].y, points[i].z)
        );
    }
    const geometry = new THREE.BufferGeometry().setFromPoints(pointsVector3);
    const line = new MeshLine();
    line.setGeometry(geometry);
    return line;
};
// 线几何体的材质
const createMeshLineMaterial = (data) => {
    return new MeshLineMaterial({
        lineWidth: data.linewidth,
        color: data.color || "white",
        dashArray: data.dashArray || 0,
        transparent: true,
    });
};
// 物体
const createMesh = (geometry, materialBasic) => {
    return new THREE.Mesh(geometry, materialBasic);
};
// 文本
const createText = ({ text, size, color }) => {
    let textClass = new SpriteText(text, size);
    textClass.color = color;
    return textClass;
};
// 轨道控制
const createControl = (camera, dom) => {
    return new OrbitControls(camera, dom);
};

// 3d树布局算法 灵感来源 梯形/三角形转换为圆锥
const computedDataWeight = (data) => {
    let weight = 0;
    for (let i = 0; i < data.length; i++) {
        let item = data[i];
        if (item?.children?.length) {
            item.weight = computedDataWeight(item.children);
            weight += item.weight;
        } else {
            item.weight = 1;
            weight += 1;
        }
    }
    return weight;
};
const computedArcRArr = (data, pointInterval) => {
    let ArcRArr = [];
    let ArcData = [];
    let ArcWeight = [];
    formatTreeToArcData(data, ArcData);

    for (let i = 0; i < ArcData.length; i++) {
        let item = ArcData[i];
        let weight = 0;
        for (let j = 0; j < item.length; j++) {
            weight += item[j].weight;
        }
        ArcWeight.push(weight);
    }
    let R = computedArcR(pointInterval, ArcWeight[0]);

    // 半径算法
    for (let i = 0; i < ArcData.length; i++) {
        let item = ArcData[i];
        if (ArcWeight[i] < ArcWeight[0]) {
            // 不是完全层
            ArcRArr.push(R);
        } else {
            if (item.length > 1) {
                // 完全层
                let DValue = 0;
                item.forEach((weight) => {
                    DValue += Math.floor(weight.weight / 2);
                });
                ArcRArr.push(((ArcWeight[i] - DValue) / ArcWeight[i]) * R);
            } else {
                ArcRArr.push(0);
            }
        }
    }
    return { ArcRArr, R };
};
const formatTreeToArcData = (data, ArcData, deep = 0) => {
    data.forEach((element) => {
        if (!ArcData[deep]) {
            ArcData[deep] = [];
        }
        ArcData[deep].push({
            label: element.label,
            point_uuid: element.point_uuid,
            weight: element.weight,
        });
        if (element?.children?.length) {
            formatTreeToArcData(element?.children, ArcData, deep + 1);
        }
    });
};
const computedArcR = (pointInterval, points) => {
    if (points === 1) {
        return pointInterval * 2;
    }
    let arcR =
        pointInterval /
        2 /
        Math.cos((Math.PI / 180) * (((points - 2) * 180) / points / 2));
    if (arcR < pointInterval) {
        arcR = pointInterval * 2;
    }
    return arcR;
};
const computedTreeStyleAuto = (style, ArcRArr, R) => {
    if (style.yr === "auto") {
        style.yr = ArcRArr.length === 1 ? R : R / (ArcRArr.length - 1);
    }
    style.startPositionY =
        ((ArcRArr.length - 1) / 2) * style.yr + style.centerXYZ[1];
};
const computedPointPosition = (
    data,
    style,
    ArcRArr,
    startAngle = 0,
    endAngle = Math.PI * 2,
    deep = 0
) => {
    let totalWight = 0;
    for (let i = 0; i < data.length; i++) {
        totalWight += data[i].weight;
    }
    let AngleScope = endAngle - startAngle;
    let curAngle = startAngle;
    let randTranslate = ArcRArr[deep] * 10 || 1000
    for (let i = 0; i < data.length; i++) {
        let item = data[i];
        let ratioAngle = (item.weight / totalWight) * AngleScope;
        item.targetPostion = {
            x: Math.sin(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[0],
            y: style.startPositionY - deep * (style.yr || 0) + style.centerXYZ[1],
            z: Math.cos(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[2]
        }
        item.translatePostion = {
            y: rand(-randTranslate, randTranslate),
            x: rand(-randTranslate, randTranslate),
            z: rand(-randTranslate, ArcRArr[deep])
        }
        if (item?.children?.length) {
            computedPointPosition(
                item?.children,
                style,
                ArcRArr,
                curAngle,
                curAngle + ratioAngle,
                deep + 1
            );
        }
        curAngle += ratioAngle;
    }
};

// 计算camera初始位置
const computedCameraStyle = (style, dom, treeStyle, R) => {
    if (style.position === "auto") {
        style.position = {
            x: 0,
            y: treeStyle.yr * 1.5,
            z: R * 3,
        };
    }
    if (style.data === "auto") {
        style.data = {
            fov: 45,
            aspect: dom.clientWidth / dom.clientHeight,
            near: 0.1,
            far: R * 1000,
        };
    }
    if (style.lookAt === "auto") {
        style.lookAt = JSON.parse(JSON.stringify(treeStyle.centerXYZ));
    }
};

// 计算
const computedBjMeshStyleAuto = (style, R) => {
    style.geometry.radius = R 
}

// 随机数
const rand = (n, m) => {
    const c = m - n + 1
    return Math.floor(Math.random() * c + n)
}

// randTree
const randTree = (deep = 0, maxDeep = 5, parentName) => {
    let tree = []
    let length = rand(1,3)
    for(let i = 0; i < length; i++){
        let data = {
            name: parentName ? parentName + '-' + i : i + ''
        }
        if(rand(0,2) && deep < maxDeep){
            data.children = randTree(deep + 1, maxDeep, data.name)
        }
        tree.push(data)
    }
    return tree
}

const originTreeStyle = {
    centerXYZ: [0, 0, 0],
    yr: "auto",
    pointInterval: 10,
};
let treeStyle = null
const originCameraStyle = {
    position: "auto",
    data: "auto",
    lookAt: "auto",
};
let cameraStyle = null
const bjMeshStyle = {
    geometry: {
        radius: 'auto',
        widthSegments: 320,
        heightSegments: 160
    },
    material: {
        url: "./sphere-bg2.jpg",
        side: true
    }
}
const sphereMeshStyle = {
    geometry: {
        radius: 1,
        widthSegments: 320,
        heightSegments: 160,
    },
    material: {
        color: "#ffffff",
        wireframe: false, //是否将几何体渲染为线框,默认值为false(即渲染为平面多边形)
    },
};
const lineMeshStyle = {
    material: {
        color: "#ffffff",
        linewidth: 0.2,
    },
};
const textMeshStyle = {
    material: {
        size: 0.5,
        color: "#ffffff",
    },
};
let scene = null;
let camera = null;
let renderer = null;
let control = null;
let bjMesh = null;
let axes = null;
let ambientLight = null;
let pointLight = null;
let animationIndex = 0
let targetAnimationIndex = 50
let sphereGeometrys = [];
let textGeometrys = [];
let lineGeometrys = [];
let old_sphereGeometrys = [];
let old_textGeometrys = [];
let cameraAnimationIndex = 0
let targetCameraAnimationIndex = 50

const init = (rendererDom, data) => {
    treeStyle = JSON.parse(JSON.stringify(originTreeStyle))
    cameraStyle = JSON.parse(JSON.stringify(originCameraStyle))

    computedDataWeight(data);
    let { ArcRArr, R } = computedArcRArr(data, treeStyle.pointInterval);
    computedTreeStyleAuto(treeStyle, ArcRArr, R);
    computedPointPosition(data, treeStyle, ArcRArr);

    computedCameraStyle(cameraStyle, rendererDom, treeStyle, R);

    if(!scene){
        scene = createScene();
    }
    if(!camera){
        camera = createPerspectiveCamera(cameraStyle.data);
        camera.position.set(
            cameraStyle.position.x,
            cameraStyle.position.y,
            cameraStyle.position.z
        );
    }
    camera.translate_camera_position = {
        x: camera.position.x - cameraStyle.position.x,
        y: camera.position.y - cameraStyle.position.y,
        z: camera.position.z - cameraStyle.position.z,
    }
    camera.camera_position = cameraStyle.position
    camera.init_camera_position = JSON.parse(JSON.stringify(cameraStyle.position))
    if(!renderer){
        renderer = createWebGLRenderer({
            dom: rendererDom,
        });
    }

    if(!control){
        control = createControl(camera, rendererDom);
    }

    if(!bjMesh){
        computedBjMeshStyleAuto(bjMeshStyle, R * 12);
        const bjGeometry = createSphereGeometry(bjMeshStyle.geometry);
        const bjMaterial = createMeshPhongMaterialTexture(bjMeshStyle.material);
        bjMesh = createMesh(bjGeometry, bjMaterial);
        bjMesh.position.set(
            cameraStyle.lookAt[0],
            cameraStyle.lookAt[1],
            cameraStyle.lookAt[2]
        );
        scene.add(bjMesh);
    }

    if(!axes){
        axes = createAxesHelper(R);
        scene.add(axes);
    }

    if(!ambientLight){
        ambientLight = createAmbientLight({ color: "#fff", intensity: 0.2 });
        scene.add(ambientLight);
    }

    if(!pointLight){
        pointLight = createPointLight({ color: "#fff", intensity: 1 });
        let pointLightL = R * 10
        pointLight.rotateSpeed =  Math.PI / 100;
        pointLight.lightAngle = Math.atan(1);
        pointLight.arcR = Math.sqrt(pointLightL * pointLightL * 2);
        pointLight.init_position = {
            x: pointLightL,
            y: pointLightL,
            z: pointLightL,
        }
        pointLight.position.set(pointLightL, pointLightL, pointLightL);
        scene.add(pointLight);
    }

    animationIndex = 0
    cameraAnimationIndex = 0
    old_sphereGeometrys = [...sphereGeometrys];
    old_textGeometrys = [...textGeometrys];
    lineGeometrys.forEach(item => {
        scene.remove(item);
    })
    sphereGeometrys = [];
    textGeometrys = [];
    lineGeometrys = [];
    initGeometrys(data, sphereGeometrys, textGeometrys, lineGeometrys);
    scene.add(...sphereGeometrys);
    scene.add(...textGeometrys);

    render();
};
const initGeometrys = (data, sphereGeometrys, textGeometrys, lineGeometrys, parentPosition) => {
    for (let i = 0; i < data.length; i++) {
        let item = data[i];

        const geometry = createSphereGeometry(sphereMeshStyle.geometry);
        const material = createMeshLambertMaterial(sphereMeshStyle.material);
        const mesh = createMesh(geometry, material);
        mesh.position.set(item.targetPostion.x + item.translatePostion.x, item.targetPostion.y + item.translatePostion.y, item.targetPostion.z + item.translatePostion.z);
        mesh.originData = item
        sphereGeometrys.push(mesh);
        geometry.dispose();
        material.dispose();

        const text = createText({
            text: item.name,
            size: textMeshStyle.material.size,
            color: textMeshStyle.material.color,
        });
        text.position.x = item.targetPostion.x + item.translatePostion.x;
        text.position.y = item.targetPostion.y + item.translatePostion.y + sphereMeshStyle.geometry.radius * 2;
        text.position.z = item.targetPostion.z + item.translatePostion.z;
        textGeometrys.push(text);

        if (parentPosition) {
            const lineGeometry = createLineGeometry([
                parentPosition,
                { x: item.targetPostion.x, y: item.targetPostion.y, z: item.targetPostion.z },
            ]);
            const lineMaterial = createMeshLineMaterial(lineMeshStyle.material);
            const lineMesh = createMesh(lineGeometry, lineMaterial);
            lineGeometrys.push(lineMesh);
            lineGeometry.dispose();
            lineMaterial.dispose();
        }

        if (item?.children?.length) {
            initGeometrys(
                item.children,
                sphereGeometrys,
                textGeometrys,
                lineGeometrys,
                { x: item.targetPostion.x, y: item.targetPostion.y, z: item.targetPostion.z }
            );
        }
    }
};
// 渲染
const render = () => {
    rotate()
    pointAnimation()
    cameraAnimation()
    //循环调用
    requestAnimationFrame(render);
    renderer.render(scene, camera);
};
const resetPointLightPosition = () => {
    pointLight.position.set(pointLight.init_position.x, pointLight.init_position.y, pointLight.init_position.z);
    pointLight.lightAngle = Math.atan(1);
}
const rotate = (angle) => {
    if(pointLight.rotateFlag){
        return
    }
    pointLight.lightAngle += angle || pointLight.rotateSpeed
    pointLight.lightAngle = pointLight.lightAngle % (Math.PI * 2)
    pointLight.position.set(Math.sin(pointLight.lightAngle) * pointLight.arcR, pointLight.position.y, Math.cos(pointLight.lightAngle) * pointLight.arcR);
}
const pointAnimation = () => {
    if(animationIndex === targetAnimationIndex){
        return
    }
    animationIndex++
    let scale = (targetAnimationIndex - animationIndex) / targetAnimationIndex
    let scale2 = animationIndex / targetAnimationIndex
    sphereGeometrys.forEach((item, index) => {
        let data = {
            x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale,
            y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale,
            z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale
        }
        item.position.set(data.x, data.y, data.z);
        textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
    })
    old_sphereGeometrys.forEach((item, index) => {
        let data = {
            x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale2,
            y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale2,
            z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale2
        }
        item.position.set(data.x, data.y, data.z);
        old_textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
    })

    if(animationIndex === targetAnimationIndex){
        old_sphereGeometrys.forEach((item,index) => {
            scene.remove(item);
            scene.remove(old_textGeometrys[index]);
        })
        old_sphereGeometrys = []
        old_textGeometrys = []

        if(lineGeometrys.length){
            scene.add(...lineGeometrys);
        }
    }
}
const cameraAnimation = () => {
    if(cameraAnimationIndex === targetCameraAnimationIndex){
        return
    }
    cameraAnimationIndex++
    let scale = (targetCameraAnimationIndex - cameraAnimationIndex) / targetCameraAnimationIndex
    camera.position.set(
        camera.camera_position.x + camera.translate_camera_position.x * scale,
        camera.camera_position.y + camera.translate_camera_position.y * scale,
        camera.camera_position.z + camera.translate_camera_position.z * scale,
    );
    camera.lookAt(
        cameraStyle.lookAt[0],
        cameraStyle.lookAt[1],
        cameraStyle.lookAt[2]
    );
}
// 点击回调
const meshOnClick = (event) => {
    const pointer = new THREE.Vector2();
    pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
    pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

    let intersects = [];
    let raycaster = new THREE.Raycaster();
    raycaster.setFromCamera( pointer, camera );
    intersects = raycaster.intersectObjects( sphereGeometrys, true );
    if ( intersects.length > 0 ) {
        meshOnClickCb(intersects[0].object)
    }
}
const meshOnClickCb = (mesh) => {
    let dx = mesh.position.x - cameraStyle.lookAt[0]
    let dy = mesh.position.y - cameraStyle.lookAt[1]
    let dz = mesh.position.z - cameraStyle.lookAt[2]
    if(Math.abs(dx) < 0.001){
        dx = 0
    }
    if(Math.abs(dy) < 0.001){
        dy = 0
    }
    if(Math.abs(dz) < 0.001){
        dz = 0
    }
    let targetPositon = {}
    if(dx === 0 && dy === 0 && dz === 0){
        targetPositon = {
            x: mesh.position.x + treeStyle.pointInterval,
            y: mesh.position.y + treeStyle.pointInterval,
            z: mesh.position.z + treeStyle.pointInterval,
        }
    }else if(dx === 0 && dy === 0){
        let dzPointInterval = dz / Math.abs(dz) * treeStyle.pointInterval * 2
        targetPositon = {
            x: mesh.position.x + dzPointInterval,
            y: mesh.position.y + dzPointInterval,
            z: mesh.position.z + dzPointInterval,
        }
    }else if(dy === 0 && dz === 0){
        let dxPointInterval = dx / Math.abs(dx) * treeStyle.pointInterval * 2
        targetPositon = {
            x: mesh.position.x + dxPointInterval,
            y: mesh.position.y + dxPointInterval,
            z: mesh.position.z + dxPointInterval,
        }
    }else if(dx === 0 && dz === 0){
        let dyPointInterval = dy / Math.abs(dy) * treeStyle.pointInterval * 2
        targetPositon = {
            x: mesh.position.x + dyPointInterval,
            y: mesh.position.y + dyPointInterval,
            z: mesh.position.z + dyPointInterval,
        }
    }else{
        let dd = null
        if(dx !== 0){
            dd = dx
        }else if(dy !== 0){
            dd = dy
        }else if(dz !== 0){
            dd = dz
        }
        let translateL = treeStyle.pointInterval * dd / Math.abs(dd)
        let dxdd = dx / dd
        let dydd = dy / dd
        let dzdd = dz / dd
        let maxdd = dxdd > dydd ? dxdd : dydd
        maxdd = maxdd > dzdd ? maxdd : dzdd
        if(Math.abs(maxdd) > 1){
            let scale = Math.abs(maxdd) / 1
            dxdd = dxdd / scale
            dydd = dydd / scale
            dzdd = dzdd / scale
        }
        targetPositon = {
            x: mesh.position.x + translateL * dxdd,
            y: mesh.position.y + translateL * dydd,
            z: mesh.position.z + translateL * dzdd,
        }
    }
    camera.translate_camera_position = {
        x: camera.position.x - targetPositon.x,
        y: camera.position.y - targetPositon.y,
        z: camera.position.z - targetPositon.z,
    }
    camera.camera_position = JSON.parse(JSON.stringify(targetPositon))
    let length = Math.sqrt(Math.pow(camera.translate_camera_position.x, 2) + Math.pow(camera.translate_camera_position.y, 2) + Math.pow(camera.translate_camera_position.z, 2))
    cameraAnimationIndex = length > treeStyle.pointInterval ? 0 : Math.floor(length / treeStyle.pointInterval * targetCameraAnimationIndex)
}

// 操作
let openOperateFlag = ref(true)
const changeOpenOperateFlag = () => {
    openOperateFlag.value = !openOperateFlag.value
}
const resetCameraPosition = () => {
    camera.translate_camera_position = {
        x: camera.position.x - camera.init_camera_position.x,
        y: camera.position.y - camera.init_camera_position.y,
        z: camera.position.z - camera.init_camera_position.z,
    }
    camera.camera_position = JSON.parse(JSON.stringify(camera.init_camera_position))
    let length = Math.sqrt(Math.pow(camera.translate_camera_position.x, 2) + Math.pow(camera.translate_camera_position.y, 2) + Math.pow(camera.translate_camera_position.z, 2))
    cameraAnimationIndex = length > treeStyle.pointInterval ? 0 : Math.floor(length / treeStyle.pointInterval * targetCameraAnimationIndex)
}
let pointLightFlag = ref(false)
const changePointLightFlag = () => {
    pointLight.rotateFlag = !pointLight.rotateFlag
    resetPointLightPosition()
    pointLightFlag.value = pointLight.rotateFlag
}
const changeData = () => {
    let data = randTree();
    let rendererDom = treeDom.value;
    init(rendererDom, data);
}

onMounted(() => {
    changeData()
    treeDom.value.addEventListener( 'click', meshOnClick );
});
</script>

<template>
    <div class="tree-new-page">
        <div class="operate">
            <div class="open-operate frosted-glass"  @click="changeOpenOperateFlag">
                <img :src="openOperateFlag ? closeSvg : openSvg"/>
            </div>
            <div :class="{'operate-list': true, 'frosted-glass': true, 'close': !openOperateFlag}">
                <div class="operate-item camera" @click="resetCameraPosition">
                    <img :src="cameraSvg"/>
                    <div class="label">返回正视角</div>
                </div>
                <div class="operate-item point-light" @click="changePointLightFlag">
                    <img :src="pointLightFlag ? stopSvg : startSvg"/>
                    <div class="label">{{pointLightFlag ? '停止灯光旋转' : '开启灯光旋转'}}</div>
                </div>
                <div class="operate-item data" @click="changeData">
                    <img :src="dataSvg"/>
                    <div class="label">更换数据</div>
                </div>
            </div>
        </div>
        <div class="tree-new" ref="treeDom"></div>
    </div>
</template>

<style scoped lang="scss">
.tree-new-page {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: #000;
    position: relative;
    .tree-new {
        width: 100%;
        height: 100%;
    }
    .operate{
        position: absolute;
        z-index: 10;
        left: 0;
        top: 50%;
        transform: translateY(-50%);
        .open-operate{
            width: 22px;
            position: relative;
            padding: 6px 10px;
            margin-bottom: 10px;
            display: flex;
            align-items: center;
            cursor: pointer;
            &.frosted-glass{
                border-radius: 0 4px 4px 0;
            }
            img{
                height: 20px;
                margin-left: 2px;
            }
        }
        .operate-list{
            position: relative;
            padding: 10px;
            &.frosted-glass{
                border-radius: 0 4px 4px 0;
            }
            &.close{
                .operate-item{
                    .label{
                        margin-left: 0px;
                        width: 0px;
                    }
                }
            }
            .operate-item{
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 10px;
                cursor: pointer;
                &:last-child{
                    margin-bottom: 0px;
                }
                img{
                    height: 20px;
                }
                .label{
                    width: 100px;
                    white-space: nowrap;
                    overflow: hidden;
                    color: $defaultWhite;
                    font-size: 16px;
                    margin-left: 10px;
                    transition: all .3s;
                }
            }
            .point-light{
                img{
                    height: 24px;
                    margin-left: -2px;
                }
            }
        }
    }
    .frosted-glass {
        &::after {
            position: absolute;
            top: 0;
            left: 0;
            z-index: -1;
            content: "";
            display: block;
            width: 100%;
            height: 100%;
            background-color: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(8px);
            box-shadow: 0 0 4px rgb(93, 93, 93);
            border-radius: 4px;
        }
    }
}
</style>

相关资源

下载地址:点击此处,如果打不开则在审核中,可以先搜藏博客,后面再来下载。
项目资源目录

public
	sphere-bg2.jpg
src
	assets
		images
			...其余图片

你可能感兴趣的:(three,javascript,3d,three.js)