👁️什么是 Low-Poly
Low-Poly 是3D建模中的术语,意思是用相对较少的点线面来制作的低精度模型。Low-Poly 中的形状由三角形网格构成的,图形彼此交替连接成折线,然后构成新的几何图形。Low-Poly 设计风格的特点是低细节,抽象又具象。它延续了扁平化的特点,又与拟物化相结合。多边形组成的面数量多,面积小,配合柔光效果,营造出一种尖锐又硬朗的感觉。[1]
一个精细渲染的球体转变为 Low-Poly 的过程[2]
😶🌫️如何让 Low-Poly 好看?
从个人的喜好出发,我认为好看的 Low-Poly 效果,满足以下几点要求:
- 合适的面数量
- 低饱和度
- 明亮的颜色
- 恰当的灯光设置
合适的面数量
面的数量不够少,将看不出 Low-Poly 风格。如上文中精细渲染的球体,面的数量太多了,形状非常光滑,导致丢失了 Low-Poly 风格尖锐硬朗的感觉,也没有抽象的感觉。
而面的数量过少,将使物体的表达丢失最低限度的具象要求。如上文中精细渲染的球体,将面减少至4个,形状会变为三棱锥,无法表达球体。
低饱和度
过高的饱和度,使得整个画面过于尖锐,丢失关注点,同时不同颜色的搭配冲击力过于“土味”。
明亮的颜色
因为在饱和度的选择上,我倾向于选择低饱和度的颜色,因此在色彩的选择上,建议尽量选明亮的颜色。否则后期配合灯光设置,暗色的阴面和阳面区分将不会很清楚,使得丢失画面细节。
恰当的灯光设置
灯光设置是最重要的一步,因为物体最终是否可见,效果如何,都取决于对场景中灯光的反射。不正确的灯光设置,将导致整个画面不协调、过度曝光、没有明暗等问题。
🕶️从零开始创建 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 可视化相关信息,请打开🔜链接
点一个小小的赞👍,如果能收藏就更好了~栓Q🙇