Files
Vulkan-Samples/python/generate_normal.py
T
2025-09-27 20:05:03 +08:00

181 lines
6.3 KiB
Python

import numpy as np
import sys
import os
def compute_vertex_normals(vertices, faces):
"""
计算顶点法线
Args:
vertices: 顶点列表,每个顶点是[x, y, z]
faces: 面列表,每个面是顶点索引列表(三角形面为3个索引)
Returns:
normals: 顶点法线列表,每个法线是[nx, ny, nz]
"""
# 初始化法线数组
normals = np.zeros_like(vertices, dtype=np.float64)
# 计算每个面的法线并累加到对应的顶点
for face in faces:
if len(face) < 3:
continue
# 获取三角形的三个顶点
v0 = vertices[face[0]]
v1 = vertices[face[1]]
v2 = vertices[face[2]]
# 计算两条边向量
edge1 = v1 - v0
edge2 = v2 - v0
# 计算面法线(叉积)
face_normal = np.cross(edge1, edge2)
# 面法线长度(用于加权平均)
face_area = np.linalg.norm(face_normal)
# 如果面法线长度不为零,则归一化并加权
if face_area > 1e-10:
face_normal_normalized = face_normal / face_area
# 将面法线累加到每个顶点
for vertex_index in face:
normals[vertex_index] += face_normal_normalized
# 归一化所有顶点法线
for i in range(len(normals)):
length = np.linalg.norm(normals[i])
if length > 1e-10:
normals[i] /= length
else:
# 如果法线长度为零(可能是孤立顶点),设置为默认向上法线
normals[i] = np.array([0.0, 1.0, 0.0])
return normals
def parse_obj_file(filename):
"""
解析OBJ文件
Returns:
vertices: 顶点列表
faces: 面索引列表
has_normals: 是否已经包含法线
"""
vertices = []
faces = []
existing_normals = []
has_normals = False
try:
with open(filename, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split()
if not parts:
continue
if parts[0] == 'v': # 顶点
if len(parts) >= 4:
vertex = [float(parts[1]), float(parts[2]), float(parts[3])]
vertices.append(vertex)
elif parts[0] == 'vn': # 法线
if len(parts) >= 4:
normal = [float(parts[1]), float(parts[2]), float(parts[3])]
existing_normals.append(normal)
has_normals = True
elif parts[0] == 'f': # 面
face_vertices = []
for part in parts[1:]:
# 处理 f v1/vt1/vn1 v2/vt2/vn2 格式
vertex_data = part.split('/')
if vertex_data[0]: # 顶点索引
# OBJ索引从1开始,转换为从0开始
vertex_index = int(vertex_data[0]) - 1
face_vertices.append(vertex_index)
if len(face_vertices) >= 3:
# 如果是四边形或多边形,分解为三角形
for i in range(1, len(face_vertices) - 1):
faces.append([face_vertices[0], face_vertices[i], face_vertices[i + 1]])
except UnicodeDecodeError:
print(f"错误: 无法读取文件 {filename},请确保文件是UTF-8编码。")
sys.exit(1)
return np.array(vertices), faces, has_normals
def write_obj_file(filename, vertices, faces, normals, original_has_normals=False):
"""
写入OBJ文件
"""
with open(filename, 'w', encoding='utf-8') as file:
file.write("# OBJ文件 - 自动计算法线\n")
file.write(f"# 顶点数: {len(vertices)}\n")
file.write(f"# 面数: {len(faces)}\n\n")
# 写入顶点
# for i, vertex in enumerate(vertices):
# file.write(f"v {vertex[0]:.6f} {vertex[1]:.6f} {vertex[2]:.6f}\n")
for i, vertex in enumerate(vertices):
file.write(f"v {vertex[0]*100:.6f} {vertex[1]*100:.6f} {vertex[2]*100:.6f}\n")
file.write("\n")
# 写入法线
for i, normal in enumerate(normals):
file.write(f"vn {normal[0]:.6f} {normal[1]:.6f} {normal[2]:.6f}\n")
file.write("\n")
# 写入面(使用顶点索引/法线索引格式)
for face in faces:
face_line = "f"
for vertex_index in face:
# 如果原始文件有法线或者我们计算了法线,使用 v//vn 格式
face_line += f" {vertex_index + 1}//{vertex_index + 1}"
file.write(face_line + "\n")
def process_obj_file(input_file, output_file=None):
"""
处理OBJ文件的主要函数
"""
if output_file is None:
base_name = os.path.splitext(input_file)[0]
output_file = f"{base_name}_with_normals.obj"
print(f"读取文件: {input_file}")
vertices, faces, has_normals = parse_obj_file(input_file)
print(f"找到 {len(vertices)} 个顶点, {len(faces)} 个面")
if has_normals:
print("警告: 输入文件已包含法线数据,将被重新计算")
if len(vertices) == 0:
print("错误: 没有找到顶点数据")
return False
if len(faces) == 0:
print("错误: 没有找到面数据")
return False
print("计算顶点法线中...")
normals = compute_vertex_normals(vertices, faces)
print("写入输出文件...")
write_obj_file(output_file, vertices, faces, normals, has_normals)
print(f"完成! 输出文件: {output_file}")
return True
if __name__ == "__main__":
input_file = "face.obj" # 默认输入文件
output_file = "face_with_normals.obj" # 默认输出文件
process_obj_file(input_file, output_file)