第53章 3D图形编程
学习目标
完成本章学习后,你将能够:
- 理解3D图形基础:坐标系、变换、投影、光照
- 使用OpenGL渲染:顶点缓冲、着色器、纹理映射
- 实现3D变换:平移、旋转、缩放、矩阵运算
- 创建3D模型:网格生成、模型加载、骨骼动画
- 实现光照效果:环境光、漫反射、镜面反射、阴影
- 开发游戏引擎:场景管理、相机控制、碰撞检测
- 实现物理模拟:刚体动力学、碰撞响应、粒子系统
- 优化渲染性能:批处理、遮挡剔除、LOD技术
53.1 3D图形基础
53.1.1 数学基础
python
from dataclasses import dataclass
from typing import List, Tuple, Optional
import math
@dataclass
class Vector3:
x: float = 0.0
y: float = 0.0
z: float = 0.0
def __add__(self, other: "Vector3") -> "Vector3":
return Vector3(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other: "Vector3") -> "Vector3":
return Vector3(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(self, scalar: float) -> "Vector3":
return Vector3(self.x * scalar, self.y * scalar, self.z * scalar)
def __truediv__(self, scalar: float) -> "Vector3":
return Vector3(self.x / scalar, self.y / scalar, self.z / scalar)
def __neg__(self) -> "Vector3":
return Vector3(-self.x, -self.y, -self.z)
def dot(self, other: "Vector3") -> float:
return self.x * other.x + self.y * other.y + self.z * other.z
def cross(self, other: "Vector3") -> "Vector3":
return Vector3(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x
)
def length(self) -> float:
return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
def length_squared(self) -> float:
return self.x ** 2 + self.y ** 2 + self.z ** 2
def normalized(self) -> "Vector3":
length = self.length()
if length == 0:
return Vector3(0, 0, 0)
return self / length
def normalize(self) -> None:
length = self.length()
if length > 0:
self.x /= length
self.y /= length
self.z /= length
def distance_to(self, other: "Vector3") -> float:
return (self - other).length()
def angle_to(self, other: "Vector3") -> float:
dot = self.dot(other)
lengths = self.length() * other.length()
if lengths == 0:
return 0
return math.acos(max(-1, min(1, dot / lengths)))
def lerp(self, other: "Vector3", t: float) -> "Vector3":
return self + (other - self) * t
def reflect(self, normal: "Vector3") -> "Vector3":
return self - normal * (2 * self.dot(normal))
@staticmethod
def zero() -> "Vector3":
return Vector3(0, 0, 0)
@staticmethod
def one() -> "Vector3":
return Vector3(1, 1, 1)
@staticmethod
def up() -> "Vector3":
return Vector3(0, 1, 0)
@staticmethod
def forward() -> "Vector3":
return Vector3(0, 0, -1)
@staticmethod
def right() -> "Vector3":
return Vector3(1, 0, 0)
@dataclass
class Matrix4:
elements: List[float] = None
def __post_init__(self):
if self.elements is None:
self.elements = [0.0] * 16
@staticmethod
def identity() -> "Matrix4":
return Matrix4([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
])
@staticmethod
def translation(x: float, y: float, z: float) -> "Matrix4":
return Matrix4([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1
])
@staticmethod
def scale(x: float, y: float, z: float) -> "Matrix4":
return Matrix4([
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1
])
@staticmethod
def rotation_x(angle: float) -> "Matrix4":
c = math.cos(angle)
s = math.sin(angle)
return Matrix4([
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
])
@staticmethod
def rotation_y(angle: float) -> "Matrix4":
c = math.cos(angle)
s = math.sin(angle)
return Matrix4([
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
])
@staticmethod
def rotation_z(angle: float) -> "Matrix4":
c = math.cos(angle)
s = math.sin(angle)
return Matrix4([
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
])
@staticmethod
def rotation_axis(axis: Vector3, angle: float) -> "Matrix4":
c = math.cos(angle)
s = math.sin(angle)
t = 1 - c
x, y, z = axis.x, axis.y, axis.z
return Matrix4([
t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0,
t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0,
t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0,
0, 0, 0, 1
])
@staticmethod
def look_at(
eye: Vector3,
target: Vector3,
up: Vector3
) -> "Matrix4":
forward = (target - eye).normalized()
right = forward.cross(up).normalized()
new_up = right.cross(forward)
return Matrix4([
right.x, new_up.x, -forward.x, 0,
right.y, new_up.y, -forward.y, 0,
right.z, new_up.z, -forward.z, 0,
-right.dot(eye), -new_up.dot(eye), forward.dot(eye), 1
])
@staticmethod
def perspective(
fov: float,
aspect: float,
near: float,
far: float
) -> "Matrix4":
f = 1.0 / math.tan(fov / 2)
range_inv = 1.0 / (near - far)
return Matrix4([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (near + far) * range_inv, -1,
0, 0, 2 * near * far * range_inv, 0
])
@staticmethod
def orthographic(
left: float,
right: float,
bottom: float,
top: float,
near: float,
far: float
) -> "Matrix4":
return Matrix4([
2 / (right - left), 0, 0, 0,
0, 2 / (top - bottom), 0, 0,
0, 0, -2 / (far - near), 0,
-(right + left) / (right - left),
-(top + bottom) / (top - bottom),
-(far + near) / (far - near), 1
])
def __mul__(self, other: "Matrix4") -> "Matrix4":
result = [0.0] * 16
for i in range(4):
for j in range(4):
for k in range(4):
result[i * 4 + j] += self.elements[i * 4 + k] * other.elements[k * 4 + j]
return Matrix4(result)
def transform_point(self, point: Vector3) -> Vector3:
e = self.elements
w = e[12] * point.x + e[13] * point.y + e[14] * point.z + e[15]
return Vector3(
(e[0] * point.x + e[1] * point.y + e[2] * point.z + e[3]) / w,
(e[4] * point.x + e[5] * point.y + e[6] * point.z + e[7]) / w,
(e[8] * point.x + e[9] * point.y + e[10] * point.z + e[11]) / w
)
def transform_direction(self, direction: Vector3) -> Vector3:
e = self.elements
return Vector3(
e[0] * direction.x + e[1] * direction.y + e[2] * direction.z,
e[4] * direction.x + e[5] * direction.y + e[6] * direction.z,
e[8] * direction.x + e[9] * direction.y + e[10] * direction.z
)
def transpose(self) -> "Matrix4":
e = self.elements
return Matrix4([
e[0], e[4], e[8], e[12],
e[1], e[5], e[9], e[13],
e[2], e[6], e[10], e[14],
e[3], e[7], e[11], e[15]
])
def inverse(self) -> "Matrix4":
e = self.elements
inv = [0.0] * 16
inv[0] = e[5] * e[10] * e[15] - e[5] * e[11] * e[14] - e[9] * e[6] * e[15] + e[9] * e[7] * e[14] + e[13] * e[6] * e[11] - e[13] * e[7] * e[10]
inv[4] = -e[4] * e[10] * e[15] + e[4] * e[11] * e[14] + e[8] * e[6] * e[15] - e[8] * e[7] * e[14] - e[12] * e[6] * e[11] + e[12] * e[7] * e[10]
inv[8] = e[4] * e[9] * e[15] - e[4] * e[11] * e[13] - e[8] * e[5] * e[15] + e[8] * e[7] * e[13] + e[12] * e[5] * e[11] - e[12] * e[7] * e[9]
inv[12] = -e[4] * e[9] * e[14] + e[4] * e[10] * e[13] + e[8] * e[5] * e[14] - e[8] * e[6] * e[13] - e[12] * e[5] * e[10] + e[12] * e[6] * e[9]
inv[1] = -e[1] * e[10] * e[15] + e[1] * e[11] * e[14] + e[9] * e[2] * e[15] - e[9] * e[3] * e[14] - e[13] * e[2] * e[11] + e[13] * e[3] * e[10]
inv[5] = e[0] * e[10] * e[15] - e[0] * e[11] * e[14] - e[8] * e[2] * e[15] + e[8] * e[3] * e[14] + e[12] * e[2] * e[11] - e[12] * e[3] * e[10]
inv[9] = -e[0] * e[9] * e[15] + e[0] * e[11] * e[13] + e[8] * e[1] * e[15] - e[8] * e[3] * e[13] - e[12] * e[1] * e[11] + e[12] * e[3] * e[9]
inv[13] = e[0] * e[9] * e[14] - e[0] * e[10] * e[13] - e[8] * e[1] * e[14] + e[8] * e[2] * e[13] + e[12] * e[1] * e[10] - e[12] * e[2] * e[9]
inv[2] = e[1] * e[6] * e[15] - e[1] * e[7] * e[14] - e[5] * e[2] * e[15] + e[5] * e[3] * e[14] + e[13] * e[2] * e[7] - e[13] * e[3] * e[6]
inv[6] = -e[0] * e[6] * e[15] + e[0] * e[7] * e[14] + e[4] * e[2] * e[15] - e[4] * e[3] * e[14] - e[12] * e[2] * e[7] + e[12] * e[3] * e[6]
inv[10] = e[0] * e[5] * e[15] - e[0] * e[7] * e[13] - e[4] * e[1] * e[15] + e[4] * e[3] * e[13] + e[12] * e[1] * e[7] - e[12] * e[3] * e[5]
inv[14] = -e[0] * e[5] * e[14] + e[0] * e[6] * e[13] + e[4] * e[1] * e[14] - e[4] * e[2] * e[13] - e[12] * e[1] * e[6] + e[12] * e[2] * e[5]
inv[3] = -e[1] * e[6] * e[11] + e[1] * e[7] * e[10] + e[5] * e[2] * e[11] - e[5] * e[3] * e[10] - e[9] * e[2] * e[7] + e[9] * e[3] * e[6]
inv[7] = e[0] * e[6] * e[11] - e[0] * e[7] * e[10] - e[4] * e[2] * e[11] + e[4] * e[3] * e[10] + e[8] * e[2] * e[7] - e[8] * e[3] * e[6]
inv[11] = -e[0] * e[5] * e[11] + e[0] * e[7] * e[9] + e[4] * e[1] * e[11] - e[4] * e[3] * e[9] - e[8] * e[1] * e[7] + e[8] * e[3] * e[5]
inv[15] = e[0] * e[5] * e[10] - e[0] * e[6] * e[9] - e[4] * e[1] * e[10] + e[4] * e[2] * e[9] + e[8] * e[1] * e[6] - e[8] * e[2] * e[5]
det = e[0] * inv[0] + e[1] * inv[4] + e[2] * inv[8] + e[3] * inv[12]
if det == 0:
return Matrix4.identity()
det = 1.0 / det
for i in range(16):
inv[i] *= det
return Matrix4(inv)
class Quaternion:
def __init__(self, x: float = 0, y: float = 0, z: float = 0, w: float = 1):
self.x = x
self.y = y
self.z = z
self.w = w
@staticmethod
def from_axis_angle(axis: Vector3, angle: float) -> "Quaternion":
half_angle = angle / 2
s = math.sin(half_angle)
return Quaternion(
axis.x * s,
axis.y * s,
axis.z * s,
math.cos(half_angle)
)
@staticmethod
def from_euler(x: float, y: float, z: float) -> "Quaternion":
c1 = math.cos(x / 2)
c2 = math.cos(y / 2)
c3 = math.cos(z / 2)
s1 = math.sin(x / 2)
s2 = math.sin(y / 2)
s3 = math.sin(z / 2)
return Quaternion(
s1 * c2 * c3 + c1 * s2 * s3,
c1 * s2 * c3 - s1 * c2 * s3,
c1 * c2 * s3 + s1 * s2 * c3,
c1 * c2 * c3 - s1 * s2 * s3
)
def normalized(self) -> "Quaternion":
length = math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2 + self.w ** 2)
if length == 0:
return Quaternion()
return Quaternion(
self.x / length,
self.y / length,
self.z / length,
self.w / length
)
def conjugate(self) -> "Quaternion":
return Quaternion(-self.x, -self.y, -self.z, self.w)
def __mul__(self, other: "Quaternion") -> "Quaternion":
return Quaternion(
self.x * other.w + self.w * other.x + self.y * other.z - self.z * other.y,
self.y * other.w + self.w * other.y + self.z * other.x - self.x * other.z,
self.z * other.w + self.w * other.z + self.x * other.y - self.y * other.x,
self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z
)
def rotate_vector(self, v: Vector3) -> Vector3:
qv = Quaternion(v.x, v.y, v.z, 0)
result = self * qv * self.conjugate()
return Vector3(result.x, result.y, result.z)
def to_matrix(self) -> Matrix4:
xx = self.x * self.x
yy = self.y * self.y
zz = self.z * self.z
xy = self.x * self.y
xz = self.x * self.z
yz = self.y * self.z
wx = self.w * self.x
wy = self.w * self.y
wz = self.w * self.z
return Matrix4([
1 - 2 * (yy + zz), 2 * (xy + wz), 2 * (xz - wy), 0,
2 * (xy - wz), 1 - 2 * (xx + zz), 2 * (yz + wx), 0,
2 * (xz + wy), 2 * (yz - wx), 1 - 2 * (xx + yy), 0,
0, 0, 0, 1
])
@staticmethod
def slerp(q1: "Quaternion", q2: "Quaternion", t: float) -> "Quaternion":
dot = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w
if dot < 0:
q2 = Quaternion(-q2.x, -q2.y, -q2.z, -q2.w)
dot = -dot
if dot > 0.9995:
return Quaternion(
q1.x + t * (q2.x - q1.x),
q1.y + t * (q2.y - q1.y),
q1.z + t * (q2.z - q1.z),
q1.w + t * (q2.w - q1.w)
).normalized()
theta = math.acos(dot)
sin_theta = math.sin(theta)
w1 = math.sin((1 - t) * theta) / sin_theta
w2 = math.sin(t * theta) / sin_theta
return Quaternion(
q1.x * w1 + q2.x * w2,
q1.y * w1 + q2.y * w2,
q1.z * w1 + q2.z * w2,
q1.w * w1 + q2.w * w2
)53.1.2 3D变换
python
from dataclasses import dataclass, field
from typing import Optional, List
@dataclass
class Transform:
position: Vector3 = field(default_factory=Vector3.zero)
rotation: Quaternion = field(default_factory=Quaternion)
scale: Vector3 = field(default_factory=lambda: Vector3(1, 1, 1))
parent: Optional["Transform"] = None
children: List["Transform"] = field(default_factory=list)
def get_model_matrix(self) -> Matrix4:
t = Matrix4.translation(self.position.x, self.position.y, self.position.z)
r = self.rotation.to_matrix()
s = Matrix4.scale(self.scale.x, self.scale.y, self.scale.z)
local_matrix = t * r * s
if self.parent:
return self.parent.get_model_matrix() * local_matrix
return local_matrix
def get_world_position(self) -> Vector3:
if self.parent:
return self.parent.get_model_matrix().transform_point(self.position)
return self.position
def get_world_rotation(self) -> Quaternion:
if self.parent:
return self.parent.get_world_rotation() * self.rotation
return self.rotation
def translate(self, delta: Vector3) -> None:
self.position = self.position + delta
def rotate(self, axis: Vector3, angle: float) -> None:
rotation_delta = Quaternion.from_axis_angle(axis, angle)
self.rotation = rotation_delta * self.rotation
def rotate_around(self, point: Vector3, axis: Vector3, angle: float) -> None:
rotation = Quaternion.from_axis_angle(axis, angle)
offset = self.position - point
rotated_offset = rotation.rotate_vector(offset)
self.position = point + rotated_offset
self.rotation = rotation * self.rotation
def look_at(self, target: Vector3, up: Vector3 = None) -> None:
if up is None:
up = Vector3.up()
forward = (target - self.position).normalized()
if forward.length() == 0:
return
right = forward.cross(up).normalized()
if right.length() == 0:
return
new_up = right.cross(forward).normalized()
rotation_matrix = Matrix4([
right.x, new_up.x, -forward.x, 0,
right.y, new_up.y, -forward.y, 0,
right.z, new_up.z, -forward.z, 0,
0, 0, 0, 1
])
self.rotation = self._matrix_to_quaternion(rotation_matrix)
def _matrix_to_quaternion(self, m: Matrix4) -> Quaternion:
e = m.elements
trace = e[0] + e[5] + e[10]
if trace > 0:
s = 0.5 / math.sqrt(trace + 1.0)
w = 0.25 / s
x = (e[6] - e[9]) * s
y = (e[8] - e[2]) * s
z = (e[1] - e[4]) * s
elif e[0] > e[5] and e[0] > e[10]:
s = 2.0 * math.sqrt(1.0 + e[0] - e[5] - e[10])
w = (e[6] - e[9]) / s
x = 0.25 * s
y = (e[4] + e[1]) / s
z = (e[8] + e[2]) / s
elif e[5] > e[10]:
s = 2.0 * math.sqrt(1.0 + e[5] - e[0] - e[10])
w = (e[8] - e[2]) / s
x = (e[4] + e[1]) / s
y = 0.25 * s
z = (e[9] + e[6]) / s
else:
s = 2.0 * math.sqrt(1.0 + e[10] - e[0] - e[5])
w = (e[1] - e[4]) / s
x = (e[8] + e[2]) / s
y = (e[9] + e[6]) / s
z = 0.25 * s
return Quaternion(x, y, z, w).normalized()
def add_child(self, child: "Transform") -> None:
child.parent = self
self.children.append(child)
def remove_child(self, child: "Transform") -> None:
if child in self.children:
child.parent = None
self.children.remove(child)
def get_forward(self) -> Vector3:
return self.rotation.rotate_vector(Vector3.forward())
def get_up(self) -> Vector3:
return self.rotation.rotate_vector(Vector3.up())
def get_right(self) -> Vector3:
return self.rotation.rotate_vector(Vector3.right())53.2 3D模型
53.2.1 网格数据
python
from dataclasses import dataclass, field
from typing import List, Tuple, Optional
import struct
@dataclass
class Vertex:
position: Vector3
normal: Vector3 = field(default_factory=Vector3.zero)
tex_coord: Tuple[float, float] = (0.0, 0.0)
tangent: Vector3 = field(default_factory=Vector3.zero)
color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)
@dataclass
class Mesh:
vertices: List[Vertex] = field(default_factory=list)
indices: List[int] = field(default_factory=list)
def calculate_normals(self) -> None:
for vertex in self.vertices:
vertex.normal = Vector3.zero()
for i in range(0, len(self.indices), 3):
i0, i1, i2 = self.indices[i], self.indices[i + 1], self.indices[i + 2]
v0 = self.vertices[i0].position
v1 = self.vertices[i1].position
v2 = self.vertices[i2].position
edge1 = v1 - v0
edge2 = v2 - v0
normal = edge1.cross(edge2).normalized()
self.vertices[i0].normal = self.vertices[i0].normal + normal
self.vertices[i1].normal = self.vertices[i1].normal + normal
self.vertices[i2].normal = self.vertices[i2].normal + normal
for vertex in self.vertices:
vertex.normal.normalize()
def calculate_tangents(self) -> None:
for i in range(0, len(self.indices), 3):
i0, i1, i2 = self.indices[i], self.indices[i + 1], self.indices[i + 2]
v0 = self.vertices[i0]
v1 = self.vertices[i1]
v2 = self.vertices[i2]
edge1 = v1.position - v0.position
edge2 = v2.position - v0.position
delta_uv1 = (v1.tex_coord[0] - v0.tex_coord[0], v1.tex_coord[1] - v0.tex_coord[1])
delta_uv2 = (v2.tex_coord[0] - v0.tex_coord[0], v2.tex_coord[1] - v0.tex_coord[1])
f = 1.0 / (delta_uv1[0] * delta_uv2[1] - delta_uv2[0] * delta_uv1[1])
tangent = Vector3(
f * (delta_uv2[1] * edge1.x - delta_uv1[1] * edge2.x),
f * (delta_uv2[1] * edge1.y - delta_uv1[1] * edge2.y),
f * (delta_uv2[1] * edge1.z - delta_uv1[1] * edge2.z)
)
self.vertices[i0].tangent = tangent
self.vertices[i1].tangent = tangent
self.vertices[i2].tangent = tangent
def get_bounds(self) -> Tuple[Vector3, Vector3]:
if not self.vertices:
return Vector3.zero(), Vector3.zero()
min_pos = Vector3(float('inf'), float('inf'), float('inf'))
max_pos = Vector3(float('-inf'), float('-inf'), float('-inf'))
for vertex in self.vertices:
min_pos.x = min(min_pos.x, vertex.position.x)
min_pos.y = min(min_pos.y, vertex.position.y)
min_pos.z = min(min_pos.z, vertex.position.z)
max_pos.x = max(max_pos.x, vertex.position.x)
max_pos.y = max(max_pos.y, vertex.position.y)
max_pos.z = max(max_pos.z, vertex.position.z)
return min_pos, max_pos
def to_bytes(self) -> bytes:
data = bytearray()
data.extend(struct.pack('<I', len(self.vertices)))
for v in self.vertices:
data.extend(struct.pack('<fff', v.position.x, v.position.y, v.position.z))
data.extend(struct.pack('<fff', v.normal.x, v.normal.y, v.normal.z))
data.extend(struct.pack('<ff', v.tex_coord[0], v.tex_coord[1]))
data.extend(struct.pack('<I', len(self.indices)))
for idx in self.indices:
data.extend(struct.pack('<I', idx))
return bytes(data)
class MeshGenerator:
@staticmethod
def create_cube(size: float = 1.0) -> Mesh:
half = size / 2
vertices = []
indices = []
faces = [
(Vector3(0, 0, 1), [(Vector3(-half, -half, half), (0, 0)),
(Vector3(half, -half, half), (1, 0)),
(Vector3(half, half, half), (1, 1)),
(Vector3(-half, half, half), (0, 1))]),
(Vector3(0, 0, -1), [(Vector3(half, -half, -half), (0, 0)),
(Vector3(-half, -half, -half), (1, 0)),
(Vector3(-half, half, -half), (1, 1)),
(Vector3(half, half, -half), (0, 1))]),
(Vector3(0, 1, 0), [(Vector3(-half, half, half), (0, 0)),
(Vector3(half, half, half), (1, 0)),
(Vector3(half, half, -half), (1, 1)),
(Vector3(-half, half, -half), (0, 1))]),
(Vector3(0, -1, 0), [(Vector3(-half, -half, -half), (0, 0)),
(Vector3(half, -half, -half), (1, 0)),
(Vector3(half, -half, half), (1, 1)),
(Vector3(-half, -half, half), (0, 1))]),
(Vector3(1, 0, 0), [(Vector3(half, -half, half), (0, 0)),
(Vector3(half, -half, -half), (1, 0)),
(Vector3(half, half, -half), (1, 1)),
(Vector3(half, half, half), (0, 1))]),
(Vector3(-1, 0, 0), [(Vector3(-half, -half, -half), (0, 0)),
(Vector3(-half, -half, half), (1, 0)),
(Vector3(-half, half, half), (1, 1)),
(Vector3(-half, half, -half), (0, 1))])
]
vertex_offset = 0
for normal, face_vertices in faces:
for pos, tex in face_vertices:
vertices.append(Vertex(
position=pos,
normal=normal,
tex_coord=tex
))
indices.extend([
vertex_offset, vertex_offset + 1, vertex_offset + 2,
vertex_offset, vertex_offset + 2, vertex_offset + 3
])
vertex_offset += 4
return Mesh(vertices=vertices, indices=indices)
@staticmethod
def create_sphere(radius: float = 1.0, segments: int = 32, rings: int = 16) -> Mesh:
vertices = []
indices = []
for ring in range(rings + 1):
phi = math.pi * ring / rings
for seg in range(segments + 1):
theta = 2 * math.pi * seg / segments
x = radius * math.sin(phi) * math.cos(theta)
y = radius * math.cos(phi)
z = radius * math.sin(phi) * math.sin(theta)
position = Vector3(x, y, z)
normal = position.normalized()
tex_coord = (seg / segments, ring / rings)
vertices.append(Vertex(
position=position,
normal=normal,
tex_coord=tex_coord
))
for ring in range(rings):
for seg in range(segments):
current = ring * (segments + 1) + seg
next_ring = current + segments + 1
indices.extend([
current, next_ring, current + 1,
current + 1, next_ring, next_ring + 1
])
return Mesh(vertices=vertices, indices=indices)
@staticmethod
def create_plane(width: float = 1.0, height: float = 1.0,
segments_x: int = 1, segments_y: int = 1) -> Mesh:
vertices = []
indices = []
half_width = width / 2
half_height = height / 2
for y in range(segments_y + 1):
for x in range(segments_x + 1):
pos_x = -half_width + (x / segments_x) * width
pos_z = -half_height + (y / segments_y) * height
vertices.append(Vertex(
position=Vector3(pos_x, 0, pos_z),
normal=Vector3.up(),
tex_coord=(x / segments_x, y / segments_y)
))
for y in range(segments_y):
for x in range(segments_x):
current = y * (segments_x + 1) + x
next_row = current + segments_x + 1
indices.extend([
current, next_row, current + 1,
current + 1, next_row, next_row + 1
])
return Mesh(vertices=vertices, indices=indices)
@staticmethod
def create_cylinder(radius: float = 1.0, height: float = 2.0,
segments: int = 32) -> Mesh:
vertices = []
indices = []
half_height = height / 2
for i in range(segments):
angle = 2 * math.pi * i / segments
x = radius * math.cos(angle)
z = radius * math.sin(angle)
normal = Vector3(math.cos(angle), 0, math.sin(angle))
vertices.append(Vertex(
position=Vector3(x, -half_height, z),
normal=normal,
tex_coord=(i / segments, 0)
))
vertices.append(Vertex(
position=Vector3(x, half_height, z),
normal=normal,
tex_coord=(i / segments, 1)
))
for i in range(segments):
current = i * 2
next_v = ((i + 1) % segments) * 2
indices.extend([
current, next_v, current + 1,
current + 1, next_v, next_v + 1
])
return Mesh(vertices=vertices, indices=indices)53.3 光照系统
53.3.1 光源类型
python
from dataclasses import dataclass, field
from typing import Tuple, Optional
from enum import Enum
class LightType(Enum):
DIRECTIONAL = "directional"
POINT = "point"
SPOT = "spot"
AMBIENT = "ambient"
@dataclass
class Light:
light_type: LightType
color: Tuple[float, float, float] = (1.0, 1.0, 1.0)
intensity: float = 1.0
position: Vector3 = field(default_factory=Vector3.zero)
direction: Vector3 = field(default_factory=lambda: Vector3(0, -1, 0))
range: float = 10.0
inner_angle: float = 30.0
outer_angle: float = 45.0
cast_shadows: bool = True
def calculate_contribution(
self,
surface_pos: Vector3,
surface_normal: Vector3
) -> Tuple[float, Tuple[float, float, float]]:
if self.light_type == LightType.AMBIENT:
return self.intensity, self.color
elif self.light_type == LightType.DIRECTIONAL:
light_dir = self.direction.normalized() * -1
ndotl = max(0, surface_normal.dot(light_dir))
return ndotl * self.intensity, self.color
elif self.light_type == LightType.POINT:
light_vec = self.position - surface_pos
distance = light_vec.length()
if distance > self.range:
return 0.0, (0.0, 0.0, 0.0)
light_dir = light_vec.normalized()
ndotl = max(0, surface_normal.dot(light_dir))
attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance)
return ndotl * self.intensity * attenuation, self.color
elif self.light_type == LightType.SPOT:
light_vec = self.position - surface_pos
distance = light_vec.length()
if distance > self.range:
return 0.0, (0.0, 0.0, 0.0)
light_dir = light_vec.normalized()
spot_dir = self.direction.normalized() * -1
theta = math.degrees(math.acos(max(-1, min(1, light_dir.dot(spot_dir) * -1))))
if theta > self.outer_angle:
return 0.0, (0.0, 0.0, 0.0)
ndotl = max(0, surface_normal.dot(light_dir))
if theta < self.inner_angle:
spot_factor = 1.0
else:
spot_factor = 1.0 - (theta - self.inner_angle) / (self.outer_angle - self.inner_angle)
attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance)
return ndotl * self.intensity * attenuation * spot_factor, self.color
return 0.0, (0.0, 0.0, 0.0)
@dataclass
class Material:
albedo: Tuple[float, float, float] = (1.0, 1.0, 1.0)
metallic: float = 0.0
roughness: float = 0.5
ambient_occlusion: float = 1.0
emissive: Tuple[float, float, float] = (0.0, 0.0, 0.0)
def calculate_color(
self,
lights: list,
surface_pos: Vector3,
surface_normal: Vector3,
view_pos: Vector3
) -> Tuple[float, float, float]:
result = list(self.emissive)
for light in lights:
intensity, color = light.calculate_contribution(surface_pos, surface_normal)
if intensity <= 0:
continue
for i in range(3):
result[i] += self.albedo[i] * color[i] * intensity
return (
min(1.0, result[0]),
min(1.0, result[1]),
min(1.0, result[2])
)
class LightingSystem:
def __init__(self):
self.lights: list = []
self.ambient_color: Tuple[float, float, float] = (0.1, 0.1, 0.1)
self.ambient_intensity: float = 1.0
def add_light(self, light: Light) -> None:
self.lights.append(light)
def remove_light(self, light: Light) -> None:
if light in self.lights:
self.lights.remove(light)
def calculate_lighting(
self,
surface_pos: Vector3,
surface_normal: Vector3,
material: Material,
view_pos: Vector3
) -> Tuple[float, float, float]:
ambient_light = Light(
light_type=LightType.AMBIENT,
color=self.ambient_color,
intensity=self.ambient_intensity
)
all_lights = [ambient_light] + self.lights
return material.calculate_color(
all_lights,
surface_pos,
surface_normal,
view_pos
)53.3.2 相机系统
python
from dataclasses import dataclass, field
from typing import Tuple
class ProjectionType(Enum):
PERSPECTIVE = "perspective"
ORTHOGRAPHIC = "orthographic"
@dataclass
class Camera:
position: Vector3 = field(default_factory=Vector3.zero)
rotation: Quaternion = field(default_factory=Quaternion.identity)
projection_type: ProjectionType = ProjectionType.PERSPECTIVE
fov: float = 60.0
near_plane: float = 0.1
far_plane: float = 1000.0
aspect_ratio: float = 16.0 / 9.0
ortho_size: float = 5.0
def get_view_matrix(self) -> Matrix4:
forward = self.rotation.rotate_vector(Vector3.forward())
up = self.rotation.rotate_vector(Vector3.up())
target = self.position + forward
return Matrix4.look_at(self.position, target, up)
def get_projection_matrix(self) -> Matrix4:
if self.projection_type == ProjectionType.PERSPECTIVE:
return Matrix4.perspective(
self.fov,
self.aspect_ratio,
self.near_plane,
self.far_plane
)
else:
half_width = self.ortho_size * self.aspect_ratio
half_height = self.ortho_size
return Matrix4.orthographic(
-half_width, half_width,
-half_height, half_height,
self.near_plane, self.far_plane
)
def get_view_projection_matrix(self) -> Matrix4:
return self.get_projection_matrix() * self.get_view_matrix()
def screen_to_world(
self,
screen_pos: Tuple[float, float],
screen_size: Tuple[int, int]
) -> Vector3:
ndc_x = (2.0 * screen_pos[0]) / screen_size[0] - 1.0
ndc_y = 1.0 - (2.0 * screen_pos[1]) / screen_size[1]
inv_proj = self.get_projection_matrix().inverse()
inv_view = self.get_view_matrix().inverse()
ray_ndc = Vector4(ndc_x, ndc_y, -1.0, 1.0)
ray_eye = inv_proj * ray_ndc
ray_eye = Vector4(ray_eye.x, ray_eye.y, -1.0, 0.0)
ray_world = inv_view * ray_eye
direction = Vector3(ray_world.x, ray_world.y, ray_world.z).normalized()
return self.position + direction * self.near_plane
def world_to_screen(
self,
world_pos: Vector3,
screen_size: Tuple[int, int]
) -> Tuple[float, float]:
vp_matrix = self.get_view_projection_matrix()
clip_pos = vp_matrix * Vector4(world_pos.x, world_pos.y, world_pos.z, 1.0)
ndc_x = clip_pos.x / clip_pos.w
ndc_y = clip_pos.y / clip_pos.w
screen_x = (ndc_x + 1.0) * 0.5 * screen_size[0]
screen_y = (1.0 - ndc_y) * 0.5 * screen_size[1]
return (screen_x, screen_y)
def look_at(self, target: Vector3) -> None:
forward = (target - self.position).normalized()
self.rotation = Quaternion.look_rotation(forward, Vector3.up())
def orbit_around(
self,
target: Vector3,
horizontal_angle: float,
vertical_angle: float,
distance: float
) -> None:
x = distance * math.sin(horizontal_angle) * math.cos(vertical_angle)
y = distance * math.sin(vertical_angle)
z = distance * math.cos(horizontal_angle) * math.cos(vertical_angle)
self.position = target + Vector3(x, y, z)
self.look_at(target)
class CameraController:
def __init__(self, camera: Camera):
self.camera = camera
self.target = Vector3.zero()
self.distance = 10.0
self.horizontal_angle = 0.0
self.vertical_angle = 0.0
self.min_vertical = -89.0
self.max_vertical = 89.0
self.pan_speed = 0.01
self.rotate_speed = 0.5
self.zoom_speed = 1.0
def rotate(self, delta_x: float, delta_y: float) -> None:
self.horizontal_angle += delta_x * self.rotate_speed
self.vertical_angle = max(
self.min_vertical,
min(self.max_vertical, self.vertical_angle + delta_y * self.rotate_speed)
)
self._update_camera()
def pan(self, delta_x: float, delta_y: float) -> None:
right = self.camera.rotation.rotate_vector(Vector3.right())
up = self.camera.rotation.rotate_vector(Vector3.up())
self.target = self.target - right * delta_x * self.pan_speed
self.target = self.target + up * delta_y * self.pan_speed
self._update_camera()
def zoom(self, delta: float) -> None:
self.distance = max(1.0, min(100.0, self.distance - delta * self.zoom_speed))
self._update_camera()
def _update_camera(self) -> None:
h_rad = math.radians(self.horizontal_angle)
v_rad = math.radians(self.vertical_angle)
self.camera.orbit_around(self.target, h_rad, v_rad, self.distance)学术注记:3D图形学的核心是渲染管线(Rendering Pipeline),它将3D场景转换为2D图像。现代图形API(OpenGL、DirectX、Vulkan)都遵循类似的管线架构:顶点处理→图元装配→光栅化→片段处理→输出合并。理解渲染管线对于优化渲染性能和实现高级视觉效果至关重要。
53.4 本章小结
本章详细介绍了Python 3D图形编程的核心概念和实践:
- 数学基础:向量运算、矩阵变换、四元数旋转
- 3D变换:平移、旋转、缩放、父子层级
- 3D模型:顶点数据、网格生成、法线计算
- 基本几何体:立方体、球体、平面、圆柱体
- 光照系统:方向光、点光源、聚光灯、材质
- 相机系统:透视投影、正交投影、相机控制
53.4.1 知识图谱
3D图形编程
├── 数学基础
│ ├── 向量运算(加减、点积、叉积)
│ ├── 矩阵运算(乘法、逆矩阵、转置)
│ ├── 四元数(旋转、插值、转换)
│ └── 变换(平移、旋转、缩放)
├── 渲染管线
│ ├── 顶点处理(变换、光照)
│ ├── 图元装配(三角形、线段)
│ ├── 光栅化(片段生成)
│ ├── 片段处理(着色、纹理)
│ └── 输出合并(深度测试、混合)
├── 3D模型
│ ├── 顶点数据(位置、法线、UV)
│ ├── 索引缓冲(三角形列表)
│ ├── 网格生成(程序化建模)
│ └── 模型加载(OBJ、FBX、glTF)
├── 光照系统
│ ├── 光源类型(方向、点、聚光)
│ ├── 光照模型(Phong、PBR)
│ ├── 材质系统(反照率、金属度)
│ └── 阴影映射(阴影贴图)
└── 相机系统
├── 投影类型(透视、正交)
├── 视图矩阵(观察变换)
├── 相机控制(轨道、飞行)
└── 视锥剔除(可见性判断)53.4.2 最佳实践清单
| 场景 | 推荐做法 | 避免做法 |
|---|---|---|
| 向量归一化 | 使用前检查长度是否为0 | 直接除以长度 |
| 矩阵乘法 | 注意顺序(右乘 vs 左乘) | 忽略矩阵顺序 |
| 四元数旋转 | 用于平滑插值 | 欧拉角插值 |
| 法线计算 | 使用叉积计算面法线 | 忽略顶点法线 |
| 相机控制 | 使用四元数避免万向锁 | 直接操作欧拉角 |
| 性能优化 | 使用实例化渲染 | 每帧重建网格 |
| 坐标系统 | 统一使用右手坐标系 | 混用不同坐标系 |
53.4.3 技术选型指南
| 需求场景 | 推荐方案 | 备选方案 |
|---|---|---|
| 学习入门 | PyGame + PyOpenGL | ModernGL |
| 游戏开发 | Panda3D、Ursina | Godot(Python绑定) |
| 科学可视化 | VTK、Mayavi | PyVista |
| 数据可视化 | PyVista、Plotly 3D | Matplotlib 3D |
| CAD/建模 | Blender Python API | FreeCAD |
| 实时渲染 | ModernGL | PyOpenGL |
| 物理模拟 | PyBullet | Panda3D Physics |
练习题
基础题
实现一个完整的Vector3类,支持所有基本运算(加减、点积、叉积、归一化)。
创建一个程序化网格生成器,生成一个圆环体(Torus)。
实现一个简单的相机系统,支持WASD键移动和鼠标旋转。
进阶题
实现一个OBJ文件加载器,支持顶点、法线和纹理坐标。
开发一个简单的Phong光照着色器,支持环境光、漫反射和镜面反射。
实现一个射线拾取系统,能够检测鼠标点击的3D物体。
项目实践
- 3D场景编辑器:构建一个简单的3D场景编辑器,要求:
- 支持多种几何体的创建(立方体、球体、圆柱)
- 实现物体的选择、移动、旋转、缩放
- 支持多光源场景
- 提供相机控制(轨道、飞行模式)
- 实现基本的材质编辑
思考题
四元数相比欧拉角有哪些优势?在什么情况下会出现万向锁问题?
透视投影和正交投影的区别是什么?它们分别适用于什么场景?
PBR(基于物理的渲染)与传统Phong光照模型相比有哪些优势?为什么现代游戏引擎普遍采用PBR?
扩展阅读
53.5.1 图形学基础
- OpenGL教程 (https://learnopengl.com/) — 最优秀的现代OpenGL教程
- 3D数学基础:图形与游戏开发 (https://gamemath.com/) — 游戏开发必读数学书籍
- Real-Time Rendering (https://www.realtimerendering.com/) — 实时渲染圣经
- Physically Based Rendering (https://www.pbr-book.org/) — PBR理论权威参考
53.5.2 Python图形库
- PyOpenGL文档 (http://pyopengl.sourceforge.net/) — Python OpenGL绑定
- ModernGL (https://moderngl.readthedocs.io/) — 现代Python OpenGL封装
- PyGame (https://www.pygame.org/docs/) — 游戏开发库
- Panda3D (https://docs.panda3d.org/) — 开源3D游戏引擎
53.5.3 游戏引擎
- Ursina引擎 (https://www.ursinaengine.org/) — 简洁的Python 3D游戏引擎
- Godot Python (https://github.com/touilleMan/godot-python) — Godot的Python绑定
- Blender Python API (https://docs.blender.org/api/) — Blender脚本接口
- Unity ML-Agents (https://unity.com/products/ml-agents) — Unity机器学习代理
53.5.4 进阶书籍
- 《游戏引擎架构》 (Jason Gregory) — 游戏引擎设计经典
- 《计算机图形学原理与实践》 — 图形学权威教材
- 《GPU Gems》系列 — GPU编程技巧合集
- 《Real-Time Shadows》 — 实时阴影技术
下一章:第54章 编译器与解释器