将顶点着色网格转换为纹理网格

发布于 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。

感谢您的阅读!

社区

注册登录 发表评论