Как я написал конвертер 3D-моделей из подручных средств

от автора

Всем привет! Меня зовут Шико, я работаю в Яндекс Маркете в команде Android-разработки. Сегодня я расскажу историю, которая случилась в 2021 году. Как-то раз перед еженедельным синком я увидел вопрос в рабочем чате: «Кто хочет поучаствовать в проекте связанным с 3D?» А я, пока учился в университете, занимался 3D-моделированием. Тогда для меня это было просто хобби, но я решил вспомнить, каково это, и предложил свою кандидатуру.

Суть задачи была в следующем: нужно было добавить в мобильное приложение AR (то есть, дополненную реальность). Оно нужно, чтобы товар с Маркета можно было «примерить» в интерьер. Например, оно полезно, когда вы хотите купить телевизор, но вам сложно представить, будет ли он гармонировать с мебелью и влезет ли он вообще в имеющееся пространство. 

На iOS к проекту подключился один разработчик, а на Android нас было двое. Сначала я расстроился: показалось, что ничего особо интересного не будет — всего-то подключить ARCore и делов. Но это ровно до тех пор, пока не выяснилось, что большинство файлов моделек было в USDZ-формате, а ArCore на тот момент с ним не работал. То есть, когда на iOS в процессе разработки таких проблем не возникало, нам нужно было придумать способ перевести существующие модельки в другой формат — GLB. Казалось бы, скачай конвертер и нажми на кнопку «Конвертировать». Не тут-то было. 

И в этой статье я расскажу, какие методы конвертации я пробовал, почему они не подошли и с чем не смогли справиться Blender и Unreal Engine. Спойлер: в итоге мне пришлось написать собственный плагин и я покажу его код.

Пробы и ошибки

Поиск существующего решения

Чтобы решить эту интересную задачку, я попробовал несколько способов решить её малой кровью. Двигался я по такому списку:

  • онлайн-конвертеры;

  • Blender;

  • другие 3D-редакторы;

  • Unreal Engine.

Онлайн-конвертеры оказались слишком слабыми: они отваливались через 5—10 минут обработки файла. Просто выдавали ошибку.

Для Blender уже был написан плагин, который позволял импортировать USDZ. К сожалению, на большинстве файлов он не работал и падал из-за ошибок в коде. Я пробовал дебажить код и разбираться, в чём же дело. Но было очень много разных ошибок, и я забросил эту идею.

Поиски других 3D-редакторов, которые поддерживали бы формат USDZ, не увенчались успехом. На страницах 3Ds Max и Maya нашёл инфу, что можно подключить плагин, который находится в альфе или бете, что тоже мне не подходило. Другие редакторы уже не припомню. При этом всём я разрабатывал на Linux, соответственно, многие платные редакторы тоже отсеивались.

Unreal Engine

В какой-то момент я решил попробовать Unreal Engine. Оказалось, из коробки он поддерживает импорт USDZ (встроенный плагин в бета-версии), только работает он весьма специфично.

Собственно, первая более-менее успешная попытка конвертировать файл вышла по следующему алгоритму:

  1. Импорт файла USDZ в UnrealEngine.

  1. Экспорт в .obj формат.

  1. Попытка применить текстуры методом тыка в Blender.

Чем этот способ не подходил: Unreal Engine довольно тяжёлый, он требует много ресурсов, и в нём неудобно импортировать/экспортировать и редактировать. При этом на некоторых модельках он неправильно импортировал нормали полигонов или UV-координаты, что приводило к странным артефактам.

Озадаченный Геральт

Озадаченный Геральт
Загадочный трицератопс

Загадочный трицератопс

В общем, я столкнулся с тем, что в свободном доступе нет ни одного конвертера, который смог бы перенести модельки из USDZ в любой другой формат.

Снова Blender

Параллельно со всем этим я решил всё-таки ещё раз покопать тот плагин к Blender. Я нашёл несколько ошибок в коде. Получилось пофиксить все краши (или почти все), но только я так и не смог пофиксить ошибку при чтении некоторых данных — на многих модельках неправильно считывалась матрица трансформации.

Попробуйте угадать, что это?

Это XBox One

Это XBox One
А это не первая ревизия PlayStation 5

А это не первая ревизия PlayStation 5

