生成缩略图_[SD]用Python脚本生成缩略图

最近学习研究了一下sd 的python脚本,做了个小练习,将sbs或者sbsar文件渲染成一张缩略图。

经查阅,Substance Automation Toolkit工具好像可以实现离线的方式创建出缩略图,但是我没有这个工具包····,遂在论坛上逛了逛,发现了一个节点:PBR Render。通过下图所示,就可以得到一张材质的缩略图,就按照这个思路,开始我们的脚本。

生成缩略图_[SD]用Python脚本生成缩略图_第1张图片

具体流程如下:

生成缩略图_[SD]用Python脚本生成缩略图_第2张图片

我们一步一步看具体实现步骤:

获取当前的Graph,和package

ctx = sd.getContext()
app = ctx.getSDApplication()
UIMgr = app.getQtForPythonUIMgr()
currentGraph = UIMgr.getCurrentGraph()
pkgMgr = app.getPackageMgr()
currentpackage = pkgMgr.getUserPackages().getItem(0)

接下来创建PBR Render Node:

def createPBRRenderNode(app, pkgMgr, currentGraph):
    
    # create PBR Rendoer Node
    resourcePath = app.getPath(SDApplicationPath.DefaultResourcesDir)
    package = pkgMgr.loadUserPackage(os.path.join(resourcePath, 'packages', 'pbr_render_ortho.sbs'))
    packageName = 'PBR_render_ortho'
    instanceNode = currentGraph.newInstanceNode(package.findResourceFromUrl(packageName))
    instanceNode.setInputPropertyValueFromId('shape', SDValueInt.sNew(1))
    instanceNode.setInputPropertyValueFromId('envrionment_horizontal_rotation', SDValueFloat.sNew(0.06))
    instanceNode.setInputPropertyValueFromId('background_color', SDValueColorRGBA.sNew(ColorRGBA(0, 0, 0, 0)))
    return instanceNode

因为PBR Render 节点是SD官方的,所以先获取到默认的资源路径,可以在ATTRIBUTES下看到,Package位置和名字:

5582f8a0d557e81b02470cb49a7b3017.png

通过loadUserPackage将包导入,newInstanceNode函数将该节点添加到当前Graph。因为PBR Render节点默认的shape是正方形,而且背景是黑色的,所以这里我又设置了三个属性,将shape设置为sphere,水平旋转0.06,背景改成透明。

我将创建环境图和连接写在了一起:(通过仔细观察,法线galazed这张环境图就是painter shelf中basematerial所采用的环境图)

def linkEnvironmentMap(app, pbrRenderNode, package, currentGraph):

    resourcePath = app.getPath(SDApplicationPath.DefaultResourcesDir)
    envMapPath = resourcePath + '/view3d/maps/glazed_patio.exr'
    envMap = SDResourceBitmap.sNewFromFile(package, envMapPath, EmbedMethod.Linked)
    envMapNode = currentGraph.newInstanceNode(envMap)
    envMapNode.newPropertyConnectionFromId('unique_filter_output', pbrRenderNode, 'environment_map')

创建过程和之前基本一样,用newPropertyConnectionFromId()将两个节点连接起来,这个函数以后还会用到。

newPropertyConnectionFromId(sdOutputPropertyId,sdInputPropertyNode,sdInputPropertyId)

这里可能比较麻烦的就是需要知道要连接两端的id。官方文档中有方法,这里我就不再重复写了。

https://docs.substance3d.com/sddoc/nodes-and-properties-172825056.html​docs.substance3d.com

做到这里我们就得到了一个最基础的“模板”:

生成缩略图_[SD]用Python脚本生成缩略图_第3张图片

接下来的内容暂时与SD API无关,我们需要获取需要处理的材质的列表。

拿出我最常用的遍历文件的函数:

MaterialPathList = []

def getMaterialList(rootpath):  
    for i in os.listdir(rootpath):
        path = os.path.join(rootpath, i)
        if path.split('')[-1] == '.autosave' or path.split('')[-1] == 'dependencies':
            continue
        filename = path.split('')[-1]
        if filename.split('.')[-1] == 'sbs' or filename.split('.')[-1] == 'sbsar':
            MaterialPathList.append(path)
        if os.path.isdir(path):
            getMaterialList(path)

