天天看點

Three.js程式化人物角色生成

最近,有人聯系我,想要為他們要制作的遊戲提供一些随機角色。

我從來沒有研究過程式化角色生成,關于如何這樣做的資訊很少,但我決定接受挑戰并讓它發揮作用。

在這篇文章中,我将探讨我用來生成随機字元的幾種方法。在這裡我們考慮随機的頭部,但這些技術也可以應用于其他身體部位。

可以在此處的 GitHub 上找到示範代碼。

Three.js程式化人物角色生成
推薦:使用 NSDT場景設計器 快速搭建 3D場景。

1、問題背景

我想到的解決這個問題的第一種方法是讓一個零件有幾個變體,然後每次都随機選擇一個來切換它們。

這是一個簡單而合乎邏輯的第一步,但我錯了。

我聯系了他們的藝術家,創造了一些頭部的變化并試了一下。

我會将每個部分及其變體導出到一個檔案中。 是以頭部及其變體位于 head.gltf 中。 head 的變體按此約定命名 - head_1、head_2…head_n。

以下是我将如何使用它們:

  • 載入檔案
  • 生成一個介于 1 和 n 之間的随機數 ®
  • 周遊檔案的場景圖
  • 将 head_r 添加到場景中

1.1 存在的問題

現在這很好,而且有效,但我遇到了對齊問題。 将所有其他部分排成一行非常困難,是以它們看起來不錯。 鼻子、耳朵、眼睛等部位都錯位了,一點都不好看。

另一個問題是 - 要生成另一個随機配置,必須再次加載檔案或将所有模型儲存在記憶體中并有選擇地渲染。

這很……笨拙。

1.2 更好的方法

一周過去了,客戶告訴我他們雇用了另一位藝術家。 這家夥用不同的藝術風格制作了模型,他們給我發了一張 GIF。

Three.js程式化人物角色生成

它示範了藝術家在 Blender 中使用形狀鍵(Shape Keys)!

如果我們将變體變成形狀鍵,并且我可以在 ThreeJS 中控制形狀鍵,那麼我們可能會有無限的組合,一切都會完美對齊! 你知道嗎,WebGL 正是我所需要的——Morph Targets。 事實上,在 ThreeJS 中,Morph Targets 非常容易控制。

1.3 使用變形目标

要使用 Morph Targets,首先,你需要在其資料中導出帶有它們的模型。 在 Blender 中,這就像建立一些形狀鍵并在啟用它們的情況下導出模型一樣簡單。

接下來,在 ThreeJS 中,導入模型後,Mesh 将包含幾個屬性:

Three.js程式化人物角色生成
  • morphTargetDictionary:這是一個對象,其中鍵是形狀鍵的名稱,值是索引。 指數成什麼? 出色地…
  • morphTargetInfluences:索引到這個數組中。 該數組儲存每個形狀鍵的權重,就像 Blender 中的“值”滑塊一樣。

網格在兩個形狀(原始形狀和目标形狀)之間平滑變換,權重控制變換的“百分比”。 是以權重 0 是原始網格,1 是目标網格。 介于兩者之間的任何東西都是兩者的結合。

1.4 為什麼這樣更好?

這更好,因為一個簡單的事實。 變形目标可以由一個數字控制。

這意味着很多事情,其中之一就是我們可以将該值連接配接到一個滑塊。 這樣我們就可以制作一個基本的角色建立工具。

Three.js程式化人物角色生成

此外,由于每個權重的範圍可以從 0 到 1,并且中間有無限多個值,是以我們可以獲得無限多種組合!

我們還可以用一個權重同時驅動多個形狀鍵。 我讓藝術家用相同的名字命名他想要一起驅動的鍵。 這樣,當不同的部分變形時,沒有元素會錯位或剪裁另一個元素。

1.5 局限性

沒有任何限制。 WebGL 将單個網格上的變形目标數量限制為八個。 這是一個進一步讨論這個問題的問題。

Three.js程式化人物角色生成

有很多方法可以解決這個問題,但它們對于我正在做的事情來說太複雜了。 是以,為了解決這個問題,藝術家隻需将網格分成更小的網格,每個網格最多有八個形狀鍵。

1.6 模型

新模型是由一位非常有才華的藝術家建立的。 他仔細地将八個形狀鍵模組化到每個網格中。 這裡隻是頭部:

Three.js程式化人物角色生成

我隔離了頭部并使用 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);
});
           
Three.js程式化人物角色生成

這位藝術家也很友善地繪制了一些紋理。 使用 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);
});
           
Three.js程式化人物角色生成

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);
});
           

運作結果如下:

Three.js程式化人物角色生成

未定義?…什麼給了? 我是否錯誤地導出了模型?

沒有! 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);
});
           
Three.js程式化人物角色生成

答對了! 我們在 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);
           
Three.js程式化人物角色生成

我們現在有了一個漂亮的對象,其中包含我們随意使用變形目标所需的所有資訊。

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;
    });
  });
}
           
Three.js程式化人物角色生成

完美! 我們現在可以使用滑塊控制變形目标。

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");
           
Three.js程式化人物角色生成

完美! 現在我們可以通過單擊按鈕來随機化面孔! 整潔吧?

你擁有的變化越多越好。 因為我這裡隻有一把,沒什麼特别的,但你明白了。

您可以将這個概念連接配接到一個循環中,并生成一組随機面孔,就像我為本文的縮略圖所做的那樣。 你可以在此處檢視示範。

原文連結:JS程式化角色生成 — BimAnt

繼續閱讀