Three.js程序化游戏角色生成 three.js开发游戏
suiw9 2024-11-09 14:50 24 浏览 0 评论
推荐:使用 NSDT场景设计器 快速搭建 3D场景。
最近,有人联系我,想要为他们要制作的游戏提供一些随机角色。
我从来没有研究过程序化角色生成,关于如何这样做的信息很少,但我决定接受挑战并让它发挥作用。
在这篇文章中,我将探讨我用来生成随机字符的几种方法。在这里我们考虑随机的头部,但这些技术也可以应用于其他身体部位。
可以在此处的 GitHub 上找到演示代码。
1、问题背景
我想到的解决这个问题的第一种方法是让一个零件有几个变体,然后每次都随机选择一个来切换它们。
这是一个简单而合乎逻辑的第一步,但我错了。
我联系了他们的艺术家,创造了一些头部的变化并试了一下。
我会将每个部分及其变体导出到一个文件中。 因此头部及其变体位于 head.gltf 中。 head 的变体按此约定命名 - head_1、head_2…head_n。
以下是我将如何使用它们:
- 载入文件
- 生成一个介于 1 和 n 之间的随机数 (r)
- 遍历文件的场景图
- 将 head_r 添加到场景中
1.1 存在的问题
现在这很好,而且有效,但我遇到了对齐问题。 将所有其他部分排成一行非常困难,因此它们看起来不错。 鼻子、耳朵、眼睛等部位都错位了,一点都不好看。
另一个问题是 - 要生成另一个随机配置,必须再次加载文件或将所有模型保存在内存中并有选择地渲染。
这很……笨拙。
1.2 更好的方法
一周过去了,客户告诉我他们雇用了另一位艺术家。 这家伙用不同的艺术风格制作了模型,他们给我发了一张 GIF。
它演示了艺术家在 Blender 中使用形状键(Shape Keys)!
如果我们将变体变成形状键,并且我可以在 ThreeJS 中控制形状键,那么我们可能会有无限的组合,一切都会完美对齐! 你知道吗,WebGL 正是我所需要的——Morph Targets。 事实上,在 ThreeJS 中,Morph Targets 非常容易控制。
1.3 使用变形目标
要使用 Morph Targets,首先,你需要在其数据中导出带有它们的模型。 在 Blender 中,这就像创建一些形状键并在启用它们的情况下导出模型一样简单。
接下来,在 ThreeJS 中,导入模型后,Mesh 将包含几个属性:
- morphTargetDictionary:这是一个对象,其中键是形状键的名称,值是索引。 指数成什么? 出色地…
- morphTargetInfluences:索引到这个数组中。 该数组保存每个形状键的权重,就像 Blender 中的“值”滑块一样。
网格在两个形状(原始形状和目标形状)之间平滑变换,权重控制变换的“百分比”。 所以权重 0 是原始网格,1 是目标网格。 介于两者之间的任何东西都是两者的结合。
1.4 为什么这样更好?
这更好,因为一个简单的事实。 变形目标可以由一个数字控制。
这意味着很多事情,其中之一就是我们可以将该值连接到一个滑块。 这样我们就可以制作一个基本的角色创建工具。
此外,由于每个权重的范围可以从 0 到 1,并且中间有无限多个值,因此我们可以获得无限多种组合!
我们还可以用一个权重同时驱动多个形状键。 我让艺术家用相同的名字命名他想要一起驱动的键。 这样,当不同的部分变形时,没有元素会错位或剪裁另一个元素。
1.5 局限性
没有任何限制。 WebGL 将单个网格上的变形目标数量限制为八个。 这是一个进一步讨论这个问题的问题。
有很多方法可以解决这个问题,但它们对于我正在做的事情来说太复杂了。 因此,为了解决这个问题,艺术家只需将网格分成更小的网格,每个网格最多有八个形状键。
1.6 模型
新模型是由一位非常有才华的艺术家创建的。 他仔细地将八个形状键建模到每个网格中。 这里只是头部:
我隔离了头部并使用 Blender 的 glTF 2.0 插件将其导出为 GLTF 文件格式。 我们终于可以开始编码了!
2、代码
了解所有这些上下文后,让我们深入研究代码。 本节将快速移动,因为这并不是一个“初学者指南”。
2.1 加载模型
设置一些样板代码后,使用 ThreeJS 的 GLTFLoader 类加载模型。
const loader = new GLTFLoader();
loader.load(`./Assets/head.gltf`, (gltf) => {
const object = gltf.scene;
object.traverse((child) => {
if (child.isMesh) {
child.material.metalness = 0;
child.material.vertexColors = false;
}
});
scene.add(object);
});
这位艺术家也很友善地绘制了一些纹理。 使用 ThreeJS 的 TextureLoader 类从 .png 图像加载纹理。
const loader = new GLTFLoader();
// Loading the texture
const texture = new THREE.TextureLoader().load("./Diffuse.png");
texture.flipY = false;
texture.encoding = THREE.sRGBEncoding;
loader.load(`./Assets/head.gltf`, (gltf) => {
const object = gltf.scene;
object.traverse((child) => {
if (child.isMesh) {
child.material.metalness = 0;
child.material.vertexColors = false;
child.material.map = texture; // Using the texture
}
});
scene.add(object);
});
2.2 变形目标
我们可以通过记录网格的 morphTargetDictionary 属性来检查模型的变形目标。
loader.load(`./Assets/head.gltf`, (gltf) => {
const object = gltf.scene;
object.traverse((child) => {
if (child.isMesh) {
child.material.metalness = 0;
child.material.vertexColors = false;
child.material.map = texture;
}
});
// Here
console.log(object.morphTargetDictionary);
scene.add(object);
});
运行结果如下:
未定义?...什么给了? 我是否错误地导出了模型?
没有! GLTF 场景对象不是我们的网格,它只包含我们所有的网格。 我们需要遍历GLTF场景的场景图,找到我们的对象。
当然,我们可以使用 Object3D.getObjectByName 按名称找到我们的对象,但我在导出网格时没有命名我的网格,所以我将使用老式的方法。
loader.load(`./Assets/head.gltf`, (gltf) => {
const object = gltf.scene;
object.traverse((child) => {
if (child.isMesh) {
child.material.metalness = 0;
child.material.vertexColors = false;
child.material.map = texture;
// Here
if (child.morphTargetDictionary) console.log(child.morphTargetDictionary);
}
});
scene.add(object);
});
答对了! 我们在 ThreeJS 中找到了形状键。
2.3 异步模型加载
回调让我很困惑。 我很快就会像这样Promisify处理 GLTFLoader.load 函数:
const head = await new Promise((res, rej) => {
loader.load(
`./Assets/head.gltf`,
(gltf) => {
const object = gltf.scene;
object.traverse((child) => {
if (child.isMesh) {
child.material.metalness = 0;
child.material.vertexColors = false;
child.material.map = texture;
}
});
res(object);
},
(e) => rej(e)
);
});
scene.add(head);
有点难看,但我现在可以确定在模型加载后头部将可用。 我可能很快就会做出一个three-promises库,因为我不喜欢回调。
2.4 储存变形目标
现在我们知道变形目标在哪里,我们可以简单地将它们随机化到 console.log 所在的位置。 但这意味着我们只能在再次加载模型时才能获得新的变体。
我想在按下按钮时生成一个新角色。
为此,我们可以将目标与其他一些数据一起存储,并在以后使用它们。 这样我们也可以有额外的逻辑来一起控制同名目标。 这是我将如何存储它们:
// A little TypeScript pseudo-code just for demonstration purposes.
type morphTarget = {
index: typeof child.morphTargetDictionary[morphTargetName];
child: typeof child;
};
type morphTargetMap = {
morphTargetName: morphTarget[];
};
// I will use this object to store the data I need.
const morphTargets: morphTargetMap = {};
这样,所有同名的目标连同对其相应网格的引用和网格 morphTargetInfluences 中的索引一起存储。 这是它在代码中的样子:
const morphTargets = {};
const head = await new Promise((res, rej) => {
loader.load(
`./Assets/head.gltf`,
(gltf) => {
const object = gltf.scene;
object.traverse((child) => {
if (child.isMesh) {
child.material.metalness = 0;
child.material.vertexColors = false;
child.material.map = texture;
// Here is where I add stuff to the object
if (child.morphTargetDictionary) {
for (const key in child.morphTargetDictionary) {
const index = child.morphTargetDictionary[key];
if (Array.isArray(morphTargets[key])) {
morphTargets[key].push({ index, child });
} else {
morphTargets[key] = [];
morphTargets[key].push({ index, child });
}
}
}
}
});
res(object);
},
(e) => rej(e)
);
});
scene.add(head);
// Lets log this object
console.log(morphTargets);
我们现在有了一个漂亮的对象,其中包含我们随意使用变形目标所需的所有信息。
2.5 图形用户界面
让我们创建一些滑块来帮助我们改变模型的外观。 我将使用由 ThreeJS = MrDoob 的创建者创建的库 dat.GUI。
我将遍历我们所有独特的变形目标并为每个目标创建一个滑块。 由于权重应介于 0 和 1 之间,因此我会将滑块的最小值和最大值设置为这些值。
const gui = new dat.GUI();
// Temporary object holds our influences. It's a
// weird little quirk of dat.GUI
const influences = {};
// Loop through all targets
for (const key in morphTargets) {
// Get the individual targets associated with that key
const targets = morphTargets[key];
// Set an initial weight by using the first
// target.
const { child, index } = targets[0];
influences[key] = child.morphTargetInfluences[index];
// Add stuff to the GUI
gui.add(influences, key, 0, 1, 0.01).onChange((v) => {
targets.forEach(({ child, index }) => {
child.morphTargetInfluences[index] = v;
});
});
}
完美! 我们现在可以使用滑块控制变形目标。
2.6 随机化
这个过程的最后一步也是我们最初打算做的是随机化权重,这样每次点击按钮都会创建一个随机角色。
为此,我们只需遍历我们的 morphTargets 对象并为每个 morphTargetInfluence 分配一个随机权重。
const funcs = {
Randomize: () => {
// Loop over all morph targets by name
for (const key in morphTargets) {
// Set each of them individual weights assciated with that name
influences[key] = Math.random();
morphTargets[key].forEach(({ child, index }) => {
child.morphTargetInfluences[index] = influences[key];
});
}
// Update the GUI to use the latest weigths
gui.updateDisplay();
},
};
我会将此功能作为按钮添加到 GUI。 对于这一部分,我还将滑块分组到一个文件夹中。
const gui = new dat.GUI({ autoPlace: false });
const folder = gui.addFolder("Sliders"); // Using a folder
const influences = {};
for (const key in morphTargets) {
const targets = morphTargets[key];
const { child, index } = targets[0];
influences[key] = child.morphTargetInfluences[index];
folder.add(influences, key, 0, 1, 0.01).onChange(function (v) {
targets.forEach(({ child, index }) => {
child.morphTargetInfluences[index] = v;
});
});
}
// Closing the folder by default
folder.close();
// Our randomization function
const funcs = {
Randomize: () => {
for (const key in morphTargets) {
influences[key] = Math.random();
morphTargets[key].forEach(({ child, index }) => {
child.morphTargetInfluences[index] = influences[key];
});
}
gui.updateDisplay();
},
};
// Add that function as a button to the GUI
gui.add(funcs, "Randomize");
完美! 现在我们可以通过单击按钮来随机化面孔! 整洁吧?
你拥有的变化越多越好。 因为我这里只有一把,没什么特别的,但你明白了。
您可以将这个概念连接到一个循环中,并生成一组随机面孔,就像我为本文的缩略图所做的那样。 你可以在此处查看演示。
原文链接:http://www.bimant.com/blog/js-procedural-character-generation/
相关推荐
- 俄罗斯的 HTTPS 也要被废了?(俄罗斯网站关闭)
-
发布该推文的ScottHelme是一名黑客,SecurityHeaders和ReportUri的创始人、Pluralsight作者、BBC常驻黑客。他表示,CAs现在似乎正在停止为俄罗斯域名颁发...
- 如何强制所有流量使用 HTTPS一网上用户
-
如何强制所有流量使用HTTPS一网上用户使用.htaccess强制流量到https的最常见方法可能是使用.htaccess重定向请求。.htaccess是一个简单的文本文件,简称为“.h...
- https和http的区别(https和http有何区别)
-
“HTTPS和HTTP都是数据传输的应用层协议,区别在于HTTPS比HTTP安全”。区别在哪里,我们接着往下看:...
- 快码住!带你十分钟搞懂HTTP与HTTPS协议及请求的区别
-
什么是协议?网络协议是计算机之间为了实现网络通信从而达成的一种“约定”或“规则”,正是因为这个“规则”的存在,不同厂商的生产设备、及不同操作系统组成的计算机之间,才可以实现通信。简单来说,计算机与网络...
- 简述HTTPS工作原理(简述https原理,以及与http的区别)
-
https是在http协议的基础上加了一层SSL(由网景公司开发),加密由ssl实现,它的目的是为用户提供对网站服务器的身份认证(需要CA),以至于保护交换数据的隐私和完整性,原理如图示。1、客户端发...
- 21、HTTPS 有几次握手和挥手?HTTPS 的原理什么是(高薪 常问)
-
HTTPS是3次握手和4次挥手,和HTTP是一样的。HTTPS的原理...
- 一次安全可靠的通信——HTTPS原理
-
为什么HTTPS协议就比HTTP安全呢?一次安全可靠的通信应该包含什么东西呢,这篇文章我会尝试讲清楚这些细节。Alice与Bob的通信...
- 为什么有的网站没有使用https(为什么有的网站点不开)
-
有的网站没有使用HTTPS的原因可能涉及多个方面,以下是.com、.top域名的一些见解:服务器性能限制:HTTPS使用公钥加密和私钥解密技术,这要求服务器具备足够的计算能力来处理加解密操作。如果服务...
- HTTPS是什么?加密原理和证书。SSL/TLS握手过程
-
秘钥的产生过程非对称加密...
- 图解HTTPS「转」(图解http 完整版 彩色版 pdf)
-
我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取。所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。...
- HTTP 和 HTTPS 有何不同?一文带你全面了解
-
随着互联网时代的高速发展,Web服务器和客户端之间的安全通信需求也越来越高。HTTP和HTTPS是两种广泛使用的Web通信协议。本文将介绍HTTP和HTTPS的区别,并探讨为什么HTTPS已成为We...
- HTTP与HTTPS的区别,详细介绍(http与https有什么区别)
-
HTTP与HTTPS介绍超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的...
- 一文让你轻松掌握 HTTPS(https详解)
-
一文让你轻松掌握HTTPS原文作者:UC国际研发泽原写在最前:欢迎你来到“UC国际技术”公众号,我们将为大家提供与客户端、服务端、算法、测试、数据、前端等相关的高质量技术文章,不限于原创与翻译。...
- 如何在Spring Boot应用程序上启用HTTPS?
-
HTTPS是HTTP的安全版本,旨在提供传输层安全性(TLS)[安全套接字层(SSL)的后继产品],这是地址栏中的挂锁图标,用于在Web服务器和浏览器之间建立加密连接。HTTPS加密每个数据包以安全方...
- 一文彻底搞明白Http以及Https(http0)
-
早期以信息发布为主的Web1.0时代,HTTP已可以满足绝大部分需要。证书费用、服务器的计算资源都比较昂贵,作为HTTP安全扩展的HTTPS,通常只应用在登录、交易等少数环境中。但随着越来越多的重要...
你 发表评论:
欢迎- 一周热门
-
-
Linux:Ubuntu22.04上安装python3.11,简单易上手
-
宝马阿布达比分公司推出独特M4升级套件,整套升级约在20万
-
MATLAB中图片保存的五种方法(一)(matlab中保存图片命令)
-
别再傻傻搞不清楚Workstation Player和Workstation Pro的区别了
-
Linux上使用tinyproxy快速搭建HTTP/HTTPS代理器
-
如何提取、修改、强刷A卡bios a卡刷bios工具
-
Element Plus 的 Dialog 组件实现点击遮罩层不关闭对话框
-
日本组合“岚”将于2020年12月31日停止团体活动
-
SpringCloud OpenFeign 使用 okhttp 发送 HTTP 请求与 HTTP/2 探索
-
tinymce 号称富文本编辑器世界第一,大家同意么?
-
- 最近发表
- 标签列表
-
- dialog.js (57)
- importnew (44)
- windows93网页版 (44)
- yii2框架的优缺点 (45)
- tinyeditor (45)
- qt5.5 (60)
- windowsserver2016镜像下载 (52)
- okhttputils (51)
- android-gif-drawable (53)
- 时间轴插件 (56)
- docker systemd (65)
- slider.js (47)
- android webview缓存 (46)
- pagination.js (59)
- loadjs (62)
- openssl1.0.2 (48)
- velocity模板引擎 (48)
- pcre library (47)
- zabbix微信报警脚本 (63)
- jnetpcap (49)
- pdfrenderer (43)
- fastutil (48)
- uinavigationcontroller (53)
- bitbucket.org (44)
- python websocket-client (47)