这里稍微做了处理,屏蔽掉了.autosava和dependencies文件夹。这里可能会出现一个材质既有sbs又有sbsar,或者只有sbs,再或者只有sbsar,暂且先不管他。

(我这里其实有做处理,将所有的sbs文件导出成为sbsar文件,但是目前还有一个小bug,等我解决之后再更新)

有了MaterialPathList我们就可以循环遍历所有的材质了:

MaterialPath = r'D:/.../.../...'

for materialpath in MaterialPathList:
    package = pkgMgr.loadUserPackage(materialpath)
    packageName = materialpath.split('')[-1].split('.')[0]
    instanceNode = currentGraph.newInstanceNode(package.findResourceFromUrl(packageName))
    connectNode(instanceNode, pbrRenderNode)
    outputNode = createOutputNode(currentGraph, pbrRenderNode)
    currentGraph.compute()
    texturename = materialpath.split('.')[0] + '.png'
    renderThumbnail(pbrRenderNode, texturename)
    currentGraph.deleteNode(instanceNode)
    currentGraph.deleteNode(outputNode)
    pkgMgr.unloadUserPackage(package)

流程就和我上面贴的流程图一样

connectNode():

LabelList = ['Base Color', 'Normal', 'Roughness', 'Metallic', 'Ambient Occlusion', 'Height', 'Opacity']

ConnectionProList = {'channel_basecolor': 'basecolor', 
'channel_normal': 'normal', 
'channel_roughness': 'roughness', 
'channel_metallic': 'metallic', 
'channel_ambientocclusion': 'ambient_occlusion', 
'channel_height': 'height',
'channel_opacity': 'opacity',
'basecolor': 'basecolor',
'normal': 'normal', 
'roughness': 'roughness',
'metallic': 'metallic',
'height': 'height',
'opacity': 'opacity',
'ambient_occlusion': 'ambient_occlusion',
'ambientocclusion': 'ambient_occlusion'}

def connectNode(materialNode, pbrRenderNode):
    
    definition = materialNode.getDefinition()
    inprops = definition.getProperties(SDPropertyCategory.Output)
    connectproperties = []
    for prop in inprops:
        propId = prop.getId()
        label = prop.getLabel()
       #value = materialNode.getPropertyValue(prop)
        if label in LabelList:
            if propId in ConnectionProList:
                #print(propId)
                try:
                    materialNode.newPropertyConnectionFromId(propId, pbrRenderNode, ConnectionProList[propId])
                except:
                    pass
                else:
                    print(propId + ' connected!')

这里其实要将对应的属性连接起来并不那么简单,由于不同人命名的不同,导致了会有很多“不规范”的id,比如环境光遮蔽,可能会遇到各种各样的id(AO,AmbientOcclusion等),但是PBR Render节点上的这些属性的id是固定的,所以我干脆就写个字典,至少说把我遇到过的情况的对应关系记录下来。字典是key是导入进来的材质的id,value是PBR Render 的id。

连接完这些节点之后,本以为可以出来图了,但是!结果如下:

生成缩略图_[SD]用Python脚本生成缩略图_第4张图片

???我图呢???这时候如果你对designer比较熟悉的话,空白处右键

生成缩略图_[SD]用Python脚本生成缩略图_第5张图片

Compute Nodes Thumbnail解决问题。OK,那我们脚本该如何处理呢?

文档中一查,sd.api.sbs.sdsbscompgraph.SDSBSCompGraph中有compute()函数,太好了。当时我没有仔细看这个函数的介绍,直接就添加进脚本了,发现无论怎么样都不起作用。冥冥中想到了一个办法,可能是需要一个output节点,compute()才能起作用。一试,果然!

那么接下来,创建output节点:

def createOutputNode(currentGraph, pbrRenderNode):
    
    outputNode = currentGraph.newNode('sbs::compositing::output')
    sdValueArray = SDValueArray.sNew(SDTypeUsage.sNew(), 0)
    sdValueUsage = SDValueUsage.sNew(SDUsage.sNew('baseColor', 'RGBA', 'sRGB'))
    sdValueArray.pushBack(sdValueUsage)
    outputNode.setAnnotationPropertyValueFromId('usages', sdValueArray)
    pbrRenderNode.newPropertyConnectionFromId('basecolor', outputNode, 'inputNodeOutput')

这里如何创建output节点并设置属性,其实在sample内都有,顺便我也将output和PBR Render连起来了。

compute完,就该保存那张图了:

def renderThumbnail(pbrRenderNode, texturename):

    prop = pbrRenderNode.getPropertyFromId('basecolor', SDPropertyCategory.Output)
    value = pbrRenderNode.getPropertyValue(prop)
    sdtexture = value.get()
    if os.path.exists(texturename):
        os.remove(texturename)
    sdtexture.save(texturename)

(因为我这里发现了有些材质已经有了缩略图,所以就先remove掉了)

接下来按照流程就是删除不需要的节点,并将package卸载。

----------------9.12更新---------------------

为了避免之前的bug,我写了个ui

这里提醒一下,19版本的插件和之前的不一样,可以去看官方的文档

插件路径:

通过preference添加插件脚本的路径。

生成缩略图_[SD]用Python脚本生成缩略图_第6张图片

通过文档得知,SD插件是一个py文件,或者py module,在其中定义initializeSDPlugin()函数

下面是我的插件程序:

import sd
from PySide2 import QtCore, QtWidgets
import export2SBSAR as e2s
import createThumbnail as cT

def createMenu():
    # Get the application and the UI Manager.
    app = sd.getContext().getSDApplication()
    uiMgr = app.getQtForPythonUIMgr()
    
    # Function that will be called when our menu item is selected.
    def export2SBSAR():
    	e2s.main()
    
    def createThumbnail():
        cT.main()
    
    # Create a new menu.
    menu = uiMgr.newMenu(menuTitle="CreateThumbnail", objectName="doc.example.my_menu")
    # Create a new action.
    act = QtWidgets.QAction("export2SBSAR", menu)
    act.triggered.connect(export2SBSAR)
    act_1 = QtWidgets.QAction("saveThumbnail", menu)
    act_1.triggered.connect(createThumbnail)
    
    # Add the action to the menu.
    menu.addAction(act)
    menu.addAction(act_1)

def initializeSDPlugin():
    createMenu()
import export2SBSAR 
import createThumbnail 

是我的两个功能脚本,会得到如下的插件button

ca6b042ae00961c59f9b78e26830e150.png

先说明一下第一步: 把sbs导出成sbsar,代码如下

export2SBSAR.py

import sd
import os
from sd.api.sdapplication import SDApplicationPath
from sd.api.sdpackagemgr import SDPackageMgr
from sd.api.sdresourcebitmap import SDResourceBitmap
from sd.api.sdvalueserializer import SDValueSerializer
from sd.api.sdtexture import SDTexture
from sd.api.sbs.sdsbscompgraph import SDSBSCompGraph
from sd.api.sdresource import EmbedMethod
from sd.api.sdproperty import SDPropertyCategory
from sd.api.sdvaluearray import SDValueArray
from sd.api.sdtypeusage import SDTypeUsage
from sd.api.sdvalueusage import SDValueUsage
from sd.api.sdusage import SDUsage
from sd.api.sdvaluetexture import SDValueTexture
from sd.api.sdtexture import SDTexture
from sd.api.sdvalueint import SDValueInt
from sd.api.sdvaluefloat import SDValueFloat
from sd.api.sdvaluecolorrgba import SDValueColorRGBA
from sd.api.sdbasetypes import ColorRGBA
from sd.api.sbs.sdsbsarexporter import SDSBSARExporter
from sd.api.sdarray import SDArray

MaterialPathList = []
MaterialPathRoot = r'your path'

def getMaterialList(rootpath):
    for i in os.listdir(rootpath):
        path = os.path.join(rootpath, i)
        if path.split('')[-1] == '.autosave' or path.split('')[-1] == 'dependencies':
            continue
        filename = path.split('')[-1]
        if filename.split('.')[-1] == 'sbs' and not os.path.isfile(path + 'ar'):
            print(path)
            MaterialPathList.append(path)
        if os.path.isdir(path):
            getMaterialList(path)