Я начал прочёсывать GitHub и вышел на библиотеку Pixar для работы с USDZ. Оказалось, они предоставили все исходники для работы с этим форматом. На всякий попробовал собрать их. 

И вот тут мне улыбнулась удача. Оказалось, среди библиотек есть несколько интересных утилит. Среди них меня особенно заинтересовал USDView. Она позволяет просмотреть визуально модельку с дополнительной информацией по ней.

Полученную информацию я решил использовать для дебага плагина к Blender. Спустя пару вечеров, проведённых в USDView, питоновском дебаггере и Blender, я выяснил, что автор библиотеки самостоятельно написал код для парсинга бинарного файла. На каком-то из шагов по непонятной мне ошибке (потратил почти 4 дня на эти трансформации!) этот код иногда пропускает одну из множества матриц трансформаций, что и приводит к покорёженным результатам. 

Руками править модельки нереально, учитывая сжатые сроки. Фикс этой проблемы мною был оценен на уровне «вроде изян» по

Код
# Точка входа в конвертер def try_to_convert(input_file, blend_file, output_file, debug_file=None):     # Ищем утилиту usdcat в окружении     usd_cat = find_exe('usdcat')     # Создаём пустую сцену в blender     create_empty_scene()     # Запускаем usdcat, скармливаем ему модельку, считываем результат     file_data = read_file(usd_cat, input_file)     # Придётся распаковать файл, чтобы была возможность импортировать текстуры в Blender     extracted_path = extract_file(input_file)     # На всякий случай сохраняем полученные данные, чтобы была возможность просто и быстро дебажить, если что-то пойдёт не так     save_data(file_data, debug_file)     # Парсим полученное текстовое представление файла     parsed_data = parse_data(file_data) # Нам интересно вот это     # По распаршенным данным создаём модельку     import_scene_data(parsed_data, extracted_path) # и это     # Сохраняем полученную сцену для дальнейшего анализа ошибок     save_scene(blend_file)     # Экспортируем в glb     export_scene(output_file)     # Очищаем ресурсы     clean(extracted_path)

Начнём с парсинга. В текстовом представлении USDC/USDZ/USDA выглядит следующим образом:

Код
#usda 1.0 (     customLayerData = {         string creator = "usdzconvert preview 0.62"     }     defaultPrim = "modul_01"     metersPerUnit = 1     upAxis = "Y" )  def Xform "modul_01" (     assetInfo = {         string name = "modul_01"     }     kind = "component" ) def Scope "Materials"     {         def Material "DVP_komod_2_Letta_Malta_modul_01"         {             token outputs:surface.connect = </modul_01/Materials/DVP_komod_2_Letta_Malta_modul_01/surfaceShader.outputs:surface>              def Shader "surfaceShader"             {                 uniform token info:id = "UsdPreviewSurface"                 color3f inputs:diffuseColor.connect = </modul_01/Materials/DVP_komod_2_Letta_Malta_modul_01/diffuseColor_texture.outputs:rgb>                 float inputs:roughness = 1                 token outputs:surface             }             ...     }     ... def Scope "Geom"     {         def Mesh "DVP_komod_2_Letta_Malta_modul_01"         {             uniform bool doubleSided = 1             int[] faceVertexCounts = [3, 3,...]             rel material:binding = </modul_01/Materials/DVP_komod_2_Letta_Malta_modul_01>             point3f[] points = [(-0.34295827, 0.2727909, -0.1854379), (0.3439359, 0.27179047, -0.1864381), ...]             normal3f[] primvars:normals = [(2.7196302e-7, 2.3841858e-7, 1), (1, -0.0000013478456, -1.2789769e-13), ...]             texCoord2f[] primvars:st = [(0.6426813, 0.66292274), (0.8511379, 0.32041615), ...]             uniform token subdivisionScheme = "none"             quatf xformOp:orient = (1, 0, 0, 0)             float3 xformOp:scale = (1, 1, 1)             double3 xformOp:translate = (0, 0, 0)             uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]         }         ...     }

На что я обратил внимание. Практически во всех файлах всегда были:

  • Заголовок с описанием сцены (масштабы, верхняя ось и другие данные).

  • Xform — структура, которая может содержать другие xform, инфу о геометрии и материалах, а также информацию о матрице трансформации.

  • Scope — по сути, это разные xform, объединённые по разному признаку (например, геометрия или материалы).

  • Mesh — информация о геометрии модели (все вершины, грани, полигоны и прочее), также может содержать информацию о трансформации.

  • Material — информация о материале, набор шейдеров разного типа.

