阅读视图

发现新文章,点击刷新页面。

学习Three.js--柱状图

学习Three.js--柱状图

前置核心说明

开发目标

基于Three.js实现ECharts标准风格的3D柱状图,还原ECharts的视觉特征(配色、标签样式、坐标轴风格),同时具备3D场景的交互性与真实光影效果,核心能力包括:

  1. 视觉风格还原:精准复刻ECharts柱状图的配色、坐标轴样式、标签样式(轴刻度、轴名称、柱顶数值),兼顾3D立体感与2D可视化的简洁性;
  2. 3D场景构建:通过几何体、材质、光照系统打造真实的3D视觉效果,阴影系统增强立体感,避免3D场景的平面化;
  3. 2D标签渲染:借助CSS2D渲染器实现灵活的文字标签,解决Three.js原生文字渲染样式受限的问题,贴合ECharts的标签风格;
  4. 流畅交互体验:通过轨道控制器实现360°拖拽旋转、滚轮缩放,且启用阻尼效果提升交互顺滑度;
  5. 响应式适配:适配不同屏幕尺寸,保证柱状图在PC/平板等设备上无拉伸变形;
  6. 循环动画驱动:通过帧动画循环维持交互阻尼与场景渲染的流畅性。

c01cf879-a892-4478-b422-08456d7cbcf8.png

核心技术栈(关键知识点)

技术点 作用
THREE.Scene/Camera/Renderer 搭建3D场景基础框架,定义视角、渲染尺寸、抗锯齿等核心属性,是所有3D效果的载体
THREE.OrbitControls 实现3D场景的轨道交互(拖拽旋转、滚轮缩放),启用阻尼让交互更自然,适配3D柱状图的查看需求
CSS2DRenderer/CSS2DObject 将HTML/CSS创建的文字标签(轴刻度、柱顶数值)绑定到3D空间坐标,实现灵活的样式控制,还原ECharts标签风格
THREE.MeshStandardMaterial/LineBasicMaterial 分别实现几何体(柱子、地面)的物理材质(支持光影)和线条(坐标轴)的基础材质,保证视觉质感
THREE.BoxGeometry/CircleGeometry/BufferGeometry 构建柱子(立方体)、地面(圆形)、坐标轴(线条)的几何形态,是3D物体的形状基础
THREE.DirectionalLight/AmbientLight/PointLight 组合环境光、方向光、点光源,打造分层级的光照效果,增强3D柱子的立体感与真实感
THREE.ShadowMap(阴影系统) 开启软阴影并配置阴影参数,让柱子投射自然阴影到地面,提升场景的真实度
响应式窗口监听(resize事件) 动态更新相机比例与渲染器尺寸,保证柱状图在不同屏幕下无拉伸变形
requestAnimationFrame 驱动动画循环,维持轨道控制器阻尼更新与场景渲染的流畅性(60帧/秒)

核心开发流程

graph TD
    A["初始化场景/相机/渲染器/轨道控制器"] --> B["初始化CSS2D渲染器(标签渲染)"]
    B --> C["构建光照系统(环境光+主光源+辅助光)"]
    C --> D["定义图表数据与视觉配置参数"]
    D --> E["构建地面与网格辅助线(视觉锚点)"]
    E --> F["构建坐标轴系统(X/Y轴+刻度+名称标签)"]
    F --> G["构建柱状图主体(几何体+材质+柱顶数值标签)"]
    G --> H["添加辅助线/装饰元素(Z轴短线、原点装饰)"]
    H --> I["绑定窗口resize事件(响应式适配)"]
    I --> J["启动动画循环(更新控制器+渲染场景)"]

分步开发详解

步骤1:基础环境搭建(场景/相机/渲染器/控制器)

搭建Three.js 3D场景的核心框架,为柱状图提供基础的展示载体与交互能力,兼顾视角合理性与渲染清晰度。

1.1 核心代码(对应原代码1)
// 初始化场景、相机、渲染器
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf5f7fa); // 柔和灰白背景,贴合ECharts容器

const camera = new THREE.PerspectiveCamera(
    45, // 更小视角,减少畸变,更接近2D感
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);
camera.position.set(9, 7, 14);
camera.lookAt(4, 3, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;        // 开启阴影,更真实
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

// 轨道控制器,带阻尼
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.autoRotate = false;
controls.enableZoom = true;
controls.target.set(4, 3, 0);
controls.minDistance = 8;
controls.maxDistance = 30;
1.2 关键说明
  • 相机参数优化
    • 视角45°:相比默认的75°,更小的视角减少3D畸变,让柱状图更接近ECharts的2D视觉风格,同时保留3D纵深;
    • 位置(9,7,14)+注视点(4,3,0):采用“斜上方俯视”视角,既可以清晰看到柱子的高度与X轴分类,又能体现3D立体感,避免视角过平/过陡导致的视觉失衡。
  • 渲染器核心配置
    • antialias: true:开启抗锯齿,让柱子边缘、坐标轴线条更细腻,避免“锯齿边”;
    • shadowMap.type = PCFSoftShadowMap:启用软阴影,让柱子投射的阴影边缘更柔和,贴合真实物理效果,避免硬阴影的生硬感;
    • setPixelRatio:适配Retina屏幕,保证高清渲染,标签文字与柱子细节无模糊。
  • 轨道控制器优化
    • 阻尼系数0.06:拖拽旋转场景后,控制器会自然减速,交互更顺滑;
    • 缩放范围8~30:限制最小/最大缩放距离,避免缩放过近导致柱子遮挡、过远导致细节丢失;
    • autoRotate: false:关闭自动旋转,让用户自主控制查看视角,更贴合数据可视化的使用场景。

步骤2:CSS2D渲染器初始化(标签渲染核心)

初始化CSS2D渲染器,为坐标轴标签、柱顶数值标签提供渲染载体,解决Three.js原生文字渲染样式不灵活的问题。

2.1 核心代码(对应原代码2)
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
labelRenderer.domElement.style.pointerEvents = 'none'; // 不阻挡点击
document.body.appendChild(labelRenderer.domElement);
2.2 关键说明
  • CSS2D渲染器的核心价值:将HTML元素(<div>标签)映射到3D空间坐标,既能利用CSS灵活控制文字样式(颜色、字体、背景、圆角等),又能跟随3D场景的旋转/缩放同步更新位置,完美还原ECharts的标签风格。
  • pointerEvents: none:关闭标签的鼠标事件响应,避免标签遮挡轨道控制器的交互(如拖拽旋转时点击到标签无反应)。

步骤3:光照系统构建(光影质感核心)

组合环境光、方向光、点光源,打造分层级的光照效果,增强柱子的立体感,同时避免光线过亮/过暗导致的视觉失衡。

3.1 核心代码(对应原代码3)
// 环境光提供基础照明
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);

// 主光源 - 产生阴影
const mainLight = new THREE.DirectionalLight(0xfff5e6, 1.0);
mainLight.position.set(8, 12, 8);
mainLight.castShadow = true;
mainLight.receiveShadow = true;
mainLight.shadow.mapSize.width = 1024;
mainLight.shadow.mapSize.height = 1024;
mainLight.shadow.camera.near = 0.5;
mainLight.shadow.camera.far = 30;
mainLight.shadow.camera.left = -15;
mainLight.shadow.camera.right = 15;
mainLight.shadow.camera.top = 15;
mainLight.shadow.camera.bottom = -15;
scene.add(mainLight);

// 辅助背光,增加柱子暗部细节
const backLight = new THREE.DirectionalLight(0xe0f0ff, 0.5);
backLight.position.set(-5, 0, 10);
scene.add(backLight);

// 补充侧光
const fillLight = new THREE.PointLight(0xccddff, 0.3);
fillLight.position.set(2, 5, 12);
scene.add(fillLight);
3.2 关键说明
  • 光照分层逻辑
    • 环境光(强度0.6):提供基础照明,避免场景暗部全黑,保证所有元素的基础可见性;
    • 主方向光(强度1.0):模拟主光源(如自然光),同时开启阴影,是柱子立体感的核心;
    • 背光(强度0.5):补充柱子暗部细节,避免暗部“死黑”,提升光影层次感;
    • 点光源(强度0.3):柔和填充侧光,进一步优化光影过渡,让柱子的材质质感更真实。
  • 阴影参数优化
    • shadow.mapSize = 1024x1024:设置阴影贴图分辨率,数值越高阴影越清晰(但性能开销越大),1024是“清晰度+性能”的平衡值;
    • 阴影相机范围(-15~15):覆盖整个柱状图场景,保证所有柱子的阴影都能被正确渲染。

步骤4:图表数据与配置参数定义

定义柱状图的业务数据与视觉配置参数,实现“数据与样式解耦”,便于后续调整视觉风格。

4.1 核心代码(对应原代码4)
// 图表数据(ECharts 标准:一月到六月,数值清晰)
const chartData = [
    { label: '一月', value: 2 },
    { label: '二月', value: 3 },
    { label: '三月', value: 4 },
    { label: '四月', value: 5 },
    { label: '五月', value: 6 },
    { label: '六月', value: 7 }
];

// 配置参数 —— 完全符合柱状图审美
const config = {
    columnWidth: 0.9,          // 柱子宽一些,更饱满
    columnDepth: 0.9,          // Z轴厚度
    yMax: 10,                 // Y轴最大值固定为10,留白更ECharts(原数据最大7,顶部留空间显示数值)
    xStep: 2.1,              // 柱间距适中
    yStep: 2,                // Y轴刻度步长
    axisColor: 0x5d6d7e,     // 轴体深蓝灰,专业感
    axisLineWidth: 2.2,      // 轴线条加粗
    columnColors: [0x5470c6, 0x91cc75, 0xfac858, 0xee6666, 0x73c0de, 0x3ba272], // ECharts 经典配色
    columnXOffset: 1.8,      // 柱子起点与Y轴留白,避免拥挤(ECharts风格)
    groundOpacity: 0.15,      // 地面极浅,仅作视觉参考
    shadowOpacity: 0.3
};
4.2 关键说明
  • 数据结构设计chartData采用“标签+数值”的对象数组,贴合ECharts的数据格式,便于后续对接真实业务数据;
  • 配置参数的ECharts适配
    • yMax: 10:预留顶部空间(原数据最大7),避免柱顶数值标签与柱子顶部重叠,符合ECharts的留白审美;
    • columnColors:使用ECharts经典配色数组,按柱子循环使用,保证视觉风格一致;
    • xStep: 2.1/columnXOffset: 1.8:控制柱子间距与X轴偏移,避免柱子与Y轴拥挤,贴合ECharts的轴边距风格。

