背景
你是嘿嘿嘿侦探社实习侦探🕵️,接到下级指派工作,到甄开心小镇🏠考察市民甄不戳👨宝石💎失窃案,依据线人流浪汉老石👨🎤提供的线索,小偷就躲在小镇,快把他找进去,帮甄不戳寻回失窃的宝石吧!
本文应用 Three.js
SphereGeometry
创立 3D
全景图预览性能,并在全景图中增加二维 SpriteMaterial
、Canvas
、三维 GLTF
等交互点,实现具备场景切换、点击交互的侦探小游戏。
实现成果
左右滑动屏幕,找到 3D
全景场景中的 交互点
并点击,找出嫌疑人真正藏匿的地位。
已适配挪动端,能够在手机上关上拜访。
💡
在线预览:https://dragonir.github.io/3d…
代码实现
初始化场景
创立场景,增加摄像机、光源、渲染。
// 透视摄像机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1100);
camera.target = new THREE.Vector3(0, 0, 0);
scene = new THREE.Scene();
// 增加环境光
light = new THREE.HemisphereLight(0xffffff);
light.position.set(0, 40, 0);
scene.add(light);
// 增加平行光
light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 40, -10);
scene.add(light);
// 渲染
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
应用球体实现全景性能
// 创立全景场景
geometry = new THREE.SphereGeometry(500, 60, 60);
// 按z轴翻转
geometry.scale(1, 1, -1);
// 增加室外低画质贴图
outside_low = new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load('./assets/images/outside_low.jpg')
});
// 增加室内低画质贴图
inside_low = new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load('./assets/images/inside_low.jpg')
});
mesh = new THREE.Mesh(geometry, outside_low);
// 异步加载高清纹理图
new THREE.TextureLoader().load('./assets/images/outside.jpg', texture => {
outside = new THREE.MeshBasicMaterial({
map: texture
});
mesh.material = outside;
});
// 增加到场景中
scene.add(mesh);
📌
全景贴图如上图所示,图片来源于Bing
。
💡
球体 SphereGeometry
构造函数:
THREE.SphereGeometry(radius, segmentsWidth, segmentsHeight, phiStart, phiLength, thetaStart, thetaLength)
radius
:半径;segmentsWidth
:经度上的分段数;segmentsHeight
:纬度上的分段数;phiStart
:经度开始的弧度;phiLength
:经度跨过的弧度;thetaStart
:纬度开始的弧度;thetaLength
:纬度跨过的弧度。
💡
根底网格材质 MeshBasicMaterial
球体的材质应用的是 MeshBasicMaterial
, 是一种简略的材质,这种材质不受场景中光照的影响。应用这种材质的网格会被渲染成简略的立体多边形,而且也能够显示几何体的线框。
构造函数:
MeshBasicMaterial(parameters: Object)
parameters
:(可选)用于定义材质外观的对象,具备一个或多个属性。
属性:
.alphaMap[Texture]
:alpha
贴图是一张灰度纹理,用于管制整个外表的不透明度。(彩色:齐全通明;红色:齐全不通明
)。默认值为null
。.aoMap[Texture]
:该纹理的红色通道用作环境遮挡贴图。默认值为null
。.aoMapIntensity[Float]
:环境遮挡成果的强度。默认值为1
。零是不遮挡成果。.color[Color]
:材质的色彩,默认值为红色0xffffff
。.combine[Integer]
:如何将外表色彩的后果与环境贴图(如果有)联合起来。选项为THREE.Multiply
(默认值),THREE.MixOperation
,THREE.AddOperation
。如果抉择多个,则应用.reflectivity
在两种色彩之间进行混合。.envMap[Texture]
:环境贴图。默认值为null
。.lightMap[Texture]
:光照贴图。默认值为null
。.lightMapIntensity[Float]
:烘焙光的强度。默认值为1
。.map[Texture]
:纹理贴图。默认为null
。.morphTargets[Boolean]
:材质是否应用morphTargets
。默认值为false
。.reflectivity[Float]
:环境贴图对外表的影响水平,默认值为1
,无效范畴介于0
(无反射)和1
(齐全反射)之间。.refractionRatio[Float]
:折射率不应超过1
。默认值为0.98
。.specularMap[Texture]
:材质应用的高光贴图。默认值为null
。.wireframe[Boolean]
:将几何体渲染为线框。默认值为false
(即渲染为立体多边形)。.wireframeLinecap[String]
:定义线两端的外观。可选值为butt
,round
和square
。默认为round
。.wireframeLinejoin[String]
:定义线连贯节点的款式。可选值为round
,bevel
和miter
。默认值为round
。.wireframeLinewidth[Float]
:控制线框宽度。默认值为1
。
💡
TextureLoader
TextureLoader
从给定的URL开始加载并将齐全加载的 texture
传递给 onLoad
。该办法还返回一个新的纹理对象,该纹理对象能够间接用于材质创立,加载材质的一个类,外部应用 ImageLoader
来加载文件。
构造函数:
TextureLoader(manager: LoadingManager)
manager
:加载器应用的loadingManager
,默认值为THREE.DefaultLoadingManager
。
办法:
.load(url: String, onLoad: Function, onProgress: Function, onError: Function) : Texture
url
:文件的URL
或者门路,也能够为Data URI
。onLoad
:加载实现时将调用。回调参数为将要加载的texture
。onProgress
:将在加载过程中进行调用。参数为XMLHttpRequest
实例,实例蕴含total
和loaded
参数。onError
:在加载谬误时被调用。
增加交互点
新建交互点数组,蕴含每个交互点的名称、缩放比例、空间坐标。
var interactPoints = [
{ name: 'point_0_outside_house', scale: 2, x: 0, y: 1.5, z: 24 },
{ name: 'point_1_outside_car', scale: 3, x: 40, y: 1, z: -20 },
{ name: 'point_2_outside_people', scale: 3, x: -20, y: 1, z: -30 },
{ name: 'point_3_inside_eating_room', scale: 2, x: -30, y: 1, z: 20 },
{ name: 'point_4_inside_bed_room', scale: 3, x: 48, y: 0, z: -20 }
];
增加二维动态图片交互点
let pointMaterial = new THREE.SpriteMaterial({
map: new THREE.TextureLoader().load('./assets/images/point.png')
});
interactPoints.map(item => {
let point = new THREE.Sprite(pointMaterial);
point.name = item.name;
point.scale.set(item.scale * 1.2, item.scale * 1.2, item.scale * 1.2);
point.position.set(item.x, item.y, item.z);
scene.add(point);
});
💡
精灵材质 SpriteMaterial
构造函数:
SpriteMaterial(parameters : Object)
parameters
:可选,用于定义材质外观的对象,具备一个或多个属性。材质的任何属性都能够从此处传入(包含从Material
和ShaderMaterial
继承的任何属性)。SpriteMaterials
不会被Material.clippingPlanes
裁剪。
属性:
.alphaMap[Texture]
:alpha
贴图是一张灰度纹理,用于管制整个外表的不透明度。默认值为 null
。.color[Color]
:材质的色彩,默认值为红色 0xffffff
。 .map
会和 color
相乘。.map[Texture]
:色彩贴图。默认为 null
。.rotation[Radians]
:sprite
的转动,以弧度为单位。默认值为 0
。.sizeAttenuation[Boolean]
:精灵的大小是否会被相机深度衰减。(仅限透视摄像头。)默认为 true
。
应用同样的办法,加载嫌疑人二维图片增加到场景中。
function loadMurderer() {
let material = new THREE.SpriteMaterial({
map: new THREE.TextureLoader().load('./assets/models/murderer.png')
});
murderer = new THREE.Sprite(material);
murderer.name = 'murderer';
murderer.scale.set(12, 12, 12);
murderer.position.set(43, -3, -20);
scene.add(murderer);
}
增加三维动静模型锚点
通过加载地标锚点形态的 gltf
模型来实现三维动静锚点,加载 gltf
须要独自引入 GLTFLoader.js
,地标模型应用 Blender
构建。
var loader = new THREE.GLTFLoader();
loader.load('./assets/models/anchor.gltf', object => {
object.scene.traverse(child => {
if (child.isMesh) {
// 批改材质款式
child.material.metalness = .4;
child.name.includes('黄') && (child.material.color = new THREE.Color(0xfffc00))
}
});
object.scene.rotation.y = Math.PI / 2;
interactPoints.map(item => {
let anchor = object.scene.clone();
anchor.position.set(item.x, item.y + 3, item.z);
anchor.name = item.name;
anchor.scale.set(item.scale * 3, item.scale * 3, item.scale * 3);
scene.add(anchor);
})
});
须要在 requestAnimationFrame
中通过批改模型的 rotation
来实现自传动画成果。
function animate() {
requestAnimationFrame(animate);
anchorMeshes.map(item => {
item.rotation.y += 0.02;
});
}
增加二维文字提醒
能够应用 Canvas
创立文字提醒增加到场景中。
function makeTextSprite(message, parameters) {
if (parameters === undefined) parameters = {};
var fontface = parameters.hasOwnProperty("fontface") ? parameters["fontface"] : "Arial";
var fontsize = parameters.hasOwnProperty("fontsize") ? parameters["fontsize"] : 32;
var borderThickness = parameters.hasOwnProperty("borderThickness") ? parameters["borderThickness"] : 4;
var borderColor = parameters.hasOwnProperty("borderColor") ? parameters["borderColor"] : { r: 0, g: 0, b: 0, a: 1.0 };
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
context.font = fontsize + "px " + fontface;
var metrics = context.measureText(message);
var textWidth = metrics.width;
context.strokeStyle = "rgba(" + borderColor.r + "," + borderColor.g + "," + borderColor.b + "," + borderColor.a + ")";
context.lineWidth = borderThickness;
context.fillStyle = "#fffc00";
context.fillText(message, borderThickness, fontsize + borderThickness);
context.font = 48 + "px " + fontface;
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
var spriteMaterial = new THREE.SpriteMaterial({ map: texture });
var sprite = new THREE.Sprite(spriteMaterial);
return sprite;
}
应用办法:
outsideTextTip = makeTextSprite('进入室内查找');
outsideTextTip.scale.set(2.2, 2.2, 2)
outsideTextTip.position.set(-0.35, -1, 10);
scene.add(outsideTextTip);
💡
Canvas
画布能够作为Three.js
纹理贴图CanvasTexture
。Canvas
画布能够通过2D API
绘制各种各样的几何形态,能够通过Canvas
绘制一个轮廓后而后作为Three.js
网格模型、精灵模型等模型对象的纹理贴图。💡
measureText()
办法返回一个对象,该对象蕴含以像素计的指定字体宽度。如果您须要在文本向画布输入之前,就理解文本的宽度,那么请应用该办法。measureText
语法:context.measureText(text).width
。
增加三维文字提醒
因为工夫无限,三维文字
本示例中并未用到,然而在页面中应用 3D
文字会实现更好的视觉效果,想理解具体实现细节,能够浏览我的另一篇文章,后续的鼠标捕捉等内容也在该文中有具体解说。
🔗
传送门:应用three.js实现炫酷的酸性格调3D页面
鼠标捕捉
应用 Raycaster
获取点击选中网格对象,并增加点击交互。
function onDocumentMouseDown(event) {
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(interactMeshes);
if (intersects.length > 0) {
let name = intersects[0].object.name;
if (name === 'point_0_outside_house') {
camera_time = 1;
} else if (name === 'point_4_inside_bed_room') {
Toast('小偷就在这里', 2000);
loadMurderer();
} else {
Toast(`小偷不在${name.includes('car') ? '车里' : name.includes('people') ? '人群' : name.includes('eating') ? '餐厅' : '这里'}`, 2000);
}
}
onPointerDownPointerX = event.clientX;
onPointerDownPointerY = event.clientY;
onPointerDownLon = lon;
onPointerDownLat = lat;
}
场景切换
function update() {
lat = Math.max(-85, Math.min(85, lat));
phi = THREE.Math.degToRad(90 - lat);
theta = THREE.Math.degToRad(lon);
camera.target.x = 500 * Math.sin(phi) * Math.cos(theta);
camera.target.y = 500 * Math.cos(phi);
camera.target.z = 500 * Math.sin(phi) * Math.sin(theta);
camera.lookAt(camera.target);
if (camera_time > 0 && camera_time < 50) {
camera.target.x = 0;
camera.target.y = 1;
camera.target.z = 24;
camera.lookAt(camera.target);
camera.fov -= 1;
camera.updateProjectionMatrix();
camera_time++;
outsideTextTip.visible = false;
} else if (camera_time === 50) {
lat = -2;
lon = 182;
camera_time = 0;
camera.fov = 75;
camera.updateProjectionMatrix();
mesh.material = inside_low;
// 加载新的全景图场景
new THREE.TextureLoader().load('./assets/images/inside.jpg', function (texture) {
inside = new THREE.MeshBasicMaterial({
map: texture
});
mesh.material = inside;
});
loadMarker('inside');
}
renderer.render(scene, camera);
}
💡
透视相机的属性创立实现后咱们能够依据集体需要随便批改,然而相机的属性批改后,须要调用updateProjectionMatrix()
办法来更新。💡
THREE.Math.degToRad
:将度转化弧度。
到这里,3D
全景性能全副实现。
🔗
残缺代码:https://github.com/dragonir/3…
总结
本案例次要波及到的知识点包含:
- 球体
SphereGeometry
- 根底网格材质
MeshBasicMaterial
- 精灵材质
SpriteMaterial
- 材质加载
TextureLoader
- 文字纹理
Canvas
- 鼠标捕捉
Raycaster
参考资料
- [1]. 在React中用Three.js实现Web VR全景看房
- [2]. 应用three.js实现炫酷的酸性格调3D页面
发表回复