全部产品
云市场

3Dmax DAG作业最佳实践

更新时间:2019-09-23 10:36:21

本文主要介绍如何通过 BatchCompute DAG 作业的方式提交 3ds MAX 渲染作业;由于 3ds MAX 主要使用场景在 Windows 平台上,本文代码主要是基于 Windows 平台开发。3D Studio Max,常简称为 3d Max 或 3ds MAX,是 Discreet 公司开发的(后被 Autodesk 公司合并)基于 PC 系统的三维动画渲染和制作软件,具体详细信息参考其官方文档: https://www.autodesk.com/products/3ds-max/overview。

1. 准备工作

1.1 选择区域

本篇例子所有阿里云服务都需要使用相同的地域。

本篇例子使用地域: 华东2 (上海)

1.2 开通服务

注意:使用批量计算时,所选地域和 OSS bucket 的地域必须保持一致。

1.3 制作运行时镜像

制作运行时镜具体步骤请参考指导文档, 请严格按文档的步骤创建镜像。镜像制作完成后,通过以下方式可以获取到对应的镜像信息。

image

1.4 上传素材

可以下载 3ds MAX 官方提供的免费素材包进行测试。

通过 OSSBrowser 工具将渲染素材到指定的 OSS bucket中,如下图:

upload

1.5 安装批量计算 SDK 库

在需要提交作业的机器上,安装批量计算 SDK 库;已经安装请忽略。Linux 安装执行如下命令;Windows 平台请参考文档

  1. pip install batchcompute

2 编写work脚本

work.py

  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. import os
  4. import math
  5. import sys
  6. import re
  7. import argparse
  8. NOTHING_TO_DO = 'Nothing to do, exit'
  9. def _calcRange(a,b, id, step):
  10. start = min(id * step + a, b)
  11. end = min((id+1) * step + a-1, b)
  12. return (start, end)
  13. def _parseContinuedFrames(render_frames, total_nodes, id=None, return_type='list'):
  14. '''
  15. 解析连续帧, 如: 1-10
  16. '''
  17. [a,b]=render_frames.split('-')
  18. a=int(a)
  19. b=int(b)
  20. #print(a,b)
  21. step = int(math.ceil((b-a+1)*1.0/total_nodes))
  22. #print('step:', step)
  23. mod = (b-a+1) % total_nodes
  24. #print('mod:', mod)
  25. if mod==0 or id < mod:
  26. (start, end) = _calcRange(a,b, id, step)
  27. #print('--->',start, end)
  28. return (start, end) if return_type!='list' else range(start, end+1)
  29. else:
  30. a1 = step * mod + a
  31. #print('less', a1, b, id)
  32. (start, end) = _calcRange(a1 ,b, id-mod, step-1)
  33. #print('--->',start, end)
  34. return (start, end) if return_type!='list' else range(start, end+1)
  35. def _parseIntermittentFrames(render_frames, total_nodes, id=None):
  36. '''
  37. 解析不连续帧, 如: 1,3,8-10,21
  38. '''
  39. a1=render_frames.split(',')
  40. a2=[]
  41. for n in a1:
  42. a=n.split('-')
  43. a2.append(range(int(a[0]),int(a[1])+1) if len(a)==2 else [int(a[0])])
  44. a3=[]
  45. for n in a2:
  46. a3=a3+n
  47. #print('a3',a3)
  48. step = int(math.ceil(len(a3)*1.0/total_nodes))
  49. #print('step',step)
  50. mod = len(a3) % total_nodes
  51. #print('mod:', mod)
  52. if mod==0 or id < mod:
  53. (start, end) = _calcRange(0, len(a3)-1, id, step)
  54. #print(start, end)
  55. a4= a3[start: end+1]
  56. #print('--->', a4)
  57. return a4
  58. else:
  59. #print('less', step * mod , len(a3)-1, id)
  60. (start, end) = _calcRange( step * mod ,len(a3)-1, id-mod, step-1)
  61. if start > len(a3)-1:
  62. print(NOTHING_TO_DO)
  63. sys.exit(0)
  64. #print(start, end)
  65. a4= a3[start: end+1]
  66. #print('--->', a4)
  67. return a4
  68. def parseFrames(render_frames, return_type='list', id=None, total_nodes=None):
  69. '''
  70. @param render_frames {string}: 需要渲染的总帧数列表范围,可以用"-"表示范围,不连续的帧可以使用","隔开, 如: 1,3,5-10
  71. @param return_type {string}: 取值范围[list,range]。 list样例: [1,2,3], range样例: (1,3)。
  72. 注意: render_frames包含","时有效,强制为list。
  73. @param id, 节点ID,从0开始。 正式环境不要填写,将从环境变量 BATCH_COMPUTE_DAG_INSTANCE_ID 中取得。
  74. @param total_nodes, 总共的节点个数。正式环境不要填写,将从环境变量 BATCH_COMPUTE_DAG_INSTANCE_COUNT 中取得。
  75. '''
  76. if id==None:
  77. id=os.environ['BATCH_COMPUTE_DAG_INSTANCE_ID']
  78. if type(id)==str:
  79. id = int(id)
  80. if total_nodes==None:
  81. total_nodes = os.environ['BATCH_COMPUTE_DAG_INSTANCE_COUNT']
  82. if type(total_nodes)==str:
  83. total_nodes = int(total_nodes)
  84. if re.match(r'^(\d+)\-(\d+)$',render_frames):
  85. # 1-2
  86. # continued frames
  87. return _parseContinuedFrames(render_frames, total_nodes, id, return_type)
  88. else:
  89. # intermittent frames
  90. return _parseIntermittentFrames(render_frames, total_nodes, id)
  91. if __name__ == "__main__":
  92. parser = argparse.ArgumentParser(
  93. formatter_class = argparse.ArgumentDefaultsHelpFormatter,
  94. description = 'python scripyt for 3dmax dag job',
  95. usage='render3Dmax.py <positional argument> [<args>]',
  96. )
  97. parser.add_argument('-s', '--scene_file', action='store', type=str, required=True, help = 'the name of the file with .max subffix .')
  98. parser.add_argument('-i', '--input', action='store', type=str, required=True, help = 'the oss dir of the scene_file, eg: xxx.max.')
  99. parser.add_argument('-o', '--output', action='store', type=str, required=True, help = 'the oss of dir the result file to upload .')
  100. parser.add_argument('-f', '--frames', action='store', type=str, required=True, help = 'the frames to be renderd, eg: "1-10".')
  101. parser.add_argument('-t', '--retType', action='store', type=str, default="test.jpg", help = 'the tye of the render result,eg. xxx.jpg/xxx.png.')
  102. args = parser.parse_args()
  103. frames=parseFrames(args.frames)
  104. framestr='-'.join(map(lambda x:str(x), frames))
  105. s = "cd \"C:\\Program Files\\Autodesk\\3ds Max 2018\\\" && "
  106. s +='3dsmaxcmd.exe -o="%s%s" -frames=%s "%s\\%s"' % (args.output, args.retType, framestr, args.input, args.scene_file)
  107. print("exec: %s" % s)
  108. rc = os.system(s)
  109. 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等。

work

3. 编写作业提交脚本

test.py

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from batchcompute import Client, ClientError
  4. from batchcompute.resources import (
  5. ClusterDescription, GroupDescription, Configs, Networks, VPC,
  6. JobDescription, TaskDescription, DAG,Mounts,
  7. AutoCluster,Disks,Notification,
  8. )
  9. import time
  10. import argparse
  11. from batchcompute import CN_SHANGHAI as REGION #需要根据 region 做适配
  12. access_key_id = "xxxx" # your access key id
  13. access_key_secret = "xxxx" # your access key secret
  14. instance_type = "ecs.g5.4xlarge" # instance type #需要根据 业务需要 做适配
  15. image_id = "m-xxx"
  16. workossPath = "oss://xxxxx/work/work.py"
  17. client = Client(REGION, access_key_id, access_key_secret)
  18. def getAutoClusterDesc(InstanceCount):
  19. auto_desc = AutoCluster()
  20. auto_desc.ECSImageId = image_id
  21. #任务失败保留环境,程序调试阶段设置。环境保留费用会继续产生请注意及时手动清除环境任务失败保留环境,
  22. # 程序调试阶段设置。环境保留费用会继续产生请注意及时手动清除环境
  23. auto_desc.ReserveOnFail = False
  24. # 实例规格
  25. auto_desc.InstanceType = instance_type
  26. #case3 按量
  27. auto_desc.ResourceType = "OnDemand"
  28. #Configs
  29. configs = Configs()
  30. #Configs.Networks
  31. networks = Networks()
  32. vpc = VPC()
  33. # CidrBlock和VpcId 都传入,必须保证VpcId的CidrBlock 和传入的CidrBlock保持一致
  34. vpc.CidrBlock = '172.26.0.0/16'
  35. # vpc.VpcId = "vpc-8vbfxdyhx9p2flummuwmq"
  36. networks.VPC = vpc
  37. configs.Networks = networks
  38. # 设置系统盘type(cloud_efficiency/cloud_ssd)以及size(单位GB)
  39. configs.add_system_disk(size=40, type_='cloud_efficiency')
  40. #设置数据盘type(必须和系统盘type保持一致) size(单位GB) 挂载点
  41. # case1 linux环境
  42. # configs.add_data_disk(size=40, type_='cloud_efficiency', mount_point='/path/to/mount/')
  43. # 设置节点个数
  44. configs.InstanceCount = InstanceCount
  45. auto_desc.Configs = configs
  46. return auto_desc
  47. def getTaskDesc(inputOssPath, outputossPath, scene_file, frames, retType, clusterId, InstanceCount):
  48. taskDesc = TaskDescription()
  49. timestamp = time.strftime("%Y_%m_%d_%H_%M_%S", time.localtime())
  50. inputLocalPath = "D:"
  51. outputLocalPath = "C:\\\\tmp\\\\" + timestamp + "\\\\"
  52. outputossBase = outputossPath + timestamp + "/"
  53. stdoutOssPath = outputossBase + "stdout/" #your stdout oss path
  54. stderrOssPath = outputossBase + "stderr/" #your stderr oss path
  55. outputossret = outputossBase + "ret/"
  56. taskDesc.InputMapping = {inputOssPath: inputLocalPath}
  57. taskDesc.OutputMapping = {outputLocalPath: outputossret}
  58. taskDesc.Parameters.InputMappingConfig.Lock = True
  59. # 设置程序的标准输出地址,程序中的print打印会实时上传到指定的oss地址
  60. taskDesc.Parameters.StdoutRedirectPath = stdoutOssPath
  61. # 设置程序的标准错误输出地址,程序抛出的异常错误会实时上传到指定的oss地址
  62. taskDesc.Parameters.StderrRedirectPath = stderrOssPath
  63. #触发程序运行的命令行
  64. # PackagePath存放commandLine中的可执行文件或者二进制包
  65. taskDesc.Parameters.Command.PackagePath = workossPath
  66. taskDesc.Parameters.Command.CommandLine = "python work.py -i %s -o %s -s %s -f %s -t %s" % (inputLocalPath, outputLocalPath, scene_file, frames, retType)
  67. # 设置任务的超时时间
  68. taskDesc.Timeout = 86400
  69. # 设置任务所需实例个数
  70. taskDesc.InstanceCount = InstanceCount
  71. # 设置任务失败后重试次数
  72. taskDesc.MaxRetryCount = 3
  73. if clusterId:
  74. # 采用固定集群提交作业
  75. taskDesc.ClusterId = clusterId
  76. else:
  77. #采用auto集群提交作业
  78. taskDesc.AutoCluster = getAutoClusterDesc(InstanceCount)
  79. return taskDesc
  80. def getDagJobDesc(inputOssPath, outputossPath, scene_file, frames, retType, clusterId = None, instanceNum = 1):
  81. job_desc = JobDescription()
  82. dag_desc = DAG()
  83. job_desc.Name = "testBatch"
  84. job_desc.Description = "test 3dMAX job"
  85. job_desc.Priority = 1
  86. # 任务失败
  87. job_desc.JobFailOnInstanceFail = False
  88. # 作业运行成功后户自动会被立即释放掉
  89. job_desc.AutoRelease = False
  90. job_desc.Type = "DAG"
  91. render = getTaskDesc(inputOssPath, outputossPath, scene_file, frames, retType, clusterId, instanceNum)
  92. # 添加任务
  93. dag_desc.add_task('render', render)
  94. job_desc.DAG = dag_desc
  95. return job_desc
  96. if __name__ == "__main__":
  97. parser = argparse.ArgumentParser(
  98. formatter_class = argparse.ArgumentDefaultsHelpFormatter,
  99. description = 'python scripyt for 3dmax dag job',
  100. usage='render3Dmax.py <positional argument> [<args>]',
  101. )
  102. parser.add_argument('-n','--instanceNum', action='store',type = int, default = 1,help = 'the parell instance num .')
  103. parser.add_argument('-s', '--scene_file', action='store', type=str, required=True, help = 'the name of the file with .max subffix .')
  104. parser.add_argument('-i', '--inputoss', action='store', type=str, required=True, help = 'the oss dir of the scene_file, eg: xxx.max.')
  105. parser.add_argument('-o', '--outputoss', action='store', type=str, required=True, help = 'the oss of dir the result file to upload .')
  106. parser.add_argument('-f', '--frames', action='store', type=str, required=True, help = 'the frames to be renderd, eg: "1-10".')
  107. parser.add_argument('-t', '--retType', action='store', type=str, default = "test.jpg", help = 'the tye of the render result,eg. xxx.jpg/xxx.png.')
  108. parser.add_argument('-c', '--clusterId', action='store', type=str, default=None, help = 'the clusterId to be render .')
  109. args = parser.parse_args()
  110. try:
  111. job_desc = getDagJobDesc(args.inputoss, args.outputoss, args.scene_file, args.frames,args.retType, args.clusterId, args.instanceNum)
  112. # print job_desc
  113. job_id = client.create_job(job_desc).Id
  114. print('job created: %s' % job_id)
  115. except ClientError,e:
  116. 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. 提交作业

根据以上示例文档,执行以下命令:

  1. 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

示例运行结果:

restulr

picture