步骤5:地面与网格辅助线构建

构建地面几何体与网格辅助线,为3D场景提供视觉锚点,增强坐标感,同时接收柱子的阴影。

5.1 核心代码(对应原代码5)
// 地面(接收阴影,视觉锚点)
const groundGeometry = new THREE.CircleGeometry(30, 32); // 圆形地面更柔和
const groundMaterial = new THREE.MeshStandardMaterial({
    color: 0xe9ecef,
    roughness: 0.8,
    metalness: 0.1,
    transparent: true,
    opacity: config.groundOpacity,
    side: THREE.DoubleSide
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.1;
ground.receiveShadow = true;
scene.add(ground);

// 添加一个极细的网格辅助线(可选,增强坐标感,但不抢眼)
const gridHelper = new THREE.GridHelper(26, 20, 0x9aa6b2, 0xcbd5e0);
gridHelper.position.y = -0.09;
scene.add(gridHelper);
5.2 关键说明
  • 地面设计优化
    • 圆形地面(CircleGeometry):相比方形地面更柔和,避免直角的生硬感;
    • 透明度0.15:地面仅作视觉锚点,不抢夺柱状图的视觉焦点;
    • receiveShadow: true:开启阴影接收,让柱子的阴影投射到地面,增强3D真实感。
  • 网格辅助线
    • 位置y=-0.09:略高于地面,避免与地面重叠导致的视觉混乱;
    • 浅灰色调:增强坐标感的同时,不干扰主视觉,符合数据可视化“辅助元素不抢戏”的原则。

步骤6:坐标轴系统构建(X/Y轴+刻度+标签)

构建符合ECharts风格的X/Y坐标轴,包括轴线、刻度线、分类/数值标签、轴名称,是数据可视化的核心骨架。

6.1 核心代码(对应原代码6,以X轴为例)
// 坐标轴材质
const axisMaterial = new THREE.LineBasicMaterial({ 
    color: config.axisColor,
    linewidth: config.axisLineWidth
});

// ===== X轴 =====
const xAxisStart = -0.5;
const xAxisEnd = (chartData.length - 1) * config.xStep + config.columnXOffset + 1.2;
const xAxisPoints = [new THREE.Vector3(xAxisStart, 0, 0), new THREE.Vector3(xAxisEnd, 0, 0)];
const xAxisGeo = new THREE.BufferGeometry().setFromPoints(xAxisPoints);
const xAxisLine = new THREE.Line(xAxisGeo, axisMaterial);
scene.add(xAxisLine);

// X轴刻度与分类标签
chartData.forEach((item, i) => {
    const xPos = i * config.xStep + config.columnXOffset;
    
    // 刻度线(向下,长度0.6,清晰可见)
    const tickPoints = [
        new THREE.Vector3(xPos, 0, 0),
        new THREE.Vector3(xPos, -0.6, 0)
    ];
    const tickGeo = new THREE.BufferGeometry().setFromPoints(tickPoints);
    const tick = new THREE.Line(tickGeo, axisMaterial);
    scene.add(tick);

    // 分类标签 (CSS2D) —— ECharts风格:居中,字体适中
    const labelDiv = document.createElement('div');
    labelDiv.className = 'axis-tick-label';
    labelDiv.textContent = item.label;
    labelDiv.style.transform = 'translate(-50%, 0)';
    labelDiv.style.fontWeight = '500';
    labelDiv.style.color = '#2e4053';
    const labelObj = new CSS2DObject(labelDiv);
    labelObj.position.set(xPos, -1.1, 0);
    scene.add(labelObj);
});

// X轴名称 “月份” (ECharts标准:置于轴末端附近,加粗)
const xNameDiv = document.createElement('div');
xNameDiv.className = 'axis-name-label';
xNameDiv.textContent = '月份';
xNameDiv.style.transform = 'translate(-50%, 0)';
const xNameLabel = new CSS2DObject(xNameDiv);
xNameLabel.position.set(xAxisEnd - 0.3, -1.8, 0);
scene.add(xNameLabel);
6.2 关键说明
  • 轴线长度自适应xAxisEnd通过chartData.length动态计算,保证X轴长度适配数据条数,避免硬编码导致的适配问题;
  • 刻度线与标签对齐
    • 刻度线长度0.6:清晰可见且不突兀,符合ECharts的刻度线尺寸;
    • 标签位置y=-1.1:位于刻度线下方,避免与轴线/刻度线重叠;
    • transform: translate(-50%, 0):让标签水平居中对齐刻度线,保证视觉整齐;
  • 轴名称样式:采用加粗、深色、末端放置的样式,完全复刻ECharts的轴名称风格,增强数据可读性。

步骤7:柱状图主体构建(几何体+材质+标签)

构建3D柱子几何体,配置贴合ECharts风格的材质,同时添加柱顶数值标签,是数据可视化的核心展示层。

7.1 核心代码(对应原代码7)
chartData.forEach((item, i) => {
    const xPos = i * config.xStep + config.columnXOffset;
    const height = item.value;     // 实际数值
    const color = config.columnColors[i % config.columnColors.length];

    // 柱子材质:轻微光泽,略带透明感但清晰
    const columnMaterial = new THREE.MeshStandardMaterial({
        color: color,
        roughness: 0.45,
        metalness: 0.2,
        emissive: new THREE.Color(color).multiplyScalar(0.1),
        emissiveIntensity: 0.2,
        transparent: true,
        opacity: 0.95
    });

    const columnGeo = new THREE.BoxGeometry(
        config.columnWidth,
        height,
        config.columnDepth
    );
    const column = new THREE.Mesh(columnGeo, columnMaterial);
    column.castShadow = true;
    column.receiveShadow = true;
    column.position.set(xPos, height / 2, 0);
    scene.add(column);

    // 柱顶数值标签(核心要求:每一个柱子顶部显示相应的值,ECharts 风格)
    const valueDiv = document.createElement('div');
    valueDiv.className = 'bar-value-label';
    valueDiv.textContent = item.value;   // 简洁显示数值
    // 仿ECharts: 白色底框+红褐色字,小圆角
    valueDiv.style.background = 'rgba(255, 255, 255, 0.8)';
    valueDiv.style.border = '1px solid ' + new THREE.Color(color).getStyle();
    valueDiv.style.color = new THREE.Color(color).multiplyScalar(0.6).getStyle();
    
    const valueLabel = new CSS2DObject(valueDiv);
    // 标签置于柱子顶部上方0.8处,醒目不重叠
    valueLabel.position.set(xPos, height + 0.8, 0);
    scene.add(valueLabel);

    // 柱顶圆点装饰
    const topDotGeo = new THREE.SphereGeometry(0.1);
    const topDotMat = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.2 });
    const topDot = new THREE.Mesh(topDotGeo, topDotMat);
    topDot.position.set(xPos, height, 0);
    topDot.castShadow = false;
    scene.add(topDot);
});
7.2 关键说明
  • 柱子位置计算position.set(xPos, height / 2, 0),Three.js的立方体几何体锚点在中心,因此Y轴位置需设置为height/2,保证柱子底部对齐X轴(y=0);
  • 材质风格优化
    • roughness: 0.45/metalness: 0.2:轻微的金属光泽,让柱子有质感但不刺眼;
    • emissive:轻微自发光,增强柱子的视觉层次感,贴合ECharts的高亮风格;
    • 透明度0.95:略带透明感,避免柱子过于厚重,符合现代数据可视化的轻盈风格;
  • 柱顶标签优化
    • 位置height + 0.8:位于柱子顶部上方,避免与柱子重叠;
    • 背景半透+圆角:复刻ECharts的数值标签样式,增强可读性;
    • 颜色适配:标签边框/文字颜色与柱子颜色联动,保证视觉统一性。

步骤8:辅助线与装饰元素构建

添加Z轴短线、背面辅助线等装饰元素,增强3D场景的方位感,同时不干扰主视觉。

8.1 核心代码(对应原代码8)
// Z轴短线示意(虽然2D柱状图,但3D空间给出方位)
const zAxisPoints = [new THREE.Vector3(0, 0, -2), new THREE.Vector3(0, 0, 2)];
const zAxisGeo = new THREE.BufferGeometry().setFromPoints(zAxisPoints);
const zAxisLine = new THREE.Line(zAxisGeo, new THREE.LineBasicMaterial({ color: 0xb0bec5, linewidth: 1 }));
scene.add(zAxisLine);

// 背面辅助线,增加空间感
const helperExtent = 14;
const axisHelperMaterial = new THREE.LineBasicMaterial({ color: 0xd0d8e0, linewidth: 1 });
const backLine1 = [new THREE.Vector3(-1, 0, -1.5), new THREE.Vector3(xAxisEnd, 0, -1.5)];
const backLineGeo1 = new THREE.BufferGeometry().setFromPoints(backLine1);
const backLineObj1 = new THREE.Line(backLineGeo1, axisHelperMaterial);
scene.add(backLineObj1);
8.2 关键说明
  • Z轴短线:提示3D场景的纵深方向,让用户感知柱子的厚度(Z轴),避免误以为是纯2D图表;
  • 背面辅助线:浅灰色细线条,增强场景的空间层次感,同时不抢夺柱状图的视觉焦点。

步骤9:响应式窗口适配

监听窗口尺寸变化,动态更新相机比例、渲染器尺寸,保证柱状图在不同屏幕下无拉伸变形。

9.1 核心代码(对应原代码9)
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    labelRenderer.setSize(window.innerWidth, window.innerHeight);
}
9.2 关键说明
  • camera.updateProjectionMatrix():窗口尺寸变化后,相机的宽高比会修改,必须调用该方法更新投影矩阵,否则柱状图会出现拉伸变形;
  • 同步更新labelRenderer尺寸:保证CSS2D标签与3D场景同步适配,避免标签位置偏移。

步骤10:动画循环驱动

通过requestAnimationFrame驱动动画循环,维持轨道控制器阻尼更新与场景渲染的流畅性。

10.1 核心代码(对应原代码10)
function animate() {
    requestAnimationFrame(animate);
    controls.update(); // 启用阻尼后需要每帧更新
    renderer.render(scene, camera);
    labelRenderer.render(scene, camera);
}
animate();
10.2 关键说明
  • controls.update():轨道控制器启用阻尼后,必须在每帧动画中调用该方法,否则阻尼效果失效;
  • 双渲染器调用:同时渲染3D场景(renderer)和CSS2D标签(labelRenderer),保证标签与3D场景同步显示。

核心技术深度解析

1. CSS2D标签渲染的核心逻辑

CSS2D标签是还原ECharts风格的关键,其核心渲染逻辑如下:

graph LR
    A["创建HTML div标签(设置样式/文字)"] --> B["封装为CSS2DObject(绑定3D坐标)"]
    B --> C["添加到Three.js场景"] --> D["动画循环中调用labelRenderer.render"]
    D --> E["标签随3D场景旋转/缩放同步更新位置"]
  • 核心优势:相比Three.js原生的TextGeometry,CSS2D标签支持任意CSS样式(背景、圆角、文字阴影、毛玻璃等),完全复刻ECharts的标签风格,且渲染性能更高。

2. 光影系统的层次感实现

柱状图的3D立体感核心来自分层光照设计,其逻辑如下:

graph LR
    A["环境光(基础照明,0.6强度)"] --> B["主方向光(核心光影+阴影,1.0强度)"]
    B --> C["背光(补充暗部细节,0.5强度)"] --> D["点光源(柔和填充侧光,0.3强度)"]
    D --> E["柱子材质(roughness/metalness/emissive)"] --> F["自然的3D光影质感"]
  • 核心亮点:主光源负责阴影和主要光照,背光和点光源补充细节,避免暗部死黑,让柱子的材质质感更真实。

3. ECharts风格视觉还原的核心

代码通过三大维度精准复刻ECharts风格:

  1. 配色维度:使用ECharts经典配色数组,轴体采用深蓝灰(专业感),标签采用分级配色(刻度浅灰、轴名深灰、数值醒目色);
  2. 布局维度:Y轴预留顶部空间、柱子与Y轴留白、刻度线长度统一、标签位置对齐,完全贴合ECharts的布局审美;
  3. 样式维度:标签的文字阴影、半透背景、圆角,柱子的轻微光泽/透明感,轴线条加粗,均复刻ECharts的视觉细节。

核心参数速查表(快速调整视觉/交互效果)

参数分类 参数名 当前取值 核心作用 修改建议
场景配置 相机视角 camera.fov 45 控制3D场景的视角,越小畸变越少 改为30:更接近2D视觉;改为60:3D纵深感更强
场景配置 相机位置 camera.position (9,7,14) 控制观察视角(斜上方俯视) 改为(0,10,15):正面视角;改为(15,5,10):侧方视角
视觉配置 柱子尺寸 columnWidth/columnDepth 0.9/0.9 控制柱子的宽度/厚度 改为0.7/0.7:柱子更纤细;改为1.2/1.2:柱子更粗壮
视觉配置 柱间距 xStep 2.1 控制X轴上柱子的间距 改为1.5:柱子更密集;改为3.0:柱子更稀疏
视觉配置 Y轴最大值 yMax 10 控制Y轴高度(预留标签空间) 改为8:Y轴更紧凑;改为15:Y轴更宽松
视觉配置 轴线条宽度 axisLineWidth 2.2 控制坐标轴线的粗细 改为1.5:轴线更细;改为3.0:轴线更粗
数据配置 图表数据 chartData 6组(一月-六月) 柱状图的业务数据 新增/删除数组元素:适配更多/更少分类;修改value:调整柱子高度
光照配置 主光源强度 mainLight.intensity 1.0 控制主光源的亮度 改为0.7:光线更柔和;改为1.5:光线更明亮
交互配置 控制器阻尼 dampingFactor 0.06 控制拖拽旋转的顺滑度 改为0.03:阻尼更弱(旋转更快);改为0.1:阻尼更强(旋转更慢)
交互配置 缩放范围 minDistance/maxDistance 8/30 控制滚轮缩放的最小/最大距离 改为5/20:缩放范围更小;改为10/40:缩放范围更大

完整代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Three.js 柱状图 · ECharts 标准风格</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            font-family: "Microsoft YaHei", sans-serif;
        }
        canvas {
            display: block;
        }
        /* 坐标轴标签:清爽灰色,无干扰 */
        .axis-tick-label {
            color: #4a4a4a;
            font-size: 13px;
            font-weight: normal;
            white-space: nowrap;
            pointer-events: none;
            text-shadow: 0 0 2px rgba(255,255,255,0.8);
        }
        /* 轴名称标签 (ECharts 风格:加粗,深色) */
        .axis-name-label {
            color: #2c3e50;
            font-size: 16px;
            font-weight: 600;
            white-space: nowrap;
            pointer-events: none;
            text-shadow: 0 0 3px rgba(255,255,255,0.9);
        }
        /* 柱顶数值标签:醒目红褐色,类似ECharts 强调 */
        .bar-value-label {
            color: #c0392b;
            font-size: 14px;
            font-weight: 600;
            white-space: nowrap;
            pointer-events: none;
            text-shadow: 0 0 5px rgba(255,255,255,0.8);
            background: rgba(255, 255, 255, 0.6);
            padding: 2px 6px;
            border-radius: 10px;
            border: 1px solid rgba(192, 57, 43, 0.3);
            backdrop-filter: blur(2px);
        }
        /* 简单辅助:去掉滚动条,干净视图 */
    </style>
