Blender Python API 教程(三)

原文:The Blender Python API

协议:CC BY-NC-SA 4.0

八、纹理和渲染

到目前为止,我们已经将代码示例限制为在 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.pnghttp://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[]对象是一一对应的。换句话说,对于任意两个整数,fvbm.faces[f].loops[v][uv_layer].uv访问的(u,v)坐标对应于bm.faces[f].verts[v].co访问的(x,y,z)坐标。

值得注意的是,两个整数fv可能不指定 3D 空间中的唯一点。在默认的 Blender 2.78c 立方体中,如在启动文件中出现的那样,f:v0:23:34: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]]vvert_vectors = v.co,并使用该信息沿着立方体定位我们的纹理。

我们的另一个选择是违背第 [3 章“关于索引和交叉兼容性的注释”的建议,假设我们在纹理化之前知道一个对象的面顶点的位置和方向。我们通常可以预先确定这些信息,并将其硬编码到我们的纹理脚本中,就像我们在清单 8-1 中所做的那样。对于受控和内部使用,这是一个可行的选择,但对于我们将与社区共享并经过跨版本兼容性测试的代码,建议不要这样做。

基于我们到目前为止的讨论,读者应该有工具和知识来实现他们想要的动态(或非动态)纹理脚本。第三章的引用部分,以及随后的部分,与读者可能承担的任何动态纹理任务非常相似。

我们现在继续讨论 Blender 中的渲染和它的一些用途。

移除未使用的纹理和材质

我们已经讨论了在 Blender 中删除网格和对象的许多有用的功能。随着我们不断地测试脚本,我们的材质和纹理数据会在我们没有意识到的情况下很快变得混乱。Blender 会将纹理重命名为my_texture.001my_texture.002等。当我们忘记删除它们时。

纹理和材质必须没有用户才能被删除。在这种情况下,用户指的是当前为其分配了对象的数量。为了删除纹理和材质,我们循环遍历我们的bpy.data.materialsbpy.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决定光线的类型。我们有SUNPOINTSPOTHEMIAREA等选项。每种类型都有自己的参数集需要配置。

当谈到程序生成的照明时,我们主要关心的是位置和方向。我们将介绍一些管理布局和方向的工具。例如,要懒洋洋地照亮整个场景,我们可能希望在场景的聚集边界框周围创建点光源。此外,我们可能希望将聚光灯直接指向另一个任意放置的对象。见清单 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 社区的研究和开发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值