Decorative image frame

【笔记】【Maya工具】Python For Maya(Cmds)(四)Controller Library & LightManager

一、Controller Library

1.1 Qt Environment

使用QT创建controller lib

和Maya cmdsUi相比,更推荐使用qt

CMDS是为MEL设计的,UI函数只有qt的一小部分

直接(mayapy -m )pip install Qt.py

1
2
3
import Qt
win = Qt.QtWidgets.QDialog()
win.show()

在maya上这样就直接运行了

但是在正常的python上,根据文档要这样

1
2
3
4
5
6
7
import sys
from Qt import QtWidgets

app = QtWidgets.QApplication(sys.argv)
button = QtWidgets.QPushButton("Hello World")
button.show()
app.exec_()

好像是现代maya版本就是Qt管理窗口,所以已经有QtApp了

1
2
3
from Qt import QtWidgets,QtCore,QtGui
from PySide2 import QtWidgets,QtCore,QtGui
from PyQt5 import QtWidgets,QtCore,QtGui

甚至我只mayapy安装了Qt.py,这三个命令都能用了

1.2 Controller Library

controllerLibrary.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from maya import cmds
import os
import json
import pprint

USERAPPDIR = cmds.internalVar(userAppDir = True)
print(USERAPPDIR)
DIRECTORY = os.path.join(USERAPPDIR,'controllerLibrary')

def createDirectory(directory = DIRECTORY):
"""
Creates the given directory
Args:
directory(str):The directory to create
"""
if not os.path.exists(directory):
os.mkdir(directory)

class ControllerLibrary(dict):# stm继承了个字典……
def save(self, name, directory = DIRECTORY,screenshot = True, **info):
createDirectory(directory)# 不存在路径则创建文件夹路径

path = os.path.join(directory,"{}.ma".format(name))
infoFile = os.path.join(directory,'{}.json'.format(name))# 保存数据

# ========保存文件========
cmds.file(rename = path)

if cmds.ls(selection = True):# 有选择则只保存选择
cmds.file(force = True, type = 'mayaAscii', exportSelected = True)
else:# 无选择则全部保存
cmds.file(save = True, type = 'mayaAscii', force = True)

# ========保存屏幕快照========

if screenshot:
info['screenshot'] = self.saveScreenShot(name,directory = directory)


# ========输出json========
info['name'] = name
info['path'] = path
with open(infoFile, 'w') as f:
json.dump(info, f, indent = 4)# 把输入的info储存为json
self[name] = info

def find(self, directory = DIRECTORY):

self.clear()
print('1',directory)
if not os.path.exists(directory):
return
files = os.listdir(directory)# 当前路径下所有文件
mayaFiles = [f for f in files if f.endswith('.ma')]
print(files)
for ma in mayaFiles:
name,ext = os.path.splitext(ma)# 分离文件名和扩展名
path = os.path.join(directory,ma)

infoFile = '{}'.format(name)
if infoFile in files:
infoFile = os.path.join(directory, infoFile)

with open(infoFile, 'r') as f:
info = json.load(f)
else:
print('No info file found')
info = {}

screenshot = '{}.jpg'.format(name)
if screenshot in files:
info['screenshot'] = os.path.join(directory,name)
info['name'] = name
info['path'] = path
self[name] = info
pprint.pprint(self)

def load(self, name):
path = self[name]['path']
cmds.file(path, i = True, usingNamespace = False)

def saveScreenShot(self, name, directory = DIRECTORY):
path = os.path.join(directory, '{}.jpg'.format(name))
cmds.viewFit()
cmds.setAttr('defaultRenderGlobals.imageFormat', 8)# jpeg
#cmds.setAttr("defaultArnoldDriver.aiTranslator",'png',type = "string") 阿诺德渲染器

cmds.playblast(completeFilename = path, forceOverwrite = True, format = 'image', width = 200, height = 200, showOrnaments = False, startTime = 1, endTime = 1, viewer = False)
return path

https://blog.csdn.net/tian0000hai/article/details/116952413阿诺德渲染器的渲染输出设置

libraryUI.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
from maya import cmds
from PySide2 import QtWidgets, QtCore, QtGui
import conLibrary.controllerLibrary as controllerLibrary
import pprint

import importlib
importlib.reload(controllerLibrary)

class ControllerLibraryUI(QtWidgets.QDialog):
"""
The ControllerLibraryUI is a dialog that lets us save and import controllers
"""

def __init__(self):
#super(ControllerLibraryUI, self).__init__()
super().__init__()
#super是一个,用于调用父类方法,用于多继承,而这里是为了不用考虑继承的父类是什么
#QtWidgets.QDialog.__init__(self)因为是单继承,和这个相等
self.setWindowTitle('Controller LibraryUI')
self.library = controllerLibrary.ControllerLibrary()

self.buildUI()
self.populate()

def buildUI(self):
"""
This method builds out the UI
"""
print('bulding UI')
# 整体行分布
layout = QtWidgets.QVBoxLayout(self)

# 行分布中的一行,提供纵向分布,编辑栏view
saveWidget = QtWidgets.QWidget()
saveLayout = QtWidgets.QHBoxLayout(saveWidget)
layout.addWidget(saveWidget)

# 行内组件
self.saveNameField = QtWidgets.QLineEdit()
saveLayout.addWidget(self.saveNameField)

# 行内组件
saveBtn = QtWidgets.QPushButton('Save')
saveBtn.clicked.connect(self.save)
saveLayout.addWidget(saveBtn)

# 新的一行,缩略图view
size = 64
buffer = 12
# create a grid list Widget to display our controller thumbnails
self.listWidget = QtWidgets.QListWidget()
self.listWidget.setViewMode(QtWidgets.QListWidget.IconMode)
self.listWidget.setIconSize(QtCore.QSize(size,size))
self.listWidget.setResizeMode(QtWidgets.QListWidget.Adjust)
self.listWidget.setGridSize(QtCore.QSize(size+buffer,size+buffer))
layout.addWidget(self.listWidget)

# 新的一行button view
btnWidget = QtWidgets.QWidget()
btnLayout = QtWidgets.QHBoxLayout(btnWidget)
layout.addWidget(btnWidget)

# button view组件
importBtn = QtWidgets.QPushButton('Import')
importBtn.clicked.connect(self.load)
btnLayout.addWidget(importBtn)

refreshBtn = QtWidgets.QPushButton('Refresh')
refreshBtn.clicked.connect(self.populate)#注意只提供方法名,没有括号,括号代表调用
btnLayout.addWidget(refreshBtn)

closeBtn = QtWidgets.QPushButton('Close')
closeBtn.clicked.connect(self.close)# self.close继承于QDialog
btnLayout.addWidget(closeBtn)


def populate(self):
"""This clears the listWidget and then repopulates it with the content of our library"""
self.listWidget.clear()

print("populating")
self.library.find()

for name, info in self.library.items():
item = QtWidgets.QListWidgetItem(name)
self.listWidget.addItem(item)

screenshot = info.get('screenshot')
if screenshot:
icon = QtGui.QIcon(screenshot)
item.setIcon(icon)

item.setToolTip(pprint.pformat(info))

def load(self):
"""This loads the currently selected controller"""
currentItem = self.listWidget.currentItem()

if not currentItem:
return
name = currentItem.text()
self.library.load(name)
def save(self):
"""This saves the controller with the given file name"""
name = self.saveNameField.text()
if not name.strip():
cmds.warning("You must give a name!")
return
self.library.save(name)
self.populate()
self.saveNameField.setText('')

