three中提供了22 个基础模型,此案例除了 EdgesGeometry、ExtrudeGeometry、TextGeometry、WireframeGeometry,涵盖 17 个形状。
Fog 雾化设置,这是scene场景效果
EdgesGeometry , WireframeGeometry 更多地可能作为辅助功能去查看几何体的边和线框
ExtrudeGeometry 则是将一个二维图形沿 z 轴拉伸出一个三维图形
TextGeometry 则需要从外部加载特定格式的字体文件(可在 typeface.js_three 网站上进行转换)进行渲染,其内部依然使用 ExtrudeGeometry 对字体进行拉伸,从而形成三维字体。另外,该类字体的本质是一系列类似SVG 的指令。所以,字体越简单(如直线越多),就越容易被正确渲染。
planeGeometry 底层
BoxGeometry(长方体)
CircleGeometry(圆形)
ConeGeometry(圆锥体)
CylinderGeometry(圆柱体)
DodecahedronGeometry(十二面体)
IcosahedronGeometry(二十面体)
LatheGeometry(让任意曲线绕 y 轴旋转生成一个形状,如花瓶)
OctahedronGeometry(八面体)
ParametricGeometry(根据参数生成形状)
PolyhedronGeometry(多面体)
RingGeometry(环形)
ShapeGeometry(二维形状)
SphereGeometry(球体)
TetrahedronGeometry(四面体)
TorusGeometry(圆环体)
TorusKnotGeometry(换面纽结体)
TubeGeometry(管道)
我们先看下效果图
看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body {
width: 100%;
height: 100%;
}
* {
margin: 0;
padding: 0;
}
.label {
font-size: 20px;
color: #000;
font-weight: 700;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "../three-155/build/three.module.js",
"three/addons/": "../three-155/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GPUStatsPanel } from 'three/addons/utils/GPUStatsPanel.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { createMultiMaterialObject } from 'three/addons/utils/SceneUtils.js';
import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';
import { ParametricGeometries } from 'three/addons/geometries/ParametricGeometries.js';
let stats, labelRenderer, gpuPanel;
let camera, scene, renderer, mesh, target, controls, meshs = [];
const group = new THREE.Group();
/**
在 Three.js 中可以渲染着色器的材质有两种:RawShaderMaterial 和 ShaderMaterial,
它们之间的区别是 ShaderMaterial 会自动将一些初始化着色器的参数添加到代码中(内置 attributes 和 uniforms),
而 RawShaderMaterial 则什么都不会添加。
*/
init();
initHelp();
initLight();
axesHelperWord();
animate();
meshs = addGeometries();
addPlane();
function addPlane() {
// 创建一个平面 PlaneGeometry(width, height, widthSegments, heightSegments)
const planeGeometry = new THREE.PlaneGeometry(120, 140, 1, 1);
// 创建 Lambert 材质:会对场景中的光源作出反应,但表现为暗淡,而不光亮。
const planeMaterial = new THREE.MeshLambertMaterial({
color: 0xffffff
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 以自身中心为旋转轴,绕 x 轴顺时针旋转 45 度
plane.rotation.x = -0.5 * Math.PI;
plane.position.set(0, -4, 0);
scene.add(plane);
}
function addGeometries() {
const geoms = [];
const meshs = [];
// 共 22 个形状,除了 EdgesGeometry、ExtrudeGeometry、TextGeometry、WireframeGeometry,下面涵盖 17 个(PlaneGeometry在上面)
// Box 长方体
geoms.push(new THREE.BoxGeometry(8, 8, 8));
// Circle 圆形
geoms.push(new THREE.CircleGeometry(8, 32));
// Cone 圆锥体
geoms.push(new THREE.ConeGeometry(8, 20, 32));
// Cylinder 圆柱体
geoms.push(new THREE.CylinderGeometry(5, 5, 20, 32));
// Dodecahedron 十二面体
geoms.push(new THREE.DodecahedronGeometry(8));
// Icosahedron 二十面体
geoms.push(new THREE.IcosahedronGeometry(8));
// Lathe 让任意曲线绕 y 轴旋转生成一个形状,如花瓶
const lathePoints = [];
for (let i = 0; i < 10; i++) {
lathePoints.push(new THREE.Vector2(Math.sin(i * 0.2) * 5 + 2.5, (i - 5) * 2));
}
geoms.push(new THREE.LatheGeometry(lathePoints));
// Octahedron 八面体
geoms.push(new THREE.OctahedronGeometry(8));
// Parametric:根据参数生成形状,THREE.ParametricGeometries.klein 是 ParametricGeometries.js_three 库提供
geoms.push(new ParametricGeometry(ParametricGeometries.klein, 10, 10));
const verticesOfCube = [
-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1,
-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1,
];
const indicesOfFaces = [
2, 1, 0, 0, 3, 2,
0, 4, 7, 7, 3, 0,
0, 1, 5, 5, 4, 0,
1, 2, 6, 6, 5, 1,
2, 3, 7, 7, 6, 2,
4, 5, 6, 6, 7, 4
];
// Polyhedron 多面体
geoms.push(new THREE.PolyhedronGeometry(verticesOfCube, indicesOfFaces, 8, 1));
// Ring 环形
geoms.push(new THREE.RingGeometry(3, 8, 16));
// Shape 二维形状(心形)
let x = 0, y = 0;
let heartShape = new THREE.Shape();
heartShape.moveTo(x + 5, y + 5);
heartShape.bezierCurveTo(x + 5, y + 5, x + 4, y, x, y);
heartShape.bezierCurveTo(x - 6, y, x - 6, y + 7, x - 6, y + 7);
heartShape.bezierCurveTo(x - 6, y + 11, x - 3, y + 15.4, x + 5, y + 19);
heartShape.bezierCurveTo(x + 12, y + 15.4, x + 16, y + 11, x + 16, y + 7);
heartShape.bezierCurveTo(x + 16, y + 7, x + 16, y, x + 10, y);
heartShape.bezierCurveTo(x + 7, y, x + 5, y + 5, x + 5, y + 5);
geoms.push(new THREE.ShapeGeometry(heartShape));
// Sphere 球体
geoms.push(new THREE.SphereGeometry(8, 24, 24));
// Tetrahedron 四面体
geoms.push(new THREE.TetrahedronGeometry(8));
// Torus 圆环体
geoms.push(new THREE.TorusGeometry(6, 2, 16, 100));
// TorusKnot 环面纽结体
geoms.push(new THREE.TorusKnotGeometry(6, 2, 100, 16));
// Tube 管道
const points = [];
for (let i = 0; i < 5; i++) {
const randomX = -10 + Math.round(Math.random() * 20);
const randomY = -7 + Math.round(Math.random() * 20);
const randomZ = -10 + Math.round(Math.random() * 20);
points.push(new THREE.Vector3(randomX, randomY, randomZ));
}
geoms.push(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(points), 20, 2, 8, false));
let j = 0;
// 为几何体添加材质
for (let i = 0; i < geoms.length; i++) {
let materials = [
// 内侧
new THREE.MeshLambertMaterial({
color: Math.random() * 0xffffff,
flatShading: true, // 定义是否使用平面着色渲染材质
side: THREE.DoubleSide,
wireframe: true // 将几何体渲染为线框。默认值为false(即渲染为平面多边形)
}),
// 外侧
new THREE.MeshBasicMaterial({
color: Math.random() * 0xffffff,
wireframe: false
})
];
let mesh = createMultiMaterialObject(geoms[i], materials);
meshs.push(mesh);
// 设置每个物体的位置
mesh.position.x = -36 + ((i % 4) * 24);
mesh.position.y = 4;
mesh.position.z = -36 + (j * 24);
if ((i + 1) % 4 == 0) {
j++
}
scene.add(mesh)
}
return meshs
}
function init() {
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 10, 2000 );
camera.position.set(0, 125, 125);
camera.up.set(0, 1, 0);
camera.lookAt(0, 0, 0);
scene = new THREE.Scene();
scene.background = new THREE.Color( '#ccc' );
// Fog( hex, near, far ),线性雾化。
// near 表示哪里开始应用雾化效果
// far 表示雾化效果在哪里结束
scene.fog = new THREE.Fog(0xffffff, 0.015, 100);
// FogExp2( hex, density ),指数雾化
// density 是雾化强度
scene.fog = new THREE.FogExp2(0xffffff, 0.005); //0.01 雾化就很重了
// 雾化效果默认是全局影响的,若某个材质不受雾化效果影响,则可为材质的 fog 属性设置为 false(默认值 true)
// var material = new THREE.Material({
// fog: false
// });
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize( window.innerWidth, window.innerHeight );
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.pointerEvents = 'none';
document.getElementById( 'container' ).appendChild( labelRenderer.domElement );
window.addEventListener( 'resize', onWindowResize );
controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 10;
controls.maxDistance = 1000;
// 设置为true可启用阻尼(惯性),可用于为控件提供重量感。默认值为false。
// 请注意,如果启用了此选项,则必须在动画循环中调用.update()。
controls.enableDamping = false;
controls.screenSpacePanning = false; // 定义平移时如何平移相机的位置 控制不上下移动
stats = new Stats();
stats.setMode(1); // 0: fps, 1: ms
document.body.appendChild( stats.dom );
gpuPanel = new GPUStatsPanel( renderer.getContext() );
stats.addPanel( gpuPanel );
stats.showPanel( 0 );
scene.add( group );
}
function initLight() {
const light = new THREE.DirectionalLight(new THREE.Color('rgb(253,253,253)'));
light.position.set(100, 100, -10);
light.intensity = 3; // 光线强度
light.castShadow = true; // 是否有阴影
light.shadow.mapSize.width = 2048; // 阴影像素
light.shadow.mapSize.height = 2048;
// 阴影范围
const d = 80;
light.shadow.camera.left = -d;
light.shadow.camera.right = d;
light.shadow.camera.top = d;
light.shadow.camera.bottom = -d;
light.shadow.bias = -0.0005; // 解决条纹阴影的出现
// 最大可视距和最小可视距
light.shadow.camera.near = 0.01;
light.shadow.camera.far = 2000;
const AmbientLight = new THREE.AmbientLight(new THREE.Color('rgb(255, 255, 255)'));
scene.add( light );
scene.add( AmbientLight );
}
function initHelp() {
// const size = 100;
// const divisions = 5;
// const gridHelper = new THREE.GridHelper( size, divisions );
// scene.add( gridHelper );
// The X axis is red. The Y axis is green. The Z axis is blue.
const axesHelper = new THREE.AxesHelper( 100 );
scene.add( axesHelper );
}
function axesHelperWord() {
let xP = addWord('X轴');
let yP = addWord('Y轴');
let zP = addWord('Z轴');
xP.position.set(50, 0, 0);
yP.position.set(0, 50, 0);
zP.position.set(0, 0, 50);
}
function addWord(word) {
let name = `<span>${word}</span>`;
let moonDiv = document.createElement( 'div' );
moonDiv.className = 'label';
// moonDiv.textContent = 'Moon';
// moonDiv.style.marginTop = '-1em';
moonDiv.innerHTML = name;
const label = new CSS2DObject( moonDiv );
group.add( label );
return label;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
// 各个网格(mesh)绕自身中心的 y 轴自转
if (meshs.length > 0) {
for (let i = 0; i < meshs.length; i++) {
meshs[i].rotation.y += 0.01;
}
}
stats.update();
controls.update();
labelRenderer.render( scene, camera );
renderer.render( scene, camera );
}
</script>
</body>
</html>
直接复制粘贴到html文件即可看到效果,当然前提是已经提前下载了three的代码库,然后在script type="importmap"标签中改下路径就可以了。
到这一步,我们开始往场景 scene 中添加模型,也就是mesh,案例中用到了 SceneUtils.js 中的createMultiMaterialObject 创建 mesh,这个在官网中用法是这样的:
我们发现按官网看调用应该是SceneUtils.createMultiMaterialObject,但是这样会报错,我们看three 155版本中发现源码为:
看来新的版本已经更简洁了直接使用createMultiMaterialObject即可
从这个解错的过程中,我们可以总结一些规律,当我们发现有时官网提供的方法也会报错,解错思路是什么呢?
1、百度查问题,事实我也是这么做的,但由于这是最新版本,查询的结果大家也是推荐的官网用法,一通折腾没结果,这也是使用最新版本可能出现的一些无法预知的问题
2、这时大家可以调转思路,为什么不看一眼源码,虽然大部分源码晦涩难懂,多层嵌套,比较难发现问题,本身我也不喜欢读源码,但结果是看完源码几秒钟这个问题就解决了,所以当我们遇到问题,第一步不一定要百度,看一眼源码也是很好的
因为要写案例,在这个过程中遇到了一些问题,我就把解决问题的思路写下来分享给大家,我想这远比教会大家怎么实现效果更有助于大家成长。
回到主题我们在讲createMultiMaterialObject做了什么时,先看下一般情况下mesh是怎么做的,在代码中有一个addPlane函数:
// 创建一个平面 PlaneGeometry(width, height, widthSegments, heightSegments)
const planeGeometry = new THREE.PlaneGeometry(120, 140, 1, 1);
// 创建 Lambert 材质:会对场景中的光源作出反应,但表现为暗淡,而不光亮。
const planeMaterial = new THREE.MeshLambertMaterial({
color: 0xffffff
});
// 这里new THREE.Mesh 大部分情况我们都是这样做的
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 以自身中心为旋转轴,绕 x 轴顺时针旋转 45 度
plane.rotation.x = -0.5 * Math.PI;
plane.position.set(0, -4, 0);
scene.add(plane);
Mesh
表示基于三角形多边形网格的对象的类。还可以作为其他类(如SkinnedMesh)的基础。
很显然PlaneGeometry 和 MeshLambertMaterial大家可能是第一次见到
Geometry
对原生WebGL中的顶点位置position、顶点法向量normal、顶点颜色color、顶点纹理坐标uv、顶点索引index等顶点数据进行了封装
立方体BoxGeometry、圆柱体CylinderGeometry、球体SphereGeometry等Three.js几何体类都是基于基类BufferGeometry二次封装
Materials
材质就像对象的蒙皮。它定义了几何图形的外观。Three.js提供了许多工作材料。我们应该根据我们的需要选择材料的类型。
Three.js中最常用的材料:
1、MeshBasicMateria
Three.js中非常基本的材料
2、MeshDepthMaterial
它使用与摄影机的距离来确定如何以灰度为网格着色
3、MeshNormalMaterial
此材质使用面法线向量的x/y/z值的大小来计算和设置面上显示的颜色的红/绿/蓝值
4、MeshLambertMaterial
您可以使用此材质创建外观暗淡、无光泽的表面
5、MeshPhongMaterial
此材质类似于MeshLambertMaterial,但可以创建更有光泽的曲面
6、MeshStandardMaterial
它与MeshLambertMaterial或MeshPhongMaterial类似,但提供了更准确、更逼真的外观结果。它有两个特性:粗糙度和金属性,而不是光泽
7、MeshPhysicalMaterial
它与MeshStandardMaterial非常相似。可以控制材质的反射率
以上介绍了const plane = new THREE.Mesh(planeGeometry, planeMaterial);各个部分的大概定义,
所以可以看到一个立方体就是由geometry(什么形状)、materia(什么样子),代入到Mesh中实现。
后面对各个材质大部分都会用到,具体的后面再说,想了解更深入些,大家可以用这个例子根据官方文档依次使用下,想学会这点主观能动性还是要有的
createMultiMaterialObject再看这个就按理解了,官方定义是
为材质中定义的每个材质创建一个包含新网格的新组。请注意,这与为1个网格定义多个材质的材质数组不同。这对于同时需要材质和线框实现的对象非常有用。其实就是可以实现一个模型同时具有两种材质的效果。