Three.js教程:第一个3D场景
Posted 3D建模
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Three.js教程:第一个3D场景相关的知识,希望对你有一定的参考价值。
推荐:将NSDT场景编辑器加入你3D工具链
其他工具系列:https://nsdt.cloud/
下面的代码完整展示了通过three.js引擎创建的一个三维场景,在场景中绘制并渲染了一个立方体的效果,为了大家更好的宏观了解three.js引擎, 尽量使用了一段短小但完整的代码实现一个实际的三维效果图。
学习建议
刚一开始学习,不需要完全看懂下面的代码,能够修改增删部分代码就可以,随着时间的推移就能够很好的使用三维引擎three.js。
本课程风格和大多数课程风格不同,注意一定要结合案例代码学习,在案例代码的基础上调试体验总结,就像做化学实验一样,不要仅仅阅读文字。
.html文件引入three.js引擎
在.html文件中引入three.js就像前端经常使用的jquery.js一样引入即可。
<!--相对地址加载-->
<script src="./three.js"></script>
我已经把three.js文件上传到了我的博客服务器, 可以通过下面的URL地址去加载。
<!--http绝对地址远程加载-->
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
案例源码
第一个Three.js案例,麻雀虽小,五脏俱全,整体展示了Three.js的整套渲染系统。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>第一个three.js文件_WebGL三维场景</title>
<style>
body
margin: 0;
overflow: hidden;
/* 隐藏body窗口区域滚动条 */
</style>
<!--引入three.js三维引擎-->
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
<!-- <script src="./three.js"></script> -->
<!-- <script src="http://www.yanhuangxueyuan.com/threejs/build/three.js"></script> -->
</head>
<body>
<script>
/**
* 创建场景对象Scene
*/
var scene = new THREE.Scene();
/**
* 创建网格模型
*/
// var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象
var geometry = new THREE.BoxGeometry(100, 100, 100); //创建一个立方体几何对象Geometry
var material = new THREE.MeshLambertMaterial(
color: 0x0000ff
); //材质对象Material
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
/**
* 光源设置
*/
//点光源
var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
//环境光
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);
// console.log(scene)
// console.log(scene.children)
/**
* 相机设置
*/
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
/**
* 创建渲染器对象
*/
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
//执行渲染操作 指定场景、相机作为参数
renderer.render(scene, camera);
</script>
</body>
</html>
体验测试
直接看上面的代码大家可能不太理解,如果是第一次接触会比较陌生,可以尝试更改代码的参数看看有什么效果,代码的功能都有注释, 看着注释也能大概猜出一个参数的含义。通过修改代码,同时刷新浏览器查看效果形成一个互动来提高自己学习的驱动力。
几何体Geometry
//创建一个立方体几何对象Geometry
var geometry = new THREE.BoxGeometry(100, 100, 100);
代码var box=new THREE.BoxGeometry(100,100,100);
通过构造函数THREE.BoxGeometry()
创建了一个长宽高都是100的立方体,通过构造函数名字BoxGeometry也能猜出这个构造函数的意义,利用new关键字操作构造函数可以创建一个对象, 这都是Javascript语言的基本知识,至于THREE.BoxGeometry()
构造函数具体是什么可以不用关心, 就像你使用前端使用JQuery库一样查找官方文档就可以,你可以把代码THREE.BoxGeometry(100,100,100)
中的第一个参数更改为为50,刷新浏览器查看数据更改后长方体的效果图,可以看到已经不是长宽高一样的立方体, 而是普通的长方体。
你也可以用下面一段代码代替上面的长方体代码,你会发现会渲染出来一个球体效果。
//创建一个球体几何对象
var geometry = new THREE.SphereGeometry(60, 40, 40);
材质Material
代码var material=new THREE.MeshLambertMaterial(color:0x0000ff);
通过构造函数THREE.MeshLambertMaterial()创建了一个可以用于立方体的材质对象, 构造函数的参数是一个对象,对象包含了颜色、透明度等属性,本案例中只定义了颜色color
,颜色属性值0x0000ff
表示蓝色,可以把颜色值改为0x00ff00
,可以看到是绿色的立方体效果, 这里使用的颜色值表示方法是16进制RGB三原色模型。使用过渲染类软件、设计过网页或者学习过图形学应该能知道RGB三原色模型,这里就不再详述。
光源Light
代码var point=new THREE.PointLight(0xffffff);
通过构造函数THREE.PointLight()创建了一个点光源对象,参数0xffffff
定义的是光照强度, 你可以尝试把参数更改为为0x444444
,刷新浏览器你会看到立方体的表面颜色变暗,这很好理解,实际生活中灯光强度变低了,周围的景物自然暗淡,three.js引擎对WebGL光照模型算法都进行了封装,不需要你了解计算机图形学, 可以直接使用调用three.js光源相关API直接创建一个光源对象,就像你使用普通的三维建模渲染软件一样,只是这里多了一个Javascript编程语言而已。
相机Camera
代码var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
通过构造函数THREE.OrthographicCamera()创建了一个正射投影相机对象, 什么是“正射投影”,什么是“相机对象”,每个人的基础不一样,或许你不太理解,或许你非常理解,如果不清楚还是那句话,刚一开始不用深究,改个参数测试一下看看视觉效果你就会有一定的感性认识。 比如把该构造函数参数中用到的参数s,也就是代码var s = 200;
中定义的一个系数,可以把200更改为300,你会发现立方体显示效果变小,这很好理解,相机构造函数的的前四个参数定义的是拍照窗口大小, 就像平时拍照一样,取景范围为大,被拍的人相对背景自然变小了。camera.position.set(200, 300, 200);
和camera.lookAt(scene.position);
定义的是相机的位置和拍照方向,可以更改camera.position.set(200,300,200);
参数重新定义的相机位置,把第一个参数也就是x坐标从200更改为250, 你会发现立方的在屏幕上呈现的角度变了,这就像你生活中拍照人是同一个人,但是你拍照的位置角度不同,显示的效果肯定不同。这些具体的参数细节可以不用管, 至少你知道相机可以缩放显示三维场景、对三维场景的不同角度进行取景显示。
整个程序的结构
场景——相机——渲染器
从实际生活中拍照角度或是使用三维渲染软件角度理解本节课的案例代码,立方体网格模型和光照组成了一个虚拟的三维场景,相机对象就像你生活中使用的相机一样可以拍照,只不过一个是拍摄真实的景物,一个是拍摄虚拟的景物,拍摄一个物体的时候相机的位置和角度需要设置,虚拟的相机还需要设置投影方式,当你创建好一个三维场景,相机也设置好,就差一个动作“咔”,通过渲染器就可以执行拍照动作。
对象、方法和属性
从面向对象编程的角度理解上面的程序,使用three.js和使用其它传统前端Javascript库或框架一样,通过框架提供的构造函数可以创建对象,对象拥有方法和属性,只不过three.js是一款3D引擎, 如果你对HTML、Javascript语言、三维建模渲染软件都能够理解应用,即使你不懂计算机图形学和WebGL,也能够学习three.js引擎,创建可以在线预览的三维场景。
案例源码分别使用构造函数THREE.Scene()
、THREE.OrthographicCamera()
、THREE.WebGLRenderer()
创建了场景、相机、渲染器三个最顶层的总对象,然后通过总对象的子对象、方法和属性进行设置, 相机对象和渲染对象相对简单,最复杂的是场景对象,new THREE.Mesh(box,material);
使用构造函数Mesh()
创建了一个网格模型对象,该对象把上面两行含有顶点位置信息的几何体对象和含有颜色信息的材质对象作为参数,网格模型创建好之后, 需要使用场景对象的方法.add()
把三维场景的子对象添加到场景中,new THREE.PointLight(0xffffff);
、new THREE.AmbientLight(0x444444);
定义了两个点光源、环境光对象,然后作为场景的子对象插入场景中。 场景、相机、渲染器设置完成后,设置代码renderer.render(scene,camera)
把场景、相机对象作为渲染器对象方法render()
的参数,这句代码的意义相当于告诉浏览器根据相机的放置方式拍摄已经创建好的三维场景对象。
WebGL封装
如果你有WebGL基础,可以通过下面介绍了解Three.js对WebGL的封装,如果不了解WebGL或计算机图形学,随便阅读一下或者直接跳过。
从WebGL的角度来看,three.js提供的构造函数基本是对原生WebGL的封装,如果你有了WebGL的基础,在学习three.js的很多对象、方法和属性是很容易理解的。在three.js入门教程中不会去过多讲解WebGL的基础知识, 但是为了大家更好的理解three.js的很多命令,与three.js相关的WebGL API知识、GPU渲染管线的知识。图形学可能很多人会觉得比较难,其实主要是算法部分,大家先可以学习一些基本的WebGL知识,初学的时候尽量不关注算法,主要了解顶点数据处理的过程,GPU渲染管线的基本功能单元。实际的工作中如果不是开发3D引擎可能不会使用原生WebGL API,但是学习了这些之后,对于three.js的深度开发学习很有好处,如果你了解你WebGL知识,可以联系绘制函数drawArrays()
来理解渲染器的渲染操作方法render()
。
下一篇:Three.js教程:旋转动画、requestAnimationFrame周期性渲染 (mvrlink.com)
Three.js纹理投影简明教程
纹理投影是一种将纹理映射到 3D 对象并使其看起来像是从单个点投影的方法。 把它想象成投射到云上的蝙蝠侠符号,云是我们的对象,蝙蝠侠符号是我们的纹理。 它用于游戏和视觉效果,以及创意世界的更多部分。
工具:使用 NSDT场景编辑器 快速搭建 数字孪生3D场景。
1、纹理投影MVP
首先,让我们设置场景。 每个Three.js项目的场景搭建代码都是一样的,这里就不赘述了。 如果你以前没有做过,你可以熟悉下官方指南。 我个人使用 threejs-modern-app 中的一些实用程序,所以我不需要担心样板代码。
首先我们需要一个相机来投影纹理。
const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 3)
camera.position.set(-1, 1.2, 1.5)
camera.lookAt(0, 0, 0)
然后,我们需要在其上投影纹理的对象。 为了进行投影映射,我们将编写一些自定义着色器代码,因此让我们创建一个新的 ShaderMaterial:
// create the mesh with the projected material
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.ShaderMaterial(
uniforms:
texture: value: assets.get(textureKey) ,
,
vertexShader: '',
fragShader: '',
)
const box = new THREE.Mesh(geometry, material)
但是,由于我们可能需要多次使用我们投影的材质,我们可以将它单独放在一个组件中,然后像这样使用它:
class ProjectedMaterial extends THREE.ShaderMaterial
constructor( camera, texture )
// ...
const material = new ProjectedMaterial(
camera,
texture: assets.get(textureKey),
)
现在让我们写着色器!
在着色器代码中,我们基本上会对纹理进行采样,就好像它是从相机投射出来的一样。 不幸的是,这涉及到一些矩阵乘法。 但不要害怕! 我将以一种简单易懂的方式对其进行解释。 如果你想更深入地研究这个主题,可以查看这篇关于矩阵运算的非常好的文章。
在顶点着色器中,我们必须将每个顶点视为从投影相机中查看,所以我们只使用投影相机的 projectionMatrix 和 viewMatrix 而不是场景相机中的那些。 我们使用可变变量将这个转换后的位置传递给片段着色器。
vTexCoords = projectionMatrixCamera * viewMatrixCamera * modelMatrix * vec4(position, 1.0);
在片元着色器中,我们必须将位置从世界空间转换到剪切空间。 我们通过将向量除以它的 .w 分量来做到这一点。 GLSL 内置函数 texture2DProj(或更新的 textureProj)也在内部执行此操作。
在同一行中,我们还将剪切空间范围 [-1, 1] 转换为 uv 查找范围 [0, 1]。 我们使用这个变量稍后从纹理中采样。
vec2 uv = (vTexCoords.xy / vTexCoords.w) * 0.5 + 0.5;
结果如下:
请注意,我们编写了一些代码以仅将纹理投影到立方体面向相机的面上。 默认情况下,每个面都会得到纹理投影,因此我们通过查看法线和相机方向的点积来检查脸是否真的面向相机。 这种技术在照明中确实很常见,如果你想阅读更多有关此主题的信息,请阅读这篇文章。
// this makes sure we don't render the texture also on the back of the object
vec3 projectorDirection = normalize(projPosition - vWorldPosition.xyz);
float dotProduct = dot(vNormal, projectorDirection);
if (dotProduct < 0.0)
outColor = vec4(color, 1.0);
第一部分,我们现在想让它看起来像贴在物体上的纹理。
我们只需在开始时保存对象的位置,然后使用它而不是更新后的对象位置来计算投影,这样即使对象之后移动,投影也不会改变。
我们可以将对象初始模型矩阵存储在统一的 savedModelMatrix 中,因此我们的计算变为:
vTexCoords = projectionMatrixCamera * viewMatrixCamera * savedModelMatrix * vec4(position, 1.0);
我们可以公开一个 project() 函数,它将 savedModelMatrix 设置为对象的当前 modelMatrix。
export function project(mesh)
// make sure the matrix is updated
mesh.updateMatrixWorld()
// we save the object model matrix so it's projected relative
// to that position, like a snapshot
mesh.material.uniforms.savedModelMatrix.value.copy(mesh.matrixWorld)
这是我们的最终结果:
就是这样! 现在立方体看起来像是贴上了纹理! 这可以扩展到任何类型的 3D 模型,所以让我们举一个更有趣的例子。
2、纹理投影的有趣案例
对于前面的示例,我们创建了一个用于投影的新相机,但是如果我们使用渲染场景的相同相机进行投影呢? 这样我们就可以准确地看到 2D 图像! 这是因为投影点与视点重合。
另外,让我们尝试投影到多个对象上:
看起来很有趣! 然而,正如你从示例中看到的那样,图像看起来有点扭曲,这是因为纹理被拉伸以填充相机平截头体。 但是如果我们想保留图像的原始比例和尺寸怎么办?
此外,我们根本没有考虑照明。 片段着色器中需要一些代码来说明我们在场景中放置的灯光如何照亮表面。
此外,如果我们想投影到更多的对象上怎么办? 性能会迅速下降。 这就是 GPU 实例化提供帮助的地方! 实例化将繁重的工作转移到 GPU 上,Three.js 最近为其实现了一个易于使用的 API。 唯一的要求是所有实例化对象必须具有相同的几何体和材质。 幸运的是,这就是我们的情况! 所有对象都具有相同的几何形状和材质,唯一的区别是 savedModelMatrix,因为每个对象在投影时都有不同的位置。 但是我们可以将它作为统一传递给每个实例,就像在这个 Three.js 示例中一样。
事情开始变得复杂,但别担心! 我已经对这些东西进行了编码并将其放入three-projected-material库中,因此使用起来更容易,而且你不必每次都重写相同的东西! 如果你对我如何克服剩下的挑战感兴趣,可以去看看。
从现在开始,我们将使用该库。
3、纹理投影进阶
现在我们可以投影到许多对象上并为其设置动画,让我们尝试从中制作一些真正有用的东西。
例如,让我们尝试将其集成到幻灯片中,将图像投影到大量 3D 对象上,然后以有趣的方式对对象进行动画处理。
对于第一个例子,灵感来自 Refik Anadol。 他做了一些非常棒的事情。 但是,我们不能像他那样对速度和力进行全面的模拟,我们需要控制物体的运动; 我们需要它在正确的时间到达正确的地方。
我们通过将对象放置在一些轨迹上来实现这一点:我们定义对象必须遵循的路径,然后在该路径上为对象设置动画。 这是一个 Stack Overflow 答案,解释了实现的原理。
为了进行投影,我们
- 将元素移动到中间点
- 执行调用 project() 的纹理投影
- 将元素放回起点
这是同步发生的,所以用户不会看到任何东西。
现在我们可以自由地以任何我们想要的方式对这些路径进行建模!
但首先,我们必须确保在中间点,元素将正确覆盖图像区域。 为此,我使用了泊松盘采样算法,该算法将点更均匀地分布在表面上,而不是随机定位它们。
this.points = poissonSampling([this.width, this.height], 7.73, 9.66) // innerradius and outerradius
// here is what this.points looks like,
// the z component is 0 for every one of them
// [
// [
// 2.4135735314978937, --> x
// 0.18438944023363374 --> y
// ],
// [
// 2.4783704056100464,
// 0.24572635574719284
// ],
// ...
下面我们来看看第一个demo中路径是如何生成的。 在此演示中,大量使用了 perlin 噪声(或者更确切地说是它的开源对应物,open simple noise)。 还要注意 mapRange() 函数(处理中的 map()),它基本上将一个数字从一个区间映射到另一个区间。 另一个执行此操作的库是 d3-scale 及其 d3.scaleLinear()。 还使用了一些缓动函数。
const segments = 51 // must be odds so we have the middle frame
const halfIndex = (segments - 1) / 2
for (let i = 0; i < segments; i++)
const offsetX = mapRange(i, 0, segments - 1, startX, endX)
const noiseAmount = mapRangeTriple(i, 0, halfIndex, segments - 1, 1, 0, 1)
const frequency = 0.25
const noiseAmplitude = 0.6
const noiseY = noise(offsetX * frequency) * noiseAmplitude * eases.quartOut(noiseAmount)
const scaleY = mapRange(eases.quartIn(1 - noiseAmount), 0, 1, 0.2, 1)
const offsetZ = mapRangeTriple(i, 0, halfIndex, segments - 1, startZ, 0, endZ)
// offsetX goes from left to right
// scaleY shrinks the y before and after the center
// noiseY is some perlin noise on the y axis
// offsetZ makes them enter from behind a little bit
points.push(new THREE.Vector3(x + offsetX, y * scaleY + noiseY, z + offsetZ))
我们可以处理的另一件事是每个元素到达的延迟。 我们在这里也使用了 Perlin 噪音,这使得它们看起来像是“成群结队”地到达。
const frequency = 0.5
const delay = (noise(x * frequency, y * frequency) * 0.5 + 0.5) * delayFactor
我们还在波浪效果中使用了柏林噪声,它修改了曲线的每个点,使其具有“旗帜波浪”效果。
const frequency, speed, amplitude = this.webgl.controls.turbulence
const z = noise(x * frequency - time * speed, y * frequency) * amplitude
point.z = targetPoint.z + z
对于鼠标交互,我们检查路径的点是否比某个半径更近,如果是,我们计算从鼠标点到路径点的向量。 然后我们沿着该向量的方向稍微移动路径点。 为此,我们使用 lerp() 函数,它以特定百分比返回指定范围内的插值。 例如 0.2 表示 20%。
// displace the curve points
if (point.distanceTo(this.mousePoint) < displacement)
const direction = point.clone().sub(this.mousePoint)
const displacementAmount = displacement - direction.length()
direction.setLength(displacementAmount)
direction.add(point)
point.lerp(direction, 0.2) // magic number
// and move them back to their original position
if (point.distanceTo(targetPoint) > 0.01)
point.lerp(targetPoint, 0.27) // magic number
剩下的代码处理幻灯片样式的动画,有兴趣的可以去看看源码!
在另外两个演示中,我使用了一些不同的函数来塑造元素移动的路径,但总体而言,代码非常相似。
4、纹理投影结束语
我希望这篇文章简单易懂,足以让你深入了解纹理投影技术。 可以查看 GitHub 上的代码并下载它! 我确保以易于理解的方式编写代码并提供大量注释。
以上是关于Three.js教程:第一个3D场景的主要内容,如果未能解决你的问题,请参考以下文章