def showUI():
"""
This shows and returns a handle to the UI
Returns:
Qdialog
"""
ui = ControllerLibraryUI()
ui.show()
return ui

二、Light Manager

创建一个场景的灯光管理窗口,并且可以和maya的窗口融合

2.1 PyMel

pymel包含了cmds、openmaya

1
2
3
4
import pymel.core as pm
cube = pm.polyCube()
print(cube)
#[nt.Transform('pCube1'), nt.PolyCube('polyCube1')]

哥们试了半天,maya2023只有pymel==1.3.0a1能用

可以看到pyme创建的polyCube是和openmaya一样附带Node的,并且有一个nt来封装

1
2
#cmds.rename(cube[0],'newName')
cube[0].rename('Foo')

image-20220703154445036

pymel返回的对象不像cmds里面直接是string,是有一个nt对象封装(==PyNode class==)的,因此使用rename方法也需要调用这个nt对象内部的方法

这就和OpenMaya是差不多的

  • mayatool使用cmd是很常见的
  • pymel一些情况会很慢,因为它封装了Node对象,不想cmd使用string
  • pymel也不被Audodesk直接支持,官方文档里也写了这一点

2.2 UI and Partial funcitons

lightingManager.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
from Qt import QtWidgets,QtCore,QtGui
import pymel.core as pm
from functools import partial
import Qt
import logging
from maya import OpenMayaUI as omui

logging.basicConfig()
logger = logging.getLogger('LightingManager')
logger.setLevel(logging.DEBUG)

if Qt.__binding__ == 'PySide':
logger.debug('Using Pyside with shiboken')
from shiboken import warpInstance
from Qt.QtCore import Signal
elif Qt.__binding__.startswith('PyQt'):
logger.debug('Using PyQt with sip')
from sip import warpInstance as warpInstance
from Qt.QtCore import pyqtSignal as Signal
else:
logger.debug('Using PySide2 with shiboken')
import shiboken2
from Qt.QtCore import Signal


def getMayaMainWindow():
win = omui.MQtUtil_mainWindow()
ptr = shiboken2.wrapInstance(int(win),QtWidgets.QMainWindow)# long是python2
# 并且 shiboken2有改动,不能直接import warpInstance
return ptr


def getDock(name = 'LightingManagerDock'):
deleteDock(name)
ctrl = pm.workspaceControl(name, dockToMainWindow = ('right',1), label = "LightingManager")
qtCtrl = omui.MQtUtil_findControl(ctrl)
ptr = shiboken2.wrapInstance(int(qtCtrl),QtWidgets.QWidget)
return ptr

def deleteDock(name = 'LightingManagerDock'):
if pm.workspaceControl(name, query = True, exists = True):
pm.deleteUI(name)

class LightManager(QtWidgets.QWidget):# QtWidgets.QDialog

lightTypes = {
"Point Light": pm.pointLight,
"Spot Light": pm.spotLight,
"Directional Light": pm.directionalLight,
"Area Light": partial(pm.shadingNode, 'areaLight', asLight = True),
"Volume Light": partial(pm.shadingNode, 'volumeLight', asLight = True)
}

def __init__(self, dock = True):
#
if dock:
parent = getDock()
else:
deleteDock()
try:
pm.deleteUI('LightingManager')
except:
logger.debug("No previous LightManagerUI exists")

#parent = getMayaMainWindow()
parent = QtWidgets.QDialog(parent = getMayaMainWindow())

parent.setObjectName('lightingManager')
parent.setWindowTitle('Lighting Manager')
layout = QtWidgets.QVBoxLayout(parent)

super().__init__(parent = parent)# 把这个窗口的父节点设为maya窗口
#self.setWindowTitle('Lighting Manager')
self.buildUI()
self.populate()

self.parent().layout().addWidget(self)

if not dock:
parent.show()
def populate(self):
while self.scrollLayout.count():
widget = self.scrollLayout.takeAt(0).widget()
if widget:
widget.setVisible(False)
widget.deleteLater()
for light in pm.ls(type=['areaLight','spotLight','pointLight','directionalLight','volumeLight']):
self.addLight(light)

def buildUI(self):
layout = QtWidgets.QGridLayout(self)

self.lightTypeCB = QtWidgets.QComboBox()# 下拉菜单
for lightType in sorted(self.lightTypes):
self.lightTypeCB.addItem(lightType)
layout.addWidget(self.lightTypeCB, 0,0)


createBtn = QtWidgets.QPushButton('Create')
createBtn.clicked.connect(self.createLight)
layout.addWidget(createBtn, 0,1)

scrollWidget = QtWidgets.QWidget()
scrollWidget.setSizePolicy(QtWidgets.QSizePolicy.Maximum,QtWidgets.QSizePolicy.Maximum)
self.scrollLayout = QtWidgets.QVBoxLayout(scrollWidget)

scrollArea = QtWidgets.QScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setWidget(scrollWidget)
layout.addWidget(scrollArea,1,0,1,2)# 位于layout第二行第一列,占据1行2列


refreshBtn = QtWidgets.QPushButton('Refresh')
refreshBtn.clicked.connect(self.populate)
layout.addWidget(refreshBtn, 2,1)

def createLight(self):
lightType = self.lightTypeCB.currentText()
func = self.lightTypes[lightType]

light = func()
self.addLight(light)


def addLight(self, light):
widget = LightWidget(light)
self.scrollLayout.addWidget(widget)
widget.onSolo.connect(self.onSolo)

def onSolo(self, value):
lightWidgets = self.findChildren(LightWidget)
for widget in lightWidgets:
if widget != self.sender():
widget.disableLight(value)


class LightWidget(QtWidgets.QWidget):

onSolo = Signal(bool)#QtCore.pyqtSignal 使用pyqt;QtCore.Singal使用pyside2
# 在我们后面的定义中,对不同版本的Qt进行了判断,可以统一使用
def __init__(self, light):
super().__init__()

if isinstance(light, str):
light = pm.PyNode(light)
self.light = light
self.buildUI()


def buildUI(self):
layout = QtWidgets.QGridLayout(self)

# 可见性选择框
self.name = QtWidgets.QCheckBox(str(self.light.getTransform()))
self.name.setChecked(self.light.visibility.get())
self.name.toggled.connect(lambda val: self.light.getTransform().visibility.set(val))
#def setLightVisibility(val):
# self.light.visibility.set(val)
# self.light 只表示shapeNode,因此需要在transofrm上设置可见项
layout.addWidget(self.name, 0,0)


# Solo按钮
soloBtn = QtWidgets.QPushButton('Solo')
soloBtn.setCheckable(True)
soloBtn.toggled.connect(lambda val: self.onSolo.emit(val))

layout.addWidget(soloBtn, 0,1)

# 删除按钮
deleteBtn = QtWidgets.QPushButton('X')
deleteBtn.clicked.connect(self.deleteLight)
deleteBtn.setMaximumWidth(10)
layout.addWidget(deleteBtn,0,2)

#灯光强度
intensity = QtWidgets.QSlider(QtCore.Qt.Horizontal)
intensity.setMinimum(1)
intensity.setMaximum(1000)
intensity.setValue(self.light.intensity.get())
intensity.valueChanged.connect(lambda val:self.light.intensity.set(val))
layout.addWidget(intensity, 1,0,1,2)

#
self.colorBtn = QtWidgets.QPushButton()
self.colorBtn.setMaximumWidth(20)
self.colorBtn.setMaximumHeight(20)
self.setButtonColor()
self.colorBtn.clicked.connect(self.setColor)
layout.addWidget(self.colorBtn, 1, 2)

def setButtonColor(self,color = None):
if not color:
color = self.light.color.get()
assert len(color) == 3, "You must provide a list of 3 Color"

r,g,b = [c*255 for c in color]
self.colorBtn.setStyleSheet('background-color: rgba({},{},{},1.0)'.format(r,g,b))


def setColor(self):
lightColor = self.light.color.get()
color = pm.colorEditor(rgbValue = lightColor)
#print(color) maya编辑器给返回的是个字符串
r,g,b,a = [float(c) for c in color.split()]
color = (r,g,b)
# 改变灯光颜色
self.light.color.set(color)
# 改变UI上的颜色
self.setButtonColor(color)

def disableLight(self,value):
self.name.setChecked(not value)

def deleteLight(self):
# 删除UI
self.setParent(None)
self.setVisible(False)
self.deleteLater()

# 删除灯光
pm.delete(self.light.getTransform())

def showUI():
ui = LightManager()
ui.show()
return ui

1
2
3
4
5
import lightingManager
import importlib
importlib.reload(lightingManager)

ui = lightingManager.LightManager().show()

image-20220703181152950

将窗口设为maya窗口的子窗口后,窗口和maya工作区成为一个整体,在maya工作区工作不会让组件窗口消失了,在任务栏也不会看到额外的maya窗口

1
2
3
4
5
6
7
import lightingManager
import importlib
importlib.reload(lightingManager)

#ui = lightingManager.LightManager().show()
#lightingManager.LightManager().show()
lightingManager.getDock()

image-20220703181632027

2.3 Handle error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#raise "message"#raise会停止后面的语句

def do():
raise RuntimeError()
print("hello")
#用try except来处理,当出现错误又不希望程序停止时
try:
do()
except:
print("There was an Error")
print("Goodbye")

#Hello
#There was an Error
#Goodbye
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try:
raise RuntimeError()#IOError(),Exception()
except RuntimeError as e:
print("RuntimeError")
except:#else也可以用
print("other error")

try:
print("Running")
except RuntimeError as e:
print("RuntimeError")
else:
print("no error")
finally:
print("done")
#Running
#no error
#done
#执行try后,else一定会被执行(except 可以看作一种if)

2.4 Exporting and Importing Our Lights

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
saveBtn = QtWidgets.QPushButton('Save')
saveBtn.clicked.connect(self.saveLights)
layout.addWidget(saveBtn,2,0)


def saveLights(self):
properties = {}
for lightWidget in self.findChildren(LightWidget):
light = lightWidget.light
transform = light.getTransform()

properties[str(transform)] = {
"translate": list(transform.translate.get()),
"rotation": list(transform.rotate.get()),
"lightType": pm.objectType(light),
"intensity": light.intensity.get(),
"color": light.color.get()
}
directory = os.path.join(pm.internalVar(userAppDir = True),'lightManager')
if not os.path.exists(directory):
os.mkdir(directory)
lightFile = os.path.join(directory,'lightFile_{}.json'.format(time.strftime('%m%d')))
with open(lightFile, 'w') as f:
json.dump(properties, f, indent = 4)
logger.info("Saving file to {}".format(lightFile))


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
importBtn = QtWidgets.QPushButton('Import')
importBtn.clicked.connect(self.importLights)
layout.addWidget(importBtn,2,1)
def importLights(self):
directory = self.getDirectory()
fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Light Browser", directory)
#print(fileName)#('C:/Users/XZYW/文档/maya/lightManager/lightFile_0703.json', '所有文件 (*)')
with open(fileName[0], 'r') as f:
properties = json.load(f)

for light, info in properties.items():
lightType = info.get('lightType')
for lt in self.lightTypes:
if "{}Light".format(lt.split()[0].lower()) == lightType:
break
else:
logger.info("Cannot find a corresponding lightType for {} ({})".format(light, lightType))
continue
light = self.createLight(lightType = lt)
light.intensity.set(info.get('intensity'))
light.color.set(info.get('color'))
transform = light.getTransform()
transform.translate.set(info.get('translate'))
transform.rotate.set(info.get('rotation'))

self.populate()

def createLight(self, lightType = None, add = True):
if not lightType:
lightType = self.lightTypeCB.currentText()
func = self.lightTypes[lightType]

light = func()
if add:
self.addLight(light)

return light

三、补充

3.1 命令行 文件重命名

cliRenamer.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import argparse
import re
import os
import shutil

def main():
parser = argparse.ArgumentParser(description = "This is a batch renamer", usage = "To replace all files with hello with goodbye instead: python clirenamer.py hello goodbye")
parser.add_argument('inString', help = "The world to replace")
parser.add_argument('outString', help = "The world to replace it with")

parser.add_argument('-d', '--duplicate', help = "Whether to duplicate or replace in spot", action = 'store_true')#默认为False
parser.add_argument('r', '--regex',
help = "Whether the patterns are regex or not",
action = "store_true")
parser.add_argument('o', '--out',
help = "The output location. Defaults to here")
args = parser.parser_args()

print(args)
rename(args.inString,args.outString, duplicate = args.duplicate,
outDirectory = args.out, regex = args.regex)

def rename(inString, outString, duplicate = True, inDirectory = None,
outDirectory = None, regex = False):
if not inDirectory:
inDirectory = os.getcwd()
if not outDirectory:
outDIrectory = inDirectory
outDirectory = os.path.abspath(outDirectory)

if not os.path.exists(outDirectory):
raise IOError('{} does not exist'.format(outDirectory))
if not os.path.exists(inDirectory):
raise IOError('{} does not exist'.format(inDirectory))
for f in os.listdir(inDirectory):
if .startwith('.'):#linux,mac中.开头表示隐藏文件
continue

if regex:
name = re.sub(inString, outString, f)
else:
name = f.replace(inString, outString)

print(name)
if name == f:
continue

src = os.path.join(inDirectory, f)
dest = os.path.join(outDirectory, name)

if duplicate:
shutil.copy2(src, dest)
else:
os.rename(src, dest)

if __name__ == '__main__':z
main()#分离命名空间,只有当这个文件命名空间是main的时候才运行

3.2 sys模块

1
2
3
4
5
import sys
import subprocess#子进程
subprocess.call(['python', '../cliRenamer.py', '-h'])


【笔记】【Maya工具】Python For Maya(Cmds)(三)The Animation Tweener

一、UI library

  • QT

    • maya的UI就是用QT建立的
  • PySide

image-20220630000053014

选取某一帧进行百分比插值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from maya import cmds

def tween(percentage, obj = None, attrs = None, selection = True):
if not obj and not selection:
raise ValueError("No object given to tween")
if not obj:
obj = cmds.ls(selection = True)[0]
if not attrs:
attrs = cmds.listAttr(obj,keyable = True)
currentTime = cmds.currentTime(query = True)

for attr in attrs:
# construct the full name of the attr with its obj
attrFull = '{}.{}'.format(obj,attr)
# get the keyframes of the attr on this obj
keyframes = cmds.keyframe(attrFull, query = True)

if not keyframes:
continue
previousKeyframes = [frame for frame in keyframes if frame < currentTime]

