將頂點著色網格轉換為紋理網格

釋出於 2024 年 9 月 30 日
在 GitHub 上更新

Open In Colab

將頂點著色網格轉換為 UV 對映的紋理網格。

引言

頂點顏色是一種直接向網格頂點新增顏色資訊的簡單方法。這通常是像 InstantMesh 這樣的生成式 3D 模型生成網格的方式。然而,大多數應用程式更喜歡 UV 對映的紋理網格。

本教程將介紹一種快速解決方案,用於將頂點著色網格轉換為 UV 對映的紋理網格。其中包括 簡要版本 以快速獲得結果,以及 詳細版本 用於深入講解。

簡要版本

安裝 InstantTexture 庫以方便轉換。這是一個我們編寫的小型庫,實現了下面 詳細版本 中描述的步驟。

pip install git+https://github.com/dylanebert/InstantTexture

用法

以下程式碼將一個頂點著色的 .obj 網格轉換為 UV 對映的紋理 .glb 網格,並將其儲存為 output.glb

from instant_texture import Converter

input_mesh_path = "https://raw.githubusercontent.com/dylanebert/InstantTexture/refs/heads/main/examples/chair.obj"

converter = Converter()
converter.convert(input_mesh_path)

讓我們視覺化輸出網格。

import trimesh

mesh = trimesh.load("output.glb")
mesh.show()

就是這樣!

要了解詳細步驟,請繼續閱讀。

詳細版本

安裝以下依賴項:

  • numpy 用於數值運算
  • trimesh 用於載入和儲存網格資料
  • xatlas 用於生成 UV 貼圖
  • Pillow 用於影像處理
  • opencv-python 用於影像處理
  • httpx 用於下載輸入網格
pip install numpy trimesh xatlas opencv-python pillow httpx

匯入依賴項。

import cv2
import numpy as np
import trimesh
import xatlas
from PIL import Image, ImageFilter

載入頂點著色輸入網格。這應該是一個位於 input_mesh_path.obj 檔案。

如果是本地檔案,請使用 trimesh.load() 而不是 trimesh.load_remote()

mesh = trimesh.load_remote(input_mesh_path)
mesh.show()

訪問網格的頂點顏色。

如果失敗,請確保網格是帶有頂點顏色的有效 .obj 檔案。

vertex_colors = mesh.visual.vertex_colors

使用 xatlas 生成 UV 貼圖。

這是過程中最耗時的部分。

vmapping, indices, uvs = xatlas.parametrize(mesh.vertices, mesh.faces)

將頂點和頂點顏色重新對映到 UV 貼圖。

vertices = mesh.vertices[vmapping]
vertex_colors = vertex_colors[vmapping]

mesh.vertices = vertices
mesh.faces = indices

定義所需的紋理大小。

構建一個透過 upscale_factor 放大以建立更高質量紋理的紋理緩衝區。

texture_size = 1024

upscale_factor = 2
buffer_size = texture_size * upscale_factor

texture_buffer = np.zeros((buffer_size, buffer_size, 4), dtype=np.uint8)

使用重心插值填充 UV 對映網格的紋理。

  1. 重心插值:計算點 p 在由頂點 v0v1v2 以及對應顏色 c0c1c2 定義的三角形內部的插值顏色。
  2. 點在三角形內測試:判斷點 p 是否位於由頂點 v0v1v2 定義的三角形內。
  3. 紋理填充迴圈:
    • 迭代網格的每個面。
    • 檢索當前面的 UV 座標(uv0uv1uv2)和顏色(c0c1c2)。
    • 將 UV 座標轉換為緩衝區座標。
    • 確定紋理緩衝區上三角形的邊界框。
    • 對於邊界框中的每個畫素,使用點在三角形內測試檢查畫素是否位於三角形內。
    • 如果在內部,使用重心插值計算插值顏色。
    • 將顏色分配給紋理緩衝區中對應的畫素。
# Barycentric interpolation
def barycentric_interpolate(v0, v1, v2, c0, c1, c2, p):
    v0v1 = v1 - v0
    v0v2 = v2 - v0
    v0p = p - v0
    d00 = np.dot(v0v1, v0v1)
    d01 = np.dot(v0v1, v0v2)
    d11 = np.dot(v0v2, v0v2)
    d20 = np.dot(v0p, v0v1)
    d21 = np.dot(v0p, v0v2)
    denom = d00 * d11 - d01 * d01
    if abs(denom) < 1e-8:
        return (c0 + c1 + c2) / 3
    v = (d11 * d20 - d01 * d21) / denom
    w = (d00 * d21 - d01 * d20) / denom
    u = 1.0 - v - w
    u = np.clip(u, 0, 1)
    v = np.clip(v, 0, 1)
    w = np.clip(w, 0, 1)
    interpolate_color = u * c0 + v * c1 + w * c2
    return np.clip(interpolate_color, 0, 255)


