当前位置:主页 > 查看内容

3ds Max DAG作业最佳实践 - 批量计算

发布时间:2021-09-19 00:00| 位朋友查看

简介:1. 准备工作 1.1. 选择区域 所有阿里云服务都需要使用相同的地域。 1.2. 开通服务 开通批量计算服务(BatchCompute) ; 开通对象存储服务(OSS) 。 1.3. 制作镜像 制作镜像具体步骤请参考 集群镜像 , 请严格按文档的步骤创建镜像。镜像制作完成后,通过以……

1. 准备工作

1.1. 选择区域

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

1.2. 开通服务

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


本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:Blender渲染App最佳实践 - 批量计算 下一篇:没有了

推荐图文


随机推荐