laterKeyframes = [frame for frame in keyframes if frame> currentTime]
if not previousKeyframes and not laterKeyframes:
continue


if previousKeyframes:
previousFrame = max(previousKeyframes)
else:
previousFrame = None
nextFrame = min(laterKeyframes) if laterKeyframes else None

if not previousFrame or not nextFrame:
continue
previousValue = cmds.getAttr(attrFull, time = previousFrame)
nextValue = cmds.getAttr(attrFull, time = nextFrame)

difference = nextValue - previousValue
weightedDifference = (difference*percentage) / 100.0
currentValue = previousValue + weightedDifference
print(previousValue)
print(nextValue)
print(currentValue)
cmds.setKeyframe(attrFull, time = currentTime, value = currentValue)

tween(50)



二、创建窗口

1
2
3
4
5
6
7
8
9
10
11
import maya.cmds as cmds

# Make a new window
#
window = cmds.window( title="Long Name", iconName='Short Name', widthHeight=(200, 55) )
cmds.columnLayout( adjustableColumn=True )
cmds.button( label='Do Nothing' )
cmds.button( label='Close', command=('cmds.deleteUI(\"' + window + '\", window=True)') )
cmds.setParent( '..' )
cmds.showWindow( window )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TweenWindow(object):
windowName = "TweenerWindow"
def show(self):
if cmds.window(self.windowName, query = True, exists = True):
cmds.deleteUI(self.windowName)
cmds.window(self.windowName)
self.buildUI()
cmds.showWindow()
def buildUI(self):
column = cmds.colunmLayout()
cmds.text(label = "Use this slider to set the tween amout")
row = cmds.rowLayout(numberOfColumns = 2)
self.slider = cmds.floatSlider(min = 0, max = 100, value = 50,step = 1,changeCommand = tween)
cmds.button(labe = "Reset",command = self.reset)

cmds.setParent(column)
cmds.button(label = "Close", command = self.close)
def reset(self,*args):#button默认传递参数,但不重要
print("resetting UI")
cmds.floatSlider(self.slider, edit = True, value = 50)
def close(self,*args):
cmds.deleteUI(self.windowName)

三、Base UI for different function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import tween
import Gear
from maya import cmds
class BaseWindow(object):
windowName = "BaseWindow"
def show(self):
if cmds.window(self.windowName, query = True, exists = True):
cmds.deleteUI(self.windowName)
cmds.window(self.windowName)
self.buildUI()
cmds.showWindow()
def buildUI(self):
pass
def reset(self,*args):#button默认传递参数,但不重要
pass
def close(self,*args):
cmds.deleteUI(self.windowName)

class TweenerUI(BaseWindow):#继承basewindow基类
windowName = "TweenerWindow"
def buildUI(self):
column = cmds.colunmLayout()
cmds.text(label = "Use this slider to set the tween amout")
row = cmds.rowLayout(numberOfColumns = 2)
self.slider = cmds.floatSlider(min = 0, max = 100, value = 50,step = 1,changeCommand = tween)
cmds.button(labe = "Reset",command = self.reset)

cmds.setParent(column)
cmds.button(label = "Close", command = self.close)
def reset(self,*args):#button默认传递参数,但不重要
print("resetting UI")
cmds.floatSlider(self.slider, edit = True, value = 50)

class GearUI(BaseWindow):
windowName = "GearWindow"
def __init__(self):
self.gear = None
def buildUI(self):
column = cmds.columnLayout()
cmds.text(label = "Use the silider to modify the gear")
cmds.rowLayout(numberOfColumns = 4)
self.label = cmds.text(label = "10")
self.slider = cmds.intSlider(min = 5, max = 30, value = 10,step = 1, dragCommand = self.modifyGear)
cmds.button(label = "Make Gear", command = self.makeGear)
cmds.button(label = "Reset",command = self.reset)
cmds.setParent(column)
cmds.button(label = "Close",command = self.close)
def makeGear(self,*args):
teeth = cmds.intSlider(self.slider,query = True, value = True)
self.gear = Gear()
self.gear.createGear(teeth = teeth)

