背景

近期学习了 WebGLThree.js 的一些基础知识,于是想结合最近流行的酸性设计风格,装饰一下个人主页,同时总结一些学到的知识。本文内容主要介绍,通过使用 React + three.js 技术栈,加载 3D模型、添加 3D文字、增加动画、点击交互等,配合样式设计,实现充满设计感的 酸性风格页面。

基础知识

Three.js

Three.js 是一款基于原生 WebGL封装运行在浏览器中的 3D引擎,可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象。是一款使用非常广泛的三维引擎。可以在 three.js官方中文文档 进一步深入学习。

酸性设计

酸性设计 一词翻译自 Acid Graphics,起源于 上世纪90年代 的酸浩室音乐、电子舞曲以及嬉皮士文化。在设计领域,这种酸性美学承载一种 自由的主张,怪诞的图形,大胆鲜明的配色,特殊的材料质感,搭配多种字体,组成了独特的酸性设计风格。

总之,鲜艳高饱和度 的色彩组合;黑灰色打底高饱和 荧光色 点缀画面的 五彩斑斓的黑;充满未来感、炫酷、充满科技感的液态金属玻璃铝箔塑料等材质;随机 的元素、图形的布局;不断 重复、裁切、组合 几何图形 等都是酸性设计风格。酸性风格在音乐专辑封面、视觉海报、书籍电影封面、网页设计中也逐渐开始流行。

实现效果

在线预览:https://tricell.fun

实现

3D模型

场景初始化

创建场景
scene = new THREE.Scene();
初始化相机

透视相机 PerspectiveCamera4个 参数分别是指:视场、长宽比、近面、远面。

camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
// 设置相机位置
camera.position.set(600, 20, -200);
// 相机聚焦到屏幕中央
camera.lookAt(new THREE.Vector3(0, 0, 0));
初始化光源

添加 半球光源 HemisphereLight:创建室外效果更加自然的光源

light = new THREE.HemisphereLight(0xffffff, 0x444444);
light.position.set(0, 20, 0);
scene.add(light);
light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 20, 10);
light.castShadow = true;
scene.add(light);

添加 环境光 AmbientLight

var ambiColor = '#0C0C0C';
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);

添加辅助工具(可选)

添加辅助网格

GridHelper 可用于添加网格辅助线,也可用于装饰,通过 GridHelper(size, divisions, colorCenterLine, colorGrid) 实现。

  • size:网格宽度,默认值为 10
  • divisions:等分数,默认值为 10
  • colorCenterLine:中心线颜色,默认值为 0x444444
  • colorGrid: 网格线颜色,默认值为 0x888888
var grid = new THREE.GridHelper(1000, 100, 0x000000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
grid.position.set(0, -240, 0);
scene.add(grid);
添加相机控件

通过相机控件 OrbitControls 可以对三维场景进行缩放、平移、旋转操作,本质上改变的并不是场景,而是相机的参数。开发时 OrbitControls.js 需要单独引入。

controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.update();
添加性能查看插件

stats 是一个 Three.js 开发的辅助库,主要用于检测动画运行时的帧数。stats.js 也需要单独引入。

stats = new Stats();
container.appendChild(stats.dom);

加载模型

本文示例用到的 扔铁饼的人 雕像 3D 模型来源于 threedscans.com,可 免费 下载使用,本文末尾提供了多个免费模型下载网站,有 200多页 免费模型,大家感兴趣的话可以挑选自己夏欢的自己下载使用。当然,有建模能力的同学,也可以使用 blender3dmax 等专业建模软件生成自己喜欢的模型。

加载 objfbx 模型

需要单独引入 FBXLoader.jsOBJLoader.js.fbx.obj 格式的模型加载方法是一样的。

// var loader = new THREE.FBXLoader();
var loader = new THREE.OBJLoader();
loader.load(model, function (object) {
object.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
object.rotation.y = Math.PI / 2;
object.position.set(0, -200, 0);
object.scale.set(0.32, 0.32, 0.32);
model = object;
scene.add(object);
});
加载 gltf 模型

需要单独引入 GLTFLoader.js,加载 .gltf 格式模型方法稍有不同,需要注意的是模型的遍历对象和最终添加到场景中的是 object.scene 而不是 object

var loader = new THREE.GLTFLoader();
loader.load(model, function (object) {
object.scene.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
object.scene.rotation.y = Math.PI / 2;
object.scene.position.set(0, -240, 0);
object.scene.scale.set(0.33, 0.33, 0.33);
model = object.scene;
scene.add(object.scene);
});

添加网格、加载完成模型之后的效果如下图所示。

添加转盘动画

通过 requestAnimationFrame 刷新页面的方法添加转盘动画效果。window.requestAnimationFrame() 告诉浏览器希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

function animate () {
requestAnimationFrame(animate);
// 随着页面重回不断改变场景的rotation.y来实现旋转
scene.rotation.y -= 0.015;
renderer.render(scene, camera);
}

添加点击交互

Three.js 场景中我们要点击某个模型获取它的信息、或者做一些其他操作,要用到 Raycaster(光线投射),原理就是在你鼠标点击的位置发射一束射线,被射线中的物体都被记录下来。基本语法是 Raycaster(origin, direction, near, far),其中:

  • origin:射线的起点向量。
  • direction:射线的方向向量。
  • near:所有返回的结果应该比 near 远。值不能为负,默认值为 0
  • far:所有返回的结果应该比 far 近。不能小于 near,默认值为 无穷大

代码实现的基本步骤是:获取鼠标在屏幕的坐标 屏幕坐标转标准设备坐标 标准设备坐标转世界坐标 拿到鼠标在场景的世界坐标 根据世界坐标和相机生成射线投射方向单位向量 根据射线投射方向单位向量创建射线投射器对象。

//声明raycaster和mouse变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
onMouseClick = event => {
// 将鼠标点击位置的屏幕坐标转成threejs中的标准坐标,以屏幕中心为原点,值的范围为-1到1.
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
let intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
alert('HELLO WORLD')
// 可以通过遍历实现点击不同mesh触发不同交互,如:
let selectedObj = intersects[0].object;
if (selectedObj.name === 'car') {
alert('汽车')
}
}
}
window.addEventListener('click', onMouseClick, false);

