八、纹理和渲染
到目前为止,我们已经将代码示例限制为在 Blender 中创建网格和附加组件。对于 3D 艺术家和动画师来说,3D 建模的目标是通过渲染图像和视频使场景变得栩栩如生。Blender Python 中的渲染实际上非常简单,通常只需要一次函数调用。为了把我们带到我们想要渲染场景的地方,我们将讨论纹理、光照和摄像机的放置。
在本章结束时,用户将能够为纹理、照明、相机放置和静态渲染创建自动化管道。虽然使用 Blender Python 渲染动画视频是可能的,但我们在这里的讨论将仅限于渲染静态图像。
纹理词汇
一般来说,有许多类型的纹理,在 Blender 中有许多额外的参数化类型。我们的第一个例子使用漫射纹理和法线贴图来说明材质在 Blender 中的作用。在我们继续之前,我们将建立一些关于纹理的新词汇。
Blender 中的影响类型
虽然这些效果在 Blender 中被归类为影响,但它们在传统上是指 3D 建模的广阔领域中的纹理类型。Blender 有自己的纹理类型,每种类型都可以采用这些影响。参见图 8-1 了解这些影响在 Blender GUI 中的位置。他们可以在➤材料➤影响的属性中找到。
图 8-1。
Influences in Blender
- 漫射纹理用于给对象着色。漫射纹理可以描述 Blender 中对象的颜色、强度、alpha 级别和透明度。为了在物体表面覆盖图像,我们使用了漫反射颜色纹理。
- 着色纹理描述了对象如何与场景中的其他对象进行交互。如果我们希望对象镜像另一个对象,将颜色发射到另一个对象上,或者将环境光投射到场景中,我们在 Blender 中指定必要的着色属性。
- 镜面纹理描述了对象对光线的反应。例如,如果我们提供一个静态模糊的图像(就像在旧电视屏幕上看到的一样)作为镜面纹理,光线会像闪亮的沙粒一样从对象上反射回来。我们可以通过指定颜色对光的反应强度和方向来微调高光贴图。
- 几何体纹理允许对象影响对象的几何外观。例如,如果我们为几何地图提供黑白条纹并指定法线地图,我们将在模型中看到 3D 山脊。值得注意的是,这些效果仅在渲染中实现,而不是在网格数据本身中实现。
Blender 中的纹理类型
虽然我们将主要使用图像纹理,Blender 有许多可定制的纹理供我们选择。这些是从图 8-2 所示的➤材料➤类型菜单中选择的。
图 8-2。
Texture types in Blender
图像和视频以及环境贴图选项可以导入图像和视频文件。剩余的纹理可以在 Blender 中参数化,以获得所需的结果。我们没有详细说明如何具体处理这些参数化纹理,因为要讨论的参数有很多。清单 8-1 解释了如何使用图像和视频类型的参数来纹理化一个对象。从这里开始,读者应该能够使用 Blender 的 Python 工具提示为任何剩余的类型复制这个过程。
添加和配置纹理
在讨论文件交换格式时,我们在第四章中提到了纹理的定义。纹理通过 uv 坐标映射到 3D 空间中的面。为了将正方形图像作为纹理映射到网格的正方形表面,我们将 uv 坐标[(0, 0), (1, 0), (0, 1), (1, 1)]
分别指定到网格的左下角、右下角、左上角和右上角。随着面的形状变得越来越复杂,实现所需纹理映射的过程也越来越复杂。接下来我们讨论将 uv 坐标映射到普通形状的方法。
加载纹理和生成 UV 贴图
由于 Blender 处理纹理导入和材质的方式,uv 贴图并不是一项简单的任务。我们必须克服一些程序上的障碍,才能在脚本中明确定义对象的 uv 坐标。一旦我们达到这一点,精确指定紫外线坐标是相当简单的。我们在清单 8-1 中举例说明。
我们在示例中使用数字 1 和 2 的样本图像,这些图像可以在 http://blender.chrisconlan.com/number_1.png
和 http://blender.chrisconlan.com/number_2.png
下载。读者可以使用这些图像或任何其他想要的图像来列出 8-1 。结果见图 8-4 。我们将在下面的章节中讨论清单 8-1 中使用的函数。
Note
运行该脚本后,通过在 3D 视口标题中选择渲染视图来查看结果,如图 8-3 所示。
图 8-3。
Selecting rendered view
图 8-4。
Explicitly mapping UV coordinates
import bpy
import bmesh
from mathutils import Color
# Clear scene
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create cube
bpy.ops.mesh.primitive_cube_add(radius = 1, location = (0, 0, 0))
bpy.ops.object.mode_set(mode = 'EDIT')
# Create material to hold textures
material_obj = bpy.data.materials.new('number_1_material')
### Begin configure the number one ###
# Path to image
imgpath = '/home/cconlan/Desktop/blender-book/ch08_pics/number_1.png'
image_obj = bpy.data.images.load(imgpath)
# Create image texture from image
texture_obj = bpy.data.textures.new('number_1_tex', type='IMAGE')
texture_obj.image = image_obj
# Add texture slot for image texture
texture_slot = material_obj.texture_slots.add()
texture_slot.texture = texture_obj
### Begin configuring the number two ###
# Path to image
imgpath = '/home/cconlan/Desktop/blender-book/ch08_pics/number_2.png'
image_obj = bpy.data.images.load(imgpath)
# Create image texture from image
texture_obj = bpy.data.textures.new('number_2_tex', type='IMAGE')
texture_obj.image = image_obj
# Add texture slot for image texture
texture_slot = material_obj.texture_slots.add()
texture_slot.texture = texture_obj
# Tone down color map, turn on and tone up normal mapping
texture_slot.diffuse_color_factor = 0.2
texture_slot.use_map_normal = True
texture_slot.normal_factor = 2.0
### Finish configuring textures ###
# Add material to current object
bpy.context.object.data.materials.append(material_obj)
### Begin configuring UV coordinates ###
bm = bmesh.from_edit_mesh(bpy.context.edit_object.data)
bm.faces.ensure_lookup_table()
# Index of face to texture
face_ind = 0
bpy.ops.mesh.select_all(action='DESELECT')
bm.faces[face_ind].select = True
# Unwrap to instantiate uv layer
bpy.ops.uv.unwrap()
# Grab uv layer
uv_layer = bm.loops.layers.uv.active
# Begin mapping...
loop_data = bm.faces[face_ind].loops
# bottom right
uv_data = loop_data[0][uv_layer].uv
uv_data.x = 1.0
uv_data.y = 0.0
# top right
uv_data = loop_data[1][uv_layer].uv
uv_data.x = 1.0
uv_data.y = 1.0
# top
left
uv_data = loop_data[2][uv_layer].uv
uv_data.x = 0.0
uv_data.y = 1.0
# bottom left
uv_data = loop_data[3][uv_layer].uv
uv_data.x = 0.0
uv_data.y = 0.0
# Change background color to white to match our example
bpy.data.worlds['World'].horizon_color = Color((1.0, 1.0, 1.0))
# Switch to object mode to add lights
bpy.ops.object.mode_set(mode='OBJECT')
# Liberally add lights
dist = 5
for side in [-1, 1]:
for coord in [0, 1, 2]:
loc = [0, 0, 0]
loc[coord] = side * dist
bpy.ops.object.lamp_add(type='POINT', location=loc)
# Switch to rendered mode to view results
Listing 8-1.Loading Textures and Generating UV Maps
纹理与 Blender 中的材料
纹理是 3D 建模中的一个广义术语。它可以指漫射纹理、颜色纹理、渐变纹理、凹凸贴图等等。值得注意的是,我们可以同时将所有这些形式的纹理映射到一个对象上。例如,屋顶上的一组木瓦可能需要图像纹理、漫反射贴图和凹凸贴图,以便在渲染时看起来逼真。
此外,现实世界材质的图像、漫反射贴图和凹凸贴图通常是专门为彼此构建的。在我们的瓦片区示例中,凹凸贴图将定义物理瓦片区之间的脊线,因为它们出现在图像纹理中。漫射贴图将进一步定义我们通常在屋顶木瓦上看到的闪亮颗粒。按照设计,表示图像和地图的文件不一定要与集合外的其他文件一起工作。这就是 Blender 中材料的动机。
Blender 中的材质是纹理相关数据的集合。它可能包括前面提到的任何图像和贴图,也可能包括其他图像和贴图。因此,我们必须首先从其组成纹理构建材质,然后将材质指定给对象。不管一个材质是由一个还是多个纹理组成,纹理数据都必须分配给这个材质。然后,必须将材质指定给对象。
这一讨论揭示了清单 8-1 中物料管理背后的动机。我们首先声明并操作所有需要的纹理,然后通过bpy.context.object.data.materials.append()
将整个材质添加到对象中。从这里,我们可以操纵整个材料的 uv 坐标。
UV 坐标和循环
清单 8-1 的后半部分访问一个我们以前没有使用过的数据端点。我们要访问的 uv 坐标数据层包含在一个 loops 对象中。可以将循环视为跟踪 3D 对象的一组顶点的 3D 多边形。循环可以跨越多个面,但必须在同一点开始和结束。当循环跨越多个面时,它们旨在捕捉相邻面的局部集合。
3D 艺术家可以使用帮助他们创建循环的高级工具。然后,这些循环帮助他们手动指定 uv 坐标。虽然我们不会在 Blender Python 中操作这些循环,但理解它们如何工作是很重要的,因为loops
数据对象位于网格本身和 uv 层之间。
幸运的是,Blender 中的循环数据对象与我们习惯使用的bmesh.faces[].verts[]
对象是一一对应的。换句话说,对于任意两个整数,f
和v
,bm.faces[f].loops[v][uv_layer].uv
访问的(u,v)坐标对应于bm.faces[f].verts[v].co
访问的(x,y,z)坐标。
值得注意的是,两个整数f
和v
可能不指定 3D 空间中的唯一点。在默认的 Blender 2.78c 立方体中,如在启动文件中出现的那样,f:v
对0:2
、3:3
和4:0
都对应于 3D 空间中的点(-1.0, -1.0, -1.0)
。当立方体被纹理化时,这些 uv 坐标通常是唯一的,因为它们都对应于纹理贴图的不同部分。
关于索引和交叉兼容性的另一个说明
当动态纹理化对象时,我们会遇到一个类似于第三章中提到的问题。在那一节中,我们注意到顶点索引的行为是可复制的,但是不可控制的,因此证明了通过特征选择作为一种解决方法的合理性(在清单 3-13 中实现)。同样的概念也适用于这里,除了我们必须与bm.faces[f].verts[v].co
一起工作,而不仅仅是与bm.verts[v].co
一起工作。
例如,假设我们想在一个立方体的顶部沿 y 轴垂直放置一个纹理。一个可能的解决方案是使用我们的ut.py
工具包中的ut.act.select_by_loc()
来根据它的位置选择立方体的顶面。从这里,我们可以用f_ind = [f.index
代替bm.faces if f.select][0]
中的f
,返回所选的人脸索引。使用面索引,我们可以将面的顶点存储为bm.faces[f_ind.verts]]
中v
的vert_vectors = v.co
,并使用该信息沿着立方体定位我们的纹理。
我们的另一个选择是违背第 [3 章“关于索引和交叉兼容性的注释”的建议,假设我们在纹理化之前知道一个对象的面顶点的位置和方向。我们通常可以预先确定这些信息,并将其硬编码到我们的纹理脚本中,就像我们在清单 8-1 中所做的那样。对于受控和内部使用,这是一个可行的选择,但对于我们将与社区共享并经过跨版本兼容性测试的代码,建议不要这样做。
基于我们到目前为止的讨论,读者应该有工具和知识来实现他们想要的动态(或非动态)纹理脚本。第三章的引用部分,以及随后的部分,与读者可能承担的任何动态纹理任务非常相似。
我们现在继续讨论 Blender 中的渲染和它的一些用途。
移除未使用的纹理和材质
我们已经讨论了在 Blender 中删除网格和对象的许多有用的功能。随着我们不断地测试脚本,我们的材质和纹理数据会在我们没有意识到的情况下很快变得混乱。Blender 会将纹理重命名为my_texture.001
、my_texture.002
等。当我们忘记删除它们时。
纹理和材质必须没有用户才能被删除。在这种情况下,用户指的是当前为其分配了对象的数量。为了删除纹理和材质,我们循环遍历我们的bpy.data.materials
和bpy.data.textures
数据块,并调用那些没有被使用的.remove()
。参见清单 8-2 了解该实施。
import bpy
mats = bpy.data.materials
for dblock in mats:
if not dblock.users:
mats.remove(dblock)
texs = bpy.data.textures
for dblock in mats:
if not dblock.users:
texs.remove(dblock)
Listing 8-2.Loading Textures and Generating UV Maps
使用 Blender 渲染进行渲染
使用 Blender 的内置渲染功能非常简单。我们介绍并解释如何在场景中定位灯光和摄像机,然后调用渲染函数来创建图像。我们的大部分讨论集中在语义和相机和灯光的辅助函数上。
添加灯光
在清单 8-1 中,我们在立方体周围添加了六盏灯,使其在 3D 视口中的 Blender 渲染视图中可见。正确使用这个视图和渲染通常需要灯光。光照是 3D 建模中一个重要的大领域。在本节中,我们将重点关注与照明相关的 Blender Python 函数,而不是美观的照明的一般实践。
在三维视口标题,我们可以导航到添加➤灯,以选择任何 Blender 的内置灯。使用 Python 工具提示,我们可以看到它们都依赖于函数bpy.ops.object.lamp_add()
,用type= parameter
决定光线的类型。我们有SUN
、POINT
、SPOT
、HEMI
和AREA
等选项。每种类型都有自己的参数集需要配置。
当谈到程序生成的照明时,我们主要关心的是位置和方向。我们将介绍一些管理布局和方向的工具。例如,要懒洋洋地照亮整个场景,我们可能希望在场景的聚集边界框周围创建点光源。此外,我们可能希望将聚光灯直接指向另一个任意放置的对象。见清单 8-3 中可能有助于程序性添加灯光的实用程序列表。我们在清单 8-3 中声明的所有函数都已经添加到我们的工具包ut.py
中,可以在 http://blender.chrisconlan.com/ut.py
下载。
参见表 8-1 了解每种类型灯的基本描述
表 8-1。
Types of Lights
| 类型 | 描述 | | --- | --- | | 要点 | 向各个方向均匀发光;旋转没有效果 | | 地点 | 向特定的方向发射光锥 | | 面积 | 从矩形区域发光;遵循朗伯分布 | | 半球的 | 类似于面积,但具有球形曲率 | | 太阳 | 在特定方向发射正交光;位置没有影响 |添加摄像机
渲染场景需要摄像机。为了程序化地添加摄像机,我们必须定位它,调整它的方向,并修改它的参数。我们将使用清单 8-3 中的函数来定位和引导摄像机以及灯光。
当程序性地生成摄像机时,我们必须解决的最大问题是确定距离和视野,以使整个场景在渲染时不会显得太小。我们将使用一些基本的三角学来解决这些问题。
视场(FoV)是从相机向外投射的一对两个角度(θ x ,θ y ,定义了一个无限延伸的四棱锥。如果在它前面没有任何东西,位于这个四棱锥内的所有东西都可以被摄像机看到。举个例子,iPhone 6 相机在风景模式下的视场角约为(63,47)度。请注意,当摄影师通俗地提到 FoV 时,他们通常只指两个角度中较大的一个。
我们必须了解 FoV,这样我们才能确保相机的放置和校准捕捉到我们想要渲染的场景。
给定一个具有 FoV (θ x ,θ y )的相机,该相机沿着并且面对具有高 h 和宽 w 的边界框的场景居中,捕获该场景所需的离场景 d 的距离是 max(d x ,d y )。对于本讨论,d x 和 d y 分别表示沿水平和垂直维度捕捉场景所需的距离。直观表示见图 8-5 。利用基本的三角学,我们得到
)
图 8-5。
Field of view along the y-axis
这只说明了相机指向 x 轴或 y 轴的简单情况,但它足以满足我们的目的。在清单 8-4 中,我们使用之前建立的效用函数来控制摄像机,使其能够渲染整个可视场景。
# Point a light or camera at a location specified by "target"
def point_at(ob, target):
ob_loc = ob.location
dir_vec = target - ob.location
ob.rotation_euler = dir_vec.to_track_quat('-Z', 'Y').to_euler()
# Return the aggregate bounding box of all meshes in a scene
def scene_bounding_box():
# Get names of all meshes
mesh_names = [v.name for v in bpy.context.scene.objects if v.type == 'MESH']
# Save an initial value
# Save as list for single-entry modification
co = coords(mesh_names[0])[0]
bb_max = [co[0], co[1], co[2]]
bb_min = [co[0], co[1], co[2]]
# Test and store maxima and minima
for i in range(0, len(mesh_names)):
co = coords(mesh_names[i])
for j in range(0, len(co)):
for k in range(0, 3):
if co[j][k] > bb_max[k]:
bb_max[k] = co[j][k]
if co[j][k] < bb_min[k]:
bb_min[k] = co[j][k]
# Convert to tuples
bb_max = (bb_max[0], bb_max[1], bb_max[2])
bb_min = (bb_min[0], bb_min[1], bb_min[2])
return [bb_min, bb_max]
Listing 8-3.Utilities for Lights and Cameras
渲染图像
渲染是在给定 3D 数据的情况下计算高分辨率影像和视频的过程。渲染不是即时的。当我们平移和旋转相机时,Blender 中的 3D 视口似乎会流畅地移动,渲染可能会花费相当多的时间。3D 视口是 3D 数据的即时渲染,但它并不代表与传统渲染相同的质量或清晰度水平。
在清单 8-4 中,我们使用 Blender 渲染和 OpenGL 渲染来渲染清单 8-1 的输出。此示例假设将相机定位为从场景的 yz 中心点沿 x 轴向上指向场景的中心点,这样它将捕捉整个场景。我们使用前面讨论过的公式来实现这一点。回想一下,这些等式假设了一个简单的情况,即我们沿着一个轴对准摄像机。
最终的渲染将对象直接捕获到框架中。清单 8-1 中创建的立方体的混合渲染见图 8-6 。对于 Blender 渲染,场景的摄影机用作渲染摄影机。这就是为什么知道如何在程序上设置相机的位置是很重要的。如果我们想要循环并渲染许多场景,我们需要确信场景将在帧内被捕获。
图 8-6。
Blender Render
我们还可以使用 OpenGL 渲染来渲染 3D 视口的快照。这将捕捉场景的基本特征,类似于我们在实体视图中在对象模式下看到的 3D 视口。结果如图 8-7 所示。请注意,在此视图中,我们可以看到灯光和相机,但不能看到材质。当我们调用bpy.ops.render.opengl()
时,设置view_context = True
将导致 Blender 使用 3D 视口相机(用户的视图)而不是场景相机。
图 8-7。
OpenGL rendering
### Assumes output of Listing 8-1 is in scene at runtime ###
import bpy
import bmesh
import ut
from math import pi, tan
from mathutils import Vector
# Get scene's bounding box (meshes only)
bbox = ut.scene_bounding_box()
# Calculate median of bounding box
bbox_med = ( (bbox[0][0] + bbox[1][0])/2,
(bbox[0][1] + bbox[1][1])/2,
(bbox[0][2] + bbox[1][2])/2 )
# Calculate size of bounding box
bbox_size = ( (bbox[1][0] - bbox[0][0]),
(bbox[1][1] - bbox[0][1]),
(bbox[1][2] - bbox[0][2]) )
# Add camera to scene
bpy.ops.object.camera_add(location=(0, 0, 0), rotation=(0, 0, 0))
camera_obj = bpy.context.object
camera_obj.name = 'Camera_1'
# Required for us to manipulate FoV as angles
camera_obj.data.lens_unit = 'FOV'
# Set image resolution in pixels
# Output will be half the pixelage set here
scn = bpy.context.scene
scn.render.resolution_x = 1800
scn.render.resolution_y = 1200
# Compute FoV angles
aspect_ratio = scn.render.resolution_x / scn.render.resolution_y
if aspect_ratio > 1:
camera_angle_x = camera_obj.data.angle
camera_angle_y = camera_angle_x / aspect_ratio
else:
camera_angle_y = camera_obj.data.angle
camera_angle_x = camera_angle_y * aspect_ratio
# Set the scene's camera to our new camera
scn.camera = camera_obj
# Determine the distance to move the camera away from the scene
camera_dist_x = (bbox_size[1]/2) * (tan(camera_angle_x / 2) ** -1)
camera_dist_y = (bbox_size[2]/2) * (tan(camera_angle_y / 2) ** -1)
camera_dist = max(camera_dist_x, camera_dist_y)
# Multiply the distance by an arbitrary buffer
camera_buffer = 1.10
camera_dist *= camera_buffer
# Position the camera to point up the x-axis
camera_loc = (bbox[0][1] - camera_dist, bbox_med[1], bbox_med[2])
# Set new location and point camera at median of scene
camera_obj.location = camera_loc
ut.point_at(camera_obj, Vector(bbox_med))
# Set render path
render_path = '/home/cconlan/Desktop/blender_render.png'
bpy.data.scenes['Scene'].render.filepath = render_path
# Render using Blender Render
bpy.ops.render.render( write_still = True )
# Set render path
render_path = '/home/cconlan/Desktop/opengl_render.png'
bpy.data.scenes['Scene'].render.filepath = render_path
# Render 3D viewport using OpenGL render
bpy.ops.render.opengl( write_still = True , view_context = True )
Listing 8-4.Rendering Using Blender Render and OpenGL Render
结论
本章总结了我们对 Blender Python API 的讨论。即使有许多例子,这篇文章也不是一个全面的指南。这是对 Blender 复杂性和模块化的最好证明。Blender 可以使用 Python API 进行编辑、调整、定制和扩展。这本书的作者和协助其开发的专业人士希望这些知识有助于鼓励 Blender 社区的研究和开发。