def modifyGear(self,teeth):
if self.gear:
self.gear.changeTeeth(teeth = teeth)
cmds.text(self.label,edit = True, label = teeth
def reset(self,*args):
self.gear = None
cmds.intSlider(self.slider,edit = True,value = 10)
cmds.text(self.label,eidt = True,label = '10')
GearUI().show()

四、Add script to maya shelf

image-20220701001501311

直接选中脚本代码,然后拖到上面,就出现了一个按钮,一点按钮,自动运行,右键可以修改

【笔记】【百人计划】图形2.8 flowmap的实现

一、Flowmap是什么

  • Flowmap的实质
    • 一张记录了2D向量信息的纹理
    • Flowmap上的颜色(RG)记录该处向量场的方向,让模型上某一点表现出定量流动的特征
    • 通过在shader中偏移uv再对纹理进行采样,来模拟流动效果。

image-20220806235307299

==ue4与unity相比反转了绿通道==

二、Flowmap shader

  1. 采样Flowmap获取向量场信息
    • Flowmap不能直接使用,需要从0,1映射到-1,1
  2. 用向量场信息,使采样贴图时的uv随时间变化
  3. 对同一贴图以半个周期的相位差采集两次,并线性插值,使贴图流动连续
  • 随着时间进行,变形越来越大,为了把偏移控制在一定范围内
1
2
3
4
5
float phase = frac(_Time);
// 解决frac产生的跳变
// 构造周期相同,相位相差半个周期的波形函数
float phase0 = frac(_Time * 0.1 * _TimeSpeed);
float phase1 = frac(_Time * 0.1 * _TimeSpeed + 0.5);

image-20220807000102114

  • 用相位差半个周期的两层采样进行加权混合,使纹理流动一个周期重新开始时的不自然情况被另一层采样覆盖

image-20220807000117694

1
2
3
4
5
6
7
float2 tiling_uv = i.uv * _MainTex_ST.xy + _MainTex_ST.zw;
half3 tex0 = tex2D(_MainTex, tiling_uv - floatDir.xy * phase0);
half3 tex1 = tex2D(_MainTex, tiling_uv - floatDir.xy * phase1);
//构造函数计算随波形函数变化的权值,
//使得纹理采样值在接近最大偏移时有权值0,并因此消隐,构造较平滑的循环
float flowLerp = abs((0.5-phase0)/0.5);
half3 finalColor = lerp(tex0,tex1,flowLerp);

三、Flowmap的制作

Flowmap painter

Flowmap painter是unity制作的绘制flowmap的工具(用该工具得到的flowmap是线性空间颜色,不需要gamma矫正,在unity中取消勾选sRGB)

本来还以为这种东西要手动在ps里画RG通道。。。有点头疼来着,没想到这个工具这么方便,并且可以支持flow的效果实时可视化。

Houdini Labs

Houdini Labs是内置在Houdini中的一组游戏开发相关的节点,可以在github中搜索sidefx Labs或直接在houdini中安装(在较早版本中的shoudini中无法在shelf内找到,在这些未被内置到houdini的版本中,这组工具名称为gamedev)

  1. flowmap相关节点功能
  • grid
    • 细分
  • flowmap
    • 初始化向量场
  • flowmap_brush
    • 向量场笔刷
  • flowmap_to_color
    • 将向量映射到颜色
    • 并添加uv(如有uv则直接沿用)
  • flowmap_visualize
    • 可视化flowmap效果
  • maps_baker
    • 导出
    • 输出顶点色,并设置gamma(1-线性空间)
  • flowmap_guide
    • 输入flowmap field(flowmap向量场)和曲线
    • 利用曲线修改向量场
  • flowma_obstacle
    • 输入模型和向量场
    • 以模型和flowmap绘制平面进行检测,模型作为障碍,阻挡flow

Flowmap的烘焙和相关设置

  • flowmap贴图设置:

    • 无压缩或高质量
    • 确认色彩空间
  • Houdini

    • Labs Map Baker节点
    • 导出时注意gamma矫正选项、uv匹配
    • 用Labs UV transfer节点来匹配高模和低模的UV

作业

实现流动效果

我也是第一次听说flowmap这个概念,然后看看我的桌面壁纸发现:

image-20220802184553447

这不就是flowmap做的水流效果吗?也是非常有趣。

作业直接拿去年图形学作业用的水面贴图拿过来用了。flowmap是用Flowmap painter画的。

中间平滑插值的一步看到图像其实很想用三角函数来做,但实操发现影响好像不太大,并且这个计算比起教程里的消耗会更大一点,所以还是没什么必要。

1
float flowLerp = (cos(2*UNITY_PI * _Time*0.1 * _TimeSpeed)+1)/2;//abs((0.5-phase0)/0.5);//

中间过程中一直发现有锯齿,最后发现是纹理压缩的问题,默认是法线质量压缩,在一些边缘处就会出现奇怪的锯齿

image-20220816163919751

把压缩模式换成无压缩就好了(这也是为什么人家工具导出的就是png格式)。

感觉flowmap很适合放在球面上,加一点自发光和半透明,做成朝一个方向旋转的水元素效果。一时间想不起来这种效果的案例哪里有了。

Flowmap

感觉可以再进一步做一个顶点噪声扰动也许效果更好

参考资料

[1] https://www.bilibili.com/video/BV1Zq4y157c9

【技术美术百人计划】图形 2.8 flowmap的实现——流动效果实现

2022年8月16日 周二

现在是00:51,10分钟前提交了笔试邮件。
这两周怎么说呢。感觉也积累了学到了一些东西。
下次再面对这样的笔试题心里也会有底一点了(前提是知道要求的风格要怎么去做)。
这次的认真程度和完成程度综合给自己打个75分吧,毕竟还是有些遗憾的。
总得来说最终的结果勉强能让自己满意,虽然大概率达不到要求。

这两天要等一等友塔和不鸣的结果。感觉秋招的终局其实已经注定,剩下半个月,还需要更多更多地提升自己。
首先是百人计划,继续往后,一天一个,8月份也只能做完第二页。
另外的内容呢?想一想是接着百人还是搞一搞GAMES和Cherno的课程。
我感觉可以偏向后者。

记住晚上要练钢琴,以及每周运动一次

Read More...

【笔记】【百人计划】图形2.5 BUMP图改进

一、凹凸贴图Bump Mapping

  • 把物体的细节分为三种尺度
    • 宏观尺度(覆盖很多像素)
      • 由几何图元来表示
    • 中观尺度(覆盖少量像素)
      • 细节复杂,无法使用单个三角形渲染,并且足够大
    • 微观尺度(可能覆盖小于一个像素)
      • 在着色模型当中表现,模拟物体表面微观几何形状的相互作用

凹凸映射是模拟中观尺度的常用方法之一,能够让观察者感知到比几何模型尺度更小的细节

基本思想:在纹理中把尺度细节相关的信息编码进去,在着色过程中用受到干扰的表面代替真实表面,就让表面看起来具有小尺度的细节。

总之,凹凸贴图是对物体表面贴图进行变化再进行光照计算的一种技术。(增加物体真实感,但不需要额外的几何复杂度)

  • 分类
    • 法线贴图
    • 视差贴图
    • 浮雕贴图

在这三种技术中都会用到法线(贴图)

二、法线贴图Normal Mapping

法线贴图是一张存有物体局部表面法线信息的贴图。

计算光照时,程序读取法线图,并获取当前着色点的法线信息,结合光照信息进行光照计算。

法线贴图一般由高模映射到对应的底模上来生成,但像金属、木头等细节丰富的物体,可借助程序化软件如:Photoshop,Substance Designer等生成对应法线贴图

切线空间

法线的储存一般放在模型的切线空间中

  • 切线空间
    • 物体表面切线、副切线、法线方向为基,组成的几何空间
  • 读取切线空间法线,需要将法线从切线空间转换到世界空间
image-20220731003919885

世界和切线空间转换

切线空间坐标系的正交基是世界空间下的顶点法线(N)、切线(T)、副切线(B),法线为z轴,切线为x轴,副切线为y轴

构建一个3x3的矩阵做空间向量的坐标系转换。
$$
TBN = \begin{bmatrix}T_x&B_x&N_x\
T_y&B_y&N_y\
T_z&B_z&N_z\
\end{bmatrix}\
\ \
TBN^{-1}=TBN^T=\begin{bmatrix}T_x&T_y&T_z\
B_x&B_y&B_z\
N_x&N_y&N_z\
\end{bmatrix}\
$$
想不清哪个是世界-切线,哪个是切线-世界,考虑一个单位阵,左乘矩阵,看看会变成什么就知道了。

  • 切线空间的好处
    • 切线空间记录的是相对的法线信息,对于一个物体表面记录的法线扰动,可以同样应用到球形物体上(植物的光照处理),但是模型空间记录法线就是绝对的,只能在该物体上用。
    • 方便制作UV动画,贴图采样变化一致
    • 法线纹理可重用
    • 便于计算储存,0-1的储存映射范围,知道两个可以计算另一个
  • Unity中法线贴图的压缩格式
    • 非移动平台,unity会把法线贴图转换成DXRT5nm格式,这种格式只有两个有效通道AG通道,可以节省空间
      • 在DXRT5nm格式中,AG通道分别储存对应法线的x,y分量,z分量需要通过一个简单的计算求得。
    • 移动平台,unity使用传统RGB通道
7416ead6-fc7e-4e52-9dba-b56d68996a2a 8b45fbb7-0987-4b01-9c95-c47166d160b4

三、视差贴图Parallax Mapping

法线贴图只能改变法线而改变光照,无法使模型表面产生遮挡效果

视差贴图Parallax Mapping是一种类似法线贴图的技术。它用于提高模型表面细节并赋予其遮挡关系,可以和法线贴图一起使用。

视差贴图需要引进一张新的贴图——高度图。高度图一般是用于顶点位移使用的(位移/置换贴图 Displacement mapping),但性能消耗高,需要大量三角形。视差贴图的核心是改变纹理坐标来改变遮挡关系,视差贴图就利用储存模型信息的高度图,利用模型表面高度信息来对纹理进行偏移

image-20220731010656743

在着色时,模型在切线空间下所有点都在切平面内(0.0),核心就是对于要计算的片元A时,真正应该计算的点是视线与物体的“实际”交点B点。

image-20220731011441775

要计算B点,就需要AB两点在平面上的UV偏差,为了简便,采取近似计算的方法,根据高(深)度图及切线空间下视角方向,近似求解偏移量,视角方向(v)与切平面的正切值与A点的高度值相乘来近似求解,并通过一个缩放值来控制。(有比较大的误差,必须要用这个scale来调整)
$$
d = \frac{v.xy}{v.z}\cdot ha\cdot scale
$$

陡峭视察映射Steep Parallax Mapping

陡峭视察映射也是近似,但更准确一些

陡峭视察映射将深度分为等距的若干层,从顶端开始采样,并且每次沿视角方向偏移一定值,若当前层深度大于采样出的深度,则停止检查并返回结果

(有点ray marching的感觉,那其实在优化上也可以借鉴一下分级采样?https://xzyw7.github.io/post/CbZTf-uM4/#real-time-global-illuminationscreen-space)

image-20220731012950141

也可以根据v和n的角度来对采样层数进行控制

四、浮雕贴图Relief Mapping

image-20220731014135800

视差贴图在使用较大的uv偏移时存在失真。

浮雕贴图更容易提供更多的深度,还可以做自阴影、AO效果

实现方法

浮雕映射一般采用射线步进、二分查找来决定uv偏移量

第一种使用射线步进来查找可能的交点(直接用二分查找可能漏掉较薄的区域导致结果不准确),确定交点位于哪一个步进内。之后在该步进内使用二分查找快速确定交点位置,最后返回结果,偏移贴图。

image-20220731014352296

  • 解决最后一步二分查找性能开销问题
    • 视差闭塞贴图(Parallax Occlusion Mapping)
    • 在步进后,分别对步进两端uv值采样,对结果插值,作为p点的结果(插值导致表面平滑效果更好)

作业

结合先行版基础渲染光照介绍(一)将本次课所讲的案例结合进先前的光照效果

这里就4个案例嘛

法线贴图

左一:standard shader

左二:custom shader,使用法线贴图

右一:custom shader,无法线贴图

image-20220801232026790

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct v2f {
float4 pos : SV_POSITION;
float3 normal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
float3 tangent : TEXCOORD3;
float3 bitangent : TEXCOORD4;


};

fixed3 normal = normalize(i.normal);
fixed3x3 TBN = fixed3x3(normalize(i.tangent),normalize(i.bitangent),normal);
TBN = transpose(TBN);//Unity shader的矩阵是行优先的,所以我们要取个转置;
fixed3 bump = normalize(UnpackNormal(tex2D(_Normal, i.uv)));
normal = normalize(mul(TBN,bump));

不想像入门精要那样传一整个矩阵,我们可以传递的变量也是有限的,甚至可以只传tangent,副切线用叉乘计算。甚至也可以用之前的ddx和ddy的trick来计算。

https://xzyw7.github.io/post/zezxM-QCJ/#ddxddy%E4%B8%8E%E6%B3%95%E7%BA%BF%E8%B4%B4%E5%9B%BE

(Tips:有注意到在learnopengl中有描述,在一些网格较大的时候,出现TBN不互相垂直的情况,可以用施密特正交化来解决。)

视差贴图

这个时候发现……狮子模型这个素材没有高度图……还得换个素材……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fixed3 LightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));//normalize(_WorldSpaceLightPos0.xyz);//
fixed3 ViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));//normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
float3 h = normalize(LightDir + ViewDir);


fixed3 normal = normalize(i.normal);
fixed3 bitangent = normalize(cross(normal,i.tangent.xyz) * i.tangent.w);
fixed3x3 TBN = fixed3x3(normalize(i.tangent.xyz),bitangent,normal);
TBN = transpose(TBN);

//视差贴图
float height = tex2D(_heightMap,i.uv).r;
float3 ViewDirTS = normalize(mul(transpose(TBN),ViewDir));
float2 offUV = ViewDirTS.xy/ViewDirTS.z * height * _heightScale;
i.uv -= offUV;

//法线贴图
fixed3 bump = normalize(UnpackNormal(tex2D(_Normal, i.uv)));
normal = normalize(mul(TBN,bump));

image-20220802000836213

视差贴图在视线接近垂直的时候效果还是很好的,但是正如learnopengl中所说,当从一个角度看过去的时候,会有一些问题产生(和法线贴图相似),陡峭的地方会产生不正确的结果。并且它的效果非常依赖于_heightScale这一参数

陡峭视差贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float2 steepParallaxMapping (float2 uv, float3 viewDir) {
float numLayers = 20;
float layerHeight = 1.0/numLayers;
float2 deltaUV = 1.0/numLayers * viewDir.xy / viewDir.z * _heightScale;
float2 currentUV = uv;
float currentHeight = tex2D(_heightMap,uv).r;
float currentLayerHeight = 0.0;
while(currentLayerHeight < currentHeight)
{
currentUV -= deltaUV;
currentHeight = tex2Dlod(_heightMap, float4(currentUV,0.0,0.0)).r;
//currentHeight = tex2Dgrad(_heightMap, currentUV,0.0,0.0).r;
currentLayerHeight += layerHeight;
}
return currentUV;
}

中间一直出现的报错“unable to unroll loop”,给tex2D改成tex2Dlod或tex2Dgrad就好了

参考

https://zhuanlan.zhihu.com/p/391443312

https://zhuanlan.zhihu.com/p/144434084

https://stackoverflow.com/questions/57994423/why-i-cant-use-tex2d-inside-a-loop-in-unity-shaderlab

tex2D只能从“均匀控制流”调用,因为它必须通过计算“导数”来计算LOD。tex2Dlod没有,因为您提供了LOD。

tex2Dlod和tex2Dgrad都能指定纹理层,所以能够在循环中调用。

image-20220802161513119

浮雕贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
float2 steepParallaxMapping (float2 uv, float3 viewDir) {
float numLayers = 20;
float layerHeight = 1.0/numLayers;
float2 deltaUV = 1.0/numLayers * viewDir.xy / viewDir.z * _heightScale;
float2 currentUV = uv;
float currentHeight = tex2D(_heightMap,uv).r;
float currentLayerHeight = 0.0;
while(currentLayerHeight < currentHeight)
{
currentUV -= deltaUV;
currentHeight = tex2Dlod(_heightMap, float4(currentUV,0.0,0.0)).r;
currentLayerHeight += layerHeight;
}
float2 left = currentUV;
float2 right = currentUV+deltaUV;
float dist = 1;
float2 midpoint = (left+right)/2;
int i = 0;
while (i<10) {
midpoint = (left+right)/2;
currentHeight = tex2Dlod(_heightMap, float4(midpoint,0.0,0.0)).r;
currentLayerHeight = length(midpoint)/length(viewDir.xy) * viewDir.z;
if (currentLayerHeight < currentHeight) {
right = midpoint;
dist = currentHeight - currentLayerHeight;
} else if (currentLayerHeight > currentHeight) {
left = midpoint;
dist = -currentHeight + currentLayerHeight;
} else {
break;
}
i++;
}
return midpoint;

}

视察闭塞贴图

1
2
3
4
5
6
7
8
9
10
float2 left = currentUV;
float2 right = currentUV+deltaUV;

float afterDepth = currentHeight-currentLayerHeight;
float beforeDepth = tex2D(_heightMap, right).r -currentLayerHeight + layerHeight;

float weight = afterDepth / (afterDepth - beforeDepth);
float2 finalTexCoords = right * weight + left * (1.0 - weight);

return finalTexCoords;

POM是肉眼可见的效果不错(上:SPM,中:RPM,下:POM),RPM就不太能看得出变化了,但其实还是有的

image-20220802175923830 image-20220802180825768 image-20220802175853495

参考资料

[1] https://www.bilibili.com/video/BV1Ub4y1Z765 【技术美术百人计划】图形 2.5 BUMP图改进

[2] Unity Shader入门精要 p146-155

[3]
https://learnopengl-cn.github.io/05%20Advanced%20Lighting/05%20Parallax%20Mapping/

【笔记】【百人计划】图形2.7 LDR与HDR

一、基本概念

HDR = High Dynamic Range(高动态范围)

LDR = Low Dynamic Range(低动态范围)

动态范围 = 最高亮度/最低亮度

  • 对于显示器来说,实际的物理亮度是不同意的。因此需要匹配LDR0-1的范围

  • 自然界中的亮度是HDR的

    • 在显示时需要转换到LDR的范围(Tone mapping色调映射)
  • LDR

    • 8位精度
    • 单通道0-1(0-255)
    • 常用LDR图片储存格式
      • jpg、png等
    • 拾色器、一般图片、电脑屏幕
  • HDR

    • 远高于8位精度
    • 单通道可以超过1
    • 常用HDR图片储存格式
      • hdr/tif/exr/raw
    • HDRI、真实世界

为什么需要HDR

  • 更好地色彩,更高的动态范围和更丰富的细节,并有效地防止画面国宝,超过亮度值1的色彩也能很好地表现,像素光亮度变得正常,视觉传达更真实。
  • HDR有超过1的数值,范围更大,能让bloom的表现更好。

一些HDR图网站

http://www.hdrlabs.com/sibl/archive.html

https://www.openfootage.net/hdri-panorama/

二、Unity中的HDR

Camera-HDR设置

  • 场景将渲染为HDR图像缓冲区
  • 后处理:Bloom&Tone mapping
  • 完成转换HDR->LDR
  • LDR图像发送给显示器
image-20220730183404967

Lightmap HDR设置

  • 选择High Quality 将启用HDR光照贴图支持,而Normal Quality将切换为使用RGBM编码
  • RGBM编码:将颜色存储在RGB通道中,将乘数M储存在Alpha通道中

(项目设置-Player-Other settings-Lightmap Encoding)

image-20220730183556522

拾色器HDR设置

1
[HDR] _BackColor("BackColor", Color) = (1,1,1,1)
  • 使用Intensity滑动条可调整颜色强度
  • 每增加1,提供的光亮增加一倍

image-20220730183703721

HDR的优缺点

  • 优点
    • 画面中亮度超过1的部分不会被截为1,增加亮部细节并减少曝光
    • 减少画面较暗部分的色阶感
    • 更好地支持Bloom
  • 缺点
    • 渲染速度较慢,需要更多显存
    • 不支持硬件AA
    • 部分手机不支持

三、HDR与Bloom

Bloom用于表现高光的晕光效果

为实现泛光,我们像平时那样渲染一个有光场景,提取出场景的HDR颜色缓冲以及只有这个场景明亮区域可见的图片。被提取的带有亮度的图片接着被模糊,结果被添加到HDR场景上面。(learnopengl)

unity中的BLoom

Untiy会首先进行下采样(down sample)来计算高光像素,并存在RT中,完成次数由一个参数控制,再up回去,并将下采样的RT加入进去。

image-20220730184829163

我对这个操作目前的理解是,和learnopengl中为了完成高斯模糊是一样的

img

在毛星云大佬的 《高品质后处理:十种图像模糊算法的总结与实现》中也可以看到对各种模糊算法的解释,如果说是以多次的Box filtering来近似高斯模糊或者其他模糊效果的话,就很好理解了。

四、HDR与Tone mapping

色调映射就是为了把HDR转化为LDR

  • 线性映射效果不好
image-20220730185314765
  • 把高光区域和阴影区域像中等亮度方向压缩->S曲线

ACES

ACES(Academy Color Encoding System学院颜色编码系统)

效果:对比度提高,很好地保留暗处和亮出的细节

image-20220730185423773
1
2
3
4
5
6
7
8
9
10
11
12
13
// ACES
//https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
float3 ACESToneMapping(float3 color, float adapted_lum)
{
const float A = 2.51f;
const float B = 0.03f;
const float C = 2.43f;
const float D = 0.59f;
const float E = 0.14f;

color *= adapted_lum;
return (color * (A * color + B)) / (color * (C * color + D) + E);
}

其他Tone mapping曲线

image-20220730185538359
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Reinhard
float3 ReinhardToneMapping(float3 color, float adapted_lum)
{
const float MIDDLE_GREY = 1;
color *= MIDDLE_GREY / adapted_lum;
return color / (1.0f + color);
}
// CE
float3 CEToneMapping(float3 color, float adapted_lum)
{
return 1 - exp(-adapted_lum * color);
}
// Filmic
float3 F(float3 x)
{
const float A = 0.22f;
const float B = 0.30f;
const float C = 0.10f;
const float D = 0.20f;
const float E = 0.01f;
const float F = 0.30f;

return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
}

float3 Uncharted2ToneMapping(float3 color, float adapted_lum)
{
const float WHITE = 11.2f;
return F(1.6f * adapted_lum * color) / F(WHITE);
}

LUT

在Color Grading的All None 模式中有一个Lookup Texture(滤镜)

  • LUT在LDR之间做变换,Tone mapping是映射HDR到LDR

  • 调整rgb三个通道的LUT被称为3D LUT

img

img

作业

结合先行版基础渲染光照介绍(一)试试IBL在HDR和LDR的区别

相机设置HDR

左侧探针设置LDR,右侧探针设置HDR,都是1024的分辨率

在生成的贴图上可以看到,一张是exr后缀的HDR贴图,一张是png后缀的LDR贴图。

把它放在我们的材质上,说实话我没看出来有啥区别……

image-20220731223034086

但是随着Lod层级的增加,就可以看出来区别了

image-20220731223343672 image-20220731223409296 image-20220731223432177

非常地合情合理,在HDR中储存的辐射度范围是大于0-1的,在mipmap中也会储存更高动态范围的光照,在ibl里的细节也就更丰富。

参考资料

[1] https://www.bilibili.com/video/BV1VA41137Wp ,【技术美术百人计划】图形 2.7 LDR与HDR

[2] https://learnopengl-cn.github.io/05%20Advanced%20Lighting/07%20Bloom/

[3] https://zhuanlan.zhihu.com/p/125744132 ,高品质后处理:十种图像模糊算法的总结与实现

[4] Unity Shader入门精要 p361,362

[5] https://zhuanlan.zhihu.com/p/21983679 ,Tone mapping进化论

2022年7月31日 周日

今天刚好是个特别的日子,7月的最后一天,也是这周的最后一天。
以7月15日来划分的话,前半个月在上短学期的maya课程,这段时间的状态非常好,准时起床,坚持学习,每天的作息非常规律,也每天能感觉到学到东西。

但是7月15日以后的半个月,稍微有点放纵。没有回家,是希望能冲刺一下,准备秋招。
虽然这半个月也有在推进百人计划,但是我觉得完全可以效率更高。每天的有效学习时间也没有达到理想的状况,作息更是稳定的不理想,尤其是今天。

也可以理解吧,毕竟出了一些烦心事,常常陷入自我怀疑,一些提前批的失败,以及出乎意料的需要提前做米哈游的笔试。早知道就晚点投了,主要还是上当了……
明天就要开始,但我感觉完全不是准备好的状态。有点不知道怎么做了。

为了减少精神内耗与自我放纵,买了一个电子琴,明天就能到。
而接下来的半个月时间里,真的需要进入认真的状态了,就像7月前半个月一样,甚至需要更努力。