# Point-in-Triangle test
def is_point_in_triangle(p, v0, v1, v2):
    def sign(p1, p2, p3):
        return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1])

    d1 = sign(p, v0, v1)
    d2 = sign(p, v1, v2)
    d3 = sign(p, v2, v0)

    has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
    has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)

    return not (has_neg and has_pos)

# Texture-filling loop
for face in mesh.faces:
    uv0, uv1, uv2 = uvs[face]
    c0, c1, c2 = vertex_colors[face]

    uv0 = (uv0 * (buffer_size - 1)).astype(int)
    uv1 = (uv1 * (buffer_size - 1)).astype(int)
    uv2 = (uv2 * (buffer_size - 1)).astype(int)

    min_x = max(int(np.floor(min(uv0[0], uv1[0], uv2[0]))), 0)
    max_x = min(int(np.ceil(max(uv0[0], uv1[0], uv2[0]))), buffer_size - 1)
    min_y = max(int(np.floor(min(uv0[1], uv1[1], uv2[1]))), 0)
    max_y = min(int(np.ceil(max(uv0[1], uv1[1], uv2[1]))), buffer_size - 1)

    for y in range(min_y, max_y + 1):
        for x in range(min_x, max_x + 1):
            p = np.array([x + 0.5, y + 0.5])
            if is_point_in_triangle(p, uv0, uv1, uv2):
                color = barycentric_interpolate(uv0, uv1, uv2, c0, c1, c2, p)
                texture_buffer[y, x] = np.clip(color, 0, 255).astype(
                    np.uint8
                )

讓我們看看目前為止紋理的效果。

from IPython.display import display

image_texture = Image.fromarray(texture_buffer)
display(image_texture)

Texture with holes

正如我們所看到的,紋理有很多孔洞。

為了糾正這個問題,我們將結合四種技術:

  1. 修復:使用周圍畫素的平均顏色填充孔洞。
  2. 中值濾波:透過將每個畫素替換為其周圍畫素的中值顏色來消除噪聲。
  3. 高斯模糊:平滑紋理以消除任何剩餘的噪聲。
  4. 下采樣:使用 Lanczos 重取樣將大小調整到 texture_size
# Inpainting
image_bgra = texture_buffer.copy()
mask = (image_bgra[:, :, 3] == 0).astype(np.uint8) * 255
image_bgr = cv2.cvtColor(image_bgra, cv2.COLOR_BGRA2BGR)
inpainted_bgr = cv2.inpaint(
    image_bgr, mask, inpaintRadius=3, flags=cv2.INPAINT_TELEA
)
inpainted_bgra = cv2.cvtColor(inpainted_bgr, cv2.COLOR_BGR2BGRA)
texture_buffer = inpainted_bgra[::-1]
image_texture = Image.fromarray(texture_buffer)

# Median filter
image_texture = image_texture.filter(ImageFilter.MedianFilter(size=3))

# Gaussian blur
image_texture = image_texture.filter(ImageFilter.GaussianBlur(radius=1))

# Downsample
image_texture = image_texture.resize((texture_size, texture_size), Image.LANCZOS)

# Display the final texture
display(image_texture)

Texture without holes

正如我們所看到的,紋理現在平滑了許多,並且沒有孔洞。

這可以透過更高階的技術或手動紋理編輯進一步改進。

最後,我們可以使用生成的 UV 座標和紋理構建一個新的網格。

material = trimesh.visual.material.PBRMaterial(
    baseColorFactor=[1.0, 1.0, 1.0, 1.0],
    baseColorTexture=image_texture,
    metallicFactor=0.0,
    roughnessFactor=1.0,
)

visuals = trimesh.visual.TextureVisuals(uv=uvs, material=material)
mesh.visual = visuals
mesh.show()

Final mesh

瞧!網格已進行 UV 對映和紋理化處理。

要在本地執行時匯出,請呼叫 mesh.export("output.glb")

侷限性

如您所見,網格仍有許多小的瑕疵。

UV 貼圖和紋理的質量也遠低於生產就緒網格的標準。

然而,如果您正在尋找一種快速解決方案,將頂點著色網格對映到 UV 對映網格,那麼這種方法可能對您有用。

結論

本教程介紹瞭如何將頂點著色網格轉換為 UV 對映的紋理網格。

如果您有任何問題或反饋,請隨時在 GitHubSpace 上提交 issue。

感謝您的閱讀!

社群

註冊登入 發表評論

© . This site is unofficial and not affiliated with Hugging Face, Inc.