Парсер из себя представляет обычный конечный автомат с большим количеством переходов. Так как задача подразумевала разово преобразовать пару десятков или сотен моделей, в красоту и удобство кода я не вкладывался совсем. Алгоритм простой: проходим по всем строкам, на начало каждой. Если совпадает с константой — переходим в нужное состояние или считываем нужный тип данных, если нет — крашим парсинг с указанием неизвестной структуры.

Список констант, который пришлось обработать:

Код
class ParseConstants:     usda_desc = '#usda'     up_axis = 'upAxis'     meters_per_unit = 'metersPerUnit'     scope = 'def Scope '      mesh = 'def Mesh '     xform = 'def Xform '     subset = 'def GeomSubset '     material = 'def Material '     shader = 'def Shader'      # operations     matrix4d = 'matrix4d'     op_orient = 'quatf xformOp:orient'     op_scale = 'float3 xformOp:scale'     op_translate = 'double3 xformOp:translate'     op_transform = 'matrix4d xformOp:transform'     op_order = 'uniform token[] xformOpOrder'     ops_map = {         'xformOp:translate': Operation.TRANSLATE,         'xformOp:orient': Operation.ORIENT,         'xformOp:scale': Operation.SCALE,         'xformOp:transform': Operation.TRANSFORM     }      double_sided = 'uniform bool doubleSided'     face_vertex_count = 'int[] faceVertexCounts'     face_vertex_index = 'int[] faceVertexIndices'     material_binding = 'rel material:binding'     points = 'point3f[] points'     interpolation = 'interpolation'     normals_indices = 'int[] primvars:normals:indices'     normals = 'normal3f[] normals'     normals_primvars = 'normal3f[] primvars:normals'     int_primvars = 'int[] primvars:'     subdivision_scheme = 'uniform token subdivisionScheme'     extent = 'float3[] extent'     text_coord = 'texCoord2f[] '      element_type = 'uniform token elementType = "face"'     family_name = 'uniform token familyName = "materialBind"'     indices = 'int[] indices'      # materials     token = 'token'     metallic = 'metallic'     roughness = 'roughness'     emissive_color = 'emissiveColor'     normal = 'normal'     occlusion = 'occlusion'     diffuse_color = 'diffuseColor'     opacity = 'opacity'     specular_color = 'specularColor'     use_specular_workflow = 'useSpecularWorkflow'     varname = 'varname'     file = 'file'     st = 'st'     default = 'default'     bias = 'bias'     scale = 'scale'     ior = 'ior'     displacement = 'displacement'     clearcoat = 'clearcoat'     clearcoat_roughness = 'clearcoatRoughness'     opacity_threshold = 'opacityThreshold'     wrap_s = 'wrapS'     wrap_t = 'wrapT'     st_primvar_name = 'stPrimvarName'     surface = 'surface'     result = 'result'     rgb = 'rgb'     r = 'r'     b = 'b'     g = 'g'     a = 'a'     connect = '.connect'      type_to_mat_type = {         'uniform token': InfoType.UNIFORM_TOKEN,         'float': InfoType.FLOAT,         'color3f': InfoType.COLOR3F,         'normal3f': InfoType.NORMAL3F,         'int': InfoType.INT,         'float2': InfoType.FLOAT2,         'float3': InfoType.FLOAT3,         'float4': InfoType.FLOAT4,         'token': InfoType.TOKEN,         'asset': InfoType.ASSET,     }      desc_to_direction = {         'inputs': InfoDirection.INPUT,         'outputs': InfoDirection.OUTPUT,     }      token_id_to_shader_type = {         'UsdPreviewSurface': ShaderTokenType.PREVIEW_SURFACE,         'UsdPrimvarReader_float2': ShaderTokenType.UV_COORDINATES,         'UsdUVTexture': ShaderTokenType.UV_TEXTURE,     }

Пример кода чтения материала (примерно то же самое для других структур, только больше строк):

Код
def read_material(data, counter):     current_line = data[counter.line]     name = read_name(current_line)     shaders = []     outputs = []     while True:         counter.inc()         current_line = data[counter.line]         if current_line.startswith(ParseConstants.token):             outputs.append(read_mat_info(current_line))         elif current_line.startswith(ParseConstants.shader):             shaders.append(read_shader(data, counter))         elif current_line == '{':             continue         elif current_line == '}':             break         else:             raise_parse_error(current_line)     return Material(         name=name,         shaders=shaders,         outputs=outputs,     )

После парсинга получаем структуру, в которой есть заголовок, набор материалов и набор геометрии. Преобразуем их в 3D-сцену.

Основные операции в Blender выполняются через объект bpy. Позабавило, что некоторые операции выглядят как описание действий непосредственно в редакторе. Например, вот удаление объекта:

def remove_mat_dummy():     bpy.ops.object.select_all(action='DESELECT') # Снимаем выделение со всех объектов, чтобы ненароком не удалить ничего лишнего      bpy.data.objects['materials_dummy'].select_set(True) # Выделяем нужный нам объект     bpy.ops.object.delete() # Вызываем операцию удаления

Рекурсивно проходимся по всем xform и отрисовываем их контент. При этом не забываем посчитать матрицу трансформации — это произведение матрицы дочернего объекта на матрицу родительского.

Код
def import_xform(xform, materials, parent_matrix, parent=None):     matrix = xform.matrix4d * parent_matrix     if len(xform.meshes) == 0:         if len(xform.children) == 0:             return         obj = create_empty_object(xform.name)         add_to_default_collection(obj)         if parent is not None:             obj.parent = parent         import_xforms(xform.children, materials, matrix, obj)     else:         # todo read uvs         for mesh in xform.meshes:             obj = create_mesh_object(mesh.name)             add_to_default_collection(obj)             add_mesh_data(obj, mesh, materials, matrix)             if parent is not None:                 obj.parent = parent             import_xforms(xform.children, materials, matrix, obj)

Самые сложные части в конвертации: импорт геометрии и импорт материалов.

Импорт геометрии

Работа с геометрией была во многом подсмотрена в плагине к Blender, на который я ссылался выше.

Код
def add_mesh_data(obj, data, materials, parent_matrix):     # Для каждой текстуры создаём слот для uv-координат     for name, coordinates in data.text_coordinates.items():         obj.data.uv_layers.new(name=name)     # Считаем матрицу трансформации     matrix = data.transform * parent_matrix     counts = data.face_vertex_count     indices = data.face_vertex_indices     verts = data.points          faces = []     smooth = []     index = 0     for count in counts:         # Записываем полигоны. По сути полигоны состоят из двух массивов: массив координат точек и массив, описывающий соединения этих точек. Здесь описываем соединения.          faces.append(tuple([indices[index + i] for i in range(count)]))         if len(normals) > 0:             smooth.append(len(set(normals[index + i] for i in range(count))) > 1)         else:             smooth.append(True)         index += count     bm = bmesh.new()     bm.from_mesh(obj.data)     # Сохраняем вершины     v_base = len(bm.verts)     for vert in verts:         bm.verts.new(vert)     bm.verts.ensure_lookup_table()      # Применяем материалы     main_mat_index = 0     if data.material is not None:         main_mat_index = add_material_to_obj(obj, data.material, materials)      mat_indices = [main_mat_index for _ in range(len(faces))]     # Некоторые полигоны могут отличаться от основного материала, здесь это учитываем     for s in data.subsets:         if s.indices is not None and s.material is not None:             index = add_material_to_obj(obj, s.material, materials)             for i in s.indices:                 mat_indices[i] = index      # Add the Faces     for i, face in enumerate(faces):         if len(face) == len(set(face)):             f = bm.faces.new((bm.verts[i + v_base] for i in face))             f.material_index = mat_indices[i]             f.smooth = smooth[i]      # Сохраняем uv-координаты     for name, coordinates in data.text_coordinates.items():         uv_indices = data.indices[name] if name in data.indices else data.face_vertex_indices         mapped_uv = [coordinates[i] for i in uv_indices]          obj.data.uv_layers.new(name=name)         uv_index = bm.loops.layers.uv[name]         index = 0         for f in bm.faces[-len(faces):]:             for i, l in enumerate(f.loops):                 if index + i < len(mapped_uv):                     l[uv_index].uv = mapped_uv[index + i]                 else:                     l[uv_index].uv = (0.0, 0.0)             index += len(f.loops)      bm.to_mesh(obj.data)     bm.free()      # Применяем матрицу трансформации     obj.data.transform(matrix=tuple(x for x in matrix.tolist()))     obj.data.update()     mat_indices.clear()

