In this tutorial you’ll learn how to create a customizer app that lets you change the colors of a 3D model of a chair using Three.js.
在本教程中,您将学习如何创建定制器应用程序,该应用程序可让您使用Three.js更改椅子的3D模型的颜色。
See the demo in action: 3D Model Color Customizer App with Three.js
观看实际的演示:具有Three.js的3D模型颜色定制程序
This tool is built inspired by the Vans shoe customizer, and uses the amazing JavaScript 3D library Three.js.
该工具的灵感来自Vans鞋子定制器,并使用了令人惊叹JavaScript 3D库Three.js 。
For this tutorial, I’ll assume you are comfortable with JavaScript, HTML and CSS.
对于本教程,我假设您对JavaScript,HTML和CSS感到满意。
I’m going to do something a little bit different here in the interest of actually teaching you, and not making you copy/paste parts that aren’t all that relevant to this tutorial, we’re going to start with all of the CSS in place. The CSS really is just for the dressing around the app, it focusses on the UI only. That being said, each time we paste some HTML, I’ll explain quickly what the CSS does. Let’s get started.
为了实际教您,我将在这里做一些不同的事情,而不是让您复制/粘贴与本教程无关的部分,我们将从所有CSS开始到位。 CSS确实仅是围绕应用程序的外观,它仅关注UI。 话虽如此,每次我们粘贴一些HTML时,我都会快速解释CSS的作用。 让我们开始吧。
If you want to skip this part entirely, feel free to do so, but it may pay to read it just so you have a deeper understanding of how everything works.
如果您想完全跳过这一部分,请随时这样做,但是阅读它可能有好处,因为这样您就可以更深入地了解一切。
This isn’t a 3D modelling tutorial, but I will explain how the model is set up in Blender, and if you’d like to create something of your own, change a free model you found somewhere online, or instruct someone you’re commissioning. Here’s some information about how our chairs 3D model is authored.
这不是3D建模教程,但是我将解释如何在Blender中设置模型,如果您要创建自己的东西,请更改在网上某个地方找到的免费模型,或指示某人调试。 这是有关我们的椅子3D模型是如何创作的一些信息。
The 3D model for this tutorial is hosted and included within the JavaScript, so don’t worry about downloading or having to do any of this unless you’d like to look further into using Blender, and learning how to create your own model.
本教程的3D模型已托管并包含在JavaScript中,因此,除非您想进一步使用Blender并学习如何创建自己的模型,否则不必担心下载或执行任何此类操作。
The scale is set to approximately what it would be in the real world; I don’t know if this is important, but it feels like the right thing to do, so why not?
该比例设置为大约与现实世界中的比例相同。 我不知道这是否重要,但这听起来像是对的事,那为什么不呢?
This part is important: each element of the object you want to customize independently needs to be its own object in the 3D scene, and each item needs to have a unique name. Here we have back, base, cushions, legs and supports. Note that if you have say, three items all called supports, Blender is going to name them as supports, supports.001, supports.002. That doesn’t matter, because in our JavaScript we’ll be using includes(“supports”) to find all of those objects that contain the string supports in it.
这部分很重要:要独立定制的对象的每个元素在3D场景中都必须是其自己的对象,并且每个项目都必须具有唯一的名称。 在这里,我们有靠背,底座,靠垫,腿和支撑。 请注意,如果您说了三个项目,都称为supports ,Blender会将其命名为supports , supports.001,supports.002 。 没关系,因为在我们JavaScript中,我们将使用include(“ supports”)查找其中包含字符串支持的所有那些对象。
The model should be placed at the world origin, ideally with its feet on the floor. It should ideally be facing the right way, but this can easily be rotated via JavaScript, no harm, no foul.
该模型应放置在世界原点,最好将其脚放在地板上。 理想情况下,它应该面向正确的方向,但是可以很容易地通过JavaScript进行旋转,没有危害,没有犯规。
Before exporting, you want to use Blender’s Smart UV unwrap option. Without going too much into detail, this makes textures keep its aspect ratio in tact as it wraps around the different shapes in your model without stretching in weird ways (I’d advise reading up on this option only if you’re making your own model).
导出之前,您要使用Blender的Smart UV unwrap选项。 无需过多介绍细节,这可以使纹理保持纵横比不变,因为它可以包裹模型中的不同形状而不会怪异地伸展(建议您仅在制作自己的模型时阅读此选项)。
You want to be sure to select all of your objects, and apply your transformations. For instance, if you changed the scale or transformed it in any way, you’re telling Blender that this is the new 100% scale, instead of it still being 32.445% scale if you scaled it down a bit.
您要确保选择所有对象,然后应用转换。 例如,如果您更改了缩放比例或以任何方式对其进行了转换,就告诉Blender这是新的100%缩放比例,而不是将其缩小一点仍为32.445%缩放比例。
Apparently Three.js supports a bunch of 3D object file formats, but the one it recommends is glTF (.glb). Blender supports this format as an export option, so no worries there.
显然Three.js支持多种3D对象文件格式,但它推荐的格式是glTF(.glb)。 Blender支持将此格式作为导出选项,因此不用担心。
Go ahead and fork this pen, or start your own one and copy the CSS from this pen. This is a blank pen with just the CSS we’re going to be using in this tutorial.
快来拿起这支笔,或者开始自己的一支并从这支笔复制CSS。 这只是一支空白笔,仅包含我们将在本教程中使用CSS。
See the Pen 3D Chair Customizer Tutorial – Blank by Kyle Wetton (@kylewetton) on CodePen.
请参阅CodePen上的Pen 3D Chair Customizer教程–由Kyle Wetton( @kylewetton )撰写的空白。
If you don’t choose to fork this, grab the HTML as well; it has the responsive meta tags and Google fonts included.
如果您不选择分叉,也请获取HTML; 它具有响应性元标记和Google字体。
We’re going to use three dependencies for this tutorial. I’ve included comments above each that describe what they do. Copy these into your HTML, right at the bottom:
在本教程中,我们将使用三个依赖项。 我在每个评论的上方都包含了描述其功能的评论。 将这些内容复制到HTML中的底部:
Let’s include the canvas element. The entire 3D experience gets rendered into this element, all other HTML will be UI around this. Place the canvas at the bottom of your HTML, above your dependencies.
让我们包括canvas元素。 整个3D体验都将渲染到此元素中,所有其他HTML都将是与此相关的UI。 将画布放在您的依赖项上方HTML底部。
Now, we’re going to create a new Scene for Three.js. In your JavaScript, lets make a reference to this scene like so:
现在,我们将为Three.js创建一个新的场景。 在您JavaScript中,让我们像这样对这个场景进行引用:
// Init the scene
const scene = new THREE.Scene();
Below this, we’re going to reference our canvas element
在此之下,我们将引用我们的canvas元素
const canvas = document.querySelector('#c');
Three.js requires a few things to run, and we will get to all of them. The first was scene, the second is a renderer. Let’s add this below our canvas reference. This creates a new WebGLRenderer, we’re passing our canvas to it, and we’ve opted in for antialiasing, this creates smoother edges around our 3D model.
Three.js需要一些操作才能运行,我们将全部完成。 第一个是场景,第二个是渲染器。 让我们将其添加到我们的画布参考下面。 这将创建一个新的WebGLRenderer,我们将画布传递给它,并且我们选择了抗锯齿,这将在3D模型周围创建更平滑的边缘。
// Init the renderer
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
And now we’re going to append the renderer to the document body
现在,我们将渲染器附加到文档主体
document.body.appendChild(renderer.domElement);
The CSS for the canvas element is just stretching it to 100% height and width of the body, so your entire page has now turned black, because the entire canvas is now black!
canvas元素CSS只是将其拉伸到主体的高度和宽度的100%,因此您的整个页面现在都变成了黑色,因为整个画布现在都变成了黑色!
Our scene is black, we’re on the right track here.
我们的场景是黑色的,我们在正确的轨道上。
The next thing Three.js needs is an update loop, basically this is a function that runs on each frame draw and is really important to the way our app will work. We’ve called our update function animate(). Let’s add it below everything else in our JavaScript.
Three.js接下来需要做的是一个更新循环,基本上这是一个在每次绘制框架时都运行的函数,对于我们的应用程序的工作方式确实非常重要。 我们已将更新函数称为animate() 。 让我们将其添加到JavaScript的其他所有内容之下。
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
Note that we’re referencing a camera here, but we haven’t set one up yet. Let’s add one now.
请注意,我们此处引用的是摄像机,但尚未设置。 现在添加一个。
At the top of your JavaScript, we’ll add a variable called cameraFar. When we add our camera to our scene, it’s going to be added at position 0,0,0. Which is where our chair is sitting! so cameraFar is the variable that tells our camera how far off this mark to move, so that we can see our chair.
在您JavaScript的顶部,我们将添加一个名为cameraFar的变量。 当我们将摄像机添加到场景中时,它将被添加到位置0,0,0。 我们的椅子坐在哪儿! 所以cameraFar是一个变量,它告诉我们的相机该标记移动了多远,以便我们可以看到椅子。
var cameraFar = 5;
Now, above our function animate() {….} lets add a camera.
现在,在我们的函数animate(){…。}上方,添加一个摄像机。
// Add a camera
var camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = cameraFar;
camera.position.x = 0;
This is a perspective camera, with the field of view of 50, the size of the whole window/canvas, and some default clipping planes. The planes determine how near or far the camera should be before the object isn’t rendered. It’s not something we need to pay attention to in our app.
这是一台透视相机,视野为50,整个窗口/画布的大小以及一些默认的裁剪平面。 这些平面确定了在不渲染对象之前相机应该接近或远近。 这不是我们在应用程序中需要注意的事情。
Our scene is still black, let’s set a background color.
我们的场景仍然是黑色的,让我们设置背景色。
At the top, above our scene reference, add a background color variable called BACKGROUND_COLOR.
在场景引用上方的顶部,添加一个名为Background_COLOR的背景颜色变量。
const BACKGROUND_COLOR = 0xf1f1f1;
Notice how we used 0x instead of # in our hex? These are hexadecimal numbers, and the only thing you need to remember about that is that its not a string the way you’d handle a standard #hex variable in JavaScript. It’s an integer and it starts with 0x.
注意我们在十六进制中如何使用0x而不是#? 这些是十六进制数字,关于此,您唯一需要记住的是它不是字符串,而不是您在JavaScript中处理标准#hex变量的方式。 这是一个整数,以0x开头。
Below our scene reference, let’s update the scenes background color, and add some fog of the same color off in the distance, this is going to help hide the edges of the floor once we add that in.
在场景参考下方,让我们更新场景的背景色,并在远处添加一些相同颜色的雾,这将有助于在添加后隐藏地板的边缘。
const BACKGROUND_COLOR = 0xf1f1f1;
// Init the scene
const scene = new THREE.Scene();
// Set background
scene.background = new THREE.Color(BACKGROUND_COLOR );
scene.fog = new THREE.Fog(BACKGROUND_COLOR, 20, 100);
Now it’s an empty world. It’s hard to tell that though, because there’s nothing in there, nothing casting shadows. We have a blank scene. Now it’s time to load in our model.
现在是一个空的世界。 但是,很难说出来,因为那里什么也没有,也没有投下阴影。 我们有一个空白的场景。 现在是时候加载我们的模型了。
We’re going to add the function that loads in models, this is provided by our second dependency we added in our HTML.
我们将添加在模型中加载的函数,这是由我们在HTML中添加的第二个依赖项提供的。
Before we do that though, let’s reference the model, we’ll be using this variable quite a bit. Add this at the top of your JavaScript, above your BACKGROUND_COLOR. Let’s also add a path to the model. I’ve hosted it for us, it’s about 1Mb in size.
在进行此操作之前,让我们先参考模型,我们将大量使用此变量。 将此添加到您JavaScript顶部,BACKGROUND_COLOR上方。 我们还为模型添加路径。 我已经为我们托管了它,它的大小约为1Mb。
var theModel;
const MODEL_PATH = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/chair.glb";
Now we can create a new loader, and use the load method. This sets theModel
as our 3D models entire scene. We’re also going to set the size for this app, the right size seems to be about twice as big as it’s loaded. Thirdly, we’re going to offset the y position by -1 to bring it down a little bit, and finally we’re going to add the model to the scene.
现在,我们可以创建一个新的loader ,并使用load方法。 这theModel
设置为我们整个场景的3D模型。 我们还将设置该应用程序的大小,正确的大小似乎是已加载大小的两倍。 第三,我们将y位置偏移-1以将其降低一点,最后我们将模型添加到场景中。
The first parameter is the model’s filepath, the second is a function that runs once the resource is loaded, the third is undefined for now but can be used for a second function that runs while the resource is loading, and the final parameter handles errors.
第一个参数是模型的文件路径,第二个参数是在加载资源后运行的函数,第三个参数目前尚未定义,但可用于第二个在资源加载时运行的函数,最后一个参数用于处理错误。
Add this below our camera.
将此添加到我们的相机下方。
// Init the object loader
var loader = new THREE.GLTFLoader();
loader.load(MODEL_PATH, function(gltf) {
theModel = gltf.scene;
// Set the models initial scale
theModel.scale.set(2,2,2);
// Offset the y position a bit
theModel.position.y = -1;
// Add the model to the scene
scene.add(theModel);
}, undefined, function(error) {
console.error(error)
});
At this point you should be seeing a stretched, black, pixelated chair. As awful as it looks, this is right so far. So don’t worry!
此时,您应该看到一张拉伸的黑色像素化椅子。 看起来很糟糕,到目前为止,这是正确的。 所以不用担心!
Along with a camera, we need lights. The background isn’t affected by lights, but if we added a floor right now, it would also be black (dark). There are a number of lights available for Three.js, and a number of options to tweak all of them. We’re going to add two: a hemisphere light, and a directional light. The settings are also sorted for our app, and they include position and intensity. This is something to play around with if you ever adopt these methods in your own app, but for now, lets use the ones I’ve included. Add these lights below your loader.
除了相机,我们还需要灯光。 背景不受灯光的影响,但是如果我们现在添加一个地板,它也将是黑色(深色)。 Three.js有许多可用的灯,还有许多选项可以对其进行调整。 我们将添加两个:半球灯和定向灯。 设置也针对我们的应用进行了排序,其中包括位置和强度。 如果您曾经在自己的应用程序中采用这些方法,则可以尝试使用这些方法,但是现在,让我们使用我包含的方法。 将这些灯添加到装载机下方。
// Add lights
var hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 0.61 );
hemiLight.position.set( 0, 50, 0 );
// Add hemisphere light to scene
scene.add( hemiLight );
var dirLight = new THREE.DirectionalLight( 0xffffff, 0.54 );
dirLight.position.set( -8, 12, 8 );
dirLight.castShadow = true;
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
// Add directional Light to scene
scene.add( dirLight );
Your chair looks marginally better! Before we continue, here’s our JavaScript so far:
您的椅子看起来稍微好一点! 在继续之前,这里是到目前为止JavaScript:
var cameraFar = 5;
var theModel;
const MODEL_PATH = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/chair.glb";
const BACKGROUND_COLOR = 0xf1f1f1;
// Init the scene
const scene = new THREE.Scene();
// Set background
scene.background = new THREE.Color(BACKGROUND_COLOR );
scene.fog = new THREE.Fog(BACKGROUND_COLOR, 20, 100);
const canvas = document.querySelector('#c');
// Init the renderer
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
document.body.appendChild(renderer.domElement);
// Add a camerra
var camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = cameraFar;
camera.position.x = 0;
// Init the object loader
var loader = new THREE.GLTFLoader();
loader.load(MODEL_PATH, function(gltf) {
theModel = gltf.scene;
// Set the models initial scale
theModel.scale.set(2,2,2);
// Offset the y position a bit
theModel.position.y = -1;
// Add the model to the scene
scene.add(theModel);
}, undefined, function(error) {
console.error(error)
});
// Add lights
var hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 0.61 );
hemiLight.position.set( 0, 50, 0 );
// Add hemisphere light to scene
scene.add( hemiLight );
var dirLight = new THREE.DirectionalLight( 0xffffff, 0.54 );
dirLight.position.set( -8, 12, 8 );
dirLight.castShadow = true;
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
// Add directional Light to scene
scene.add( dirLight );
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
Here’s what we should be looking at right now:
这是我们现在应该查看的内容:
Let’s fix the pixelation and the stretching. Three.js needs to update the canvas size when it shifts, and it needs to set its internal resolution not only to the dimensions of the canvas, but also the device pixel ratio of the screen (which is much higher on phones).
让我们修复像素化和拉伸。 Three.js需要在移动时更新画布的大小,并且不仅需要将其内部分辨率设置为画布的尺寸,还需要设置屏幕的设备像素比率(在手机上要高得多)。
Lets head to the bottom of our JavaScript, below where we call animate(), and add this function. This function basically listens to both, the canvas size and the window size, and returns a boolean depending on whether the two sizes are the same or not. We will use that function inside the animate function to determine whether to re-render the scene. This function is also going to take into account the device pixel ratio to be sure that the canvas is sharp on mobile phones too.
让我们进入JavaScript的底部,在此处调用animate() ,并添加此函数。 此函数基本上侦听画布大小和窗口大小,并根据两个大小是否相同返回布尔值。 我们将在animate函数中使用该函数来确定是否重新渲染场景。 此功能还将考虑设备像素比率,以确保画布在手机上也很清晰。
Add this function at the bottom of your JavaScript.
在JavaScript底部添加此函数。
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
var width = window.innerWidth;
var height = window.innerHeight;
var canvasPixelWidth = canvas.width / window.devicePixelRatio;
var canvasPixelHeight = canvas.height / window.devicePixelRatio;
const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
Now update your animate function to look like this:
现在更新您的动画函数,如下所示:
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}
Instantly, our chair is looking so much better!
立刻,我们的椅子看起来好多了!
I need to mention a couple things before we continue:
在继续之前,我需要提及几件事:
- The chair is backwards, this is my bad. We’re going to simply rotate the model on its Y position 椅子向后,这是我的坏事。 我们将简单地将模型旋转到其Y位置
- The supports are black? but the rest is white? This is because the model has some material information that has been imported with it that I had set up in Blender. This doesn’t matter, because we’re going to add a function that lets us define textures in our app, and add them to different areas of the chair when the model loads. So, if you have a wood texture and a denim texture (spoiler: we will), we will have the ability to set these on load without the user having to choose them. So the materials on the chair right now don’t matter all that much. 支架是黑色的? 但是剩下的是白色的? 这是因为模型具有一些我在Blender中设置的材料信息。 没关系,因为我们要添加一个函数,使我们可以在应用程序中定义纹理,并在加载模型时将其添加到椅子的不同区域。 因此,如果您有木质纹理和牛仔布纹理(扰流板:我们会),我们将能够在负载下设置它们,而无需用户选择。 因此,现在椅子上的材料并不重要。
Humour me quickly, head to the loader function, and remember where we set the scale to (2,2,2)? Lets add this under it:
Swift为我幽默,使用加载程序功能,还记得我们将比例尺设置为(2,2,2)的位置吗? 让我们在下面添加它:
// Set the models initial scale
theModel.scale.set(2,2,2);
theModel.rotation.y = Math.PI;
Yeah, much better, sorry about that. One more thing: Three.js doesn’t have support for degrees as far as I know (?), everyone appears to be using Math.PI. This equals 180 degrees, so if you want something angled at a 45 degree angle, you’d use Math.PI / 4.
是的,更好,对此感到抱歉。 还有一件事:就我所知(?)而言,Three.js不支持学位,每个人似乎都在使用Math.PI。 这等于180度,因此,如果您想要以45度角倾斜的物体,则可以使用Math.PI / 4。
Okay, we’re getting there! We need a floor though, without a floor there can’t really be any shadows right?
好的,我们到了! 但是,我们需要一个地板,没有地板,就不可能有阴影吗?
Add a floor, what we’re doing here is creating a new plane (a two-dimensional shape, or a three-dimensional shape with no height).
添加一个地板,我们在这里要做的是创建一个新的平面(一个二维形状,或者一个没有高度的三维形状)。
Add this below our lights…
将此添加到我们的灯光下...
// Floor
var floorGeometry = new THREE.PlaneGeometry(5000, 5000, 1, 1);
var floorMaterial = new THREE.MeshPhongMaterial({
color: 0xff0000,
shininess: 0
});
var floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -0.5 * Math.PI;
floor.receiveShadow = true;
floor.position.y = -1;
scene.add(floor);
Let’s take a look at whats happening here.
让我们看看这里发生了什么。
First, we made a geometry, we won’t be needing to make another geometry in Three.js in this tutorial, but you can make all sorts.
首先,我们制作了一个几何图形,在本教程中,我们不需要在Three.js中制作另一个几何图形,但是您可以进行各种修改。
Secondly, notice how we also made a new MeshPhongMaterial and set a couple options. It’s color, and it’s shininess. Check out some of Three.js other materials later on. Phong is great because you can adjust its reflectiveness and specular highlights. There is also MeshStandardMaterial which has support for more advanced texture aspects such as metallic and ambient occlusion, and there is also the MeshBasicMaterial, which doesn’t support shadows. We will just be creating Phong materials in this tutorial.
其次,请注意我们如何制作新的MeshPhongMaterial并设置几个选项。 它是颜色,它是发光的。 稍后再查看Three.js其他材料。 Phong非常好,因为您可以调整其反射率和镜面高光。 另外还有MeshStandardMaterial ,它支持更高级的纹理方面,例如金属和环境光遮挡,还有MeshBasicMaterial ,它不支持阴影。 在本教程中,我们将仅创建Phong材料。
We created a variable called floor
and merged the geometry and material into a Mesh.
我们创建了一个名为floor
的变量,并将几何图形和材质合并为一个Mesh。
We set the floor’s rotation to be flat, opted in for the ability to receive shadows, moved it down the same way we moved the chair down, and then added it to the scene.
我们将地板的旋转设置为平坦,选择接收阴影的功能,将其向下移动的方式与将椅子向下移动的方式相同,然后将其添加到场景中。
We should now be looking at this:
现在,我们应该看一下:
We will leave it red for now, but, where are the shadows? There’s a couple of things we need to do for that. First, under our const renderer, lets include a couple of options:
我们暂时将其保留为红色,但是阴影在哪里? 为此,我们需要做几件事。 首先,在我们的const renderer下,让我们包括几个选项:
// Init the renderer
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio);
We’ve set the pixel ratio to whatever the device’s pixel ratio is, not relevant to shadows, but while we’re there, let’s do that. We’ve also enabled shadowMap
, but there are still no shadows? That’s because the materials we have on our chair are the ones brought in from Blender, and we want to author some of them in our app.
我们已经将像素比率设置为设备的像素比率,与阴影无关,但是在此期间,让我们开始吧。 我们还启用了shadowMap
,但是仍然没有阴影吗? 这是因为我们在椅子上使用的材料是Blender带来的,因此我们想在我们的应用程序中编写其中的一些内容。
Our loader function includes the ability to traverse the 3D model. So, head to our loader function and add this in below the theModel = gltf.scene; line. For each object in our 3D model (legs, cushions, etc), we’re going to to enable to option to cast shadows, and to receive shadows. This traverse method will be used again later on.
我们的加载器功能包括遍历3D模型的功能。 因此,转到我们的加载器函数,并将其添加到theModel = gltf.scene;下面; 线。 对于3D模型中的每个对象(腿,垫子等),我们将启用选项以投射阴影并接收阴影。 此遍历方法将在以后再次使用。
Add this line below theModel = gltf.scene;
将此行添加到theModel = gltf.scene下;
theModel.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});
It looks arguably worse than it did before, but at least theres a shadow on the floor! This is because our model still has materials brought in from Blender. We’re going to replace all of these materials with a basic, white PhongMaterial.
它看起来可能比以前更糟,但至少地板上有阴影! 这是因为我们的模型仍然具有从Blender引入的材料。 我们将用基本的白色PhongMaterial材料替换所有这些材料。
Lets create another PhongMaterial and add it above our loader function:
让我们创建另一个PhongMaterial并将其添加到我们的加载器函数上方:
// Initial material
const INITIAL_MTL = new THREE.MeshPhongMaterial( { color: 0xf1f1f1, shininess: 10 } );
This is a great starting material, it’s a slight off-white, and it’s only a little bit shiny. Cool!
这是很好的起始材料,略带灰白色,只有一点点光泽。 凉!
We could just add this to our chair and be done with it, but some objects may need a specific color or texture on load, and we can’t just blanket the whole thing with the same base color, the way we’re going to do this is to add this array of objects under our initial material.
我们可以将其添加到椅子上并完成它,但是某些对象在加载时可能需要特定的颜色或纹理,并且我们不能只用相同的基色覆盖整个对象,我们将采用这种方法这样做是在我们的初始材质下添加此对象数组。
// Initial material
const INITIAL_MTL = new THREE.MeshPhongMaterial( { color: 0xf1f1f1, shininess: 10 } );
const INITIAL_MAP = [
{childID: "back", mtl: INITIAL_MTL},
{childID: "base", mtl: INITIAL_MTL},
{childID: "cushions", mtl: INITIAL_MTL},
{childID: "legs", mtl: INITIAL_MTL},
{childID: "supports", mtl: INITIAL_MTL},
];
We’re going to traverse through our 3D model again and use the childID to find different parts of the chair, and apply the material to it (set in the mtl property). These childID’s match the names we gave each object in Blender, if you read that section, consider yourself informed!
我们将再次遍历3D模型,并使用childID查找椅子的不同部分,然后将材料应用到该椅子上(在mtl属性中设置)。 这些childID与我们在Blender中为每个对象指定的名称相匹配,如果您阅读了该部分,请认为您已被告知!
Below our loader function, let’s add a function that takes the the model, the part of the object (type), and the material, and sets the material. We’re also going to add a new property to this part called nameID so that we can reference it later.
在我们的加载器函数下方,让我们添加一个函数,该函数采用模型,对象的一部分(类型)和材质,并设置材质。 我们还将向此部分添加一个名为nameID的新属性,以便稍后引用。
// Function - Add the textures to the models
function initColor(parent, type, mtl) {
parent.traverse((o) => {
if (o.isMesh) {
if (o.name.includes(type)) {
o.material = mtl;
o.nameID = type; // Set a new property to identify this object
}
}
});
}
Now, inside our loader function, just before we add our model to the scene (scene.add(theModel);)
现在,在我们的加载器函数内部,就在将模型添加到场景之前( scene.add(theModel); )
Let’s run that function for each object in our INITIAL_MAP array:
让我们为INITIAL_MAP数组中的每个对象运行该函数:
// Set initial textures
for (let object of INITIAL_MAP) {
initColor(theModel, object.childID, object.mtl);
}
Finally, head back to our floor, and change the color from red (0xff0000) to a light grey(0xeeeeee).
最后,回到地板,将颜色从红色(0xff0000)更改为浅灰色(0xeeeeee)。
// Floor
var floorGeometry = new THREE.PlaneGeometry(5000, 5000, 1, 1);
var floorMaterial = new THREE.MeshPhongMaterial({
color: 0xeeeeee, // <------- Here
shininess: 0
});
It’s worth mentioning here that 0xeeeeee is different to our background color. I manually dialed this in until the floor with the lights shining on it matched the lighter background color. We’re now looking at this:
这里值得一提的是0xeeeeee与我们的背景色不同。 我手动拨入,直到地板上的灯光与较浅的背景色匹配为止。 我们现在在看这个:
See the Pen 3D Chair Customizer Tutorial – Part 1 by Kyle Wetton (@kylewetton) on CodePen.
请参阅CodePen上的Kyle Wetton( @kylewetton )编写的Pen 3D Chair Customizer教程–第1部分。
Congratulations, we’ve got this far! If you got stuck anywhere, fork this pen or investigate it until you find the issue.
恭喜,我们已经做到了! 如果您在任何地方都被卡住,请叉这支笔或对其进行调查,直到发现问题为止。
For real though this is a very small part, and is super easy thanks to our third dependency OrbitControls.js.
实际上,这只是很小的一部分,并且由于我们的第三个依赖OrbitControls.js而非常容易。
Above our animate function, we add this in our controls:
在动画功能之上,我们将其添加到控件中:
// Add controls
var controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.maxPolarAngle = Math.PI / 2;
controls.minPolarAngle = Math.PI / 3;
controls.enableDamping = true;
controls.enablePan = false;
controls.dampingFactor = 0.1;
controls.autoRotate = false; // Toggle this if you'd like the chair to automatically rotate
controls.autoRotateSpeed = 0.2; // 30
Inside the animate function, at the top, add:
在动画功能的顶部,添加:
controls.update();
So our controls variable is a new OrbitControls class. We’ve set a few options that you can change here if you’d like. These include the range in which the user is allowed to rotate around the chair (above and below). We’ve disabled panning to keep the chair centered, enabled dampening to give it weight, and included auto rotate ability if you choose to use them. This is currently set to false.
因此,我们的控件变量是一个新的OrbitControls类。 我们设置了一些选项,您可以根据需要在此处进行更改。 这些范围包括允许用户围绕椅子(上方和下方)旋转的范围。 我们禁用了平移功能以保持椅子居中,启用了减震功能使其具有重量,并且如果您选择使用自动旋转功能,则还具有自动旋转功能。 当前设置为false。
Try click and drag your chair, you should be able to explore the model with full mouse and touch functionality!
尝试单击并拖动椅子,您应该能够使用完整的鼠标和触摸功能探索模型!
See the Pen Scrollable by Kyle Wetton (@kylewetton) on CodePen.
见笔滚动凯尔Wetton( @kylewetton上) CodePen 。
Our app currently doesn’t do anything, so this next part will focus on changing our colors. We’re going to add a bit more HTML. Afterwards, I’ll explain a bit about what the CSS is doing.
我们的应用程序当前不执行任何操作,因此下一部分将重点介绍更改颜色。 我们将添加更多HTML。 然后,我将解释一下CSS的功能。
Add this below your canvas element:
将其添加到您的canvas元素下面:
Basically, the .controls DIV is stuck to the bottom of the screen, the .tray is set to be 100% width of the body, but its child, .tray__slide is going to fill with swatches and can be as wide as it needs. We’re going to add the ability to slide this child to explore colors as one of the final steps of this tutorial.
基本上, .controls DIV粘贴在屏幕的底部, .tray设置为主体宽度的100%,但其子级.tray__slide将填充色板,并且可以根据需要的宽度来设置。 作为本教程的最后步骤,我们将添加使这个孩子滑动以探索颜色的功能。
Let’s start by adding in a couple colors. At the top of our JavaScript, lets add an array of five objects, each with a color property.
让我们从添加几种颜色开始。 在我们JavaScript顶部,让我们添加五个对象的数组,每个对象都有一个color属性。
const colors = [
{
color: '66533C'
},
{
color: '173A2F'
},
{
color: '153944'
},
{
color: '27548D'
},
{
color: '438AAC'
}
]
Note that these neither have # or 0x to represent the hex. We will use these colors for both in functions. Also, it’s an object because we will be able to add other properties to this color, like shininess, or even a texture image (spoiler: we will, and we will).
请注意,这些都没有#或0x代表十六进制。 我们将在函数中同时使用这些颜色。 同样,它也是一个对象,因为我们将能够为该颜色添加其他属性,例如光泽度,甚至是纹理图像(破坏者:我们会,而且将会)。
Lets make swatches out of these colors!
让我们用这些颜色制作色板!
First, let’s reference our tray slider at the top of our JavaScript:
首先,让我们在JavaScript顶部引用托盘滑块:
const TRAY = document.getElementById('js-tray-slide');
Right at the bottom of our JavaScript, lets add a new function called buildColors and immediately call it.
在我们JavaScript底部,让我们添加一个名为buildColors的新函数,然后立即调用它。
// Function - Build Colors
function buildColors(colors) {
for (let [i, color] of colors.entries()) {
let swatch = document.createElement('div');
swatch.classList.add('tray__swatch');
swatch.style.background = "#" + color.color;
swatch.setAttribute('data-key', i);
TRAY.append(swatch);
}
}
buildColors(colors);
We’re now creating swatches out of our colors array! Note that we set the data-key attribute to the swatch, we’re going to use this to look up our color and make them into materials.
我们现在要从颜色阵列中创建色板! 请注意,我们将data-key属性设置为样本,我们将使用它来查找颜色并将它们变成材质。
Below our new buildColors function, let’s add an event handler to our swatches:
在新的buildColors函数下方,让我们向样本添加事件处理程序:
// Swatches
const swatches = document.querySelectorAll(".tray__swatch");
for (const swatch of swatches) {
swatch.addEventListener('click', selectSwatch);
}
Our click handler calls a function called selectSwatch
. This function is going to build a new PhongMaterial out of the color and call another function to traverse through our 3d model, find the part it’s meant to change, and update it!
我们的点击处理程序调用了一个名为selectSwatch
的函数。 该函数将用颜色构建新的PhongMaterial,并调用另一个函数来遍历我们的3d模型,找到要更改的部分并进行更新!
Below the event handlers we just added, add the selectSwatch
function:
在我们刚刚添加的事件处理程序下方,添加selectSwatch
函数:
function selectSwatch(e) {
let color = colors[parseInt(e.target.dataset.key)];
let new_mtl;
new_mtl = new THREE.MeshPhongMaterial({
color: parseInt('0x' + color.color),
shininess: color.shininess ? color.shininess : 10
});
setMaterial(theModel, 'legs', new_mtl);
}
This function looks up our color by its data-key attribute, and creates a new material out of it.
此函数通过其data-key属性查找我们的颜色,并以此创建新的材质。
This won’t work yet, we need to add the setMaterial function, (see the final line of the function we just added).
这还行不通,我们需要添加setMaterial函数(请参阅刚刚添加的函数的最后一行)。
Take note of this line: setMaterial(theModel, ‘legs’, new_mtl);. Currently we’re just passing ‘legs’ to this function, soon we will add the ability to change out the different sections we want to update. But first, lets add the zcode>setMaterial
注意这一行: setMaterial(theModel,'legs',new_mtl); 。 当前,我们只是将“腿”传递给此功能,不久我们将添加更改要更新的不同部分的功能。 但首先,让我们添加zcode> setMaterial
function.
功能。
Below this function, add the setMaterial
function:
在此函数下面,添加setMaterial
函数:
function setMaterial(parent, type, mtl) {
parent.traverse((o) => {
if (o.isMesh && o.nameID != null) {
if (o.nameID == type) {
o.material = mtl;
}
}
});
}
This function is similar to our initColor
function, but with a few differences. It checks for the nameID we added in the initColor
, and if its the same as the parameter type, it adds the material to it.
此函数类似于我们的initColor
函数,但有一些区别。 它检查我们在initColor中添加的initColor
,如果它与参数类型相同,则向其中添加材质。
Our swatches can now create a new material, and change the color of the legs, give it a go! Here’s everything we have so far in a pen. Investigate it if you’re lost.
我们的色板现在可以创建新的材料,并改变腿的颜色,快去尝试吧! 到目前为止,这是我们笔所能拥有的一切。 如果您迷路了,请进行调查。
See the Pen Swatches change the legs color! by Kyle Wetton (@kylewetton) on CodePen.
看到笔色板改变腿的颜色! 由Kyle Wetton( @kylewetton )在CodePen上编写。
We can now change the color of the legs, which is awesome, but let’s add the ability to select the part our swatch should add its material to. Include this HTML just below the opening body tag, I’ll explain the CSS below.
现在,我们可以更改腿的颜色,这太棒了,但是让我们添加选择样本应为其材料添加零件的功能。 将HTML包含在开始body标签下面,我将在下面解释CSS。
This is just a collection of buttons with custom icons in each. The .options DIV is stuck to the side of the screen via CSS (and shifts a bit with media queries). Each .option DIV is just a white square, that has a red border on it when a –is-active class is added to it. It also includes a data-option attribute that matches our nameID, so we can identify it. Lastly, the image element has a CSS property called pointer-events: none so that the event stays on the parent even if you click the image.
这只是每个按钮中带有自定义图标的集合。 .options DIV通过CSS固定在屏幕的一侧(并随媒体查询而移动一点)。 每个.option DIV只是一个白色正方形,当添加–is-active类时,上面带有红色边框。 它还包括一个与我们的nameID匹配的data-option属性,因此我们可以对其进行识别。 最后,image元素具有一个称为pointer-eventsCSS属性:none,因此即使您单击该图像,该事件也将保留在父级上。
Lets add another variable at the top of our JavaScript called activeOptions and by default let’s set it to ‘legs’:
让我们在JavaScript的顶部添加另一个名为activeOptions的变量,默认情况下将其设置为'legs':
var activeOption = 'legs';
Now head back to our selectSwatch function and update that hard-coded ‘legs’ parameter to activeOption
现在回到我们的selectSwatch函数,并将该硬编码的“ legs”参数更新为activeOption
setMaterial(theModel, activeOption, new_mtl);
Now all we need to do is create a event handler to change out activeOption when an option is clicked!
现在,我们需要做的就是创建一个事件处理程序,以在单击选项时更改activeOption !
Let’s add this above our const swatches and selectSwatch function.
让我们将其添加到const色板和selectSwatch函数之上。
// Select Option
const options = document.querySelectorAll(".option");
for (const option of options) {
option.addEventListener('click',selectOption);
}
function selectOption(e) {
let option = e.target;
activeOption = e.target.dataset.option;
for (const otherOption of options) {
otherOption.classList.remove('--is-active');
}
option.classList.add('--is-active');
}
We’ve added the selectOption
function, which sets the activeOption to our event targets data-option value, and toggles the –is-active class. Thats it!
我们添加了selectOption
函数,该函数将selectOption
设置为事件目标数据选项值,并切换–is-active类。 而已!
Try it out
试试看
See the Pen Changing options by Kyle Wetton (@kylewetton) on CodePen.
见笔更改选项凯尔Wetton( @kylewetton )上CodePen 。
But why stop here? An object could look like anything, it can’t all be the same material. A chair with no wood or fabric? Lets expand our color selection a little bit. Update your color array to this:
但是为什么在这里停下来? 一个对象可能看起来像任何东西,它不可能都是相同的材料。 没有木头或织物的椅子? 让我们扩展一下颜色选择。 将颜色数组更新为:
const colors = [
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/wood.jpg',
size: [2,2,2],
shininess: 60
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/denim.jpg',
size: [3, 3, 3],
shininess: 0
},
{
color: '66533C'
},
{
color: '173A2F'
},
{
color: '153944'
},
{
color: '27548D'
},
{
color: '438AAC'
}
]
The top two are now textures. We’ve got wood and denim. We also have two new properties, size and shininess. Size is how often to repeat a pattern, so the larger the number, the more dense the pattern is, or more simply put – the more it repeats.
现在,前两个是纹理。 我们有木头和牛仔布。 我们还有两个新属性,大小和光泽。 大小是重复图案的频率,因此数字越大,图案越密集,或更简单地放置-重复越多。
There are two function we need to update to add this ability. Firstly, lets head to the buildColors
function and update to this
我们需要更新两个功能以添加此功能。 首先,让我们转到buildColors
函数并对此进行更新
// Function - Build Colors
function buildColors(colors) {
for (let [i, color] of colors.entries()) {
let swatch = document.createElement('div');
swatch.classList.add('tray__swatch');
if (color.texture)
{
swatch.style.backgroundImage = "url(" + color.texture + ")";
} else
{
swatch.style.background = "#" + color.color;
}
swatch.setAttribute('data-key', i);
TRAY.append(swatch);
}
}
Now its checking to see if its a texture, if it is, it’s going to set the swatches background to be that texture, neat!
现在,它会检查其纹理是否存在(如果有的话),它将色板背景设置为该纹理整齐!
Notice the gap between the 5th and 6th swatch? The final batch of colors, which I will provide, is grouped into color schemes of 5 colors per scheme. So each scheme will have that small divider in it, this is set in the CSS and will make more sense in the final product. 注意到第5个和第6个样本之间的差距了吗? 我将提供的最后一批颜色分为每种方案5种颜色的配色方案。 因此,每个方案中都会有一个小的分隔符,它在CSS中设置,并且在最终产品中更有意义。The second function we’re going to update is the selectSwatch
function. Update it to this:
我们要更新的第二个函数是selectSwatch
函数。 将其更新为:
function selectSwatch(e) {
let color = colors[parseInt(e.target.dataset.key)];
let new_mtl;
if (color.texture) {
let txt = new THREE.TextureLoader().load(color.texture);
txt.repeat.set( color.size[0], color.size[1], color.size[2]);
txt.wrapS = THREE.RepeatWrapping;
txt.wrapT = THREE.RepeatWrapping;
new_mtl = new THREE.MeshPhongMaterial( {
map: txt,
shininess: color.shininess ? color.shininess : 10
});
}
else
{
new_mtl = new THREE.MeshPhongMaterial({
color: parseInt('0x' + color.color),
shininess: color.shininess ? color.shininess : 10
});
}
setMaterial(theModel, activeOption, new_mtl);
}
To explain what’s going on here, this function will now check if it’s a texture. If it is, it’s going to create a new texture using the Three.js TextureLoader method, it’s going to set the texture repeat using our size values, and set the wrapping of it (this wrapping option seems to work best, I’ve tried the others, so lets go with it), then its going to set the PhongMaterials map property to the texture, and finally use the shininess value.
为了解释这里发生了什么,该函数现在将检查它是否是纹理。 如果是这样,它将使用Three.js TextureLoader方法创建一个新纹理,它将使用我们的大小值设置纹理重复,并设置其包裹(此包裹选项似乎效果最好,我已经尝试过其他,所以让我们继续吧),然后将PhongMaterials贴图属性设置为纹理,最后使用光泽值。
If it’s not a texture, it uses our older method. Note that you can set a shininess property to any of our original colors!
如果不是纹理,则使用我们的旧方法。 请注意,您可以为我们的任何原始颜色设置光泽属性!
Important: if your textures just remain black when you try add them. Check your console. Are you getting cross domain CORS errors? This is a CodePen bug and I’ve done my best to try fix it. These assets are hosted directly in CodePen via a Pro feature so its unfortunate to have to battle with this. Apparently, the best bet here is to not visit those image URLs directly, otherwise I recommend signing up to Cloudinary and using their free tier, you may have better luck pointing your textures there.
重要提示:如果您尝试添加纹理时仍保持黑色。 检查您的控制台。 您是否收到跨域CORS错误? 这是一个CodePen错误,我已尽力尝试修复它。 这些资产通过Pro功能直接托管在CodePen中,因此不幸的是必须与之抗衡。 显然,这里最好的选择是不要直接访问这些图像URL,否则我建议注册Cloudinary并使用其免费套餐,这样可能更好地将纹理指向那里。
Here’s a pen with the textures working on my end at least:
这是一支至少具有纹理效果的笔:
See the Pen Texture support by Kyle Wetton (@kylewetton) on CodePen.
见笔纹理支持由凯尔Wetton( @kylewetton )上CodePen 。
I’ve had projects get run passed clients with a big button that is begging to be pressed, positively glistening with temptation to even just hover over it, and them and their co-workers (Dave from accounts) come back with feedback about how they didn’t know there was anything to be pressed (screw you, Dave).
我曾经让项目通过一个很大的按钮来传递给客户,这个按钮希望被按下,即使只是将鼠标悬停在这个按钮上,他们也会积极地闪闪发光,他们和他们的同事(来自帐户的Dave)回来时会收到有关他们如何反馈的反馈不知道有什么要紧迫的(戴夫,打给你)。
So let’s add some calls to action. First, let’s chuck in a patch of HTML above the canvas element:
因此,让我们添加一些号召性用语。 首先,让我们在canvas元素上方插入HTML补丁:
Drag to rotate 360°
The CSS places this call-to-action above the chair, it’s a nice big button that instructs the user to drag to rotate the chair. It just stays there though? We will get to that.
CSS将号召性用语放置在椅子上方,这是一个不错的大按钮,它指示用户拖动以旋转椅子。 它只是留在那里吗? 我们将做到这一点。
Let’s spin the chair once it’s loaded first, then, once the spin is done, let’s hide that call-to-action.
首先加载椅子,然后旋转,然后旋转完成,然后隐藏号召性用语。
First, lets add a loaded variable to the top of our JavaScript and set it to false:
首先,让我们在JavaScript的顶部添加一个已加载的变量,并将其设置为false:
var loaded = false;
Right at the bottom of your JavaScript, add this function
在您JavaScript底部,添加此函数
// Function - Opening rotate
let initRotate = 0;
function initialRotation() {
initRotate++;
if (initRotate <= 120) {
theModel.rotation.y += Math.PI / 60;
} else {
loaded = true;
}
}
This simply rotates the the model 360 degrees within the span of 120 frames (around 2 seconds at 60fps), and we’re going to run this in the animate
function to call it for 120 frames, once its done, it’s going to set loaded to true in our animate
function. Here’s how it will look in its entirely with the new code at the end there:
只需在120帧的范围内(360 fps时约2秒)将模型旋转360度,我们将在animate
函数中运行此模型以调用120帧,一旦完成,将设置加载在我们的animate
功能中为真。 下面是新代码的整体外观:
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
if (theModel != null && loaded == false) {
initialRotation();
}
}
animate();
We check that theModel doesn’t equal null, and that the variable loaded is false, and we run that function for 120 frames, at which point the function switches to loaded = true, and our animate function ignores it.
我们检查theModel不等于null ,并且加载的变量为false,然后将该函数运行120帧,这时该函数切换为load = true ,而我们的动画函数会忽略它。
You should have a nice spinning chair. When that chair stops is a great time to remove our call-to-action.
您应该有一把漂亮的旋转椅。 当椅子停下来时,是删除我们的号召性用语的好时机。
In the CSS, there’s a class that can be added to that call-to-action that will hide it with an animation, this animation has a delay of 3 seconds, so let’s add that class at the same time the rotation starts.
在CSS中,可以在该号召性用语中添加一个类,该类将通过动画进行隐藏,该动画的延迟时间为3秒,因此让我们在旋转开始的同时添加该类。
At the top of your JavaScript we will reference it:
在您JavaScript顶部,我们将引用它:
const DRAG_NOTICE = document.getElementById('js-drag-notice');
and update your animate function like so
并像这样更新您的动画功能
if (theModel != null && loaded == false) {
initialRotation();
DRAG_NOTICE.classList.add('start');
}
Great! Okay, here’s some more colors, update your color array, I’ve give a lightweight sliding function below it:
大! 好的,这里有更多颜色,更新您的颜色阵列,我在其下方提供了轻量级的滑动功能:
const colors = [
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/wood_.jpg',
size: [2,2,2],
shininess: 60
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/fabric_.jpg',
size: [4, 4, 4],
shininess: 0
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/pattern_.jpg',
size: [8, 8, 8],
shininess: 10
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/denim_.jpg',
size: [3, 3, 3],
shininess: 0
},
{
texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/quilt_.jpg',
size: [6, 6, 6],
shininess: 0
},
{
color: '131417'
},
{
color: '374047'
},
{
color: '5f6e78'
},
{
color: '7f8a93'
},
{
color: '97a1a7'
},
{
color: 'acb4b9'
},
{
color: 'DF9998',
},
{
color: '7C6862'
},
{
color: 'A3AB84'
},
{
color: 'D6CCB1'
},
{
color: 'F8D5C4'
},
{
color: 'A3AE99'
},
{
color: 'EFF2F2'
},
{
color: 'B0C5C1'
},
{
color: '8B8C8C'
},
{
color: '565F59'
},
{
color: 'CB304A'
},
{
color: 'FED7C8'
},
{
color: 'C7BDBD'
},
{
color: '3DCBBE'
},
{
color: '264B4F'
},
{
color: '389389'
},
{
color: '85BEAE'
},
{
color: 'F2DABA'
},
{
color: 'F2A97F'
},
{
color: 'D85F52'
},
{
color: 'D92E37'
},
{
color: 'FC9736'
},
{
color: 'F7BD69'
},
{
color: 'A4D09C'
},
{
color: '4C8A67'
},
{
color: '25608A'
},
{
color: '75C8C6'
},
{
color: 'F5E4B7'
},
{
color: 'E69041'
},
{
color: 'E56013'
},
{
color: '11101D'
},
{
color: '630609'
},
{
color: 'C9240E'
},
{
color: 'EC4B17'
},
{
color: '281A1C'
},
{
color: '4F556F'
},
{
color: '64739B'
},
{
color: 'CDBAC7'
},
{
color: '946F43'
},
{
color: '66533C'
},
{
color: '173A2F'
},
{
color: '153944'
},
{
color: '27548D'
},
{
color: '438AAC'
}
]
Awesome! These hang off the page though, right at the bottom of your JavaScript, add this function, it will allow you to drag the swatches panel with mouse and touch. For the interest of keeping on topic, I won’t delve too much into how it works.
太棒了! 尽管这些挂起在页面的右侧,但是就在JavaScript底部,添加了此功能,它将使您能够使用鼠标和触摸来拖动色板。 为了保持话题的兴趣,我不会过多地研究它的工作原理。
var slider = document.getElementById('js-tray'), sliderItems = document.getElementById('js-tray-slide'), difference;
function slide(wrapper, items) {
var posX1 = 0,
posX2 = 0,
posInitial,
threshold = 20,
posFinal,
slides = items.getElementsByClassName('tray__swatch');
// Mouse events
items.onmousedown = dragStart;
// Touch events
items.addEventListener('touchstart', dragStart);
items.addEventListener('touchend', dragEnd);
items.addEventListener('touchmove', dragAction);
function dragStart (e) {
e = e || window.event;
posInitial = items.offsetLeft;
difference = sliderItems.offsetWidth - slider.offsetWidth;
difference = difference * -1;
if (e.type == 'touchstart') {
posX1 = e.touches[0].clientX;
} else {
posX1 = e.clientX;
document.onmouseup = dragEnd;
document.onmousemove = dragAction;
}
}
function dragAction (e) {
e = e || window.event;
if (e.type == 'touchmove') {
posX2 = posX1 - e.touches[0].clientX;
posX1 = e.touches[0].clientX;
} else {
posX2 = posX1 - e.clientX;
posX1 = e.clientX;
}
if (items.offsetLeft - posX2 <= 0 && items.offsetLeft - posX2 >= difference) {
items.style.left = (items.offsetLeft - posX2) + "px";
}
}
function dragEnd (e) {
posFinal = items.offsetLeft;
if (posFinal - posInitial < -threshold) { } else if (posFinal - posInitial > threshold) {
} else {
items.style.left = (posInitial) + "px";
}
document.onmouseup = null;
document.onmousemove = null;
}
}
slide(slider, sliderItems);
Now, head to your CSS and under .tray__slider, uncomment this small animation
现在,转到CSS并在.tray__slider下,取消注释此小动画
/* transform: translateX(-50%);
animation: wheelin 1s 2s ease-in-out forwards; */
Okay, let’s finish it off with a the final two touches, and we’re done!
好的,让我们完成最后的两次接触,就可以完成了!
Let’s update our .controls div to include this extra call-to-action:
让我们更新.controls div以包括以下额外的号召性用语:
Note that we have a new info section that includes some instructions on how to control the app.
请注意,我们有一个新的信息部分,其中包含有关如何控制应用程序的一些说明。
Finally, let’s add a loading overlay so that our app is clean while everything loads, and we will remove it once the model is loaded.
最后,让我们添加一个加载叠加层,以便在加载所有内容时我们的应用程序保持干净,并且在加载模型后将其删除。
Add this to the top of our HTML, below the body tag.
将此添加到我们HTML顶部,在body标记下方。
Here’s the thing about our loader, in order for it to load first, we’re going to add the CSS to the head tag instead of being included in the CSS. So simply add this CSS just above the closing head tag.
这是关于我们的加载器的事情,为了使其首先加载,我们将CSS添加到head标签中,而不是包含在CSS中。 因此,只需在封闭的head标签上方添加此CSS。
Almost there! Let’s remove it once the model is loaded.
差不多好了! 加载模型后,将其删除。
At the top of our JavaScript, lets reference it:
让我们在JavaScript的顶部引用它:
const LOADER = document.getElementById('js-loader');
Then in our loader function, after scene.add(theModel), include this line
然后在我们的加载器函数中,位于scene.add(theModel)之后,包括以下行
// Remove the loader
LOADER.remove();
Now our app loads behind this DIV, polishing it off:
现在,我们的应用程序将在此DIV后面加载,并对其进行完善:
And that’s it! Here’s the completed pen for reference.
就是这样! 这是完成的笔供参考。
See the Pen 3D Chair Customizer Tutorial – Part 4 by Kyle Wetton (@kylewetton) on CodePen.
请参阅CodePen上的Kyle Wetton( @kylewetton )编写的Pen 3D Chair Customizer教程–第4部分。
You can also check out the demo hosted here on Codrops.
您也可以查看Codrops上托管的演示。
This is a big tutorial. If you feel I made a mistake somewhere, please let me know in the comments, and thanks again for following with me as we create this absolute unit.
这是一个很大的教程。 如果您觉得我在某个地方犯了一个错误,请在评论中让我知道,并再次感谢您在我们创建此绝对单位时关注我。
翻译自: https://tympanus.net/codrops/2019/09/17/how-to-build-a-color-customizer-app-for-a-3d-model-with-three-js/