【笔记】【百人计划】图形2.6 伽马校正

Gamma矫正

image-20220730122434969

传递函数

  • OETF
    • 光转电传递函数 ,负责把场景线性光转到非线性视频信号值
  • EOTF
    • 电转光传递函数,负责把非线性视频信号值转换成显示光亮度
image-20220730122710864

Gamma矫正

  • Gamma是指对线性三色值和非线性视频信号之间进行编码和解码的操作
  • 图像经过gamma编码储存在硬盘中,将获取到的物理数据做一次gamma值约为0.45(1/2.2)的映射,这样的过程叫做gamma编码(此时的图像像素比实际物理像素要更亮——线性空间)
  • 在显示图像时,需要将每个像素做一次gamma值约为2.2的矫正,使最终的结果为正确的物理数据(经过显示的gamma矫正后,之前偏亮的图像亮度降低——gamma空间)

$$
V_{out}=V_{in}^{gamma}
$$

image-20220730144948798

为什么需要gamma矫正

  • 非线性转换的目的主要是为了优化储存空间和带宽,传递函数能够更好地利用编码空间
  • 显示图像的数据都是8bit,但是人眼对暗部变化更敏感,为了充分利用带宽,那么需要使用更多空间去存储暗部值,也就是说暗部使用更高精度保存,亮部使用更低精度保存。

以这张图来说明,对于人眼来说,上面的图像变化更均匀,但实际下面的图像才是物理上亮度均匀变化的。

(上面这张的中间灰度叫做美术中灰,下面的中间灰度叫做物理中灰)

image-20220730145336723

将这两种变化进行一个映射,就是gamma曲线

image-20220730145547142

韦伯定律

  • 感觉的差别阈限随原来刺激量的变化而变化,而且表现为一定的规律性,用公式表达就是$\Delta\Phi/\Phi = C$ ,其中$\Phi$ 为原刺激量,$\Delta\Phi$ 为此时的差别阈限,C为常数,又称为韦伯率。(所受刺激越大,需要增加的刺激也要足够大才会让人感觉到明显变化,但是只适用于中等强度的刺激)

总结

  • 人眼对暗部变化更敏感
  • 我们目前使用的真彩格式RGBA32,每个通道只有8位用于记录信息,为了合理使用带宽和存储空间,需要进行非线性转换
  • 目前我们所普遍使用的sRGB颜色空间标准,他的传递函数gamma值位2.2(2.4)

CRT与gamma矫正

  • CRT
    • 早期使用的CRT显示器(阴极射线显像管),设备的亮度与电压并不成线性关系,而是gamma值约为2.2类似幂律的关系
    • 这种硬件特性与gamma矫正的需求正好是一种巧合
image-20220730150841828

人眼对于中灰的感受取决于环境

image-20220730151319961
  • 线性工作流
    • 在生产各个环境,需要正确使用gamma编码与解码,使最终得到的颜色数据与最初输入的物理数据一致
    • 如果是使用gamma空间的贴图,在传给着色器前需要从gamma空间转到线性空间
image-20220730154948879

image-20220730155223847

Unity中的颜色空间

Edit-Project Settings-Player-Other Settings下的Rendering部分,修改Color Space

image-20220730155540487

  • 选择Gamma Space,Unity不会做任何处理
  • 选择Linear Space,引擎的渲染流程在线性空间计算,理想情况下项目使用线性空间的贴图,不需要勾选sRGB,勾选sRGB的贴图会通过硬件特性采样时进行线性转换。

硬件支持

线性空间需要图形API的硬件支持,目前支持的平台

  1. Windows,Mac OS x和Linux(Standalone)
  2. Xbox One
  3. PS 4
  4. Android(Opengl ES 3.0)
  5. iOS(Metal)
  6. WebGL

硬件特性支持

主要由两个硬件特性来支持

  • sRGB Frame Buffer

    • 将Shader的计算结果输出到显示器前做Gamma矫正
    • 作为纹理被读取时会自动把储存的颜色从sRGB空间转换到线性空间
    • 调用ReadPixels()、ReadBackImage()时,会直接返回sRGB空间下的颜色
    • sRGB Frame Buffer只支持每通道8bit的格式,不支持浮点格式
    • HDR开启后会先把渲染结果绘制到浮点格式的FB中,最后绘制到sRGB FB上
  • sRGB Sampler

    • 将sRGB的贴图进行线性采样的转换

使用硬件完成sRGB贴图的线性采样和shader计算结果的gamma矫正,比起在shader里对贴图采样和计算结果的矫正要快

贴图制作导出的处理

Substance Painter

sp贴图导出时,线性的颜色经过gamma变换,颜色被提亮了,所以需要在Unity中勾选sRGB选项,让它在采样时能还原回线性值。

image-20220730160341610

Photoshop

如果使用线性空间,一般来说Photoshop可以什么都不改,导出的贴图只要在Unity中勾上sRGB就可以了。

如果调整Photoshop的gamma值为1,导出的贴图在Unity中也不需要勾选sRGB

image-20220730160653259

  • ps对颜色管理特别精确,Unity里看到的颜色要经过显示器的Gamma变换,而ps不会,ps会读取显示器的Color Profile,反向补偿回去。

  • ps中有第二个Color Profile,叫做Document Color Profile。通常默认为sRGB Color Profile,和显示器的Color Profile一致,颜色被压暗了,所以ps中看到的结果才和Unity一样。

混合

Unity中的混合是线性的(线性空间模式下),ps图层与图层之间混合时,每个上层图层都经过了gamma变换,才做了混合。需要在设置中更改选择“用灰度系数混合rgb颜色”,参数设置为1,这样图层才是直接混合的结果。

image-20220730161152441

作业

手动尝试伽马校正的几种方法

1.Unity线性空间

项目设置使用Linear空间,并且albedo贴图勾选sRGB

image-20220730165247570

2.Unity Gamma空间

在项目设置里选择gamma空间

image-20220730170411814

可以观察到输出的颜色中,高光更亮、范围更大,因为光照是线性计算的,而且直接按照gamma空间输出了;但是直接输出的颜色更暗了。

手动Gamma矫正

对于输出的radiance,我们进行gamma矫正

1
return fixed4(LinearToGammaSpace(color),1.0);

以及采样贴图时,变换到线性空间计算

1
fixed3 albedo = GammaToLinearSpace(tex2D(_Albedo, i.uv).rgb) * _Diffuse.rgb;

(理论上来说,albedo这种属性是线性空间的,但是这也关乎到贴图导出的设置,一般制作的都是sRGB的图片,所以还是需要转换一下。)

这两个封装好的函数,也可以手动地去使用2.2次方去计算。

image-20220730171629559

可以看到法线颜色球正常了,中间的我写的shader高光也压下来一些,但是这个Standard的着色器就改不了了。

所以引擎还是很方便的,我们在线性空间工作模式下,很多事情都帮我们做好了。但是需要在贴图的设置上多加注意。

  • 麻烦的地方
    • 在需要手动gamma矫正的平台上,在混合这一步会出现问题(因为在混合之前就进行gamma矫正了)
    • 所以一个解决方法是:在fs颜色输出时不进行gamma矫正,但是需要一步后处理来完成gamma矫正,也造成了一些性能损耗。

参考资料

[1] https://www.bilibili.com/video/BV1cU4y1b7UF 【技术美术百人计划】图形 2.6 伽马校正

[2] Unity Shader 入门精要 p356-363.