</head>
<body>
    <script type="module">
        import * as THREE from 'https://esm.sh/three@0.174.0';
        import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
        import { CSS2DRenderer, CSS2DObject } from 'https://esm.sh/three@0.174.0/examples/jsm/renderers/CSS2DRenderer.js';

        // ---------- 1. 初始化场景、相机、渲染器(增强抗锯齿与性能)----------
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0xf5f7fa); // 柔和灰白背景,类似ECharts容器

        const camera = new THREE.PerspectiveCamera(
            45, // 更小视角,减少畸变,更接近2D感
            window.innerWidth / window.innerHeight,
            0.1,
            1000
        );
        camera.position.set(9, 7, 14);
        camera.lookAt(4, 3, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.shadowMap.enabled = true;        // 开启阴影,更真实
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.setPixelRatio(window.devicePixelRatio);
        document.body.appendChild(renderer.domElement);

        // 轨道控制器,带阻尼
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.06;
        controls.autoRotate = false;
        controls.enableZoom = true;
        controls.target.set(4, 3, 0);
        controls.minDistance = 8;
        controls.maxDistance = 30;

        // ---------- 2. CSS2渲染器(用于所有文字标签:轴刻度、轴名称、柱顶数值)----------
        const labelRenderer = new CSS2DRenderer();
        labelRenderer.setSize(window.innerWidth, window.innerHeight);
        labelRenderer.domElement.style.position = 'absolute';
        labelRenderer.domElement.style.top = '0px';
        labelRenderer.domElement.style.left = '0px';
        labelRenderer.domElement.style.pointerEvents = 'none'; // 不阻挡点击
        document.body.appendChild(labelRenderer.domElement);

        // ---------- 3. 灯光系统(呈现立体感,但不刺眼)----------
        // 环境光提供基础照明
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
        scene.add(ambientLight);

        // 主光源 - 产生阴影
        const mainLight = new THREE.DirectionalLight(0xfff5e6, 1.0);
        mainLight.position.set(8, 12, 8);
        mainLight.castShadow = true;
        mainLight.receiveShadow = true;
        mainLight.shadow.mapSize.width = 1024;
        mainLight.shadow.mapSize.height = 1024;
        mainLight.shadow.camera.near = 0.5;
        mainLight.shadow.camera.far = 30;
        mainLight.shadow.camera.left = -15;
        mainLight.shadow.camera.right = 15;
        mainLight.shadow.camera.top = 15;
        mainLight.shadow.camera.bottom = -15;
        scene.add(mainLight);

        // 辅助背光,增加柱子暗部细节
        const backLight = new THREE.DirectionalLight(0xe0f0ff, 0.5);
        backLight.position.set(-5, 0, 10);
        scene.add(backLight);

        // 补充侧光
        const fillLight = new THREE.PointLight(0xccddff, 0.3);
        fillLight.position.set(2, 5, 12);
        scene.add(fillLight);

        // ---------- 4. 图表数据(ECharts 标准:一月到六月,数值清晰)----------
        const chartData = [
            { label: '一月', value: 2 },
            { label: '二月', value: 3 },
            { label: '三月', value: 4 },
            { label: '四月', value: 5 },
            { label: '五月', value: 6 },
            { label: '六月', value: 7 }
        ];

        // 配置参数 —— 完全符合柱状图审美
        const config = {
            columnWidth: 0.9,          // 柱子宽一些,更饱满
            columnDepth: 0.9,          // Z轴厚度
            yMax: 10,                 // Y轴最大值固定为10,留白更ECharts(原数据最大7,顶部留空间显示数值)
            xStep: 2.1,              // 柱间距适中
            yStep: 2,                // Y轴刻度步长
            axisColor: 0x5d6d7e,     // 轴体深蓝灰,专业感
            axisLineWidth: 2.2,      // 轴线条加粗
            columnColors: [0x5470c6, 0x91cc75, 0xfac858, 0xee6666, 0x73c0de, 0x3ba272], // ECharts 经典配色
            columnXOffset: 1.8,      // 柱子起点与Y轴留白,避免拥挤(ECharts风格)
            groundOpacity: 0.15,      // 地面极浅,仅作视觉参考
            shadowOpacity: 0.3
        };

        // ---------- 5. 地面(接收阴影,视觉锚点)----------
        const groundGeometry = new THREE.CircleGeometry(30, 32); // 圆形地面更柔和
        const groundMaterial = new THREE.MeshStandardMaterial({
            color: 0xe9ecef,
            roughness: 0.8,
            metalness: 0.1,
            transparent: true,
            opacity: config.groundOpacity,
            side: THREE.DoubleSide
        });
        const ground = new THREE.Mesh(groundGeometry, groundMaterial);
        ground.rotation.x = -Math.PI / 2;
        ground.position.y = -0.1;
        ground.receiveShadow = true;
        scene.add(ground);

        // 添加一个极细的网格辅助线(可选,增强坐标感,但不抢眼)
        const gridHelper = new THREE.GridHelper(26, 20, 0x9aa6b2, 0xcbd5e0);
        gridHelper.position.y = -0.09;
        scene.add(gridHelper);

        // ---------- 6. 坐标轴系统(完全参照ECharts:轴线明显,刻度清晰,轴标签明确)----------
        const axisMaterial = new THREE.LineBasicMaterial({ 
            color: config.axisColor,
            linewidth: config.axisLineWidth
        });

        // ===== X轴 =====
        const xAxisStart = -0.5;
        const xAxisEnd = (chartData.length - 1) * config.xStep + config.columnXOffset + 1.2;
        const xAxisPoints = [new THREE.Vector3(xAxisStart, 0, 0), new THREE.Vector3(xAxisEnd, 0, 0)];
        const xAxisGeo = new THREE.BufferGeometry().setFromPoints(xAxisPoints);
        const xAxisLine = new THREE.Line(xAxisGeo, axisMaterial);
        scene.add(xAxisLine);

        // X轴刻度与分类标签
        chartData.forEach((item, i) => {
            const xPos = i * config.xStep + config.columnXOffset;
            
            // 刻度线(向下,长度0.6,清晰可见)
            const tickPoints = [
                new THREE.Vector3(xPos, 0, 0),
                new THREE.Vector3(xPos, -0.6, 0)
            ];
            const tickGeo = new THREE.BufferGeometry().setFromPoints(tickPoints);
            const tick = new THREE.Line(tickGeo, axisMaterial);
            scene.add(tick);

            // 分类标签 (CSS2D) —— ECharts风格:居中,字体适中
            const labelDiv = document.createElement('div');
            labelDiv.className = 'axis-tick-label';
            labelDiv.textContent = item.label;
            labelDiv.style.transform = 'translate(-50%, 0)';
            labelDiv.style.fontWeight = '500';
            labelDiv.style.color = '#2e4053';
            const labelObj = new CSS2DObject(labelDiv);
            labelObj.position.set(xPos, -1.1, 0);
            scene.add(labelObj);
        });

        // X轴名称 “月份” (ECharts标准:置于轴末端附近,加粗)
        const xNameDiv = document.createElement('div');
        xNameDiv.className = 'axis-name-label';
        xNameDiv.textContent = '月份';
        xNameDiv.style.transform = 'translate(-50%, 0)';
        const xNameLabel = new CSS2DObject(xNameDiv);
        xNameLabel.position.set(xAxisEnd - 0.3, -1.8, 0);
        scene.add(xNameLabel);

        // ===== Y轴 =====
        const yAxisStart = 0;
        const yAxisEnd = config.yMax;
        const yAxisPoints = [new THREE.Vector3(0, yAxisStart, 0), new THREE.Vector3(0, yAxisEnd, 0)];
        const yAxisGeo = new THREE.BufferGeometry().setFromPoints(yAxisPoints);
        const yAxisLine = new THREE.Line(yAxisGeo, axisMaterial);
        scene.add(yAxisLine);

        // Y轴刻度和数值标签(0到10,步长2,完全展示)
        for (let val = 0; val <= config.yMax; val += config.yStep) {
            // 刻度线(向左,长度0.6)
            const tickPoints = [
                new THREE.Vector3(0, val, 0),
                new THREE.Vector3(-0.6, val, 0)
            ];
            const tickGeo = new THREE.BufferGeometry().setFromPoints(tickPoints);
            const tick = new THREE.Line(tickGeo, axisMaterial);
            scene.add(tick);

            // 数值标签
            const labelDiv = document.createElement('div');
            labelDiv.className = 'axis-tick-label';
            labelDiv.textContent = val;
            labelDiv.style.transform = 'translate(0, -50%)';
            labelDiv.style.fontWeight = '500';
            const labelObj = new CSS2DObject(labelDiv);
            labelObj.position.set(-1.2, val, 0);
            scene.add(labelObj);
        }

        // Y轴名称 “数值” (ECharts风格:垂直居中,加粗)
        const yNameDiv = document.createElement('div');
        yNameDiv.className = 'axis-name-label';
        yNameDiv.textContent = '数值';
        yNameDiv.style.transform = 'translate(0, -50%)';
        const yNameLabel = new CSS2DObject(yNameDiv);
        yNameLabel.position.set(-2.4, config.yMax / 2, 0);
        scene.add(yNameLabel);

        // ===== 原点装饰(加强视觉)=====
        const originDotGeo = new THREE.SphereGeometry(0.08);
        const originDotMat = new THREE.MeshStandardMaterial({ color: 0x2c3e50, emissive: 0x1a2630, emissiveIntensity: 0.2 });
        const originDot = new THREE.Mesh(originDotGeo, originDotMat);
        originDot.position.set(0, 0, 0);
        scene.add(originDot);

        // ---------- 7. 柱状图主体(每个柱子带阴影、柱顶数值标签,严格按ECharts标准)----------
        chartData.forEach((item, i) => {
            const xPos = i * config.xStep + config.columnXOffset;
            const height = item.value;     // 实际数值
            const color = config.columnColors[i % config.columnColors.length];

            // 柱子材质:轻微光泽,略带透明感但清晰
            const columnMaterial = new THREE.MeshStandardMaterial({
                color: color,
                roughness: 0.45,
                metalness: 0.2,
                emissive: new THREE.Color(color).multiplyScalar(0.1),
                emissiveIntensity: 0.2,
                transparent: true,
                opacity: 0.95
            });

            const columnGeo = new THREE.BoxGeometry(
                config.columnWidth,
                height,
                config.columnDepth
            );
            const column = new THREE.Mesh(columnGeo, columnMaterial);
            column.castShadow = true;
            column.receiveShadow = true;
            column.position.set(xPos, height / 2, 0);
            scene.add(column);

            // ----- 柱顶数值标签(核心要求:每一个柱子顶部显示相应的值,ECharts 风格)-----
            const valueDiv = document.createElement('div');
            valueDiv.className = 'bar-value-label';
            valueDiv.textContent = item.value;   // 简洁显示数值
            // 仿ECharts: 白色底框+红褐色字,小圆角
            valueDiv.style.background = 'rgba(255, 255, 255, 0.8)';
            valueDiv.style.border = '1px solid ' + new THREE.Color(color).getStyle();
            valueDiv.style.color = new THREE.Color(color).multiplyScalar(0.6).getStyle();
            
            const valueLabel = new CSS2DObject(valueDiv);
            // 标签置于柱子顶部上方0.8处,醒目不重叠
            valueLabel.position.set(xPos, height + 0.8, 0);
            scene.add(valueLabel);

            // ----- 额外加一个微小的柱顶圆点,提升细节 (ECharts有时会有标记) -----
            const topDotGeo = new THREE.SphereGeometry(0.1);
            const topDotMat = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.2 });
            const topDot = new THREE.Mesh(topDotGeo, topDotMat);
            topDot.position.set(xPos, height, 0);
            topDot.castShadow = false;
            scene.add(topDot);
        });

        // ---------- 8. 添加一个轻量的Z轴短线示意(虽然2D柱状图,但3D空间给出方位)----------
        const zAxisPoints = [new THREE.Vector3(0, 0, -2), new THREE.Vector3(0, 0, 2)];
        const zAxisGeo = new THREE.BufferGeometry().setFromPoints(zAxisPoints);
        const zAxisLine = new THREE.Line(zAxisGeo, new THREE.LineBasicMaterial({ color: 0xb0bec5, linewidth: 1 }));
        scene.add(zAxisLine);

        // 添加微弱的辅助环境线框,不干扰主视觉
        const helperExtent = 14;
        const axisHelperMaterial = new THREE.LineBasicMaterial({ color: 0xd0d8e0, linewidth: 1 });

        // 简单在背面加两根平行线,增加空间感(可选)
        const backLine1 = [new THREE.Vector3(-1, 0, -1.5), new THREE.Vector3(xAxisEnd, 0, -1.5)];
        const backLineGeo1 = new THREE.BufferGeometry().setFromPoints(backLine1);
        const backLineObj1 = new THREE.Line(backLineGeo1, axisHelperMaterial);
        scene.add(backLineObj1);

        // ---------- 9. 响应式窗口----------
        window.addEventListener('resize', onWindowResize, false);
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
            labelRenderer.setSize(window.innerWidth, window.innerHeight);
        }

        // ---------- 10. 动画循环----------
        function animate() {
            requestAnimationFrame(animate);
            controls.update(); // 启用阻尼后需要每帧更新
            renderer.render(scene, camera);
            labelRenderer.render(scene, camera);
        }
        animate();

        // 控制台提示
        console.log('ECharts 风格柱状图已加载,坐标轴标签、柱顶数值完整展示');
    </script>
