故事背景:
美术那里有需求,需要别人游戏的模型,来借鉴一下,问我是否有工具可以一键导出模型。我就搜索了一下RenderDoc批量导出图片,结果搜到了用C++改RenderDoc源码的文章。让RenderDoc批量导出纹理_专栏-CSDN博客blog.csdn.net
然后看了一下官网,看到了可以用python写工具的(因为不会C++,而且好像很麻烦的样子),我就试着用python来写RenderDoc的工具(其实我也不会python只是这种语言应该好学)。
使用步骤:
1.在RenderDoc里面截一张图。
2.运行RenderDoc的python shell。
3.导出图片与csv(因为只能导出csv,导不出模型,我会在unity里把他转为模型)
至于RenderDoc如何写python只要看下官网就可以了,这里贴出地址。Python API - RenderDoc documentationrenderdoc.org
原理分析:
主要还是python部分的原理,怎么调用RenderDoc接口,官网虽然有解析和例子,但是并不完全,导出图片的那个例子居然是导出output的图片。就是fragment Shader return出去的那个,完全没找到input的图片,我都是自己试出来的。
其实导出顶点CSV跟导出图片,都用到个这个函数
controller.GetPipelineState()
从这个pipeline的state里面获取,就是对应RenderDoc里面的这个面板
然后用这个函数
state.GetReadOnlyResources(renderdoc.ShaderStage.Fragment)
这个里面就有所有的图片的resourceId,可以写个循环保存所有的图片。
texsave = rd.TextureSave()
texsave.resourceId = resourceId
if texsave.resourceId == rd.ResourceId.Null():
return False
filename = str(int(texsave.resourceId))
texsave.mip = 0
texsave.slice.sliceIndex = 0
texsave.alpha = rd.AlphaMapping.Preserve
texsave.destType = rd.FileType.PNG
if not os.path.exists("{0}/{1}".format(folderName,eventId)):
os.makedirs("{0}/{1}".format(folderName,eventId))
outTexPath = "{0}/{1}/{2}.png".format(folderName,eventId,filename)
controller.SaveTexture(texsave, outTexPath)
同样的vertex也是这么获取,获取vertex index的resourceId,跟vertex自身的resourceId。
ib.state.GetIBuffer()
vbs.state.GetVBuffers()
....
something
...
meshInput.indexResourceId = ib.resourceId # vertex index的resourceId
meshInput.vertexResourceId = vbs[attr.vertexBuffer].resourceId # vertex自身的resourceId
然后根据resourceId去BufferData里面去得到vertex
controller.GetBufferData(meshInput.indexResourceId, meshInput.indexByteOffset, 0)
源码:只给出python源码,C#源码可以参考这里RenderDocMeshParserForUnitygithub.com
import sys
import csv
folderName = "C:/Users/Administrator/Desktop/capMesh1"
startIndex = 475
endIndex = 759
isPrint = False
# Import renderdoc if not already imported (e.g. in the UI)
if 'renderdoc' not in sys.modules and '_renderdoc' not in sys.modules:
import renderdoc
# Alias renderdoc for legibility
rd = renderdoc
# We'll need the struct data to read out of bytes objects
import struct
import os
# We base our data on a MeshFormat, but we add some properties
class MeshData(rd.MeshFormat):
indexOffset = 0
name = ''
def pySaveTexture(resourceId,eventId,controller):
texsave = rd.TextureSave()
texsave.resourceId = resourceId
if texsave.resourceId == rd.ResourceId.Null():
return False
filename = str(int(texsave.resourceId))
# texsave.alpha = rd.AlphaMapping.BlendToCheckerboard
# Most formats can only display a single image per file, so we select the
# first mip and first slice
texsave.mip = 0
texsave.slice.sliceIndex = 0
texsave.alpha = rd.AlphaMapping.Preserve
texsave.destType = rd.FileType.PNG
if not os.path.exists("{0}/{1}".format(folderName,eventId)):
os.makedirs("{0}/{1}".format(folderName,eventId))
outTexPath = "{0}/{1}/{2}.png".format(folderName,eventId,filename)
controller.SaveTexture(texsave, outTexPath)
print("保存图片{0}".format(outTexPath))
return True
def findIndexDrawLoop(d,index):
ret = None
if d.eventId == index:
return d
for c in d.children:
ret = findIndexDrawLoop(c,index)
if ret:
return ret
return ret
# Recursively search for the drawcall with the most vertices
def findIndexDraw(index,controller):
ret = None
for d in controller.GetDrawcalls():
if d.eventId == index:
ret = d
return ret
for c in d.children:
ret = findIndexDrawLoop(c,index)
if ret:
return ret
return ret
# Unpack a tuple of the given format, from the data
def unpackData(fmt, data):
if isPrint:
print(888)
# We don't handle 'special' formats - typically bit-packed such as 10:10:10:2
# raise RuntimeError("Packed formats are not supported!")
formatChars = {}
# 012345678
formatChars[rd.CompType.UInt] = "xBHxIxxxL"
formatChars[rd.CompType.SInt] = "xbhxixxxl"
formatChars[rd.CompType.Float] = "xxexfxxxd" # only 2, 4 and 8 are valid
# These types have identical decodes, but we might post-process them
formatChars[rd.CompType.UNorm] = formatChars[rd.CompType.UInt]
formatChars[rd.CompType.UScaled] = formatChars[rd.CompType.UInt]
formatChars[rd.CompType.SNorm] = formatChars[rd.CompType.SInt]
formatChars[rd.CompType.SScaled] = formatChars[rd.CompType.SInt]
# We need to fetch compCount components
vertexFormat = str(fmt.compCount) + formatChars[fmt.compType][fmt.compByteWidth]
# Unpack the data
value = struct.unpack_from(vertexFormat, data, 0)
# If the format needs post-processing such as normalisation, do that now
if fmt.compType == rd.CompType.UNorm:
divisor = float((2 ** (fmt.compByteWidth * 8)) - 1)
value = tuple(float(i) / divisor for i in value)
elif fmt.compType == rd.CompType.SNorm:
maxNeg = -float(2 ** (fmt.compByteWidth * 8)) / 2
divisor = float(-(maxNeg-1))
value = tuple((float(i) if (i == maxNeg) else (float(i) / divisor)) for i in value)
# If the format is BGRA, swap the two components
if fmt.BGRAOrder():
value = tuple(value[i] for i in [2, 1, 0, 3])
return value
# Get a list of MeshData objects describing the vertex inputs at this draw
def getMeshInputs(controller, draw):
state = controller.GetPipelineState()
# Get the index & vertex buffers, and fixed vertex inputs
ib = state.GetIBuffer()
vbs = state.GetVBuffers()
attrs = state.GetVertexInputs()
sampleList = state.GetReadOnlyResources(renderdoc.ShaderStage.Fragment)
for sample in sampleList:
for res in sample.resources:
print(res.resourceId)
if not pySaveTexture(res.resourceId,draw.eventId,controller):
break
meshInputs = []
# for i in ib:
# if isPri:nt
# print(i)
# for v in vbs:
# print(v)
#for attr in attrs:
# print(attr.name)
for attr in attrs:
# We don't handle instance attributes
if attr.perInstance:
raise RuntimeError("Instanced properties are not supported!")
meshInput = MeshData()
meshInput.indexResourceId = ib.resourceId # 2646
meshInput.indexByteOffset = ib.byteOffset # 0
meshInput.indexByteStride = draw.indexByteWidth # 0
meshInput.baseVertex = draw.baseVertex # 0
meshInput.indexOffset = draw.indexOffset # 0
meshInput.numIndices = draw.numIndices #顶点总数 18
# If the draw doesn't use an index buffer, don't use it even if bound
if not (draw.flags & rd.DrawFlags.Indexed):
meshInput.indexResourceId = rd.ResourceId.Null()
# The total offset is the attribute offset from the base of the vertex
meshInput.vertexByteOffset = attr.byteOffset + vbs[attr.vertexBuffer].byteOffset + draw.vertexOffset * vbs[attr.vertexBuffer].byteStride # 0
meshInput.format = attr.format
meshInput.vertexResourceId = vbs[attr.vertexBuffer].resourceId # 2645
meshInput.vertexByteStride = vbs[attr.vertexBuffer].byteStride # 56
meshInput.name = attr.name
meshInputs.append(meshInput)
return meshInputs
def getIndices(controller, mesh):
# Get the character for the width of index
indexFormat = 'B'
if mesh.indexByteStride == 2:
indexFormat = 'H'
elif mesh.indexByteStride == 4:
indexFormat = 'I'
# Duplicate the format by the number of indices
indexFormat = str(mesh.numIndices) + indexFormat
# If we have an index buffer
if mesh.indexResourceId != rd.ResourceId.Null():
# Fetch the data
ibdata = controller.GetBufferData(mesh.indexResourceId, mesh.indexByteOffset, 0)
# Unpack all the indices, starting from the first index to fetch
offset = mesh.indexOffset * mesh.indexByteStride
indices = struct.unpack_from(indexFormat, ibdata, offset)
# Apply the baseVertex offset
return [i + mesh.baseVertex for i in indices]
else:
# With no index buffer, just generate a range
return tuple(range(mesh.numIndices))
def printMeshData(controller, meshData,draw):
if isPrint:
print(4444)
indices = getIndices(controller, meshData[0])
csvArray = []
fileheader = []
formatxyzw = [".x",".y",".z",".w"]
if isPrint:
print("Mesh configuration:")
fileheader.append("VTX")
fileheader.append("IDX")
for attr in meshData:
if not attr.format.Special():
if isPrint:
print("\t%s:" % attr.name)
if isPrint:
print("\t\t- vertex: %s / %d stride" % (attr.vertexResourceId, attr.vertexByteStride))
if isPrint:
print("\t\t- format: %s x %s @ %d" % (attr.format.compType, attr.format.compCount, attr.vertexByteOffset))
headFormat = "{0}{1}"
for i in range(0,attr.format.compCount):
newStr = headFormat.format(attr.name,formatxyzw[i])
fileheader.append(newStr)
# We'll decode the first three indices making up a triangle
csvArray.append(fileheader)
# 写入CSV
if not os.path.exists("{0}/{1}".format(folderName,draw.eventId)):
os.makedirs("{0}/{1}".format(folderName,draw.eventId))
outPath = "{0}/{1}/model.csv".format(folderName,draw.eventId)
csvFile = open(outPath, "w",newline='')
writer = csv.writer(csvFile)
# ##########################保存图片#############################################
for inputIter in draw.outputs:
if not pySaveTexture(inputIter,draw.eventId,controller):
break
# ##########################保存图片#############################################
i = 0
for idx in indices:
# for i in range(0, 3):
# idx = indices[i]
# 每个顶点的信息
indiceArray = []
if isPrint:
print("Vertex %d is index %d:" % (i, idx))
indiceArray.append(i)
indiceArray.append(idx)
for attr in meshData:
if not attr.format.Special():
# This is the data we're reading from. This would be good to cache instead of
# re-fetching for every attribute for every index
offset = attr.vertexByteOffset + attr.vertexByteStride * idx
data = controller.GetBufferData(attr.vertexResourceId, offset, 0)
# Get the value from the data
value = unpackData(attr.format, data)
for j in range(0,attr.format.compCount):
indiceArray.append(value[j])
# if isPri:ntWe don't go into the details of semantic matching here, just
# print both
if isPrint:
print("\tAttribute '%s': %s" % (attr.name, value))
csvArray.append(indiceArray)
i = i + 1
writer.writerows(csvArray)
csvFile.close()
print("写入{0}成功".format(outPath))
def sampleCodePreDraw(controller,draw):
if draw.eventId >= startIndex and draw.eventId <= endIndex:
# Move to that draw
controller.SetFrameEvent(draw.eventId, True)
if isPrint:
print("Decoding mesh inputs at %d: %s\n\n" % (draw.eventId, draw.name))
# # Calculate the mesh input configuration
meshInputs = getMeshInputs(controller, draw)
# if isPri:nt# Fetch and
# print the data from the mesh inputs
printMeshData(controller, meshInputs,draw)
def sampleCodeRecursion(controller,draw):
sampleCodePreDraw(controller,draw)
for d in draw.children:
sampleCodeRecursion(controller,d)
def sampleCode(controller):
for draw in controller.GetDrawcalls():
sampleCodeRecursion(controller,draw)
def loadCapture(filename):
if isPrint:
print(222)
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
status = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't open file: " + str(status))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
status,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(status))
return (cap, controller)
if 'pyrenderdoc' in globals():
if isPrint:
print(111)
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
if isPrint:
print("aaaa")
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
if len(sys.argv) <= 1:
if isPrint:
print('Usage: python3 {} filename.rdc'.format(sys.argv[0]))
sys.exit(0)
cap,controller = loadCapture(sys.argv[1])
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
print("导出完毕!!!!")
优化:
其实还可以做优化,这样导csv文件实在太慢了,我想到两个方法优化。
1.用C++直接导出csv,这样是最快的。
2.可以只顺序的导出vertex,因为现在是导了很多重复的vertex,就是把index跟vertex分开导,在c#部分把他转成三角形,可以使得程序更快,大概能快两到三倍的样子。