Blender V4.5
obj_export_mtl.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
8
9#include "BKE_image.hh"
10#include "BKE_node.hh"
12#include "BKE_node_runtime.hh"
13
14#include "BLI_math_vector.h"
15#include "BLI_path_utils.hh"
16#include "BLI_string.h"
17
18#include "DNA_material_types.h"
19#include "DNA_node_types.h"
20
21#include "obj_export_mtl.hh"
22
23#include "CLG_log.h"
24static CLG_LogRef LOG = {"io.obj"};
25
26namespace blender::io::obj {
27
29 "Base Color",
30 "Metallic",
31 "Specular IOR Level",
32 "Roughness", /* Map specular exponent to roughness. */
33 "Roughness",
34 "Sheen Weight",
35 "Metallic", /* Map reflection to metallic. */
36 "Emission Color",
37 "Alpha",
38 "Normal",
39};
41 "array size mismatch");
42
46static void copy_property_from_node(const eNodeSocketDatatype property_type,
47 const bNode *node,
48 const char *identifier,
49 MutableSpan<float> r_property)
50{
51 if (!node) {
52 return;
53 }
54 const bNodeSocket *socket = bke::node_find_socket(
55 *const_cast<bNode *>(node), SOCK_IN, identifier);
56 BLI_assert(socket && socket->type == property_type);
57 if (!socket) {
58 return;
59 }
60 switch (property_type) {
61 case SOCK_FLOAT: {
62 BLI_assert(r_property.size() == 1);
63 const bNodeSocketValueFloat *socket_def_value = static_cast<const bNodeSocketValueFloat *>(
64 socket->default_value);
65 r_property[0] = socket_def_value->value;
66 break;
67 }
68 case SOCK_RGBA: {
69 BLI_assert(r_property.size() == 3);
70 const bNodeSocketValueRGBA *socket_def_value = static_cast<const bNodeSocketValueRGBA *>(
71 socket->default_value);
72 copy_v3_v3(r_property.data(), socket_def_value->value);
73 break;
74 }
75 case SOCK_VECTOR: {
76 BLI_assert(r_property.size() == 3);
77 const bNodeSocketValueVector *socket_def_value = static_cast<const bNodeSocketValueVector *>(
78 socket->default_value);
79 copy_v3_v3(r_property.data(), socket_def_value->value);
80 break;
81 }
82 default: {
83 /* Other socket types are not handled here. */
84 BLI_assert(0);
85 break;
86 }
87 }
88}
89
93static void linked_sockets_to_dest_id(const bNode *dest_node,
94 const bNodeTree &node_tree,
95 const char *dest_socket_id,
96 Vector<const bNodeSocket *> &r_linked_sockets)
97{
98 r_linked_sockets.clear();
99 if (!dest_node) {
100 return;
101 }
102 Span<const bNode *> object_dest_nodes = node_tree.nodes_by_type(dest_node->idname);
103 Span<const bNodeSocket *> dest_inputs = object_dest_nodes.first()->input_sockets();
104 const bNodeSocket *dest_socket = nullptr;
105 for (const bNodeSocket *curr_socket : dest_inputs) {
106 if (STREQ(curr_socket->identifier, dest_socket_id)) {
107 dest_socket = curr_socket;
108 break;
109 }
110 }
111 if (dest_socket) {
112 Span<const bNodeSocket *> linked_sockets = dest_socket->directly_linked_sockets();
113 r_linked_sockets.resize(linked_sockets.size());
114 r_linked_sockets = linked_sockets;
115 }
116}
117
121static const bNode *get_node_of_type(Span<const bNodeSocket *> sockets_list, const int node_type)
122{
123 for (const bNodeSocket *socket : sockets_list) {
124 const bNode &parent_node = socket->owner_node();
125 if (parent_node.typeinfo->type_legacy == node_type) {
126 return &parent_node;
127 }
128 }
129 return nullptr;
130}
131
132/*
133 * From a texture image shader node, get the image's filepath.
134 * If packed image is found, only the file "name" is returned.
135 */
136static std::string get_image_filepath(const bNode *tex_node)
137{
138 if (!tex_node) {
139 return "";
140 }
141 Image *tex_image = reinterpret_cast<Image *>(tex_node->id);
142 if (!tex_image || !BKE_image_has_filepath(tex_image)) {
143 return "";
144 }
145
146 if (BKE_image_has_packedfile(tex_image)) {
147 /* Put image in the same directory as the `.MTL` file. */
148 const char *filename = BLI_path_basename(tex_image->filepath);
149 CLOG_INFO(&LOG,
150 1,
151 "Packed image found:'%s'. Unpack and place the image in the same "
152 "directory as the .MTL file.",
153 filename);
154 return filename;
155 }
156
157 char filepath[FILE_MAX];
158 STRNCPY(filepath, tex_image->filepath);
159
160 if (tex_image->source == IMA_SRC_SEQUENCE) {
161 char head[FILE_MAX], tail[FILE_MAX];
162 ushort numlen;
163 int framenr = static_cast<NodeTexImage *>(tex_node->storage)->iuser.framenr;
164 BLI_path_sequence_decode(filepath, head, sizeof(head), tail, sizeof(tail), &numlen);
165 BLI_path_sequence_encode(filepath, sizeof(filepath), head, tail, numlen, framenr);
166 }
167
168 return filepath;
169}
170
176static const bNode *find_bsdf_node(const bNodeTree *nodetree)
177{
178 if (!nodetree) {
179 return nullptr;
180 }
181 for (const bNode *node : nodetree->nodes_by_type("ShaderNodeOutputMaterial")) {
182 const bNodeSocket &node_input_socket0 = node->input_socket(0);
183 for (const bNodeSocket *out_sock : node_input_socket0.directly_linked_sockets()) {
184 const bNode &in_node = out_sock->owner_node();
185 if (in_node.typeinfo->type_legacy == SH_NODE_BSDF_PRINCIPLED) {
186 return &in_node;
187 }
188 }
189 }
190 return nullptr;
191}
192
196static void store_bsdf_properties(const bNode *bsdf_node,
197 const Material *material,
198 MTLMaterial &r_mtl_mat)
199{
200 float roughness = material->roughness;
201 if (bsdf_node) {
202 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Roughness", {&roughness, 1});
203 }
204 /* Empirical approximation. Importer should use the inverse of this method. */
205 float spec_exponent = (1.0f - roughness);
206 spec_exponent *= spec_exponent * 1000.0f;
207
208 float specular = material->spec;
209 if (bsdf_node) {
210 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Specular IOR Level", {&specular, 1});
211 }
212
213 float metallic = material->metallic;
214 if (bsdf_node) {
215 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Metallic", {&metallic, 1});
216 }
217
218 float refraction_index = 1.0f;
219 if (bsdf_node) {
220 copy_property_from_node(SOCK_FLOAT, bsdf_node, "IOR", {&refraction_index, 1});
221 }
222
223 float alpha = material->a;
224 if (bsdf_node) {
225 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Alpha", {&alpha, 1});
226 }
227 const bool transparent = alpha != 1.0f;
228
229 float3 diffuse_col = {material->r, material->g, material->b};
230 if (bsdf_node) {
231 copy_property_from_node(SOCK_RGBA, bsdf_node, "Base Color", {diffuse_col, 3});
232 }
233
234 float3 emission_col{0.0f};
235 float emission_strength = 0.0f;
236 if (bsdf_node) {
237 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Emission Strength", {&emission_strength, 1});
238 copy_property_from_node(SOCK_RGBA, bsdf_node, "Emission Color", {emission_col, 3});
239 }
240 mul_v3_fl(emission_col, emission_strength);
241
242 float sheen = -1.0f;
243 float coat = -1.0f;
244 float coat_roughness = -1.0f;
245 float aniso = -1.0f;
246 float aniso_rot = -1.0f;
247 float transmission = -1.0f;
248 if (bsdf_node) {
249 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Sheen Weight", {&sheen, 1});
250 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Coat Weight", {&coat, 1});
251 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Coat Roughness", {&coat_roughness, 1});
252 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Anisotropic", {&aniso, 1});
253 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Anisotropic Rotation", {&aniso_rot, 1});
254 copy_property_from_node(SOCK_FLOAT, bsdf_node, "Transmission Weight", {&transmission, 1});
255
256 /* Clearcoat used to include an implicit 0.25 factor, so stay compatible to old versions. */
257 coat *= 4.0f;
258 }
259
260 /* See https://wikipedia.org/wiki/Wavefront_.obj_file for all possible values of `illum`. */
261 /* Highlight on. */
262 int illum = 2;
263 if (specular == 0.0f) {
264 /* Color on and Ambient on. */
265 illum = 1;
266 }
267 else if (metallic > 0.0f) {
268 /* Metallic ~= Reflection. */
269 if (transparent) {
270 /* Transparency: Refraction on, Reflection: ~~Fresnel off and Ray trace~~ on. */
271 illum = 6;
272 }
273 else {
274 /* Reflection on and Ray trace on. */
275 illum = 3;
276 }
277 }
278 else if (transparent) {
279 /* Transparency: Glass on, Reflection: Ray trace off */
280 illum = 9;
281 }
282 r_mtl_mat.spec_exponent = spec_exponent;
283 if (metallic != 0.0f) {
284 r_mtl_mat.ambient_color = {metallic, metallic, metallic};
285 }
286 else {
287 r_mtl_mat.ambient_color = {1.0f, 1.0f, 1.0f};
288 }
289 r_mtl_mat.color = diffuse_col;
290 r_mtl_mat.spec_color = {specular, specular, specular};
291 r_mtl_mat.emission_color = emission_col;
292 r_mtl_mat.ior = refraction_index;
293 r_mtl_mat.alpha = alpha;
294 r_mtl_mat.illum_mode = illum;
295 r_mtl_mat.roughness = roughness;
296 r_mtl_mat.metallic = metallic;
297 r_mtl_mat.sheen = sheen;
298 r_mtl_mat.cc_thickness = coat;
299 r_mtl_mat.cc_roughness = coat_roughness;
300 r_mtl_mat.aniso = aniso;
301 r_mtl_mat.aniso_rot = aniso_rot;
302 r_mtl_mat.transmit_color = {transmission, transmission, transmission};
303}
304
308static void store_image_textures(const bNode *bsdf_node,
309 const bNodeTree *node_tree,
310 const Material *material,
311 MTLMaterial &r_mtl_mat)
312{
313 if (!material || !node_tree || !bsdf_node) {
314 /* No nodetree, no images, or no Principled BSDF node. */
315 return;
316 }
317
318 /* Normal Map Texture has two extra tasks of:
319 * - finding a Normal Map node before finding a texture node.
320 * - finding "Strength" property of the node for `-bm` option.
321 */
322
323 for (int key = 0; key < int(MTLTexMapType::Count); ++key) {
324 MTLTexMap &value = r_mtl_mat.texture_maps[key];
325 Vector<const bNodeSocket *> linked_sockets;
326 const bNode *normal_map_node{nullptr};
327
328 if (key == int(MTLTexMapType::Normal)) {
329 /* Find sockets linked to destination "Normal" socket in P-BSDF node. */
330 linked_sockets_to_dest_id(bsdf_node, *node_tree, "Normal", linked_sockets);
331 /* Among the linked sockets, find Normal Map shader node. */
332 normal_map_node = get_node_of_type(linked_sockets, SH_NODE_NORMAL_MAP);
333
334 /* Find sockets linked to "Color" socket in normal map node. */
335 linked_sockets_to_dest_id(normal_map_node, *node_tree, "Color", linked_sockets);
336 }
337 else {
338 /* Skip emission map if emission strength is zero. */
339 if (key == int(MTLTexMapType::Emission)) {
340 float emission_strength = 0.0f;
342 SOCK_FLOAT, bsdf_node, "Emission Strength", {&emission_strength, 1});
343 if (emission_strength == 0.0f) {
344 continue;
345 }
346 }
347 /* Find sockets linked to the destination socket of interest, in P-BSDF node. */
349 bsdf_node, *node_tree, tex_map_type_to_socket_id[key], linked_sockets);
350 }
351
352 /* Among the linked sockets, find Image Texture shader node. */
353 const bNode *tex_node{get_node_of_type(linked_sockets, SH_NODE_TEX_IMAGE)};
354 if (!tex_node) {
355 continue;
356 }
357 const std::string tex_image_filepath = get_image_filepath(tex_node);
358 if (tex_image_filepath.empty()) {
359 continue;
360 }
361
362 /* Find "Mapping" node if connected to texture node. */
363 linked_sockets_to_dest_id(tex_node, *node_tree, "Vector", linked_sockets);
364 const bNode *mapping = get_node_of_type(linked_sockets, SH_NODE_MAPPING);
365
366 if (normal_map_node) {
368 SOCK_FLOAT, normal_map_node, "Strength", {&r_mtl_mat.normal_strength, 1});
369 }
370 /* Texture transform options. Only translation (origin offset, "-o") and scale
371 * ("-o") are supported. */
372 copy_property_from_node(SOCK_VECTOR, mapping, "Location", {value.translation, 3});
373 copy_property_from_node(SOCK_VECTOR, mapping, "Scale", {value.scale, 3});
374
375 value.image_path = tex_image_filepath;
376 }
377}
378
380{
381 BLI_assert(material != nullptr);
382 MTLMaterial mtlmat;
383 mtlmat.name = std::string(material->id.name + 2);
384 std::replace(mtlmat.name.begin(), mtlmat.name.end(), ' ', '_');
385 const bNodeTree *nodetree = material->nodetree;
386 if (nodetree != nullptr) {
387 nodetree->ensure_topology_cache();
388 }
389
390 const bNode *bsdf_node = find_bsdf_node(nodetree);
391 store_bsdf_properties(bsdf_node, material, mtlmat);
392 store_image_textures(bsdf_node, nodetree, material, mtlmat);
393 return mtlmat;
394}
395
396} // namespace blender::io::obj
bool BKE_image_has_filepath(const Image *ima)
bool BKE_image_has_packedfile(const Image *image)
#define SH_NODE_TEX_IMAGE
#define SH_NODE_BSDF_PRINCIPLED
#define SH_NODE_NORMAL_MAP
#define SH_NODE_MAPPING
#define BLI_STATIC_ASSERT(a, msg)
Definition BLI_assert.h:83
#define BLI_assert(a)
Definition BLI_assert.h:46
MINLINE void mul_v3_fl(float r[3], float f)
MINLINE void copy_v3_v3(float r[3], const float a[3])
void void void const char * BLI_path_basename(const char *path) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT
#define FILE_MAX
int BLI_path_sequence_decode(const char *path, char *head, size_t head_maxncpy, char *tail, size_t tail_maxncpy, unsigned short *r_digits_len)
Definition path_utils.cc:57
void BLI_path_sequence_encode(char *path, size_t path_maxncpy, const char *head, const char *tail, unsigned short numlen, int pic)
char * STRNCPY(char(&dst)[N], const char *src)
Definition BLI_string.h:688
unsigned short ushort
#define ARRAY_SIZE(arr)
#define STREQ(a, b)
#define CLOG_INFO(clg_ref, level,...)
Definition CLG_log.h:179
@ IMA_SRC_SEQUENCE
@ SOCK_IN
eNodeSocketDatatype
@ SOCK_VECTOR
@ SOCK_FLOAT
@ SOCK_RGBA
constexpr int64_t size() const
Definition BLI_span.hh:493
constexpr T * data() const
Definition BLI_span.hh:539
constexpr const T & first() const
Definition BLI_span.hh:315
constexpr int64_t size() const
Definition BLI_span.hh:252
void resize(const int64_t new_size)
#define LOG(severity)
Definition log.h:32
bNodeSocket * node_find_socket(bNode &node, eNodeSocketInOut in_out, StringRef identifier)
Definition node.cc:2864
const char * tex_map_type_to_socket_id[]
static const bNode * get_node_of_type(Span< const bNodeSocket * > sockets_list, const int node_type)
static std::string get_image_filepath(const bNode *tex_node)
static const bNode * find_bsdf_node(const bNodeTree *nodetree)
static void store_bsdf_properties(const bNode *bsdf_node, const Material *material, MTLMaterial &r_mtl_mat)
MTLMaterial mtlmaterial_for_material(const Material *material)
static void copy_property_from_node(const eNodeSocketDatatype property_type, const bNode *node, const char *identifier, MutableSpan< float > r_property)
static void store_image_textures(const bNode *bsdf_node, const bNodeTree *node_tree, const Material *material, MTLMaterial &r_mtl_mat)
static void linked_sockets_to_dest_id(const bNode *dest_node, const bNodeTree &node_tree, const char *dest_socket_id, Vector< const bNodeSocket * > &r_linked_sockets)
VecBase< float, 3 > float3
closure color sheen(normal N, float roughness) BUILTIN
char name[66]
Definition DNA_ID.h:415
char filepath[1024]
short source
struct bNodeTree * nodetree
void * default_value
bNodeTypeHandle * typeinfo
struct ID * id
void * storage
char idname[64]
MTLTexMap texture_maps[int(MTLTexMapType::Count)]