def main():
    ctx = sd.getContext()
    app = ctx.getSDApplication()
    UIMgr = app.getQtForPythonUIMgr()
    currentGraph = UIMgr.getCurrentGraph()
    pkgMgr = app.getPackageMgr()

    exporterInstance = SDSBSARExporter(ctx, None)
    e = exporterInstance.sNew()
    getMaterialList(MaterialPathRoot)

    for materialpath in MaterialPathList:
        package = pkgMgr.loadUserPackage(materialpath)
        savepath = materialpath + 'ar'
        e.exportPackageToSBSAR(package, savepath)
        pkgMgr.unloadUserPackage(package)

    print('export sbsarfile sucess!')

其实没必要导入这么多,我只是懒得删了直接复制之前的。

关于sd.api.sbs.sdsbsarexporter的用法,文档中并没用将太多,我也是参考git上的一个示例:

https://gist.github.com/jasonbrackman/8f9babb0e65547b2bc0e2364768023a9​gist.github.com

作者说这个sbsarexporter会导致占用很多内存,可能我处理的比较少,感觉没有影响。

接下来就是保存缩略图:

createThumbnail.py

import sd
import os
from sd.api.sdapplication import SDApplicationPath
from sd.api.sdpackagemgr import SDPackageMgr
from sd.api.sdresourcebitmap import SDResourceBitmap
from sd.api.sdvalueserializer import SDValueSerializer
from sd.api.sdtexture import SDTexture
from sd.api.sbs.sdsbscompgraph import SDSBSCompGraph
from sd.api.sdresource import EmbedMethod
from sd.api.sdproperty import SDPropertyCategory
from sd.api.sdvaluearray import SDValueArray
from sd.api.sdtypeusage import SDTypeUsage
from sd.api.sdvalueusage import SDValueUsage
from sd.api.sdusage import SDUsage
from sd.api.sdvaluetexture import SDValueTexture
from sd.api.sdtexture import SDTexture
from sd.api.sdvalueint import SDValueInt
from sd.api.sdvaluefloat import SDValueFloat
from sd.api.sdvaluecolorrgba import SDValueColorRGBA
from sd.api.sdbasetypes import ColorRGBA

MaterialPath = r'your path'
MaterialPathList = []

LabelList = ['Base Color', 'Normal', 'Roughness', 'Metallic', 'Ambient Occlusion', 'Height', 'Opacity']

ConnectionProList = {'channel_basecolor': 'basecolor', 
'channel_normal': 'normal', 
'channel_roughness': 'roughness', 
'channel_metallic': 'metallic', 
'channel_ambientocclusion': 'ambient_occlusion', 
'channel_height': 'height',
'channel_opacity': 'opacity',
'basecolor': 'basecolor',
'normal': 'normal', 
'roughness': 'roughness',
'metallic': 'metallic',
'height': 'height',
'opacity': 'opacity',
'ambient_occlusion': 'ambient_occlusion',
'ambientocclusion': 'ambient_occlusion'}


def createPBRRenderNode(app, pkgMgr, currentGraph):
    
    # create PBR Rendoer Node
    resourcePath = app.getPath(SDApplicationPath.DefaultResourcesDir)
    package = pkgMgr.loadUserPackage(os.path.join(resourcePath, 'packages', 'pbr_render_ortho.sbs'))
    packageName = 'PBR_render_ortho'
    instanceNode = currentGraph.newInstanceNode(package.findResourceFromUrl(packageName))
    instanceNode.setInputPropertyValueFromId('shape', SDValueInt.sNew(1))
    instanceNode.setInputPropertyValueFromId('envrionment_horizontal_rotation', SDValueFloat.sNew(0.06))
    instanceNode.setInputPropertyValueFromId('background_color', SDValueColorRGBA.sNew(ColorRGBA(0, 0, 0, 0)))
    return instanceNode

    
def linkEnvironmentMap(app, pbrRenderNode, package, currentGraph):

    resourcePath = app.getPath(SDApplicationPath.DefaultResourcesDir)
    envMapPath = resourcePath + '/view3d/maps/glazed_patio.exr'
    envMap = SDResourceBitmap.sNewFromFile(package, envMapPath, EmbedMethod.Linked)
    envMapNode = currentGraph.newInstanceNode(envMap)
    envMapNode.newPropertyConnectionFromId('unique_filter_output', pbrRenderNode, 'environment_map')

def getMaterialList(rootpath):
            
    for i in os.listdir(rootpath):
        path = os.path.join(rootpath, i)
        if path.split('')[-1] == '.autosave' or path.split('')[-1] == 'dependencies':
            continue
        filename = path.split('')[-1]
        if filename.split('.')[-1] == 'sbsar':
            #print(path)
            MaterialPathList.append(path)
        if os.path.isdir(path):
            getMaterialList(path)
 
         
def connectNode(materialNode, pbrRenderNode):
    
    definition = materialNode.getDefinition()
    inprops = definition.getProperties(SDPropertyCategory.Output)
    connectproperties = []
    for prop in inprops:
        propId = prop.getId()
        label = prop.getLabel()
       #value = materialNode.getPropertyValue(prop)
        if label in LabelList:
            if propId in ConnectionProList:
                #print(propId)
                try:
                    materialNode.newPropertyConnectionFromId(propId, pbrRenderNode, ConnectionProList[propId])
                except:
                    pass
                else:
                    print(propId + ' connected!')


def renderThumbnail(pbrRenderNode, texturename):

    prop = pbrRenderNode.getPropertyFromId('basecolor', SDPropertyCategory.Output)
    value = pbrRenderNode.getPropertyValue(prop)
    sdtexture = value.get()
    if os.path.exists(texturename):
        os.remove(texturename)
    sdtexture.save(texturename)


def createOutputNode(currentGraph, pbrRenderNode):
    
    outputNode = currentGraph.newNode('sbs::compositing::output')
    sdValueArray = SDValueArray.sNew(SDTypeUsage.sNew(), 0)
    sdValueUsage = SDValueUsage.sNew(SDUsage.sNew('baseColor', 'RGBA', 'sRGB'))
    sdValueArray.pushBack(sdValueUsage)
    outputNode.setAnnotationPropertyValueFromId('usages', sdValueArray)
    pbrRenderNode.newPropertyConnectionFromId('basecolor', outputNode, 'inputNodeOutput')
    return outputNode


def main():
    
    ctx = sd.getContext()
    app = ctx.getSDApplication()
    UIMgr = app.getQtForPythonUIMgr()
    currentGraph = UIMgr.getCurrentGraph()
    #print(currentGraph)
    pkgMgr = app.getPackageMgr()
    currentpackage = pkgMgr.getUserPackages().getItem(0)
    pbrRenderNode = createPBRRenderNode(app, pkgMgr, currentGraph)
    linkEnvironmentMap(app, pbrRenderNode, currentpackage, currentGraph)
    getMaterialList(MaterialPath)
    

    # load materialpath one by one
    for materialpath in MaterialPathList:
        package = pkgMgr.loadUserPackage(materialpath)
        packageName = materialpath.split('')[-1].split('.')[0]
        print(materialpath)
        instanceNode = currentGraph.newInstanceNode(package.findResourceFromUrl(packageName))
        connectNode(instanceNode, pbrRenderNode)
        outputNode = createOutputNode(currentGraph, pbrRenderNode)
        currentGraph.compute()
        texturename = materialpath.split('.')[0] + '.png'
        renderThumbnail(pbrRenderNode, texturename)
        currentGraph.deleteNode(instanceNode)
        currentGraph.deleteNode(outputNode)
        pkgMgr.unloadUserPackage(package)

    print("Run finished!")

渲染缩略图,包括导出sbsar文件,其实是为了Painter使用的,因为Painter shelf中一旦材质球很多会导致加载速度慢,遂写个材质库一类的,在外部读取缩略图,然后以导入project 的方式导入painter,基本上也已经写完,有空更新。

你可能感兴趣的:(生成缩略图)