添加3D文字

使用 TextGeometry(text : String, parameters : Object) 添加 3D文字,以下是可设置属性的说明:

  • size:字号大小,一般为大写字母的高度。
  • height:文字的厚度。
  • weight:值为 normalbold,表示是否加粗。
  • font:字体,默认是 helvetiker,需对应引用的字体文件。
  • style:值为 normalitalics,表示是否斜体
  • bevelThickness:倒角厚度。
  • bevelSize:倒角宽度。
  • curveSegments:弧线分段数,使得文字的曲线更加光滑。
  • bevelEnabled:布尔值,是否使用倒角,意为在边缘处斜切。
var loader = new THREE.FontLoader();
loader.load('gentilis_regular.typeface.json', function (font) {
var textGeo = new THREE.TextGeometry('HELLO WORLD', {
font: font,
size: .8,
height: .8,
curveSegments: .05,
bevelThickness: .05,
bevelSize: .05,
bevelEnabled: true
});
var textMaterial = new THREE.MeshPhongMaterial({ color: 0x03c03c });
var mesh = new THREE.Mesh(textGeo, textMaterial);
mesh.position.set(0, 3.8, 0);
scene.add(mesh);
});

优化

现在模型加载已经基本完成了,但是 3D 模型的体积一般比较大,部署之后我发现网页加载非常慢,影响用户体验,减小模型体积是十分必要的,在网上找了很久压缩工具,发现在不需要安装大型 3D建模软件 的情况下,使用 obj2gltf 可以将体积较大的 OBJ 格式模型转化为 gltf 模型,有效优化模型体积,提升网页加载速度。

安装
npm install obj2gltf --save
将obj模型复制到以下目录中
node_modules\obj2gltf\bin
执行转码指令
node obj2gltf.js -i demo.obj -o demo.gltf

如图出现类似上述内容,转码完成,对比转化前后的文件体积,本例中 kas.obj 初始文件大小为 9.7M 转化后的文件 kas.gltf 只有 4.6M,体积缩小一半,此时将转化后的模型加载到页面上,肉眼几乎看不出模型效果的变化,同时页面加载速度得到明显提升。

obj2gltf 也可以作为库使用,通过 node服务 实时转化模型,感兴趣的同学可以通过文章末尾链接深入学习。

也可以是使用 3D 建模软件如 blender 等手动通过减少模型 面数缩小体积 等途径对模型压缩优化,这种优化效果更明显。

完整代码

var model = require('@/assets/models/kas.gltf');
var container, stats, controls;
var camera, scene, renderer, light, model;
class Kas extends React.Component {
render () {
return (
<div id="kas"></div>
)
}
componentDidMount () {
this.initThree();
}
initThree () {
init();
animate();
function init () {
container = document.getElementById('kas');
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xa0a0a0, 200, 1000);
// 透视相机:视场、长宽比、近面、远面
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(600, 20, -200);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// 半球光源:创建室外效果更加自然的光源
light = new THREE.HemisphereLight(0xffffff, 0x444444);
light.position.set(0, 20, 0);
scene.add(light);
light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 20, 10);
light.castShadow = true;
scene.add(light);
// 环境光
var ambiColor = '#0C0C0C';
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);
// 网格
var grid = new THREE.GridHelper(1000, 100, 0x000000, 0x000000);
grid.material.opacity = 0.1;
grid.material.transparent = true;
grid.position.set(0, -240, 0);
scene.add(grid);
// 加载gltf模型
var loader = new THREE.GLTFLoader();
loader.load(model, function (object) {
object.scene.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
object.scene.rotation.y = Math.PI / 2;
object.scene.position.set(0, -240, 0);
object.scene.scale.set(0.33, 0.33, 0.33);
model = object.scene;
scene.add(object.scene);
});
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearAlpha(0);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
stats = new Stats();
container.appendChild(stats.dom);
}
function animate () {
var clock = new THREE.Clock()
requestAnimationFrame(animate);
var delta = clock.getDelta();
scene.rotation.y -= 0.015;
renderer.render(scene, camera);
stats.update();
}
// 增加点击事件
//声明raycaster和mouse变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseClick(event) {
// 通过鼠标点击位置计算出raycaster所需要点的位置,以屏幕中心为原点,值的范围为-1到1.
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
var intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
alert('HELLO WORLD')
}
}
window.addEventListener('click', onMouseClick, false);
}
}

其他设计元素

本文主要介绍 3D元素 的加载,由于文章篇幅以及时间有限(博主太懒)其他元素的实现不做详细讲解(可能后续有时间会总结整理 maybe)感兴趣的同学可以扩展阅读以下其他大神优秀的文章。

流体背景

静态 液态背景图可以通过 SVG filter 实现,实现 动态 流体背景,可以阅读《Creating Patterns With SVG Filters》,可以使用Three.js 结合原生GLSL实现,可参考《CodePen Shader Template》示例来实现。

金属、霓虹、故障效果等酸性效果字体可以阅读我的另一篇文章《仅用CSS几步实现赛博朋克2077风格视觉效果》,也可以使用设计生成,由于时间关系,本文项目中的金属效果文字以及本文banner头图中的文字都是使用在线艺术字体生成网站生成的,感兴趣的同学可以自行尝试设计。

未来进一步优化

  • #todo 酸性风格液态背景实现。
  • #todo 3D模型液化金属化效果。

three.js 优秀案例推荐

最后给大家推荐几个非常惊艳的 three.js 项目来一起体验和学习,无论是页面交互、视觉设计还是性能优化都做到了极致,可以从中学到很多。

参考资料

作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/15350537.html

最新文章

  1. php 7.0 安装mcrypt
  2. 第5章 软件包管理(1)_RPM包安装
  3. Android内部自带的SQLite数据库操作dos命令
  4. ng animate
  5. Hibernate批处理操作优化 (批量插入、更新与删除)
  6. Imread函数不好使的替用方法
  7. linux 下使用 tc 模拟网络延迟和丢包(转)
  8. 建立Cent OS7server有些问题需要注意
  9. Java(多态练习 instanceof)
  10. webpack 将不同类型的文件输出到不同文件夹
  11. Gradle 的Daemon配置
  12. Redis在Linux中安装使用
  13. hdu3193 降维+rmq
  14. xadmin后台导出时gunicorn报错ascii
  15. 设置MyEclipse字体大小
  16. 包装印刷行业裕同集团&amp;易普优APS项目顺利验收!
  17. CMake使用入门
  18. DBCP object created 日期 by the following code was never closed:
  19. 报错AbstractStandardExpressionAttributeTagProcessor
  20. Hive 数据实战

热门文章

  1. C# wpf中关于binding的converter无效的情况
  2. 【java虚拟机】分代垃圾回收策略的基础概念
  3. Git pull and push
  4. eval()函数的使用
  5. golang GC 垃圾回收机制
  6. Python - 面向对象编程 - 多继承
  7. ThreadLocal原理简单刨析
  8. Appium自动化(3) - adb无线连接手机的方法
  9. C#中的“等待窗体”对话框
  10. WPF WPF中解决内存泄露的几点提示与解决方法