一、three.js 3d模型用什么做的?
three.js 3d模型是使用Javascript编写的。因为three.js 是一个基于WebGL的开源Javascript库,它提供了各种功能来创建和渲染3D场景,包括模型加载、材质、照明和相机等。所以使用Javascript编写three.js 3d模型更加方便快捷。除此之外,还可以使用其他3D建模软件制作模型并导出为three.js支持的格式,如blender、3ds max、Maya等。这些软件可以更好地实现模型的细节和复杂度,但相应的需要更多的时间和专业知识。
二、直播间3d魔方动态贴片怎么做?
直播间3D魔方动态贴片的做法步骤如下:
1. 设计魔方贴片:使用3D建模软件(如Blender、3ds Max等)设计一个魔方贴片模型。为了方便动态调整,建议将贴片分为几个部分,如6个面。
2. 导出贴图:将设计好的魔方贴片模型导出为图像文件,如PNG或JPG格式。
3. 直播软件设置:选择一款支持动态贴片的直播软件,如OBS Studio。在软件中设置场景和来源,并将魔方贴片图像添加到来源中。
4. 添加动态效果:在直播软件中为魔方贴片添加动态效果。例如,为每个面添加旋转、缩放或透明度变化等动画效果。
5. 实时渲染:在直播过程中,直播软件会根据设置的动态效果实时渲染魔方贴片。观众在直播间中就能看到动态的魔方贴片。
需要注意的是,不同直播软件和图形处理软件的操作可能有所不同,具体实现步骤可能因软件而异。建议根据所使用的软件和硬件环境进行调整。
三、在webgl、three.js如何对3D建模做剖切的效果?大概什么原理?
原文:How to Build a Color Customizer App for a 3D Model with Three.js
本文将阐述如何基于 Three.js 创建一个完整的 3D 模型(椅子)颜色自定义应用。
马上体验:3D Model Color Customizer App with Three.js
快速介绍
该工具的灵感来源于 Vans shoe customizer,并采用优秀的 JavaScript 3D 库 Three.js 实现。
阅读本文的前提是已掌握 JavaScript、HTML 和 CSS。
为了能让你确切学到东西,而不是单纯地粘贴/复制。本文不按常规出牌,在一开始就给出全部 CSS。CSS 起到装扮应用的作用,即仅专注于 UI。每当我们粘贴部分 HTML 时,都会讲解相应 CSS 的作用。
Part 1: 3D 模型
你可以完全跳过本节,但它可以让你对这一切有更深入的了解。
这不是一篇关于 3D 建模的教程,但我将阐述如何在 Blender 中设置模型,这有助于你创建属于自己的模型、修改网上的免费模型或指点他人调试。以下是创作 3D 模型——椅子的一些经验。
尺寸(Scale)
模型需设置为符合真实世界的尺寸。我也不知道这是否重要,但感觉没问题,为什么不这样做呢?
分层和命名约定(Layering and naming conventions)
这部分很重要:物体中每个需要独立控制的元素都必须是 3D 场景中独立的对象。这些对象也必须拥有唯一的名字。这里有 back(背部)、base(底座)、cushions(坐垫)、legs(椅腿)和 supports(支架)。若有三个元素都命名为 supports
,那么 Blender 会将它们命名为 supports
、supports.001
、supports.002
。这没问题,因为我们可以在 JavaScript 中使用 includes("supports")
找到它们。
落点(Placement)
模型应放置在场景的原点,并落在地板上。另外,最好能面向正确的方向,但这可通过 JavaScript 旋转易实现。
导出设置(Setting up for export)
导出前,要使用 Blender 的 Smart UV unwrap
。在此不深入细节,总之这会让纹理可以保持宽高比不变,保证不会在包裹模型中因各类形状而产生怪异的拉伸(建议你制作自己的模型时才仔细研究它)。
确保所有对象应用 transformations(译者注:即将缩放转为对象实际尺寸)。
文件格式(File Format)
显然 Three.js 支持很多 3D 对象文件格式,但它推荐的格式之一是 glTF(.glb)。而且 Blender 也支持导出该格式。
Part 2:建立环境
Fork 这个 pen(译者注:即 codepen 的一个案例),或创建一个 pen 并从其中复制 CSS。这是一个含有本教程所有 CSS 的空白 pen。
3D Chair Customizer Tutorial - Blank
若不选择 fork,也需要复制 HTML。这包含响应式 meta 标签和 Google 字体。
本教程使用了 3 个依赖,我在它们各自上方写有描述用途的注释。
<!-- The main Three.js file -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.min.js'></script>
<!-- This brings in the ability to load custom 3D objects in the .gltf file format. Blender allows the ability to export to this format out the box -->
<script src='https://cdn.jsdelivr.net/gh/mrdoob/Three.js@r92/examples/js/loaders/GLTFLoader.js'></script>
<!-- This is a simple to use extension for Three.js that activates all the rotating, dragging and zooming controls we need for both mouse and touch, there isn't a clear CDN for this that I can find -->
<script src='https://threejs.org/examples/js/controls/OrbitControls.js'></script>
引入 canvas 标签。整个 3D 体验将渲染于此,而其余 HTML 标签作为 UI 辅助于它。将 canvas 放在 HTML 底部(脚本前)。
<!-- The canvas element is used to draw the 3D scene -->
<canvas id="c"></canvas>
现在为 Three.js 创建一个 scene。
// Init the scene
const scene = new THREE.Scene();
接着引用 canvas 元素:
const canvas = document.querySelector('#c');
Three.js 需要具备一些元素才能跑起来。第一个是 scene,第二个是 renderer。在 canvas 变量下方添加它。创建一个 WebGLRenderer,传入 canvas 和选项参数(抗齿距,使 3D 模型的边缘更光滑)。
// Init the renderer
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
然后将 renderer 插入到 body 元素(译者注:此行代码可省略)。:
document.body.appendChild(renderer.domElement);
为 canvas 编写的 CSS 仅是将其拉伸至 body 的 100% 宽高,因此整个页面目前是黑色的(即 canvas 现在是黑色)。
虽然场景目前漆黑一片,但我们走在正确的道路上。
接着 Three.js 需要一个更新循环,这是一个在每帧都会执行的函数,对运行我们的应用程序起到重要作用。我们将更新函数命名为 animate()
,并将其放置在 JavaScript 代码的最底部。
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
请注意,上面代码了引用摄像机(camera),但我们仍未添加它。
在 JavaScript 代码顶部,添加一个名为 cameraFar
的变量。当我们添加 camera 到 scene 时,其默认位置是 0,0,0
。但这可是椅子的位置!因此 cameraFar
变量是告诉 camera 应离此多远,以确保能看到椅子。
var cameraFar = 5;
在 animate()
函数上方添加 camera。
// Add a camera
var camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = cameraFar;
camera.position.x = 0;
这是一个透视摄像机,其参数为 50 视场(field of view,fov),宽高比和默认的裁剪区域。裁剪区域指定了可视区域的前后边界。当然,这些都不是本应用关心的事情。(译者注:可参考《Three.js 现学现卖》)
我们的场景仍然是黑色,下面设置背景色。
在顶部的 scene 变量上方,添加背景色变量 BACKGROUND_COLOR
。
const BACKGROUND_COLOR = 0xf1f1f1;
注意我们这里的十六进制是使用 0x 而不是 #。这不是字符串,而是以 0x 开头的整数。
在 scence 变量下方,更新 scene 的背景色,并在远处添加同样颜色的雾,旨在隐藏地板的边界。
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);
现在仍是一个空白的世界,没有东西,没有投影。是时候加载模型了。
Part 3:加载模型
我们将使用第二个依赖来加载模型。
在此之前,我们先声明引用模型的变量,该变量会被频繁使用。我们将其放在 JavaScript 顶部的 BACKGROUND_COLOR
前。同时,添加该模型的路径。我已对其进行托管,供大家使用。它有 1Mb 左右的大小。
var theModel;
const MODEL_PATH = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/chair.glb";
现在创建一个 loader,并使用其 load 方法。theModel
就是整个场景的 3D 模型。将其设置合适的尺寸大小,这里设为原大小的 2 倍。接着,设置其 y 轴偏移量为 -1,使其往下移动。最后将其添加到场景中。
load
函数的第一个参数是模型的路径,第二个参数是资源加载成功后的回调函数,第三个参数目前是 undefined
,但它其实是资源加载期间的回调函数,最后一个参数是报错回调函数。
将这部分代码放在 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)
});
此时,你应该看到的是一张被拉伸、黑色且像素化的椅子。看起来很糟糕,但这是正常的,别担心!
除了摄像机,我们还需要光。背景不受光影响,但如果此时添加地板,那么它依然会是黑色。Three.js 有几种类型的光且有丰富的选项可供调整。这里我们添加两种:环境光和定向光。两者的设置是专门适配该应用的,其中包括位置和强度。如果你对它们有使用经验,可以尝试更改。但现在就使用我提供的参数吧。将光放在 loader 下方。
// 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 );
此时,椅子看起来稍微好一些!到目前为止,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();
现在看起来如下:
让我们解决像素化和拉伸的问题。Three.js 需要在(视口)改变时更新 canvas 尺寸,其内部分辨率需依赖于 canvas 尺寸和设备屏幕像素比(手机的像素比一般比较高)。
在 JavaScript 代码底部,即调用 animate()
的下方添加该函数。该函数会监听 canvas 的尺寸和 window 的尺寸,并返回一个判断两者是否相同的布尔值。我们将会在 animate
函数内使用该函数,以决定是否需要重新渲染场景(设置场景大小)。该函数还会考虑设备像素比,以确保 canvas 在手机上也清晰。(译者注:建议通过监听 window resize 事件进行判断,且设备像素比上限为 2)
在 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;
}
现在更新 animate
函数后看起来如下:
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}
椅子看起来好多了。
我需要提醒两点:
- 椅子虽然背对着我们,但可以简单地通过沿其 Y 轴旋转进行调整。
- 为何支架(supports)是黑色,其余是白色?这是因为导入的模型带有材质(material)信息,这是在 Blender 中设置的。但没关系,因为我们将添加一个在程序自定义纹理的函数,支持在椅子模型加载后为不同区域进行设置。如果你有木质和牛仔布纹理(小剧透:本文有),我们就能在加载后立刻进行设置,而无需用户手动选择。因此,椅子目前用什么材质都无关紧要。
跳到 loader 方法,还记得设置缩放比例的地方吗 theModal.scale.set(2,2,2)
?我们将调整的旋转角度添加在它下方:
// Set the models initial scale
theModel.scale.set(2,2,2);
theModel.rotation.y = Math.PI;
哇哦,看起来好多了。还有一件事是:据我所知,Three.js 目前仍不支持角度(deg)单位。因此,这里使用 Math.PI,即 180 度,如果想旋转至 45 度角,那么就设置 Math.PI / 4。
我们还需要一块地板,不然怎么产生投影呢?
创建一个平面(二维平面,或高度为 0 的三维体)作为地板。
在光的下方添加:
// 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);
下面讲解一下这里发生了什么。
首先,我们创建了一个几何图形。这是本文创建的唯一一个几何图形,你可以进行各种参数的调整。
第二,我们使用了 MeshPhongMaterial
,为其设置了颜色和反光度(shininess)。在讲 Three.js 其他材质前,我们先看看 Phong。我们能调整它的反光度(reflectiveness)和镜面高光(specular highlights)。另外,还有 MeshStandardMaterial,其支持更多的纹理特性,如金属(metallic)和环境光遮蔽(ambient occlusion);另外,还有不支持阴影的 MeshBasicMaterial。本文仅用到 Phong 材质。
我们创建了变量 floor,并将 geometry 和 material 合为 Mesh。
我们还对地板进行了以下操作:旋转至平坦状态,使其能接收阴影、往下移至椅子的位置。最后将其添加至场景。
现在看起来如下:
我们暂时将地板设为红色,但阴影在哪?为此,我们还需要做几件事。首先在 const renderer
下方添加这几行代码:
// Init the renderer
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio);
我们同时设置了设备像素比,这与阴影无关,恰巧是适当的位置。我们启用了 shadowMap,但仍没有阴影?
在 loader 函数内,我们能遍历 3D 模型(的组成元素)。因此,跳到 loader 函数,在 theModel = gltf.scene;
下添加以下操作:为 3D 模型的每一个元素(椅腿、坐垫等)启用投射和接收阴影的选项。该遍历方法会在后续再次使用。
在 theModel = gltf.scene;
下方添加:
theModel.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});
这看起来比以前更糟,但起码能在地板上产生阴影!之所以不好看,是因为模型仍使用 Blender 带来的材质。下面我们将所有这些材质都替换为普通的 PhongMaterial。
在 loader 函数上方创建另一个 PhongMaterial:
// Initial material
const INITIAL_MTL = new THREE.MeshPhongMaterial( { color: 0xf1f1f1, shininess: 10 } );
这是一个不错的起始材质,灰白色和略带光泽。
虽然目前只有一种材质,但为了后续方便为椅子各个部分设置不同颜色或加载的纹理,我们将材质的数据结构声明为一个数组。
// 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},
];
再次遍历 3D 模型(的组成元素),并使用 childID 查找椅子的不同部分,然后设置相应材质(mtl
属性)。模型每个元素的名字都是在 Blender 中设置的,这在第一节讲到。
在 loader 函数下方,添加一个参数为模型、模型的哪部分(type)和材质的函数。我们还为模型的组成元素添加了一个后续会用到的新属性 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
}
}
});
}
在 loader 函数内的 scene.add(theModel)
前遍历 INITIAL_MAP 数组,并执行该函数,:
// Set initial textures
for (let object of INITIAL_MAP) {
initColor(theModel, object.childID, object.mtl);
}
最后,回到地板,将其颜色从红色(0xff0000)改为亮灰色(0xeeeeee)。
// Floor
var floorGeometry = new THREE.PlaneGeometry(5000, 5000, 1, 1);
var floorMaterial = new THREE.MeshPhongMaterial({
color: 0xeeeeee, // <------- Here
shininess: 0
});
这里值得注意的是:0xeeeeee
与背景色不同。但在光的作用下,它们看起来一致。
3D Chair Customizer Tutorial - Part 1
恭喜,越来越像样了!如果你卡在某一节点,可以 fork 这个 pen 或参考它,直至找到问题所在。
Part 4:添加控制
实际上,本章节很短,这得益于第三个依赖——OrbitControls.js,让一切变得十分简单。
在声明 animate 函数上方,添加以下控制代码:
// 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
在 animate 函数内的顶部,添加:
controls.update();
controls
是 OrbitControls 的实例。你可以随意更改其参数,其中包括允许用户旋转椅子(上下)、禁用拖拽以使椅子保持在中心、启用了阻尼使其过渡更自然,还有自动旋转功能(根据个人情况启用与否),但目前是关闭状态。
用你的鼠标或触摸屏进行体验吧!
Part 5:更改颜色
到现在,我们的程序还没进入主题,所以接下来会专注于更改颜色(纹理)。
在 canvas 标签下方添加:
<div class="controls">
<!-- This tray will be filled with colors via JS, and the ability to slide this panel will be added in with a lightweight slider script (no dependency used for this) -->
<div id="js-tray" class="tray">
<div id="js-tray-slide" class="tray__slide"></div>
</div>
</div>
.controls
DIV 标签吸附在视口底部,.tray
设为 100%(相对于 body
),其子元素 .tray__slide
作为色板,色板可根据需要进行补充。
首先添加几种颜色。在 JavaScript 顶部,添加含有 5 个对象的数组,每个对象都带有 color
属性。
const colors = [
{
color: '66533C'
},
{
color: '173A2F'
},
{
color: '153944'
},
{
color: '27548D'
},
{
color: '438AAC'
}
]
注意:以上十六进制颜色值既没有 #,也没有 0x。这是因为它的使用场景不止一种(CSS 和 Three.js)。另外,之所以使用对象,是因为能添加其他属性,如亮度(shininess)、图片纹理。
使用这些颜色制作色板!
首先在 JavaScript 顶部引用滑块:
const TRAY = document.getElementById('js-tray-slide');
在 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);
上面是我们基于 colors
数组创建的色板列表!注意我们同时为它们设置了 data-key
属性,这是用于查找生成材质的颜色值。
在 buildColors
函数下方,为色板添加事件处理函数:
// Swatches
const swatches = document.querySelectorAll(".tray__swatch");
for (const swatch of swatches) {
swatch.addEventListener('click', selectSwatch);
}
点击事件的处理函数命名为 selectSwatch
。它会基于色值创建新的 PhongMaterial,并调用另一个函数来遍历 3D 模型(的组成元素),对匹配的部分更换材质!
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);
}
该函数通过 data-key 属性匹配颜色,并基于该颜色创建新材质。
该函数仍不能工作,需要添加 setMaterial
函数。
注意:setMaterial(theModel, 'legs', new_mtl);
第二个参数目前暂且传入'legs'
,很快我们就有指定不同部分的能力。目前,首先要实现 setMaterial 函数。
在该函数下方,添加 setMaterial
函数:
function setMaterial(parent, type, mtl) {
parent.traverse((o) => {
if (o.isMesh && o.nameID != null) {
if (o.nameID == type) {
o.material = mtl;
}
}
});
}
这与 initColor
函数大同小异。nameID
属性来自 initColor
,若它与参数 type
相等,就为它添加材质。
现在我们的色板能创建新材质,并更改椅腿的颜色,快来试一试!
Swatches change the legs color!
Part 6:指定哪个部分进行更改
我们已经能更改椅腿的颜色。现在就让我们添加指定更改哪部分颜色的能力。在 body
标签内的顶部添加以下 HTML:
<!-- These toggle the the different parts of the chair that can be edited, note data-option is the key that links to the name of the part in the 3D file -->
<div class="options">
<div class="option --is-active" data-option="legs">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/legs.svg" alt=""/>
</div>
<div class="option" data-option="cushions">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/cushions.svg" alt=""/>
</div>
<div class="option" data-option="base">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/base.svg" alt=""/>
</div>
<div class="option" data-option="supports">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/supports.svg" alt=""/>
</div>
<div class="option" data-option="back">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/back.svg" alt=""/>
</div>
</div>
这是带有自定义图标的按钮集合。.option
DIV 吸附在视口一侧(另外,通过 CSS 的媒介查询还会使其随着视口大小而进行调整)。每个 .option
DIV 都是白色正方形,而带有 --is-active
类名的还会有红色边框。另外,还带有用于匹配 nameID data-option
属性。最后,image
元素拥有 pointer-events
属性,即使点击了 image
,点击事件的触发始终保留在其父元素。
在 JavaScript 顶部添加另一个变量 activeOptions
,其默认值为 legs
:
var activeOption = 'legs';
回到 selectSwatch
函数,更改硬编码的 legs
参数为 activeOption
:
setMaterial(theModel, activeOption, new_mtl);
现在我们需要做的是创建事件处理函数,当点击 .option
时更改 activeOption
。
在 const swtaches
和 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');
}
该函数会将 event.target
的 data-option
值设为 activeOption
,并切换 --is-active
类。
体验一下
止步于此?物体全是一种材质类型时,难免乏味。下面就增加木和纺织布材质:
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'
}
]
前两个是纹理,分别是木和牛仔布。另外,还增加了两个新属性——size
和 shininess
。size
表示重复图案的频率,所以数值越大图案越密集。
现在我们要更新两个函数以支持该特性。首先将 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);
}
}
现在它会检查是否存在 texture
属性,若存在,则将色板的背景设为该纹理。
注意到第 5、6 块色板之间的间距了吗?我通过 CSS 将每 5 个色板作为一组,这对于拥有更多色板数量时显得尤为重要。
第二个需要更新的函数是 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);
}
该函数会检查当前色板是不是纹理,若是,则通过 Three.js 的 TextureLoader 方法创建新纹理,并将该纹理的 repeat
设为色板 size
值。另外,还为纹理设置 wrapping
(经试验后得出效果最佳的 wrapping
值),然后为 PhongMaterial 的 map
属性设置为当前纹理,最后设置 shininess
值。
如果当前色板无 texture 属性,则使用老方法。请注意,你也可以为了纯色的材质设置 shininess
值。
重要:如果添加纹理后椅子表现为黑色,请查看 console,判断是否是跨域导致的问题?这也是 CodePen 的问题,建议注册 Cloudinary 并使用其免费套餐存放图片。
这里带有纹理的 pen:
Part 7:收尾工作
我曾有个项目交付给客户验收,这个项目有一个大按钮去祈求被按,甚至在 hover 时闪闪发光,然而客户及其同事(会计部的 Dave)却反馈他们不知道要按什么(去你的,Dave)。
在 canvas 标签上方添加一些号召性语句:
<!-- Just a quick notice to the user that it can be interacted with -->
<span class="drag-notice" id="js-drag-notice">Drag to rotate 360°</span>
通过 CSS 将其放在椅子前方,用于指示用户可拖拽旋转椅子。但椅子仅仅呆滞不动?
让椅子在首次加载后进行旋转,旋转完毕后隐藏引导语。
首先在 JavaScript 上方添加 loaded
变量,并设为 false
:
var loaded = false;
在 JavaScript 底部添加该函数:
// Function - Opening rotate
let initRotate = 0;
function initialRotation() {
initRotate++;
if (initRotate <= 120) {
theModel.rotation.y += Math.PI / 60;
} else {
loaded = true;
}
}
模型需要在 120 帧内线性旋转 360 度(约 2 秒,60fps),所以我们将在 animate 函数中运行该函数 120 次,一旦完成则将 loaded
设为 true
。代码如下:
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();
我们判断 theModel
是否不等于 null
且 loaded
是否为 false
,若符合条件,则调用该函数 120 次,然后将 loaded
设为 true
,使得 animate
函数最终忽略它。
这就拥有了自旋转的椅子。椅子停止的这一刻是删除引导语的好时机。
在 CSS 中,为引导语添加一个带有隐藏动画的类,该动画的延迟时间为 3 秒,所以,在开始旋转椅子的同时为引导语添加该类。
在 JavaScript 顶部引用引导语:
const DRAG_NOTICE = document.getElementById('js-drag-notice');
更新 animate
函数:
if (theModel != null && loaded == false) {
initialRotation();
DRAG_NOTICE.classList.add('start');
}
好极了!这里有更丰富的颜色供你选择。同时,下方也提供了轻量无依赖的滑动功能(用于拖拽滑动色板列表):
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'
}
]
在 JavaScript 底部添加 slider 函数,它将使你拥有可通过鼠标或触摸屏拖拽色板的能力。为了紧扣主题,这里就不过多研究其工作原理。
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);
现在,将 CSS 内的 .tray__slider
小动画注释掉:
/* transform: translateX(-50%);
animation: wheelin 1s 2s ease-in-out forwards; */
剩下最后两步收尾工作就完成了!
更新 .controls
div,让其包含引导语:
<div class="controls">
<div class="info">
<div class="info__message">
<p><strong> Grab </strong> to rotate chair. <strong> Scroll </strong> to zoom. <strong> Drag </strong> swatches to view more.</p>
</div>
</div>
<!-- This tray will be filled with colors via JS, and the ability to slide this panel will be added in with a lightweight slider script (no dependency used for this) -->
<div id="js-tray" class="tray">
<div id="js-tray-slide" class="tray__slide"></div>
</div>
</div>
现在我们拥有了一个新的信息块,其包含描述如何控制应用的一些说明。
最后,增加一个 loading 遮罩层,以确保在应用加载期间页面是干净的,并在模型加载后将其删除。
在 body 内的顶部增加以下 HTML。
<!-- The loading element overlays all else until the model is loaded, at which point we remove this element from the DOM -->
<div class="loading" id="js-loader"><div class="loader"></div></div>
为了使其优先加载,我们将这些 CSS 单独放在 head 标签内,而不是链接式的 CSS 中。所以,在 head 闭合标签上方添加以下 CSS。
.loading {
position: fixed;
z-index: 50;
width: 100%;
height: 100%;
top: 0; left: 0;
background: #f1f1f1;
display: flex;
justify-content: center;
align-items: center;
}
.loader{
-webkit-perspective: 120px;
-moz-perspective: 120px;
-ms-perspective: 120px;
perspective: 120px;
width: 100px;
height: 100px;
}
.loader:before{
content: "";
position: absolute;
left: 25px;
top: 25px;
width: 50px;
height: 50px;
background-color: #ff0000;
animation: flip 1s infinite;
}
@keyframes flip {
0% {
transform: rotate(0);
}
50% {
transform: rotateY(180deg);
}
100% {
transform: rotateY(180deg) rotateX(180deg);
}
}
快好了!在加载模型后将其删除。
在 JavaScript 顶部引用它:
const LOADER = document.getElementById('js-loader');
在 loader
函数中,在 scene.add(theModel)
后放置以下代码:
// Remove the loader
LOADER.remove();
现在,模型会在该 DIV 背后加载:
就这样!以下就是完整的 pen,仅供参考!
See the Pen 3D Chair Customizer Tutorial - Part 4 by Kyle Wetton (@kylewetton) on CodePen.
你还可以体验托管在 Codrops 上的 案例。
感谢您的支持!
这是一篇长篇教程。如果你发现错误,请在评论告诉我。
四、3D合体立体魔方用一张纸怎么做?
要做3d合体立体魔方需要几张纸,不只用一张纸。下面是制作3d合体立体魔方的步骤:
材料:
-6张方格纸
步骤:
1. 制作6个正方形的立方体,每个立方体由4张方格纸制成。每张方格纸裁成一个正方形,边长为5cm左右。
2. 每个立方体都要把纸边上的脚边向内折叠,使边缘处有4个厚度,可以增加稳定性。
3. 用胶带把每个立方体的6个面连接起来,形成完整的立方体。
4. 最后将6个立方体组合在一起,形成一个3d合体立体魔方。
注意:
-尽可能保持每个立方体的尺寸和角度一致,这样组合时才不会出现错位。
-精心制作和精准度对于3d合体立体魔方的完成至关重要。
五、做室内设计毕业设计,想问一下想花钱让别人外包一套3d建模效果图加原文件大概需要多少钱(别墅加全景)?
市面上,全景一张的价格600--2000不等,看要求。学生毕业设计的话,不用做那么好,水一点的话,300块以内吧!
六、做3D设计绘图,组装台式机什么配置,价位4-5千加显示器?
。。四五千你还想干嘛 我是建议吧 上淘宝 买个整机 四千左右 买个i5+1060 6G的机子 攀升好像有 不过你得把内存升级16G 电源升级一下 其他的 先将就 然后显示器 上淘宝买个二手的 大概六七百就有比较不错的了 收到不满意就退货 运费也不高 跟你讲 整机是很便宜 因为硬件是定制的缩水版 显卡可能是二手卡或者矿卡 比正常的质量肯定差点 不过店铺有保修 勉强用用还是可以的 如果不想的话 那就买那种贵点的整机 要跟市场价差不多的 或者自己买硬件组 照着3000左右的机子的价格组 应该自己组差不多4000多点
- 相关评论
- 我要评论
-