Импорт материалов 

Здесь я использовал материал BSDF_PRINCIPLED, для которого мог задать в качестве ввода следующие параметры:

  • Base Color,

  • Specular,

  • Metallic,

  • Roughness,

  • Clearcoat,

  • Clearcoat Roughness,

  • Emissive,

  • IOR,

  • Opacity,

  • Normal.

Как это могло выглядеть в сцене (скрин с новой версии blender, но суть та же):

Отрывки из кода
def create_material(material, ext_dir):     mat = bpy.data.materials.new(material.name) # Создаём материал     mat.use_nodes = True # Используем систему нодов     for shader in material.shaders:         import_shader(mat, material.shaders, shader, ext_dir) # Импортируем каждый шейдер     return mat   def import_shader(blend_material, shaders, shader, ext_dir):     bsdf_node = get_node_by_type(blend_material, 'BSDF_PRINCIPLED')      # Шейдеры бывают трёх типов:     # PREVIEW_SURFACE — число или вектор;     # UV_TEXTURE — текстура или картинка;     # UV_COORDINATES — uv-координаты.     if shader.token_id == ShaderTokenType.PREVIEW_SURFACE:         import_base_preview_surface(blend_material, shaders, shader, bsdf_node, ext_dir)     elif shader.token_id == ShaderTokenType.UV_TEXTURE:         import_base_uv_texture(blend_material, shader, shaders, ext_dir)     elif shader.token_id == ShaderTokenType.UV_COORDINATES:         import_uv_coordinates(blend_material, shader)   def import_base_preview_surface(blend_material, shaders, shader, bsdf_node, ext_dir):     bsdf_node.name = shader.name     set_node_input(         blend_material=blend_material,         shaders=shaders,         node=bsdf_node,         input_desc=shader.diffuse_color,         desc='Color',         node_input_name='Base Color',         input_type=InfoType.COLOR3F,         ext_dir=ext_dir,     )     set_node_input(         blend_material=blend_material,         shaders=shaders,         node=bsdf_node,         input_desc=shader.specular,         desc='Specular',         node_input_name='Specular',         input_type=InfoType.COLOR3F,         ext_dir=ext_dir,     )     # Далее проброс данных для других примитивных данных     #...   def set_node_input(blend_material, shaders, node, input_desc, desc, node_input_name, input_type, ext_dir):     if input_desc is not None:         if input_desc.info_type != input_type:             raise_import_error('%s type is %s instead of %s' % (desc, input_desc.info_type, input_type))          # Если в описании есть connection — это, вероятно, текстура (в чём была разница между текстурой и UV-текстурой? уже не помню). Соответственно, нужно создать ноду текстуры и подключить вывод ноды текстуры с правильным вводом текущей ноды.         if input_desc.is_connection:             set_shader_input_texture(                 blend_material=blend_material,                 shaders=shaders,                 node=node,                 input_name=node_input_name,                 input_desc=input_desc,                 ext_dir=ext_dir,             )         else:             # Для цвета, числа или вектора всё проще: можно задать значение в самом вводе ноды, не заморачиваясь с созданием дополнительной             if input_desc.info_type == InfoType.COLOR3F:                 set_shader_input_value(node, node_input_name, ast.literal_eval(input_desc.value) + (1,))             elif input_desc.info_type in (InfoType.FLOAT, InfoType.NORMAL3F):                 set_shader_input_value(node, node_input_name, ast.literal_eval(input_desc.value))             else:                 # При получении неизвестного типа данных падаем и разбираемся, какой ещё тип данных мы пропустили                 raise_import_error('Unsupported data type')

Что в итоге

Конвертер не был завершён. Я остановился на отметке 80-90% сконвертированных файлов — этого вполне хватило для фичи. 

Но на старте мы решили запустить ещё одну идею. Быстро сгенерировать руками модели тысяч телевизоров, стиралок, холодильников, мебели и прочих предметов невозможно, а фичу хочется максимально заиспользовать. Было принято решение нагенерировать «коробок» с определёнными размерами и показывать их в качестве заглушки, чтобы пользователь мог «примерить» товар у себя дома хотя бы по габаритам.

Я сделал это с помощью USDA-файла. А именно создал файл-шаблон, в котором уже описана вся геометрия и материалы, только вместо позиций вершин стоят заглушки.

Код
#usda 1.0 (     customLayerData = {         string creator = "Yandex.Market. All rights reserved 2021"     }     defaultPrim = "Box"     upAxis = "Y"     metersPerUnit = 1 )  def Xform "Box" (     assetInfo = {         string name = "Box"     }     kind = "component" ) {     def Scope "Geom" {         def Xform "Root"         {             def Mesh "Cube"             {                 uniform bool doubleSided = 1                 int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]                 int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 7, 6, 6, 7, 5, 4, 4, 5, 1, 0, 2, 6, 4, 0, 7, 3, 1, 5]                 point3f[] points = [(-width, 0, 0), (-width, 0, length), (-width, height, 0), (-width, height, length), (width, 0, 0), (width, 0, length), (width, height, 0), (width, height, length)]                 normal3f[] primvars:normals = [(-1, 0, 0), (0, 1, 0), (1, 0, 0), (0, 0, 0), (0, 0, -1), (0, 0, 1)] (                     interpolation = "faceVarying"                 )                 int[] primvars:normals:indices = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5]                 uniform token subdivisionScheme = "none"                 rel material:binding = </Box/Materials/BoxMaterial>             }              def Mesh "Plane1"             {                 uniform bool doubleSided = 0                 float3[] extent = [(-0.5, 0, 0), (0.5, 1, 0.0001)]                 int[] faceVertexCounts = [3, 3]                 int[] faceVertexIndices = [0, 1, 3, 0, 3, 2]                 rel material:binding = </Box/Materials/LogoMaterial>                 normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)]                 point3f[] points = [(-plane1Left, plane1Bot, plane1Depth), (plane1Right, plane1Bot, plane1Depth), (-plane1Left, plane1Top, plane1Depth), (plane1Right, plane1Top, plane1Depth)]                 texCoord2f[] primvars:st = [(0, 0), (1, 0), (0, 1), (1, 1)] (                     interpolation = "vertex"                 )                 uniform token subdivisionScheme = "none"             }              def Mesh "Plane2"             {                 uniform bool doubleSided = 0                 float3[] extent = [(-0.5, 0, 0), (0.5, 1, 0.0001)]                 int[] faceVertexCounts = [3, 3]                 int[] faceVertexIndices = [0, 1, 3, 0, 3, 2]                 rel material:binding = </Box/Materials/MLetterMaterial>                 normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)]                 point3f[] points = [(-plane2Left, plane2Bot, plane2Depth), (plane2Right, plane2Bot, plane2Depth), (-plane2Left, plane2Top, plane2Depth), (plane2Right, plane2Top, plane2Depth)]                 texCoord2f[] primvars:st = [(0, 0), (1, 0), (0, 1), (1, 1)] (                     interpolation = "vertex"                 )                 uniform token subdivisionScheme = "none"             }              def Mesh "Plane3"             {                 uniform bool doubleSided = 0                 float3[] extent = [(-0.5, 0, 0), (0.5, 1, 0)]                 int[] faceVertexCounts = [3, 3]                 int[] faceVertexIndices = [0, 1, 3, 0, 3, 2]                 rel material:binding = </Box/Materials/MTailMaterial>                 normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)]                 point3f[] points = [(plane3Width, plane3Bot, plane3DepthClose), (plane3Width, plane3Bot, plane3DepthFar), (plane3Width, plane3Top, plane3DepthClose), (plane3Width, plane3Top, plane3DepthFar)]                 texCoord2f[] primvars:st = [(0, 0), (tex3, 0), (0, 1), (tex3, 1)] (                     interpolation = "vertex"                 )                 uniform token subdivisionScheme = "none"             }              def Mesh "Plane4"             {                 uniform bool doubleSided = 0                 float3[] extent = [(-0.5, 0, 0), (0.5, 1, 0)]                 int[] faceVertexCounts = [3, 3]                 int[] faceVertexIndices = [0, 1, 3, 0, 3, 2]                 rel material:binding = </Box/Materials/PromoMaterial>                 normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)]                 point3f[] points = [(-plane4Left, plane4Bot, plane4Depth), (plane4Right, plane4Bot, plane4Depth), (-plane4Left, plane4Top, plane4Depth), (plane4Right, plane4Top, plane4Depth)]                 texCoord2f[] primvars:st = [(0, 0), (1, 0), (0, 1), (1, 1)] (                     interpolation = "vertex"                 )                 uniform token subdivisionScheme = "none"             }         }     }      def Scope "Materials"     {         def Material "MTailMaterial"         {             token outputs:surface.connect = </Box/Materials/MTailMaterial/surfaceShader.outputs:surface>              def Shader "surfaceShader"             {                 uniform token info:id = "UsdPreviewSurface"                 color3f inputs:diffuseColor.connect = </Box/Materials/MTailMaterial/diffuseColor_opacity_texture.outputs:rgb>                 color3f inputs:emissiveColor = (0, 0, 0)                 float inputs:metallic = 0                 normal3f inputs:normal = (0, 0, 1)                 float inputs:occlusion = 1                 float inputs:opacity.connect = </Box/Materials/MTailMaterial/diffuseColor_opacity_texture.outputs:a>                 float inputs:roughness = 0.5                 token outputs:surface                 float inputs:opacityThreshold = 0.5             }              def Shader "st_texCoordReader"             {                 uniform token info:id = "UsdPrimvarReader_float2"                 token inputs:varname = "st"                 float2 outputs:result             }              def Shader "diffuseColor_opacity_texture"             {                 uniform token info:id = "UsdUVTexture"                 asset inputs:file = @textures/m_tail.png@                 float2 inputs:st.connect = </Box/Materials/MTailMaterial/st_texCoordReader.outputs:result>                 token inputs:wrapS = "clamp"                 token inputs:wrapT = "clamp"                 float3 outputs:rgb                 float outputs:a             }         }          def Material "MLetterMaterial"         {             token outputs:surface.connect = </Box/Materials/MLetterMaterial/surfaceShader.outputs:surface>              def Shader "surfaceShader"             {                 uniform token info:id = "UsdPreviewSurface"                 color3f inputs:diffuseColor.connect = </Box/Materials/MLetterMaterial/diffuseColor_opacity_texture.outputs:rgb>                 color3f inputs:emissiveColor = (0, 0, 0)                 float inputs:metallic = 0                 normal3f inputs:normal = (0, 0, 1)                 float inputs:occlusion = 1                 float inputs:opacity.connect = </Box/Materials/MLetterMaterial/diffuseColor_opacity_texture.outputs:a>                 float inputs:roughness = 1.0                 token outputs:surface                 float inputs:opacityThreshold = 0.5             }              def Shader "st_texCoordReader"             {                 uniform token info:id = "UsdPrimvarReader_float2"                 token inputs:varname = "st"                 float2 outputs:result             }              def Shader "diffuseColor_opacity_texture"             {                 uniform token info:id = "UsdUVTexture"                 asset inputs:file = @textures/m_letter.png@                 float2 inputs:st.connect = </Box/Materials/MLetterMaterial/st_texCoordReader.outputs:result>                 token inputs:wrapS = "clamp"                 token inputs:wrapT = "clamp"                 float3 outputs:rgb                 float outputs:a             }         }          def Material "LogoMaterial"         {             token outputs:surface.connect = </Box/Materials/LogoMaterial/surfaceShader.outputs:surface>              def Shader "surfaceShader"             {                 uniform token info:id = "UsdPreviewSurface"                 color3f inputs:diffuseColor.connect = </Box/Materials/LogoMaterial/diffuseColor_opacity_texture.outputs:rgb>                 color3f inputs:emissiveColor = (0, 0, 0)                 float inputs:metallic = 0                 normal3f inputs:normal = (1, 1, 1)                 float inputs:opacity.connect = </Box/Materials/LogoMaterial/diffuseColor_opacity_texture.outputs:a>                 float inputs:roughness = 1.0                 token outputs:surface                 float inputs:opacityThreshold = 0.5             }              def Shader "st_texCoordReader"             {                 uniform token info:id = "UsdPrimvarReader_float2"                 token inputs:varname = "st"                 float2 outputs:result             }              def Shader "diffuseColor_opacity_texture"             {                 uniform token info:id = "UsdUVTexture"                 asset inputs:file = @textures/plane1Name@                 float2 inputs:st.connect = </Box/Materials/LogoMaterial/st_texCoordReader.outputs:result>                 token inputs:wrapS = "clamp"                 token inputs:wrapT = "clamp"                 float3 outputs:rgb                 float outputs:a             }         }          def Material "PromoMaterial"         {             token outputs:surface.connect = </Box/Materials/PromoMaterial/surfaceShader.outputs:surface>              def Shader "surfaceShader"             {                 uniform token info:id = "UsdPreviewSurface"                 color3f inputs:diffuseColor.connect = </Box/Materials/PromoMaterial/diffuseColor_opacity_texture.outputs:rgb>                 color3f inputs:emissiveColor = (0, 0, 0)                 float inputs:metallic = 0                 normal3f inputs:normal = (0, 0, 1)                 float inputs:occlusion = 1                 float inputs:opacity.connect = </Box/Materials/PromoMaterial/diffuseColor_opacity_texture.outputs:a>                 float inputs:roughness = 1.0                 token outputs:surface                 float inputs:opacityThreshold = 0.5             }              def Shader "st_texCoordReader"             {                 uniform token info:id = "UsdPrimvarReader_float2"                 token inputs:varname = "st"                 float2 outputs:result             }              def Shader "diffuseColor_opacity_texture"             {                 uniform token info:id = "UsdUVTexture"                 asset inputs:file = @textures/promo.png@                 float2 inputs:st.connect = </Box/Materials/PromoMaterial/st_texCoordReader.outputs:result>                 token inputs:wrapS = "clamp"                 token inputs:wrapT = "clamp"                 float3 outputs:rgb                 float outputs:a             }         }          def Material "BoxMaterial"         {             token outputs:surface.connect = </Box/Materials/BoxMaterial/pbr.outputs:surface>              def Shader "pbr"             {                 token info:id = "UsdPreviewSurface"                 color3f inputs:diffuseColor = (boxRed, boxGreen, boxBlue)                 color3f inputs:emissiveColor = (0.01, 0.01, 0.01)                 float inputs:metallic = 0.4                 normal3f inputs:normal = (1, 1, 1)                 float inputs:occlusion = 1                 float inputs:opacity = boxOpacity                 float inputs:roughness = 0.3                 token outputs:surface                 float inputs:opacityThreshold = 0.2             }         }     } }

