
3D形式的各种产品演示在我们这个时代并不罕见,但是这些任务引起了初学者的很多疑问。 今天,我们将研究一些基础知识,这些基础知识将帮助您进入本主题,而不会偶然发现像在浏览器中显示三维模型这样的简单任务。 作为帮助,我们将使用Three.js作为该领域最受欢迎的工具。
开始工作
首先,让我们为自己创建一个HTML模板。 为了不使示例复杂化,我们将不使用任何多余的东西,不使用汇编器,预处理器等。
我们需要一个画布容器和一组脚本-实际上是three.js,是obj格式模型的加载器,以及一个用鼠标控制相机的脚本。
<div class='canvas-container'></div> <script src='https://unpkg.com/three@0.99.0/build/three.min.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/loaders/OBJLoader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/controls/OrbitControls.js'></script> <script src='./main.js'></script>
如果您的项目使用NPM和构建器,则可以从软件包3中导入所有这些。 实际上,如果有人不知道,Unpkg将获取所有NPM软件包。
如果您需要将某些软件包中的某些内容快速连接到您的页面,但是没有找到CDN的链接-请记住有关Unpkg的信息,很可能您需要它。
主脚本将从一堆全局变量开始。 这将简化示例。
let SCENE; let CAMERA; let RENDERER; let LOADING_MANAGER; let IMAGE_LOADER; let OBJ_LOADER; let CONTROLS; let MOUSE; let RAYCASTER; let TEXTURE; let OBJECT;
在Three.js中,所有操作都从场景开始,因此初始化它并创建几个光源:
function initScene() { SCENE = new THREE.Scene(); initLights(); } function initLights() { const ambient = new THREE.AmbientLight(0xffffff, 0.7); SCENE.add(ambient); const directionalLight = new THREE.DirectionalLight(0xffffff); directionalLight.position.set(0, 1, 1); SCENE.add(directionalLight); }
光源不同。 在此类任务中,最经常使用的是环境光-填充光,定向光-沿特定方向的光。 仍然有点光源,但是我们还不需要它们。 我们将发光颜色设置为白色,以便没有扭曲。
使用填充发光的颜色(尤其是使用灰色阴影)时,可能会很有用,因此可以使图像更柔和。
第二重要的是相机。 这种实体决定了我们所处的位置以及我们所看的方向。
function initCamera() { CAMERA = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); CAMERA.position.z = 100; }
摄像机参数通常由肉眼选择,并取决于所使用的型号。
我们需要的第三个对象是渲染器。 他负责渲染图像。 它的初始化说明了一切:
function initRenderer() { RENDERER = new THREE.WebGLRenderer({ alpha: true }); RENDERER.setPixelRatio(window.devicePixelRatio); RENDERER.setSize(window.innerWidth, window.innerHeight); }
需要加载器才能加载不同格式的数据。 在这里,您可以找到一长串的选项,但是我们只需要两个选项-一个用于图片(套件随附),一个用于模型(我们在开始时就已连接好)。
function initLoaders() { LOADING_MANAGER = new THREE.LoadingManager(); IMAGE_LOADER = new THREE.ImageLoader(LOADING_MANAGER); OBJ_LOADER = new THREE.OBJLoader(LOADING_MANAGER); }
我们继续加载模型。 如预期的那样,它异步发生。 加载模型后,我们可以使用其参数:
function loadModel() { OBJ_LOADER.load('./model.obj', (object) => { object.scale.x = 0.3; object.scale.y = 0.3; object.scale.z = 0.3; object.rotation.x = -Math.PI / 2; object.position.y = -30; OBJECT = object; SCENE.add(OBJECT); }); }
仍然需要启动桶风琴:
function animate() { requestAnimationFrame(animate); render(); } function render() { CAMERA.lookAt(SCENE.position); RENDERER.render(SCENE, CAMERA); }
结果,我们得到的只是一棵带有阴影的白色圣诞树(我从这里得到了模型)。

一,二,三,圣诞树烧! 但是,如果没有纹理,则不会燃烧。 我们将在其他时间谈论火的着色器和其他元素……但是至少我们可以看到,圣诞树的模型是“在电视上”。
在继续使用纹理之前,添加标准的浏览器窗口大小调整事件处理程序很有用:
function initEventListeners() { window.addEventListener('resize', onWindowResize); onWindowResize(); } function onWindowResize() { CAMERA.aspect = window.innerWidth / window.innerHeight; CAMERA.updateProjectionMatrix(); RENDERER.setSize(window.innerWidth, window.innerHeight); }
添加纹理
我们的模型和图片纹理基于儿童设备模型上的贴纸翻译器的原理进行工作。 众所周知,WebGL上下文中的对象由一堆三角形组成。 它们本身没有颜色。 对于每个三角形,都有一个与您需要粘贴的纹理相同的三角形“贴纸”。 但是,如果我们有1000个三角形,那么我们需要加载1000个纹理图片吗? 当然不是 制作了精灵,与CSS图标相同(您可能在工作中遇到过它们),并且将有关哪些三角形及其上的位置的信息添加到了模型本身。 然后Three.js已经独立理解了一切,我们看到了最终结果。 实际上,一切都有些复杂,但是应该以这种方式理解这个想法。
圣诞树树枝不是一个很好的例子。 他们都是一样的。 这种纹理的结构在鳞茎类动物的例子中会更好看:

但是足够多的话,让我们开始行动。 初始化纹理并使用它加载图片:
function initTexture() { TEXTURE = new THREE.Texture(); } function loadTexture() { IMAGE_LOADER.load('./texture.jpg', (image) => { TEXTURE.image = image; TEXTURE.needsUpdate = true; }); }
现在我们需要扩展模型加载功能。 如果我们的纹理与球茎相同,那么一切都会很简单。 但是对于一棵圣诞树,纹理仅覆盖树枝。 有必要以某种方式将它们分开并仅将其应用于它们。 怎么做? 您可以采用不同的方法来解决此问题。 现在该使用console.log并查看模型本身了。
如果您不知道如何突出显示模型的特定部分,请使用console.log。 这通常是找出零件之间差异的最快方法。
通常,对于如何将模型分为多个部分,我们有两个选择。 第一个(好)是3D艺术家在模型的组成部分上签名时,我们可以访问它们的名称字段,并可以从中确定什么是什么。 在我们的示例中,不是,但是有材料名称。 我们将使用它们。 对于材质“ Christmas_Tree”中的模型部分,我们将使用纹理:
function loadModel() { OBJ_LOADER.load('./model.obj', (object) => { object.traverse(function(child) { if (child instanceof THREE.Mesh) { switch (child.material.name) { case 'Christmas_Tree': child.material.map = TEXTURE; break;
所以我们得到这样的东西:

对于由“红色”和“粉红色”材料制成的零件(这些是球-圣诞球),我们只需设置随机颜色即可。 在这种情况下,使用HSL很方便:
switch (child.material.name) { case 'Christmas_Tree': child.material.map = TEXTURE; break; case 'red': child.material.color.setHSL(Math.random(), 1, 0.5); break; case 'pink': child.material.color.setHSL(Math.random(), 1, 0.5); break; }
给艺术家的注释:给模型中的所有内容起有意义的名称。 在我们的示例中,这些材料的名称简直让人难以理解。 这里红色可以是绿色。 我没有更改它们以显示正在发生的事情的全部荒谬之处。 抽象名称“球的材料”将更为通用。
等角投影
翻译成俄语的复合词等角投影是等到中间投影。 转换为家用-将球拉到矩形上。 你可以引用我。 我们所有人都在学校看到了一张世界地图-它是矩形的,但是我们知道如果稍加改动,就会得到一个地球仪。 就是这样 为了更好地理解这些失真的排列方式,请看一下图片:

创建不同产品的缩略图时,通常使用此类投影来制作背景。 我们在环境下拍摄了失真的照片,并将其显示在一个大的球体上。 相机似乎在里面。 看起来像这样:
function initWorld() { const sphere = new THREE.SphereGeometry(500, 64, 64); sphere.scale(-1, 1, 1); const texture = new THREE.Texture(); const material = new THREE.MeshBasicMaterial({ map: texture }); IMAGE_LOADER.load('./world.jpg', (image) => { texture.image = image; texture.needsUpdate = true; }); SCENE.add(new THREE.Mesh(sphere, material)); }
作为示例,我故意破坏了边缘,因此,如果您使用github中的示例,则可以在其中找到图片闭合的独特接缝。 如果有人感兴趣, 则从这里获取其原始内容。
目前共有Total这样的东西:

与色的球的一棵圣诞树看起来相当逗人喜爱。
轨道控制
为了欣赏三维空间的美丽,请添加鼠标控件。 然后一切似乎都在3D中,您需要将其扭曲。 通常,在此类任务中使用OrbitControls。
function initControls() { CONTROLS = new THREE.OrbitControls(CAMERA); CONTROLS.minPolarAngle = Math.PI * 1 / 4; CONTROLS.maxPolarAngle = Math.PI * 3 / 4; CONTROLS.update(); }
可以设置旋转相机角度的限制,缩放限制和其他选项。 查看文档非常有用,有很多有趣的事情。
关于此控制选项,您知之甚少。 连接,开启并正常工作。 只是不要忘记定期更新状态:
function animate() { requestAnimationFrame(animate); CONTROLS.update(); render(); }
在这里,您可能会分神,将圣诞树沿不同方向扭曲...
雷卡斯特
Raycaster允许您执行以下操作:它在空间中绘制一条直线,并找到与之相交的所有对象。 这使您可以做许多不同的有趣的事情,但是在产品演示的上下文中,将有两种主要情况-这是响应鼠标悬停在某物上以及响应鼠标单击某事。 为此,您将需要使用鼠标的坐标通过一个点垂直于屏幕绘制线,并寻找相交点。 这就是我们要做的。 扩展渲染功能,查找与球的交点并重新绘制它们:
function render() { RAYCASTER.setFromCamera(MOUSE, CAMERA); paintHoveredBalls();
通过简单地前后移动鼠标,我们确保一切正常。

但是有一个微妙之处-Three.js不知道如何平滑地更改颜色。 通常,该库与值的平滑变化无关。 现在是时候连接为此目的设计的一些工具了,例如Anime.js。
<script src='https://unpkg.com/animejs@2.2.0/anime.min.js'></script>
我们使用此库为值设置动画:
switch (intersects[i].object.material.name) { case 'red':
现在,颜色平滑变化,但是只有在鼠标移离球之后才可以。 需要修复某些问题。 为此,我们将使用符号-它们使我们能够安全地向对象添加元信息,而我们只需要添加有关球是否动画的信息。
ES6 +中的符号是一个非常强大的工具,除其他功能外,它还使您可以从第三方库向对象添加信息,而不必担心这会导致名称冲突或逻辑中断。
我们创建一个全局常量(理论上,为所有此类符号创建一个全局对象是值得的,但是我们有一个简单的示例,我们不会使其复杂化):
const _IS_ANIMATED = Symbol('is animated');
我们在球的重涂功能上添加了一个检查:
if (!intersects[i].object[_IS_ANIMATED]) { anime({ targets: intersects[i].object.material.color, r: 0, g: 0, b: 0, easing: 'easeInOutQuad' }); intersects[i].object[_IS_ANIMATED] = true; }
现在,他们在悬停时立即平滑地重新粉刷。 因此,借助符号,您可以在动画中快速添加类似的检查,而无需将所有球的状态保存在单独的位置。
工具提示
我们今天要做的最后一件事是工具提示。 经常遇到此任务。 对于初学者,我们只需要弥补它们。
<div class='popup-3d'> !</div>
.popup-3d { color: #fff; font-family: 'Pacifico', cursive; font-size: 10rem; pointer-events: none; }
请记住,如果不需要指针事件,请禁用它们。
仍然要添加CSS3DRenderer。 实际上这并不是一个渲染器,它只是将CSS转换添加到元素中,似乎它们处于同一场景中。 对于弹出标签-这正是您所需要的。 我们创建全局变量CSSRENDERER,对其进行初始化,并且不要忘记调用render函数本身。 一切看起来像一个常规渲染器:
function initCSSRenderer() { CSSRENDERER = new THREE.CSS3DRenderer(); CSSRENDERER.setSize(window.innerWidth, window.innerHeight); CSSRENDERER.domElement.style.position = 'absolute'; CSSRENDERER.domElement.style.top = 0; } function render() { CAMERA.lookAt(SCENE.position); RENDERER.render(SCENE, CAMERA); CSSRENDERER.render(SCENE, CAMERA); }
目前没有任何反应。 实际上,我们什么也没做。 我们初始化弹出元素,我们可以立即播放其大小和在空间中的位置:
function initPopups() { const popupSource = document.querySelector('.popup-3d'); const popup = new THREE.CSS3DObject(popupSource); popup.position.x = 0; popup.position.y = -10; popup.position.z = 30; popup.scale.x = 0.05; popup.scale.y = 0.05; popup.scale.z = 0.05; console.log(popup); SCENE.add(popup); }
现在我们看到“ 3D”字样。 实际上,它不完全是3D的,它位于画布的顶部,但是对于弹出提示,它并不那么重要,效果很重要
最后的触摸保留-在一定角度范围内平滑显示题字。 再次使用全局符号:
const _IS_VISIBLE = Symbol('is visible');
然后,我们根据摄像头的旋转角度来更新弹出元素的状态:
function updatePopups() { const popupSource = document.querySelector('.popup-3d'); const angle = CONTROLS.getAzimuthalAngle(); if (Math.abs(angle) > .9 && popupSource[_IS_VISIBLE]) { anime({ targets: popupSource, opacity: 0, easing: 'easeInOutQuad' }); popupSource[_IS_VISIBLE] = false; } else if (Math.abs(angle) < .9 && !popupSource[_IS_VISIBLE]) { anime({ targets: popupSource, opacity: 1, easing: 'easeInOutQuad' }); popupSource[_IS_VISIBLE] = true; } }
一切都很简单。 现在,铭文顺利出现并消失。 您可以添加自动旋转并享受效果。
CONTROLS.autoRotate = true; CONTROLS.autoRotateSpeed = -1.0;

结论
今天,我们研究了如何在页面上显示三维模型,如何用鼠标打开三维模型,如何制作工具提示,如何对鼠标悬停在模型的某些部分做出响应以及如何在各种动画环境中使用角色。 希望此信息对您有所帮助。 好了,所有即将到来的事情,现在您都知道在假期中可以学到什么。
PS:人字形示例的完整资源可在github上找到 。