百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

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,通常只应用在登录、交易等少数环境中。但随着越来越多的重要...

取消回复欢迎 发表评论: