数百行代码,打造一个 Low-Poly Universe

981 阅读8分钟

👁️什么是 Low-Poly

Low-Poly 是3D建模中的术语,意思是用相对较少的点线面来制作的低精度模型。Low-Poly 中的形状由三角形网格构成的,图形彼此交替连接成折线,然后构成新的几何图形。Low-Poly 设计风格的特点是低细节,抽象又具象。它延续了扁平化的特点,又与拟物化相结合。多边形组成的面数量多,面积小,配合柔光效果,营造出一种尖锐又硬朗的感觉。[1] Alex Pixel Cutter

一个精细渲染的球体转变为 Low-Poly 的过程[2]

😶‍🌫️如何让 Low-Poly 好看?

从个人的喜好出发,我认为好看的 Low-Poly 效果,满足以下几点要求:

  1. 合适的面数量
  2. 低饱和度
  3. 明亮的颜色
  4. 恰当的灯光设置

合适的面数量

面的数量不够少,将看不出 Low-Poly 风格。如上文中精细渲染的球体,面的数量太多了,形状非常光滑,导致丢失了 Low-Poly 风格尖锐硬朗的感觉,也没有抽象的感觉。

而面的数量过少,将使物体的表达丢失最低限度的具象要求。如上文中精细渲染的球体,将面减少至4个,形状会变为三棱锥,无法表达球体。

低饱和度

过高的饱和度,使得整个画面过于尖锐,丢失关注点,同时不同颜色的搭配冲击力过于“土味”。

image.png

明亮的颜色

因为在饱和度的选择上,我倾向于选择低饱和度的颜色,因此在色彩的选择上,建议尽量选明亮的颜色。否则后期配合灯光设置,暗色的阴面和阳面区分将不会很清楚,使得丢失画面细节。

恰当的灯光设置

灯光设置是最重要的一步,因为物体最终是否可见,效果如何,都取决于对场景中灯光的反射。不正确的灯光设置,将导致整个画面不协调、过度曝光、没有明暗等问题。

🕶️从零开始创建 Low-Poly Universe

接下来,我将从零开始,演示如何创建一个 Low-Poly Universe.

🍟创建一个空的场景

import * as THREE from 'three@0.151.3'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

let renderer, camera, scene, controls;
const container = document.getElementById('app')
const init = () => {
  renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: false
  });

  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement)

  scene = new THREE.Scene();

  camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    10000
  );

  camera.position.set(0, 200, 300);
  camera.lookAt(0, 0, 0);

  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.1;

  renderer.render(scene, camera);
}

init()

以上代码将创建一个背景为纯黑色,抗锯齿的空场景。同时将摄像机位置放置在 (0,200,300)处。

设置灯光

const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
const pointLightLeft = new THREE.PointLight(0xffffff, 1.0);
pointLightLeft.position.set(200, 200, 0);
const pointLightRight = new THREE.PointLight(0xffffff, 0.5, 0);
pointLightRight.position.set(-200, 200, 0);
scene.add(ambientLight, pointLightLeft, pointLightRight);

AmbientLight 是环境光,环境光是一种低强度的光,在现实中,环境光是由光线经过周围环境表面多次反射后形成的。在三维可视化开发中,我们可以通过设置环境光来模拟这种效果,环境光将从各个方向将物体照亮,同时不产生投影。在 Three.js 中,AmbientLight 接收 color,intensity 两个参数,一个控制环境光的颜色,一个控制环境光的强度。

PointLight 是点光源,点光源是有位置的光源,而且光线均匀的射向空间中的所有方向。点光源的强度可以随着距离的增加而发生衰减。在 Three.js 中,Pointlight接收color,intensity,distance,decay 四个参数,分别控制点光源颜色、强度、距离和衰减系数。其中 distance 设置为 0 代表没有距离限制。

🐕‍🦺生成低多边形

在 Three.js 中生成低多边形非常方便,只需要使用 Three.js 提供的 TetrahedronGeometry 即可。TetrahedronGeometry 接收 radius,detail两个参数。radius控制低多边形的大小,detail控制低多边形面的数量。detail等于 0 生成三棱锥,随着 detail 的增加,整个物体约接近光滑的球体。因此要获得比较好的 Low-Poly 效果,detail 数值可以设置为 0/1/2/3。

const geometry = new THREE.TetrahedronGeometry(radius, detail);

🤩给形状添加材质

形状为骨,材质为皮。材质在 Three.js 中有很多种,日常使用中,主要是考虑是否产生高光。因为我们的 Low-Poly 风格需要突出棱角分明,因此我选择使用 MeshPhongMaterial,这种材质会对光线产生强反射,形成高光。

  const material = new THREE.MeshPhongMaterial({
    color: color,
    emissive: 0x072534,
    flatShading: true
  });

在 Low-Poly 的项目中,材质设置成 flatshading:true 非常重要,这个参数使得物体根据每个三角形的法线计算着色效果。着色计算只执行一次,整个三角形都采用计算结果的颜色。如果不设置这个参数,最终渲染的物体将在不同的面上形成斑点状的高光,使 Low-Poly 风格物体的棱角不再分明。

💧生成场景

因为要生成很多不同颜色,形状和大小的 Low-Poly 物体,因此将生成形状的部分封装成一个函数。

const makeShape = (radius, detail, color) => {
  const geometry = new THREE.TetrahedronGeometry(radius, detail);
  const material = new THREE.MeshPhongMaterial({
    color: color,
    emissive: 0x072534,
    flatShading: true
  });
  const shape = new THREE.Mesh(geometry, material);
  return shape
}

在这个项目中,我想制作的是类似小行星环绕星球的感觉,因此最中间的星球要做的比较大一点。先手工创建一个星球。

const mainObject = makeShape(10, 3, 0xf1939c)
scene.add(mainObject);

同时在这个星球上有一致指示器,向用户传递星球可点击的信息。

const indicatorGeometry = new THREE.ConeGeometry(4, 8, 5, 1)
const indicatorMaterial = new THREE.MeshPhongMaterial({
  color: 0x8cc269,
  emissive: 0x072534,
  flatShading: true,
  transparent: true,
  opacity: 1
});

最外围的小行星,设置了一个最大值 maxNum=1000,意味着生成1000个形状各异的小行星。

const addOtherObjectToScene = (maxObjNum) => {
  for (let i = 0; i < maxObjNum - 1; i++) {
    let detail = Math.ceil(Math.random() * 3);
    let radius = Math.ceil(Math.random() * 4);
    let color = colors[Math.floor(Math.random() * (colors.length - 1))]
    const node = makeShape(radius, detail, color);
    scene.add(node);
  }
}

其中 colors 来自我选择的一些低饱和度,高亮度的颜色。

const colors = [0xf0c9cf, 0xc27c88, 0xc27c88, 0x983680, 0x61649f, 0x126bae, 0x126bae, 0x126bae, 0x5dbe8a]

在 Three.js 中,物体默认的位置是 (0,0,0),为了使外围的小行星可以随机分布在围绕这中心星球的圆环带中,我们通过极坐标对物体进行定位。

const distance = Math.random() * 50 + 50;// 小行星与星球的距离为 50~100
const theta = Math.random() * 2 * Math.PI;// 随机角度
node.position.x = Math.cos(theta) * distance;
node.position.z = Math.sin(theta) * distance;

这样小行星就在一个圆环面上随机散开了。高度上也做一下分散处理,使得整个小行星带更加立体。

node.position.y = Math.random() * 50 - 25;

至此,一个被2000颗小行星包围的星球就制作完成了。

🥁 让小行星环绕星球做公转和自转

所有的动画效果都通过修改物体的某些属性,并调用 requestAnimateFrame()实现。

const animate = () => {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  //修改属性的代码
  }
animate()

🥣小行星的自转

小行星的自转,实际上是不停的改变小行星的旋转角度实现的。在 Three.js 中,物体有一个 rotation 属性,修改这个属性,就可以修改小行星的旋转角度。

const animate = () => {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  //修改属性的代码
  for (let i in clickAbleObjects) {
    let obj = clickAbleObjects[i];
    obj.rotation.y = obj.rotation.y +  Math.random() * 0.02;
  }
}
animate()

clickAbleObject 是自定义的一个数组,生成的所有物体,都先 push 到这个数组中再添加到场景中。

🎪小行星的公转

通过不停的更改小行星在圆周上的位置,实现小行星的公转。在生成小行星的代码中,给小行星增加几个属性。

const addOtherObjectToScene = (maxObjNum) => {
  for (let i = 0; i < maxObjNum - 1; i++) {
    let detail = Math.ceil(Math.random() * 3);
    let radius = Math.ceil(Math.random() * 4);
    let color = colors[Math.floor(Math.random() * (colors.length - 1))]
    const node = makeShape(radius, detail, color);
    node.distance = distance;
    node.theta = theta;
    clickAbleObjects.push(node);
    scene.add(node);
  }
}

animate 函数中,修改位置。

const animate = () => {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  for (let i in clickAbleObjects) {
    let obj = clickAbleObjects[i];
    obj.theta = obj.theta + 1 / 180 / 10 * Math.PI
    obj.rotation.y = obj.rotation.y + obj.rotationSpeed
    obj.position.x = Math.cos(obj.theta) * obj.distance;
    obj.position.z = Math.sin(obj.theta) * obj.distance;
  }
}

这样,小行星就产生了自转以及公转的效果。

🪂点击散开小行星带

小行星带的散开,通过修改小行星 distance 属性实现,将该属性改大一点就可以了。因此当监测到点击事件,修改 distance 即可。但在我的这个设计中,我并不想点击任何地方都可以展开小行星带,同时我也希望展开是逐渐向四周散开,有一个移动的过程,而不是一下子跳到了更大的圆环上。因此如何判断点击的物体是哪个,以及如何实现 distance 逐渐增大,是我们需要解决的问题。

这两个问题的解决方案,我已经写在了 Make your 3D project interactive 一文中,欢迎查阅。

👹最终效果及完整代码

😀请点击运行按钮,待渲染完成后,点击中间星球查看小行星散开动画。

🥰商业应用

本项目并不是无根之水,我们有一个知识图谱可视化的需求,该图谱共四个层级,其中最顶级一个节点,顶级节点有不固定数量的二级节点,二级节点有不固定数量的三级节点,三级节点有不固定数量的四级节点。我们期望通过三维可视化的形式对图谱进行展示,一来节点与节点中间的关系比较清晰,二来效果看起来也比较新颖,更符合用户喜好。在本次的演示中,点击星球之后,你可以看到小行星带展开成了三个圆环。这便是为了传递不同层级的信息。

在实际的项目中,我们还增加了点击之后,处于同一图谱树中的节点上浮,其余节点下层并透明。以及动态飞线、文字渲染等效果。后续将逐一增加到演示中。

😍其它

(^_^)如果你喜欢这个可视化项目,或者你想了解更多 AI 可视化相关信息,请打开🔜链接

🔗code.juejin.cn/pen/7226160…

点一个小小的赞👍,如果能收藏就更好了~栓Q🙇‍

💧参考

[1] 详解Low Poly设计风格,渣特效翻身成为流行?

[2]Low Poly Art制作简述及基础讲解