</body>
</html>

总结与扩展建议

核心总结

  1. 视觉还原核心:通过CSS2D标签、ECharts经典配色、轴样式/布局优化,精准复刻ECharts柱状图的视觉风格,同时保留3D立体感;
  2. 光影构建核心:分层光照(环境光+主方向光+背光+点光源)+软阴影系统,打造真实的3D光影质感,避免场景平面化;
  3. 交互适配核心:轨道控制器阻尼优化+响应式窗口适配,保证3D交互的顺滑性与多设备的兼容性;
  4. 性能核心:通过合理的几何体/材质配置、阴影分辨率平衡,保证在普通设备上60帧流畅渲染。

扩展建议

  1. 动态数据更新
    • 新增updateData函数,修改chartData后重新构建柱子几何体,实现数据动态刷新(如实时监控数据);
    • 为柱子添加高度过渡动画,让数据更新时柱子平滑升降,提升视觉体验。
  2. 多系列柱状图
    • 在Z轴方向扩展,为每个分类添加多个柱子(如每月的“销量”“利润”),实现多系列对比;
    • 新增图例组件(CSS2D标签),标注不同系列的颜色与含义,贴合ECharts多系列图表风格。
  3. 交互增强
    • 添加tooltip交互:监听鼠标点击/悬浮事件,显示柱子的详细信息(如“一月:2(同比增长10%)”);
    • 柱子高亮效果:鼠标悬浮时修改柱子材质(如提高emissiveIntensity),增强交互反馈。
  4. 视觉主题切换
    • 新增主题配置(如浅色/深色主题),修改scene.backgroundaxisColorcolumnColors等参数,实现一键切换;
    • 适配ECharts的官方主题(如dark、macarons),提升视觉多样性。
  5. 性能优化
    • 使用InstancedMesh替代普通Mesh,批量渲染相同样式的柱子,减少DrawCall,支持更多分类数据;
    • 视锥体裁剪:剔除屏幕外的柱子/标签,减少渲染开销,适配大量数据场景。
❌