mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
setting up deadline for 3dsmax
This commit is contained in:
parent
9965e6b991
commit
95aff1808f
10 changed files with 565 additions and 2 deletions
|
|
@ -120,3 +120,36 @@ def get_all_children(parent, node_type=None):
|
|||
|
||||
return ([x for x in child_list if rt.superClassOf(x) == node_type]
|
||||
if node_type else child_list)
|
||||
|
||||
|
||||
def get_current_renderer():
|
||||
"""get current renderer"""
|
||||
return rt.renderers.production
|
||||
|
||||
|
||||
def get_default_render_folder(project_setting=None):
|
||||
return (project_setting["max"]
|
||||
["RenderSettings"]
|
||||
["default_render_image_folder"]
|
||||
)
|
||||
|
||||
|
||||
def set_framerange(startFrame, endFrame):
|
||||
"""Get/set the type of time range to be rendered.
|
||||
|
||||
Possible values are:
|
||||
|
||||
1 -Single frame.
|
||||
|
||||
2 -Active time segment ( animationRange ).
|
||||
|
||||
3 -User specified Range.
|
||||
|
||||
4 -User specified Frame pickup string (for example "1,3,5-12").
|
||||
"""
|
||||
# hard-code, there should be a custom setting for this
|
||||
rt.rendTimeType = 4
|
||||
if startFrame is not None and endFrame is not None:
|
||||
frameRange = "{0}-{1}".format(startFrame, endFrame)
|
||||
rt.rendPickupFrames = frameRange
|
||||
|
||||
|
|
|
|||
102
openpype/hosts/max/api/lib_renderproducts.py
Normal file
102
openpype/hosts/max/api/lib_renderproducts.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Render Element Example : For scanline render, VRay
|
||||
# https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-E8F75D47-B998-4800-A3A5-610E22913CFC
|
||||
# arnold
|
||||
# https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html
|
||||
import os
|
||||
from pymxs import runtime as rt
|
||||
from openpype.hosts.max.api.lib import (
|
||||
get_current_renderer,
|
||||
get_default_render_folder
|
||||
)
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
|
||||
class RenderProducts(object):
|
||||
|
||||
@classmethod
|
||||
def __init__(self, project_settings=None):
|
||||
self._project_settings = project_settings
|
||||
if not self._project_settings:
|
||||
self._project_settings = get_project_settings(
|
||||
legacy_io.Session["AVALON_PROJECT"]
|
||||
)
|
||||
|
||||
def render_product(self, container):
|
||||
folder = rt.maxFilePath
|
||||
folder = folder.replace("\\", "/")
|
||||
setting = self._project_settings
|
||||
render_folder = get_default_render_folder(setting)
|
||||
|
||||
output_file = os.path.join(folder, render_folder, container)
|
||||
context = get_current_project_asset()
|
||||
startFrame = context["data"].get("frameStart")
|
||||
endFrame = context["data"].get("frameEnd") + 1
|
||||
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"]
|
||||
full_render_list = self.beauty_render_product(output_file,
|
||||
startFrame,
|
||||
endFrame,
|
||||
img_fmt)
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
|
||||
if renderer == "VUE_File_Renderer":
|
||||
return full_render_list
|
||||
|
||||
if (
|
||||
renderer == "ART_Renderer" or
|
||||
renderer == "Redshift Renderer" or
|
||||
renderer == "V_Ray_6_Hotfix_3" or
|
||||
renderer == "V_Ray_GPU_6_Hotfix_3" or
|
||||
renderer == "Default_Scanline_Renderer" or
|
||||
renderer == "Quicksilver_Hardware_Renderer"
|
||||
):
|
||||
render_elem_list = self.render_elements_product(output_file,
|
||||
startFrame,
|
||||
endFrame,
|
||||
img_fmt)
|
||||
for render_elem in render_elem_list:
|
||||
full_render_list.append(render_elem)
|
||||
return full_render_list
|
||||
|
||||
if renderer == "Arnold":
|
||||
return full_render_list
|
||||
|
||||
|
||||
def beauty_render_product(self, folder, startFrame, endFrame, fmt):
|
||||
# get the beauty
|
||||
beauty_frame_range = list()
|
||||
|
||||
for f in range(startFrame, endFrame):
|
||||
beauty = "{0}.{1}.{2}".format(folder, str(f), fmt)
|
||||
beauty = beauty.replace("\\", "/")
|
||||
beauty_frame_range.append(beauty)
|
||||
|
||||
return beauty_frame_range
|
||||
|
||||
# TODO: Get the arnold render product
|
||||
def render_elements_product(self, folder, startFrame, endFrame, fmt):
|
||||
"""Get all the render element output files. """
|
||||
render_dirname = list()
|
||||
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
# get render elements from the renders
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
target, renderpass = str(renderlayer_name).split(":")
|
||||
|
||||
render_dir = os.path.join(folder, renderpass)
|
||||
if renderlayer_name.enabled:
|
||||
for f in range(startFrame, endFrame):
|
||||
render_element = "{0}.{1}.{2}".format(render_dir, str(f), fmt)
|
||||
render_element = render_element.replace("\\", "/")
|
||||
render_dirname.append(render_element)
|
||||
|
||||
return render_dirname
|
||||
|
||||
def image_format(self):
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"]
|
||||
return img_fmt
|
||||
125
openpype/hosts/max/api/lib_rendersettings.py
Normal file
125
openpype/hosts/max/api/lib_rendersettings.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import os
|
||||
from pymxs import runtime as rt
|
||||
from openpype.lib import Logger
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
|
||||
from openpype.hosts.max.api.lib import (
|
||||
set_framerange,
|
||||
get_current_renderer,
|
||||
get_default_render_folder
|
||||
)
|
||||
|
||||
|
||||
class RenderSettings(object):
|
||||
|
||||
log = Logger.get_logger("RenderSettings")
|
||||
|
||||
_aov_chars = {
|
||||
"dot": ".",
|
||||
"dash": "-",
|
||||
"underscore": "_"
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def __init__(self, project_settings=None):
|
||||
self._project_settings = project_settings
|
||||
if not self._project_settings:
|
||||
self._project_settings = get_project_settings(
|
||||
legacy_io.Session["AVALON_PROJECT"]
|
||||
)
|
||||
|
||||
def set_render_camera(self, selection):
|
||||
for sel in selection:
|
||||
# to avoid Attribute Error from pymxs wrapper
|
||||
found = False
|
||||
if rt.classOf(sel) in rt.Camera.classes:
|
||||
found = True
|
||||
rt.viewport.setCamera(sel)
|
||||
break
|
||||
if not found:
|
||||
raise RuntimeError("Camera not found")
|
||||
|
||||
|
||||
def set_renderoutput(self, container):
|
||||
folder = rt.maxFilePath
|
||||
# hard-coded, should be customized in the setting
|
||||
folder = folder.replace("\\", "/")
|
||||
# hard-coded, set the renderoutput path
|
||||
setting = self._project_settings
|
||||
render_folder = get_default_render_folder(setting)
|
||||
output_dir = os.path.join(folder, render_folder)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
# hard-coded, should be customized in the setting
|
||||
context = get_current_project_asset()
|
||||
|
||||
# get project reoslution
|
||||
width = context["data"].get("resolutionWidth")
|
||||
height = context["data"].get("resolutionHeight")
|
||||
# Set Frame Range
|
||||
startFrame = context["data"].get("frameStart")
|
||||
endFrame = context["data"].get("frameEnd")
|
||||
set_framerange(startFrame, endFrame)
|
||||
# get the production render
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"]
|
||||
output = os.path.join(output_dir, container)
|
||||
try:
|
||||
aov_separator = self._aov_chars[(
|
||||
self._project_settings["maya"]
|
||||
["RenderSettings"]
|
||||
["aov_separator"]
|
||||
)]
|
||||
except KeyError:
|
||||
aov_separator = "."
|
||||
outputFilename = "{0}.{1}".format(output, img_fmt)
|
||||
outputFilename = outputFilename.replace("{aov_separator}", aov_separator)
|
||||
rt.rendOutputFilename = outputFilename
|
||||
if renderer == "VUE_File_Renderer":
|
||||
return
|
||||
# TODO: Finish the arnold render setup
|
||||
if renderer == "Arnold":
|
||||
return
|
||||
|
||||
if (
|
||||
renderer == "ART_Renderer" or
|
||||
renderer == "Redshift Renderer" or
|
||||
renderer == "V_Ray_6_Hotfix_3" or
|
||||
renderer == "V_Ray_GPU_6_Hotfix_3" or
|
||||
renderer == "Default_Scanline_Renderer" or
|
||||
renderer == "Quicksilver_Hardware_Renderer"
|
||||
):
|
||||
self.render_element_layer(output, width, height, img_fmt)
|
||||
|
||||
rt.rendSaveFile= True
|
||||
|
||||
|
||||
def render_element_layer(self, dir, width, height, ext):
|
||||
"""For Renderers with render elements"""
|
||||
rt.renderWidth = width
|
||||
rt.renderHeight = height
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
if render_elem_num < 0:
|
||||
return
|
||||
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
target, renderpass = str(renderlayer_name).split(":")
|
||||
render_element = os.path.join(dir, renderpass)
|
||||
aov_name = "{0}.{1}".format(render_element, ext)
|
||||
try:
|
||||
aov_separator = self._aov_chars[(
|
||||
self._project_settings["maya"]
|
||||
["RenderSettings"]
|
||||
["aov_separator"]
|
||||
)]
|
||||
except KeyError:
|
||||
aov_separator = "."
|
||||
|
||||
aov_name = aov_name.replace("{aov_separator}", aov_separator)
|
||||
render_elem.SetRenderElementFileName(i, aov_name)
|
||||
33
openpype/hosts/max/plugins/create/create_render.py
Normal file
33
openpype/hosts/max/plugins/create/create_render.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating camera."""
|
||||
from openpype.hosts.max.api import plugin
|
||||
from openpype.pipeline import CreatedInstance
|
||||
from openpype.hosts.max.api.lib_rendersettings import RenderSettings
|
||||
|
||||
|
||||
class CreateRender(plugin.MaxCreator):
|
||||
identifier = "io.openpype.creators.max.render"
|
||||
label = "Render"
|
||||
family = "maxrender"
|
||||
icon = "gear"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
from pymxs import runtime as rt
|
||||
sel_obj = list(rt.selection)
|
||||
instance = super(CreateRender, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
container_name = instance.data.get("instance_node")
|
||||
container = rt.getNodeByName(container_name)
|
||||
# TODO: Disable "Add to Containers?" Panel
|
||||
# parent the selected cameras into the container
|
||||
for obj in sel_obj:
|
||||
obj.parent = container
|
||||
# for additional work on the node:
|
||||
# instance_node = rt.getNodeByName(instance.get("instance_node"))
|
||||
|
||||
# set viewport camera for rendering(mandatory for deadline)
|
||||
RenderSettings().set_render_camera(sel_obj)
|
||||
# set output paths for rendering(mandatory for deadline)
|
||||
RenderSettings().set_renderoutput(container_name)
|
||||
72
openpype/hosts/max/plugins/publish/collect_render.py
Normal file
72
openpype/hosts/max/plugins/publish/collect_render.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect Render"""
|
||||
import os
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.hosts.max.api.lib import get_current_renderer
|
||||
from openpype.hosts.max.api.lib_renderproducts import RenderProducts
|
||||
|
||||
|
||||
class CollectRender(pyblish.api.InstancePlugin):
|
||||
"""Collect Render for Deadline"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
label = "Collect 3dmax Render Layers"
|
||||
hosts = ['max']
|
||||
families = ["maxrender"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
folder = rt.maxFilePath
|
||||
file = rt.maxFileName
|
||||
current_file = os.path.join(folder, file)
|
||||
filepath = current_file.replace("\\", "/")
|
||||
|
||||
context.data['currentFile'] = current_file
|
||||
asset = legacy_io.Session["AVALON_ASSET"]
|
||||
|
||||
render_layer_files = RenderProducts().render_product(instance.name)
|
||||
folder = folder.replace("\\", "/")
|
||||
|
||||
imgFormat = RenderProducts().image_format()
|
||||
renderer_class = get_current_renderer()
|
||||
renderer_name = str(renderer_class).split(":")[0]
|
||||
# setup the plugin as 3dsmax for the internal renderer
|
||||
if (
|
||||
renderer_name == "ART_Renderer" or
|
||||
renderer_name == "Default_Scanline_Renderer" or
|
||||
renderer_name == "Quicksilver_Hardware_Renderer"
|
||||
):
|
||||
plugin = "3dsmax"
|
||||
|
||||
if (
|
||||
renderer_name == "V_Ray_6_Hotfix_3" or
|
||||
renderer_name == "V_Ray_GPU_6_Hotfix_3"
|
||||
):
|
||||
plugin = "Vray"
|
||||
|
||||
if renderer_name == "Redshift Renderer":
|
||||
plugin = "redshift"
|
||||
|
||||
if renderer_name == "Arnold":
|
||||
plugin = "arnold"
|
||||
|
||||
# https://forums.autodesk.com/t5/3ds-max-programming/pymxs-quickrender-animation-range/td-p/11216183
|
||||
|
||||
data = {
|
||||
"subset": instance.name,
|
||||
"asset": asset,
|
||||
"publish": True,
|
||||
"imageFormat": imgFormat,
|
||||
"family": 'maxrender',
|
||||
"families": ['maxrender'],
|
||||
"source": filepath,
|
||||
"files": render_layer_files,
|
||||
"plugin": plugin,
|
||||
"frameStart": context.data['frameStart'],
|
||||
"frameEnd": context.data['frameEnd']
|
||||
}
|
||||
self.log.info("data: {0}".format(data))
|
||||
instance.data.update(data)
|
||||
|
|
@ -184,7 +184,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
self.log.info("multipart: {}".format(
|
||||
multipart))
|
||||
assert exp_files, "no file names were generated, this is bug"
|
||||
self.log.info(exp_files)
|
||||
|
||||
# if we want to attach render to subset, check if we have AOV's
|
||||
# in expectedFiles. If so, raise error as we cannot attach AOV
|
||||
|
|
@ -320,7 +319,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
"renderSetupIncludeLights"
|
||||
)
|
||||
}
|
||||
|
||||
# Collect Deadline url if Deadline module is enabled
|
||||
deadline_settings = (
|
||||
context.data["system_settings"]["modules"]["deadline"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
import os
|
||||
import json
|
||||
import getpass
|
||||
|
||||
import requests
|
||||
import pyblish.api
|
||||
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
|
||||
class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
3DMax File Submit Render Deadline
|
||||
|
||||
"""
|
||||
|
||||
label = "Submit 3DsMax Render to Deadline"
|
||||
order = pyblish.api.IntegratorOrder
|
||||
hosts = ["max"]
|
||||
families = ["maxrender"]
|
||||
targets = ["local"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
filepath = context.data["currentFile"]
|
||||
filename = os.path.basename(filepath)
|
||||
comment = context.data.get("comment", "")
|
||||
deadline_user = context.data.get("deadlineUser", getpass.getuser())
|
||||
jobname ="{0} - {1}".format(filename, instance.name)
|
||||
|
||||
# StartFrame to EndFrame
|
||||
frames = "{start}-{end}".format(
|
||||
start=int(instance.data["frameStart"]),
|
||||
end=int(instance.data["frameEnd"])
|
||||
)
|
||||
|
||||
payload = {
|
||||
"JobInfo": {
|
||||
# Top-level group name
|
||||
"BatchName": filename,
|
||||
|
||||
# Job name, as seen in Monitor
|
||||
"Name": jobname,
|
||||
|
||||
# Arbitrary username, for visualisation in Monitor
|
||||
"UserName": deadline_user,
|
||||
|
||||
"Plugin": instance.data["plugin"],
|
||||
"Pool": instance.data.get("primaryPool"),
|
||||
"secondaryPool": instance.data.get("secondaryPool"),
|
||||
"Frames": frames,
|
||||
"ChunkSize" : instance.data.get("chunkSize", 10),
|
||||
"Comment": comment
|
||||
},
|
||||
"PluginInfo": {
|
||||
# Input
|
||||
"SceneFile": instance.data["source"],
|
||||
"Version": "2023",
|
||||
"SaveFile" : True,
|
||||
# Mandatory for Deadline
|
||||
# Houdini version without patch number
|
||||
|
||||
"IgnoreInputs": True
|
||||
},
|
||||
|
||||
# Mandatory for Deadline, may be empty
|
||||
"AuxFiles": []
|
||||
}
|
||||
# Include critical environment variables with submission + api.Session
|
||||
keys = [
|
||||
# Submit along the current Avalon tool setup that we launched
|
||||
# this application with so the Render Slave can build its own
|
||||
# similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9"
|
||||
"AVALON_TOOLS",
|
||||
"OPENPYPE_VERSION"
|
||||
]
|
||||
# Add mongo url if it's enabled
|
||||
if context.data.get("deadlinePassMongoUrl"):
|
||||
keys.append("OPENPYPE_MONGO")
|
||||
|
||||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **legacy_io.Session)
|
||||
|
||||
payload["JobInfo"].update({
|
||||
"EnvironmentKeyValue%d" % index: "{key}={value}".format(
|
||||
key=key,
|
||||
value=environment[key]
|
||||
) for index, key in enumerate(environment)
|
||||
})
|
||||
|
||||
# Include OutputFilename entries
|
||||
# The first entry also enables double-click to preview rendered
|
||||
# frames from Deadline Monitor
|
||||
output_data = {}
|
||||
# need to be fixed
|
||||
for i, filepath in enumerate(instance.data["files"]):
|
||||
dirname = os.path.dirname(filepath)
|
||||
fname = os.path.basename(filepath)
|
||||
output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/")
|
||||
output_data["OutputFilename%d" % i] = fname
|
||||
|
||||
if not os.path.exists(dirname):
|
||||
self.log.info("Ensuring output directory exists: %s" %
|
||||
dirname)
|
||||
os.makedirs(dirname)
|
||||
|
||||
payload["JobInfo"].update(output_data)
|
||||
|
||||
self.submit(instance, payload)
|
||||
|
||||
def submit(self, instance, payload):
|
||||
|
||||
context = instance.context
|
||||
deadline_url = context.data.get("defaultDeadline")
|
||||
deadline_url = instance.data.get(
|
||||
"deadlineUrl", deadline_url)
|
||||
|
||||
assert deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
plugin = payload["JobInfo"]["Plugin"]
|
||||
self.log.info("Using Render Plugin : {}".format(plugin))
|
||||
|
||||
self.log.info("Submitting..")
|
||||
self.log.debug(json.dumps(payload, indent=4, sort_keys=True))
|
||||
|
||||
# E.g. http://192.168.0.1:8082/api/jobs
|
||||
url = "{}/api/jobs".format(deadline_url)
|
||||
response = requests.post(url, json=payload, verify=False)
|
||||
if not response.ok:
|
||||
raise Exception(response.text)
|
||||
# Store output dir for unified publisher (filesequence)
|
||||
expected_files = instance.data["files"]
|
||||
self.log.info("exp:{}".format(expected_files))
|
||||
output_dir = os.path.dirname(expected_files[0])
|
||||
instance.data["outputDir"] = output_dir
|
||||
instance.data["deadlineSubmissionJob"] = response.json()
|
||||
7
openpype/settings/defaults/project_settings/max.json
Normal file
7
openpype/settings/defaults/project_settings/max.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"RenderSettings": {
|
||||
"default_render_image_folder": "renders/max",
|
||||
"aov_separator": "underscore",
|
||||
"image_format": "exr"
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,10 @@
|
|||
"type": "schema",
|
||||
"name": "schema_project_slack"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_project_max"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_project_maya"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "max",
|
||||
"label": "Max",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "RenderSettings",
|
||||
"label": "Render Settings",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "default_render_image_folder",
|
||||
"label": "Default render image folder"
|
||||
},
|
||||
{
|
||||
"key": "aov_separator",
|
||||
"label": "AOV Separator character",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"default": "underscore",
|
||||
"enum_items": [
|
||||
{"dash": "- (dash)"},
|
||||
{"underscore": "_ (underscore)"},
|
||||
{"dot": ". (dot)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "image_format",
|
||||
"label": "Output Image Format",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"defaults": "exr",
|
||||
"enum_items": [
|
||||
{"avi": "avi"},
|
||||
{"bmp": "bmp"},
|
||||
{"exr": "exr"},
|
||||
{"tif": "tif"},
|
||||
{"tiff": "tiff"},
|
||||
{"jpg": "jpg"},
|
||||
{"png": "png"},
|
||||
{"tga": "tga"},
|
||||
{"dds": "dds"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue