2545光年之外的开普勒1028星系,有一颗色彩斑斓的宜居星球
,星际移民 `` 必须穿戴**基地**发放的防辐射服才能生存。阿狸
驾驶星际飞行器 `` 降临此地,快帮它在限定时间内使用轮盘移动找到基地获取防辐射服吧!
本文使用 Three.js + React + CANNON
技术栈,实现通过滑动屏幕控制模型在 3D
世界里运动的 Low Poly
低多边形风格小游戏。本文主要涉及到的知识点包括:Three.js
阴影类型、创建粒子系统、cannon.js
基本用法、使用 cannon.js
高度场 Heightfield
创建地形、通过轮盘移动控制模型动画等。
在线预览:
已适配:
PC
端`` 小提示:站得越高看得越远,隐隐约约听说基地位于初始位置的西面,开始时应该向左前方前进哦。
游戏流程如下图所示:页面加载完成后玩家
点击开始按钮,然后在限定时间内通过控制页面底部轮盘 移动模型,找到目标基地所在的位置。寻找成功或失败都会显示结果页
,结果上面有两个按钮再试一次和自由探索,点击再试一次时间会重置,然后重新回到起点开始倒计时。点击自由探索则不在计时,玩家可以在 3D
开放世界里操作模型自由探索。同时,游戏内页面也提供一个时光倒流按钮,它的作用是玩家
可以在失败前自己手动重置倒计时 ⏳
,重新回到起点开始游戏。
加载开发所需的必备资源:GLTFLoader
用于加载狐狸 和基地
模型、CANNON
是用于创建 3D
世界的物理引擎;CannonHelper
是对 CANNON
一些使用方法的封装;JoyStick
用于创建通过监听鼠标移动位置或触碰屏幕产生的位移来控制模型移动的轮盘 ``。
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import CANNON from 'cannon';
import CannonHelper from './scripts/CannonHelper';
import JoyStick from './scripts/JoyStick';
页面结构比较简单,.webgl
用于渲染 WEBGL
;.tool
是游戏内的工具栏,用于重置游戏和显示一些提示语;.loading
是游戏加载页面,用来显示游戏加载进度、介绍游戏规则、显示游戏开始按钮;.result
是游戏结果页面,用于显示游戏成功或失败结果,并提供再试一次和自由探索两个按钮 ``。
(<div id="metaverse">
<canvas className='webgl'></canvas>
<div className='tool'>
<div className='countdown'>{ this.state.countdown }</div>
<button className='reset_button' onClick={this.resetGame}>时光倒流</button>
<p className='hint'>站得越高看得越远</p>
</div>
{ this.state.showLoading ? (<div className='loading'>
<div className='box'>
<p className='progress'>{this.state.loadingProcess} %</p>
<p className='description'>游戏描述</p>
<button className='start_button' style={{'visibility': this.state.loadingProcess === 100 ? 'visible' : 'hidden'}} onClick={this.startGame}>开始游戏</button>
</div>
</div>) : '' }
{ this.state.showResult ? (<div className='result'>
<div className='box'>
<p className='text'>{ this.state.resultText }</p>
<button className='button' onClick={this.resetGame}>再试一次</button>
<button className='button' onClick={this.discover}>自由探索</button>
</div>
</div>) : '' }
</div>)
数据变量包括加载进度、是否显示加载页面、是否显示结果页、结果页文案、倒计时、是否开启自由探索等。
state = {
loadingProcess: 0,
showLoading: true,
showResult: false,
resultText: '失败',
countdown: 60,
freeDiscover: false
}
初始化场景 、相机
、光源 ``。
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('canvas.webgl'),
antialias: true,
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
// 添加主相机
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, .01, 100000);
camera.position.set(1, 1, -1);
camera.lookAt(scene.position);
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, .4);
scene.add(ambientLight)
// 添加平行光
var light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1).normalize();
scene.add(light);
本文使用了 THREE.PCFSoftShadowMap
以开启效果更加柔和的阴影,Three.js
提供以下几种阴影类型:
THREE.BasicShadowMap
:提供未经过滤的阴影贴图,性能最快,但质量最低。THREE.PCFShadowMap
:使用 Percentage-Closer Filtering (PCF)
算法过滤阴影贴图,是默认类型。THREE.PCFSoftShadowMap
:使用 PCF
算法过滤的更加柔和的阴影贴图,尤其是在使用低分辨率阴影贴图时。THREE.VSMShadowMap
:使用方差阴影贴图 VSM
算法过滤的阴影贴图。使用 VSMShadowMap
时,所有阴影接收者也会投射阴影。使用 Cannon.js
初始化物理世界 ``。
// 初始化物理世界
const world = new CANNON.World();
// 在多个步骤的任意轴上测试刚体的碰撞
world.broadphase = new CANNON.SAPBroadphase(world);
// 设置物理世界的重力为沿y轴向上-10米每二次方秒
world.gravity.set(0, -10, 0);
// 创建默认联系材质
world.defaultContactMaterial.friction = 0;
const groundMaterial = new CANNON.Material("groundMaterial");
const wheelMaterial = new CANNON.Material("wheelMaterial");
const wheelGroundContactMaterial = new CANNON.ContactMaterial(wheelMaterial, groundMaterial, {
// 摩擦系数
friction: 0,
// 恢复系数
restitution: 0,
// 接触刚度
contactEquationStiffness: 1000
});
world.addContactMaterial(wheelGroundContactMaterial);
Cannon.js
是用 JavaScript
实现的物理引擎库,可以与任何支持浏览器的渲染或游戏引擎,可以用于模拟刚体,实现 3D
世界 `中更加真实的物理形式的移动和交互。更多
Cannon.js相关
API` 文档和示例可以参考文章末尾链接。
创建 1000
个粒子用于模型星空 ✨
,并将它们添加到场景中。本示例中通过着色器形式创建粒子,这样更有利于 GPU
渲染效率。
const textureLoader = new THREE.TextureLoader();
const shaderPoint = THREE.ShaderLib.points;
const uniforms = THREE.UniformsUtils.clone(shaderPoint.uniforms);
uniforms.map.value = textureLoader.load(snowflakeTexture);
for (let i = 0; i < 1000; i++) {
sparkGeometry.vertices.push(new THREE.Vector3());
}
const sparks = new THREE.Points(new THREE.Geometry(), new THREE.PointsMaterial({
size: 2,
color: new THREE.Color(0xffffff),
map: uniforms.map.value,
blending: THREE.AdditiveBlending,
depthWrite: false,
transparent: true,
opacity: 0.75
}));
sparks.scale.set(1, 1, 1);
sparks.geometry.vertices.map(spark => {
spark.y = randnum(30, 40);
spark.x = randnum(-500, 500);
spark.z = randnum(-500, 500);
return true;
});
scene.add(sparks);
通过 CANNON.Heightfield
高度场创建 128 x 128 x 60
可视化渐变色地形。地形的凹凸起伏状态是通过以下高度图 HeightMap
实现,它是一张黑白图片 ,通过像素点的颜色深浅来记录高度信息,根据高度图数据信息创建地形网格。可通过文章末尾提供的链接在线生成随机高度图。地形生成完成并将它添加到世界
中,然后在 animate
方法中页面重绘时调用 check
方法,用于检测和更新模型在地形上的位置。
const cannonHelper = new CannonHelper(scene);
var sizeX = 128, sizeY = 128, minHeight = 0, maxHeight = 60, check = null;
Promise.all([
// 加载高度图
img2matrix.fromUrl(heightMapImage, sizeX, sizeY, minHeight, maxHeight)(),
]).then(function (data) {
var matrix = data[0];
// 地形体
const terrainBody = new CANNON.Body({ mass: 0 });
// 地形形状
const terrainShape = new CANNON.Heightfield(matrix, { elementSize: 10 });
terrainBody.addShape(terrainShape);
// 地形位置
terrainBody.position.set(-sizeX * terrainShape.elementSize / 2, -10, sizeY * terrainShape.elementSize / 2);
// 设置从轴角度
terrainBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.add(terrainBody);
// 将生成的地形刚体可视化
cannonHelper.addVisual(terrainBody, 'landscape');
var raycastHelperGeometry = new THREE.CylinderGeometry(0, 1, 5, 1.5);
raycastHelperGeometry.translate(0, 0, 0);
raycastHelperGeometry.rotateX(Math.PI / 2);
var raycastHelperMesh = new THREE.Mesh(raycastHelperGeometry, new THREE.MeshNormalMaterial());
scene.add(raycastHelperMesh);
// 使用 Raycaster检测并更新模型在地形上的位置
check = () => {
var raycaster = new THREE.Raycaster(target.position, new THREE.Vector3(0, -1, 0));
var intersects = raycaster.intersectObject(terrainBody.threemesh.children[0]);
if (intersects.length > 0) {
raycastHelperMesh.position.set(0, 0, 0);
raycastHelperMesh.lookAt(intersects[0].face.normal);
raycastHelperMesh.position.copy(intersects[0].point);
}
target.position.y = intersects && intersects[0] ? intersects[0].point.y + 0.1 : 30;
var raycaster2 = new THREE.Raycaster(shelterLocation.position, new THREE.Vector3(0, -1, 0));
var intersects2 = raycaster2.intersectObject(terrainBody.threemesh.children[0]);
shelterLocation.position.y = intersects2 && intersects2[0] ? intersects2[0].point.y + .5 : 30;
shelterLight.position.y = shelterLocation.position.y + 50;
shelterLight.position.x = shelterLocation.position.x + 5
shelterLight.position.z = shelterLocation.position.z;
}
});
本示例中凹凸不平的地形是通过 CANNON.Heightfield
实现的,它是 Cannon.js
物理引擎的高度场。在物理学中把某个物理量在空间中一个区域内的分布称为场,高度场就是与高度相关的场。Heightfield
的高度就是关于两个变量的函数,可以表达为 HEIGHT(i,j)
。
Heightfield(data, options)
data
是一个 y值
数组,将用于构建地形。options
是一个配置项,有三个可配置参数:minValue
是数据数组中数据点的最小值。如果未给出,将自动计算。maxValue
最大值。elementSize
是 x轴
方向上数据点之间的世界间距。使用 LoadingManager
管理加载进度,当页面模型加载完成之后,加载进度页面显示开始游戏菜单。
const loadingManager = new THREE.LoadingManager();
loadingManager.onProgress = async (url, loaded, total) => {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
};
加载基地模型 `前先创建一个
shelterLocation网格用来放置基地模型,该网格对象还用于后续地形检测。然后使用
GLTFLoader加载基地模型,然后把它添加到
shelterLocation网格上。最后添加一个
PointLight给基地模型添加彩色点光源,添加一个 `DirectionalLight
用于生成阴影。
const shelterGeometry = new THREE.BoxBufferGeometry(0.15, 2, 0.15);
const shelterLocation = new THREE.Mesh(shelterGeometry, new THREE.MeshNormalMaterial({
transparent: true,
opacity: 0
}));
shelterLocation.position.set(this.shelterPosition.x, this.shelterPosition.y, this.shelterPosition.z);
shelterLocation.rotateY(Math.PI);
scene.add(shelterLocation);
// 加载模型
gltfLoader.load(Shelter, mesh => {
mesh.scene.traverse(child => {
child.castShadow = true;
});
mesh.scene.scale.set(5, 5, 5);
mesh.scene.position.y = -.5;
shelterLocation.add(mesh.scene)
});
// 添加光源
const shelterPointLight = new THREE.PointLight(0x1089ff, 2);
shelterPointLight.position.set(0, 0, 0);
shelterLocation.add(shelterPointLight);
const shelterLight = new THREE.DirectionalLight(0xffffff, 0);
shelterLight.position.set(0, 0, 0);
shelterLight.castShadow = true;
shelterLight.target = shelterLocation;
scene.add(shelterLight);
狐狸 模型的加载也是类似的,需要先创建一个目标网格,后续用于地形检测,然后把狐狸
模型添加到目标网格上。狐狸 模型完成加载后,需要保存它的 `clip1`、 `clip1` 两种动画效果,后续需要通过判断轮盘
的移动状态来判断播放哪种动画。最后添加一个 `DirectionalLight``` 光源来产生阴影。
var geometry = new THREE.BoxBufferGeometry(.5, 1, .5);
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, .5, 0));
const target = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial({
transparent: true,
opacity: 0
}));
scene.add(target);
var mixers = [], clip1, clip2;
const gltfLoader = new GLTFLoader(loadingManager);
gltfLoader.load(foxModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
child.castShadow = true;
child.material.side = THREE.DoubleSide;
}
});
var player = mesh.scene;
player.position.set(this.playPosition.x, this.playPosition.y, this.playPosition.z);
player.scale.set(.008, .008, .008);
target.add(player);
var mixer = new THREE.AnimationMixer(player);
clip1 = mixer.clipAction(mesh.animations[0]);
clip2 = mixer.clipAction(mesh.animations[1]);
clip2.timeScale = 1.6;
mixers.push(mixer);
});
const directionalLight = new THREE.DirectionalLight(new THREE.Color(0xffffff), .5);
directionalLight.position.set(0, 1, 0);
directionalLight.castShadow = true;
directionalLight.target = target;
target.add(directionalLight);
使用轮盘控制器移动阿狸 模型时,实时更新模型的方向,若轮盘产生位移,更新模型位移并播放奔跑动画,否则播放禁止动画。同时根据模型目标的位置,实时更新相机
的位置,生成第三人称视角。轮盘移动控制模型移动功能是通过引入 JoyStick
类来实现,它的主要实现原理是监听鼠标或点触位置,然后通过计算映射到模型位置变化上。
var setup = { forward: 0, turn: 0 };
new JoyStick({ onMove: (forward, turn) => {
setup.forward = forward;
setup.turn = -turn;
}});
const updateDrive = (forward = setup.forward, turn = setup.turn) => {
let maxSteerVal = 0.05;
let maxForce = .15;
let force = maxForce * forward;
let steer = maxSteerVal * turn;
if (forward !== 0) {
target.translateZ(force);
clip2 && clip2.play();
clip1 && clip1.stop();
} else {
clip2 && clip2.stop();
clip1 && clip1.play();
}
target.rotateY(steer);
}
// 生成第三人称视角
const followCamera = new THREE.Object3D();
followCamera.position.copy(camera.position);
scene.add(followCamera);
followCamera.parent = target;
const updateCamera = () => {
if (followCamera) {
camera.position.lerp(followCamera.getWorldPosition(new THREE.Vector3()), 0.1);
camera.lookAt(target.position.x, target.position.y + .5, target.position.z);
}
}
`
轮盘控制器
JoyStick类具体实现可参考文章末尾
Codepen` 链接 [5]。
在页面重绘动画中,更新相机、模型状态、Cannon
世界、场景渲染等。
var clock = new THREE.Clock();
var lastTime;
var fixedTimeStep = 1.0 / 60.0;
const animate = () => {
updateCamera();
updateDrive();
let delta = clock.getDelta();
mixers.map(x => x.update(delta));
let now = Date.now();
lastTime === undefined && (lastTime = now);
let dt = (Date.now() - lastTime) / 1000.0;
lastTime = now;
world.step(fixedTimeStep, dt);
cannonHelper.updateBodies(world);
check && check();
renderer.render(scene, camera);
requestAnimationFrame(animate);
};
页面产生缩放时,更新渲染场景 和相机
。
window.addEventListener('resize', () => {
var width = window.innerWidth, height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}, false);
到此,游戏三维世界 `` 已经全部实现完毕了。
根据前面的游戏流程设计,现在添加游戏逻辑,开始游戏时重置数据并开始 60s
倒计时 ⏳
;重置游戏时将阿狸 `位置、方向及相机位置设置为初始状态;自由探索时开启自由探索状态,并清除倒计时。
⏳`
startGame = () => {
this.setState({
showLoading : false,
showResult: false,
countdown: 60,
resultText: '失败',
freeDiscover: false
},() => {
this.interval = setInterval(() => {
if (this.state.countdown > 0) {
this.setState({
countdown: --this.state.countdown
});
} else {
clearInterval(this.interval)
this.setState({
showResult: true
});
}
}, 1000);
});
}
resetGame = () => {
this.player.position.set(this.playPosition.x, this.playPosition.y, this.playPosition.z);
this.target.rotation.set(0, 0, 0);
this.target.position.set(0, 0, 0);
this.camera.position.set(1, 1, -1);
this.startGame();
}
discover = () => {
this.setState({
freeDiscover: true,
showResult: false,
countdown: 60
}, () => {
clearInterval(this.interval);
});
}
Loading
页面、结果页面以及回到过去按钮都采用了毛玻璃效果样式 ``,通过以下几行样式代码,即可实现惊艳的毛玻璃。
background rgba(0, 67, 170, .5)
backdrop-filter blur(10px)
filter drop-shadow(0px 1px 1px rgba(0, 0, 0, .25))
本文涉及到的新知识点主要包括:
Three.js
阴影类型cannon.js
基本用法cannon.js
高度场 Heightfield
创建地形[1]Three.js 火焰效果实现艾尔登法环动态logo: https://juejin.cn/post/7077726955528781832
[2]Three.js 实现神奇的3D文字悬浮效果: https://juejin.cn/post/7072899771819622413
[3]Three.js 实现让二维图片具有3D效果: https://juejin.cn/post/7067344398912061454
[4]Three.js 实现2022冬奥主题3D趣味页面,冰墩墩 : https://juejin.cn/post/7060292943608807460
[5]Three.js 制作一个专属3D奖牌: https://juejin.cn/post/7055079293247815711
[6]Three.js 实现虎年春节3D创意页面: https://juejin.cn/post/7051745314914435102
[7]Three.js 实现脸书元宇宙3D动态Logo: https://juejin.cn/post/7031893833163997220
[8]Three.js 实现3D全景侦探小游戏: https://juejin.cn/post/7042298964468564005
[9]Three.js 实现炫酷的酸性风格3D页面: https://juejin.cn/post/7012996721693163528
[10]threejs.org: https://threejs.org/
[11]cannonjs.org: http://www.cannonjs.org/
[12]heightmap-generator: http://heightmap-generator.com/
[13]three.js cannon.js物理引擎之Heightfield: https://www.mrguo.link/article?id=53
[14]Joggin' version 0.1: https://codepen.io/b29/pen/JjyJWEg
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/TgXJNPWPHxAZT1Z8-zT_Ag
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。