А дальше всё просто:

  • копируем файл и текстуры в отдельную папку;

  • скриптом на Python считаем правильные позиции вершин;

  • подменяем в скопированном файле заглушки и подменяем их на рассчитанные позиции и другие параметры;

  • архивируем и меняем расширение файла.

И вот такую модельку мы получаем в итоге:

Имея базу данных размеров холодильников, телевизоров и прочих крупногабаритных товаров, можно за пару минут сгенерировать несколько тысяч легковесных моделей.

Заключение

Фича успешно работает: при желании, можно найти товары с 3D-моделями и «примерить» у себя дома. Тем более, вы теперь знаете, сколько всего стоит за каждой моделькой.

Спустя примерно месяц после доработок, я узнал, что в Blender версии 3.0.1 в экспериментальном режиме реализовали импорт USDZ. Из интереса поставил beta версию, чтобы проверить — материалы они так и не импортировали! Так что, по сути, моя неидеальная версия конвертера на тот момент оказалась чуточку продвинутей. Сейчас последняя версия Blender умеет и в материалы.

Написать конвертер оказалось проще, чем казалось на первый взгляд. Это был очень интересный опыт — тот редкий случай, когда чем сложнее задача, тем интереснее её решать. Для себя я пришёл к таким выводам:

  • Не бойтесь сложных задач, они позволяют получить уникальный опыт и позволяют устроить стресс-тест имеющимся навыкам и знаниям.

  • Изучайте подручные инструменты, зачастую они могут чуть больше, чем кажется. Если бы у меня не получилось окольными путями сконвертировать хотя бы одну модельку, я бы быстро перегорел и ушел с проекта.

  • Внимательно изучайте инструменты, которые относятся к предметной области. Я случайно наткнулся на библиотеку Pixar, которая помогла мне продвинуться в решении задачи. Без удобных инструментов отладки из этой библиотеки я бы не смог закончить задачу в срок.


ссылка на оригинал статьи https://habr.com/ru/companies/yandex/articles/743496/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *