My Learning Journal
This is where I will post about my journey learning tech.
使用three实现一个简单的场景
目录
- 前言
- 搭建基础环境
- 准备画布
- 增加几何体
- 可选优化
- 完整代码
前言
Three.js是一个基于 WebGL 的 3D 库,让开发者轻松在浏览器中创建三维内容。本文将展示 Three.js 的核心组件(场景、相机、渲染器、几何体、材质、网格),并实现一个非常简单的场景,包含一个彩色的平面几何体。
搭建基础环境
-
创建
HTML文件
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<canvas id="c"></canvas>
</body>
</html>
这里包含了一个
<canvas></canvas>元素,之后会用到
-
引入 Three.js
有多种方法,这里使用 CDN,还可以使用npm安装。
<script type="importmap">
{
"imports":
{
"three":"https://cdn.jsdelivr.net/npm/three@v0.181.0/build/three.module.js",
"three/addons/":"https://cdn.jsdelivr.net/npm/three@v0.181.0/examples/jsm/"
}
}
</script>
three/addons/是three的扩展包,可以提供额外的材质、几何体、后期处理工具等
-
设置css属性
设置CSS宽高(比如 width: 100%; height: 100vh;)并隐藏滚动条,这样可以让画面占满视口。
html,
body {
margin: 0;
overflow: hidden;
height: 100%;
}
准备画布
-
导入three包
import * as THREE from 'three';
-
设置宽高参数
后面设置大小时需要,提前定义
为了保证渲染与显示一致,这里的宽高从canvas元素获取
const width = canvas.clientWidth;
const height = canvas.clientHeight;
场景是所有物体的容器。
const scene = new THREE.Scene();
const fov = 75;
const aspect = width / height;
const near = 1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
fov:(视线角度)
aspect(渲染的宽高比)
near / far(渲染的远近范围)
-
设置相机位置
position决定了看物体的视角,接收的3个参数分别为z轴(上下),Y轴(远近),X轴(左右)
camera.position.set(0, -5, 30);
决定图像大小的3要素:几何体大小设置、像素设置、相机远近
-
绑定canvas元素
这里使用元素id绑定
const canvas = document.querySelector('#c');
还有一种添加canvas的方法,不在html文件中设定元素,使用js创建一个canvas画布,然后系统会自动插入
document.body.appendChild( renderer.domElement )注:系统自动插入元素会有不确定性,所以推荐手动创建并绑定元素。
three有webgl和webgpu两种renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, canvas});
antialias:抗锯齿
element:添加canvas画布
-
调用 setSize 设置渲染尺寸
目的:确保渲染尺寸和显示尺寸一致。
renderer.setSize(width,height)
为了图像清晰,要将几何体大小和渲染的像素大小保持一致
一般不使用
canvas.height=height和canvas.width=width
-
启动动画
用setAnimationLoop不断调用渲染函数。
function animate() {
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
-
第一次运行
你已经建立了一个场景,你现在可以启动本地服务器,然后在浏览器中访问index.html,你将会看到一个全黑的画布画面
three只需要
scene、camera、renderer即可运行!
设置几何体
接下来,我们将在场景中添加一个几何体。
不同的图形需要不同的参数,PlaneGeometry是一个平面几何体,接收4个参数。
width:宽度
height:高度
widthSegments:横向顶点数
heightSegments:纵向顶点数
顶点数会影响变形时的画面
const g_width = 30;
const g_height = 30;
const widthSegments = 128;
const heightSegments = 128;
const geometry = new THREE.PlaneGeometry(
g_width, g_height,
widthSegments, heightSegments);
使用 MeshBasicMaterial,设置颜色 0x8b91b8。
const material = new THREE.MeshBasicMaterial({ color: 0x8b91b8, side: THREE.DoubleSide });
可选设置side:
THREE.DoubleSide使平面双面可见(否则旋转后背面不可见)。
使用mesh将图形和材质结合,生成最终的图像
需要两个参数:geometry、material
const Plane = new THREE.Mesh(geometry, material);
-
添加到场景中
scene.add(Plane);
增加PointLight光源,参数为颜色和强度(亮度),可以使用position.set设置位置,可以产生阴影,增加立体性,还可以通过增加多种光源改善画面
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(50, 50, 100);
scene.add(pointLight);
-
旋转几何体
这边给一点角度可以使几何体看的更清除一点
geometry.rotateX(-Math.PI / 6);
可选优化
-
窗口自适应
现在画布大小是固定的,也就是说在调整浏览器窗口时,里面显示的物体会被拉伸。
我们可以通过监听 resize 事件,实时更新渲染器尺寸和相机宽高比,防止画面被拉伸.
function onWindowResize(){
//获取最新的宽高
const width = window.innerWidth;
const height = window.innerHeight;
//设置渲染大小和相机宽高比
renderer.setSize(width,height);
camera.aspect = width / height;
//让相机生效
camera.updateProjectionMatrix();
}
//监听浏览器的 resize 事件
window.addEventListener('resize', onWindowResize);
perspective 相机需要更新宽高比,因为相机的视野是固定的,如果不改,画面会被压扁
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hallo Three.js</title>
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
html,
body {
margin: 0;
/* 一直占满屏幕,没有滚动条 */
overflow: hidden;
height: 100%;
}
#c {
/* 如果父容器没有占满屏幕
就需要增加绝对定位absolute position */
width: 100%;
height: 100%;
display: block;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@v0.181.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@v0.181.0/examples/jsm/"
}
}
</script>
</head>
<body>
<script type="module" src="/main.js"></script>
<canvas id="c">
</canvas>
</body>
</html>
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const canvas = document.querySelector('#c');
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const scene = new THREE.Scene();
const fov = 75;
const aspect = width / height;
const near = 1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, -5, 30);
const renderer = new THREE.WebGLRenderer({ antialias: true, canvas});
renderer.setSize(width,height)
function onWindowResize(){
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width,height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', onWindowResize);
const g_width = 30;
const g_height = 30;
const widthSegments = 128;
const heightSegments = 128;
const geometry = new THREE.PlaneGeometry(
g_width, g_height,
widthSegments, heightSegments);
const material = new THREE.MeshBasicMaterial({ color: 0x8b91b8, side: THREE.DoubleSide });
const Plane = new THREE.Mesh(geometry, material);
geometry.rotateX(-Math.PI / 6);
scene.add(Plane);
function animate() {
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);这里提供两种识别方法
工作量证明方法——要求用户绘制画布并验证某些已知像素的数值.
生成一个特定颜色的画布,并验证特定像素的数值是否符合预期的。下面的代码片段展示了一个简单的示例,我们将画布填充为 rgba(0, 127, 255, 1)。然后,我们对每个像素进行迭代,验证 r、g、b、a 的分量是否具有期望值(因此代码中使用%4)。
var canvas = document.createElement("canvas");
canvas.height = size;
canvas.width = size;
var context = canvas.getContext("2d");
context.fillStyle = "rgba(0, 127, 255, 1)";
var pixelValues = [0, 127, 255, 255];
// 应用颜色
context.fillRect(0, 0, canvas.width, canvas.height);
var pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
for (var i = 0; i < pixels.length; i += 1) {
if (pixels[i] !== pixelValues[i % 4]) {
// 执行测试
// 如果不匹配,则代表指纹被篡改
console.log('Canvas has been overridden!')
}
}
函数一致性检查方法
通过查看函数的原型或错误堆栈痕迹等副作用检测函数是否被覆盖
检测由js代理调用的反指纹脚本
let isOverridden = true;
try {
Object.setPrototypeOf(HTMLCanvasElement.prototype.toDataURL, HTMLCanvasElement.prototype.toDataURL)
} catch (e) {
if (e.message.indexOf('Cyclic') > -1) {
isOverridden = false;
}
}
正常情况会报错
通过监控错误堆栈,来检测特定扩展
var canvas = document.createElement('canvas');
var context = canvas.getContext("2d");
try {
context.getImageData(canvas);
} catch (e) {
hasAntiCanvasExtension = e.stack.indexOf('chrome-extension') > -1;
hasCanvasBlocker = e.stack.indexOf('nomnklagbgmgghhjidfhnoelnjfndfpd') > -1;
}使用基本的html、css来实现一个简洁的时间轴
实现方法有很多种,这里只给出此博客所用的方法。
以下图为例,时间轴由大圆、小圆和淡淡的竖线组成
HTML结构
timeline
::before
year
marker //圆点
//此处输入自定义文字
posts
post-link
marker //圆点
content
//此处输入自定义文字
圆形
圆形由svg实现:
<svg
class="marker"
//定义css类名
xmlns="http://www.w3.org/2000/svg"
//声明命名空间
width="12"
height="12"
//图形长宽
>
<circle cx="6" cy="6" r="6"></circle>
//circle是一个svg的圆形元素
//cx,cy:圆心的x,y坐标
//r:半径
</svg>
数轴
数轴使用了伪元素,伪元素属于css,可以在一个元素中创建拥有特殊性质的元素,伪元素的一种典型的用法就是生成线条
.timeline::before {
content: "";
//必须设置content,否则元素不会显示
position: absolute;
//定位上下文
bottom: -20px;
//底面移动-20px,向下微调20px
height: 100%;
//长度为整个元素块
width: 2px;
//宽度
background-color: rgb(240, 240, 240);
//设置线条颜色
}
至此,一个简单的时间轴的骨架就完成了。
目前这个时间轴有以下几点问题
- 竖线横跨了整个窗口
- 小圆点没有和文字对齐
- 小圆点被竖线覆盖了
以下是示例代码
//1.
.timeline{
//这是因为将伪元素的position设置了absolute,默认会向上查找最近的static定位的元素
//我们可以将timeline元素作为定位参考
position:relative;
}
//2.
.post-link {
display: flex;
//可以让圆点和文字在同一行
align-items: baseline;
//这种对齐方式可以稍微让圆点回正一点
}
.post-link .marker {
position: relative;
//建立一个新的层叠上下文,让圆点覆盖竖线
left: -2px;
bottom: -2px;
//对圆点位置进行微调
}
至此,这个时间轴就建好了,剩下的就是对其他元素进行样式优化就好了