1. 準備工作
1.1. 選擇區域
所有阿里云服務都需要使用相同的地域。
1.2. 開通服務
1.3. 制作鏡像
制作鏡像具體步驟請參考集群鏡像, 請嚴格按文檔的步驟創建鏡像。鏡像制作完成后,通過以下方式可以獲取到對應的鏡像信息。
1.4. 上傳素材
可以下載 3ds Max 官方提供的免費素材包進行測試。
通過 OSSBrowser工具將渲染素材到指定的 OSS bucket 中,如下圖:
1.5. 安裝批量計算 SDK
在需要提交作業的機器上,安裝批量計算 SDK 庫;已經安裝請忽略。Linux 安裝執行如下命令;Windows 平臺請參考文檔。
pip install batchcompute
2. 編寫work腳本
work.py
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import math
import sys
import re
import argparse
NOTHING_TO_DO = 'Nothing to do, exit'
def _calcRange(a,b, id, step):
start = min(id * step + a, b)
end = min((id+1) * step + a-1, b)
return (start, end)
def _parseContinuedFrames(render_frames, total_nodes, id=None, return_type='list'):
'''
解析連續幀, 如:1-10
'''
[a,b]=render_frames.split('-')
a=int(a)
b=int(b)
#print(a,b)
step = int(math.ceil((b-a+1)*1.0/total_nodes))
#print('step:', step)
mod = (b-a+1) % total_nodes
#print('mod:', mod)
if mod==0 or id < mod:
(start, end) = _calcRange(a,b, id, step)
#print('--->',start, end)
return (start, end) if return_type!='list' else range(start, end+1)
else:
a1 = step * mod + a
#print('less', a1, b, id)
(start, end) = _calcRange(a1 ,b, id-mod, step-1)
#print('--->',start, end)
return (start, end) if return_type!='list' else range(start, end+1)
def _parseIntermittentFrames(render_frames, total_nodes, id=None):
'''
解析不連續幀, 如: 1,3,8-10,21
'''
a1=render_frames.split(',')
a2=[]
for n in a1:
a=n.split('-')
a2.append(range(int(a[0]),int(a[1])+1) if len(a)==2 else [int(a[0])])
a3=[]
for n in a2:
a3=a3+n
#print('a3',a3)
step = int(math.ceil(len(a3)*1.0/total_nodes))
#print('step',step)
mod = len(a3) % total_nodes
#print('mod:', mod)
if mod==0 or id < mod:
(start, end) = _calcRange(0, len(a3)-1, id, step)
#print(start, end)
a4= a3[start: end+1]
#print('--->', a4)
return a4
else:
#print('less', step * mod , len(a3)-1, id)
(start, end) = _calcRange( step * mod ,len(a3)-1, id-mod, step-1)
if start > len(a3)-1:
print(NOTHING_TO_DO)
sys.exit(0)
#print(start, end)
a4= a3[start: end+1]
#print('--->', a4)
return a4
def parseFrames(render_frames, return_type='list', id=None, total_nodes=None):
'''
@param render_frames {string}: 需要渲染的總幀數列表范圍,可以用"-"表示范圍,不連續的幀可以使用","隔開, 如: 1,3,5-10
@param return_type {string}: 取值范圍[list,range]。 list樣例: [1,2,3], range樣例: (1,3)。
注意: render_frames包含","時有效,強制為list。
@param id, 節點ID,從0開始。 正式環境不要填寫,將從環境變量 BATCH_COMPUTE_DAG_INSTANCE_ID 中取得。
@param total_nodes, 總共的節點個數。正式環境不要填寫,將從環境變量 BATCH_COMPUTE_DAG_INSTANCE_COUNT 中取得。
'''
if id==None:
id=os.environ['BATCH_COMPUTE_DAG_INSTANCE_ID']
if type(id)==str:
id = int(id)
if total_nodes==None:
total_nodes = os.environ['BATCH_COMPUTE_DAG_INSTANCE_COUNT']
if type(total_nodes)==str:
total_nodes = int(total_nodes)
if re.match(r'^(\d+)\-(\d+)$',render_frames):
# 1-2
# continued frames
return _parseContinuedFrames(render_frames, total_nodes, id, return_type)
else:
# intermittent frames
return _parseIntermittentFrames(render_frames, total_nodes, id)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
description = 'python scripyt for 3dmax dag job',
usage='render3Dmax.py <positional argument> [<args>]',
)
parser.add_argument('-s', '--scene_file', action='store', type=str, required=True, help = 'the name of the file with .max subffix .')
parser.add_argument('-i', '--input', action='store', type=str, required=True, help = 'the oss dir of the scene_file, eg: xxx.max.')
parser.add_argument('-o', '--output', action='store', type=str, required=True, help = 'the oss of dir the result file to upload .')
parser.add_argument('-f', '--frames', action='store', type=str, required=True, help = 'the frames to be renderd, eg: "1-10".')
parser.add_argument('-t', '--retType', action='store', type=str, default="test.jpg", help = 'the tye of the render result,eg. xxx.jpg/xxx.png.')
args = parser.parse_args()
frames=parseFrames(args.frames)
framestr='-'.join(map(lambda x:str(x), frames))
s = "cd \"C:\\Program Files\\Autodesk\\3ds Max 2018\\\" && "
s +='3dsmaxcmd.exe -o="%s%s" -frames=%s "%s\\%s"' % (args.output, args.retType, framestr, args.input, args.scene_file)
print("exec: %s" % s)
rc = os.system(s)
sys.exit(rc>>8)
注意:
work.py 只需要被上傳到 OSS bucket中不需要手動執行;各項參數通過作業提交腳本進行傳遞;
work.py 的112 行需要根據鏡像制作過程中 3ds MAX 的位置做對應替換;
work.py 的 scene_file 參數表示場景文件;如 Lighting-CB_Arnold_SSurface.max;
work.py 的 input 參數表示素材映射到 VM 中的位置,如:D;
work.py 的 output 參數表示渲染結果輸出的本地路徑;如 C:\tmp\;
work.py 的 frames 參數表示渲染的幀數,如:1;
work.py 的 retType 參數表示素材映射到 VM 中的位置,如:test.jpg;渲染結束后如果是多幀,則每幀的名稱為test000.jpg,test001.jpg等。
3. 編寫作業提交腳本
test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from batchcompute import Client, ClientError
from batchcompute.resources import (
ClusterDescription, GroupDescription, Configs, Networks, VPC,
JobDescription, TaskDescription, DAG,Mounts,
AutoCluster,Disks,Notification,
)
import time
import argparse
from batchcompute import CN_SHANGHAI as REGION #需要根據 region 做適配
access_key_id = "xxxx" # your access key id
access_key_secret = "xxxx" # your access key secret
instance_type = "ecs.g5.4xlarge" # instance type #需要根據 業務需要 做適配
image_id = "m-xxx"
workossPath = "oss://xxxxx/work/work.py"
client = Client(REGION, access_key_id, access_key_secret)
def getAutoClusterDesc(InstanceCount):
auto_desc = AutoCluster()
auto_desc.ECSImageId = image_id
#任務失敗保留環境,程序調試階段設置。環境保留費用會繼續產生請注意及時手動清除環境任務失敗保留環境,
# 程序調試階段設置。環境保留費用會繼續產生請注意及時手動清除環境
auto_desc.ReserveOnFail = False
# 實例規格
auto_desc.InstanceType = instance_type
#case3 按量
auto_desc.ResourceType = "OnDemand"
#Configs
configs = Configs()
#Configs.Networks
networks = Networks()
vpc = VPC()
# CidrBlock和VpcId 都傳入,必須保證VpcId的CidrBlock 和傳入的CidrBlock保持一致
vpc.CidrBlock = '172.26.0.0/16'
# vpc.VpcId = "vpc-8vbfxdyhx9p2flummuwmq"
networks.VPC = vpc
configs.Networks = networks
# 設置系統盤type(cloud_efficiency/cloud_ssd)以及size(單位GB)
configs.add_system_disk(size=40, type_='cloud_efficiency')
#設置數據盤type(必須和系統盤type保持一致) size(單位GB) 掛載點
# case1 linux環境
# configs.add_data_disk(size=40, type_='cloud_efficiency', mount_point='/path/to/mount/')
# 設置節點個數
configs.InstanceCount = InstanceCount
auto_desc.Configs = configs
return auto_desc
def getTaskDesc(inputOssPath, outputossPath, scene_file, frames, retType, clusterId, InstanceCount):
taskDesc = TaskDescription()
timestamp = time.strftime("%Y_%m_%d_%H_%M_%S", time.localtime())
inputLocalPath = "D:"
outputLocalPath = "C:\\\\tmp\\\\" + timestamp + "\\\\"
outputossBase = outputossPath + timestamp + "/"
stdoutOssPath = outputossBase + "stdout/" #your stdout oss path
stderrOssPath = outputossBase + "stderr/" #your stderr oss path
outputossret = outputossBase + "ret/"
taskDesc.InputMapping = {inputOssPath: inputLocalPath}
taskDesc.OutputMapping = {outputLocalPath: outputossret}
taskDesc.Parameters.InputMappingConfig.Lock = True
# 設置程序的標準輸出地址,程序中的print打印會實時上傳到指定的oss地址
taskDesc.Parameters.StdoutRedirectPath = stdoutOssPath
# 設置程序的標準錯誤輸出地址,程序拋出的異常錯誤會實時上傳到指定的oss地址
taskDesc.Parameters.StderrRedirectPath = stderrOssPath
#觸發程序運行的命令行
# PackagePath存放commandLine中的可執行文件或者二進制包
taskDesc.Parameters.Command.PackagePath = workossPath
taskDesc.Parameters.Command.CommandLine = "python work.py -i %s -o %s -s %s -f %s -t %s" % (inputLocalPath, outputLocalPath, scene_file, frames, retType)
# 設置任務的超時時間
taskDesc.Timeout = 86400
# 設置任務所需實例個數
taskDesc.InstanceCount = InstanceCount
# 設置任務失敗后重試次數
taskDesc.MaxRetryCount = 3
if clusterId:
# 采用固定集群提交作業
taskDesc.ClusterId = clusterId
else:
#采用auto集群提交作業
taskDesc.AutoCluster = getAutoClusterDesc(InstanceCount)
return taskDesc
def getDagJobDesc(inputOssPath, outputossPath, scene_file, frames, retType, clusterId = None, instanceNum = 1):
job_desc = JobDescription()
dag_desc = DAG()
job_desc.Name = "testBatch"
job_desc.Description = "test 3dMAX job"
job_desc.Priority = 1
# 任務失敗
job_desc.JobFailOnInstanceFail = False
# 作業運行成功后戶自動會被立即釋放掉
job_desc.AutoRelease = False
job_desc.Type = "DAG"
render = getTaskDesc(inputOssPath, outputossPath, scene_file, frames, retType, clusterId, instanceNum)
# 添加任務
dag_desc.add_task('render', render)
job_desc.DAG = dag_desc
return job_desc
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
description = 'python scripyt for 3dmax dag job',
usage='render3Dmax.py <positional argument> [<args>]',
)
parser.add_argument('-n','--instanceNum', action='store',type = int, default = 1,help = 'the parell instance num .')
parser.add_argument('-s', '--scene_file', action='store', type=str, required=True, help = 'the name of the file with .max subffix .')
parser.add_argument('-i', '--inputoss', action='store', type=str, required=True, help = 'the oss dir of the scene_file, eg: xxx.max.')
parser.add_argument('-o', '--outputoss', action='store', type=str, required=True, help = 'the oss of dir the result file to upload .')
parser.add_argument('-f', '--frames', action='store', type=str, required=True, help = 'the frames to be renderd, eg: "1-10".')
parser.add_argument('-t', '--retType', action='store', type=str, default = "test.jpg", help = 'the tye of the render result,eg. xxx.jpg/xxx.png.')
parser.add_argument('-c', '--clusterId', action='store', type=str, default=None, help = 'the clusterId to be render .')
args = parser.parse_args()
try:
job_desc = getDagJobDesc(args.inputoss, args.outputoss, args.scene_file, args.frames,args.retType, args.clusterId, args.instanceNum)
# print job_desc
job_id = client.create_job(job_desc).Id
print('job created: %s' % job_id)
except ClientError,e:
print (e.get_status_code(), e.get_code(), e.get_requestid(), e.get_msg())
注意:
代碼中 12~20 行需要根據做適配,如 AK 信息需要填寫賬號對應的AK信息;鏡像Id 就是1.3 中制作的鏡像 Id;workosspath 是步驟 2 work.py 在OSS上的位置;
參數 instanceNum 表示當前渲染作業需要幾個節點參與,默認是1個節點;若是設置為多個節點,work.py 會自動做均分;
參數 scene_file 表示需要渲染的場景文件,傳給 work.py;
參數 inputoss 表示素材上傳到 OSS 上的位置,也即1.4 中的 OSS 位置;
參數 outputoss 表示最終結果上傳到 Oss 上的位置;
參數 frames 表示需要渲染的場景文件的幀數,傳給 work.py;3ds MAX 不支持隔幀渲染,只能是連續幀,如1-10;
參數 retType 表示需要渲染結果名稱,傳給 work.py,默認是 test.jpg,則最終得到test000.jpg
參數 clusterId 表示采用固定集群做渲染時,固定集群的Id。
4. 提交作業
根據以上示例文檔,執行以下命令:
python test.py -s Lighting-CB_Arnold_SSurface.max -i oss://bcs-test-sh/3dmaxdemo/Scenes/Lighting/ -o oss://bcs-test-sh/test/ -f 1-1 -t 123.jpg
示例運行結果: