obj: vertex colors support in importer and exporter

Adds support for vertex colors to OBJ I/O.

Importer:

- Supports both "xyzrgb" and "MRGB" vertex color formats.
- Whenever vertex color is present in the file for a model, it is
  imported and a Color attribute is created (per-vertex, full float
  color data type). Color coming from the file is assumed to be sRGB,
  and is converted to linear upon import.

Exporter:

- Option to export the vertex colors. Defaults to "off", since not
  all 3rd party software supports vertex colors.
- When the option is "on", if a mesh has a color attribute layer,
  the active one is exported in "xyzrgb" form. If the mesh has
  per-face-corner colors, they are averaged on the vertices.
  Colors are converted from linear to sRGB upon export.

Reviewed By: Howard Trickey
Differential Revision: https://developer.blender.org/D15159
This commit is contained in:
Aras Pranckevicius 2022-06-14 10:19:02 +03:00
parent 827fa81767
commit 1b4f35f6a5
14 changed files with 263 additions and 10 deletions

View File

@ -100,6 +100,7 @@ static int wm_obj_export_exec(bContext *C, wmOperator *op)
export_params.export_selected_objects = RNA_boolean_get(op->ptr, "export_selected_objects");
export_params.export_uv = RNA_boolean_get(op->ptr, "export_uv");
export_params.export_normals = RNA_boolean_get(op->ptr, "export_normals");
export_params.export_colors = RNA_boolean_get(op->ptr, "export_colors");
export_params.export_materials = RNA_boolean_get(op->ptr, "export_materials");
export_params.path_mode = RNA_enum_get(op->ptr, "path_mode");
export_params.export_triangulated_mesh = RNA_boolean_get(op->ptr, "export_triangulated_mesh");
@ -160,6 +161,7 @@ static void ui_obj_export_settings(uiLayout *layout, PointerRNA *imfptr)
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Export"));
uiItemR(sub, imfptr, "export_uv", 0, IFACE_("UV Coordinates"), ICON_NONE);
uiItemR(sub, imfptr, "export_normals", 0, IFACE_("Normals"), ICON_NONE);
uiItemR(sub, imfptr, "export_colors", 0, IFACE_("Colors"), ICON_NONE);
uiItemR(sub, imfptr, "export_materials", 0, IFACE_("Materials"), ICON_NONE);
uiItemR(sub, imfptr, "export_triangulated_mesh", 0, IFACE_("Triangulated Mesh"), ICON_NONE);
uiItemR(sub, imfptr, "export_curves_as_nurbs", 0, IFACE_("Curves as NURBS"), ICON_NONE);
@ -315,6 +317,7 @@ void WM_OT_obj_export(struct wmOperatorType *ot)
"Export Normals",
"Export per-face normals if the face is flat-shaded, per-face-per-loop "
"normals if smooth-shaded");
RNA_def_boolean(ot->srna, "export_colors", false, "Export Colors", "Export per-vertex colors");
RNA_def_boolean(ot->srna,
"export_materials",
true,

View File

@ -45,6 +45,7 @@ struct OBJExportParams {
eEvaluationMode export_eval_mode;
bool export_uv;
bool export_normals;
bool export_colors;
bool export_materials;
bool export_triangulated_mesh;
bool export_curves_as_nurbs;

View File

@ -8,7 +8,9 @@
#include <cstdio>
#include "BKE_blender_version.h"
#include "BKE_geometry_set.hh"
#include "BLI_color.hh"
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_path_util.h"
#include "BLI_task.hh"
@ -241,13 +243,38 @@ void obj_parallel_chunked_output(FormatHandler<eFileType::OBJ> &fh,
}
void OBJWriter::write_vertex_coords(FormatHandler<eFileType::OBJ> &fh,
const OBJMesh &obj_mesh_data) const
const OBJMesh &obj_mesh_data,
bool write_colors) const
{
const int tot_count = obj_mesh_data.tot_vertices();
obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) {
float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor);
buf.write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]);
});
Mesh *mesh = obj_mesh_data.get_mesh();
CustomDataLayer *colors_layer = nullptr;
if (write_colors) {
colors_layer = BKE_id_attributes_active_color_get(&mesh->id);
}
if (write_colors && (colors_layer != nullptr)) {
MeshComponent component;
component.replace(mesh, GeometryOwnershipType::ReadOnly);
VArray<ColorGeometry4f> attribute = component.attribute_get_for_read<ColorGeometry4f>(
colors_layer->name, ATTR_DOMAIN_POINT, {0.0f, 0.0f, 0.0f, 0.0f});
BLI_assert(tot_count == attribute.size());
obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) {
float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor);
ColorGeometry4f linear = attribute.get(i);
float srgb[3];
linearrgb_to_srgb_v3_v3(srgb, linear);
buf.write<eOBJSyntaxElement::vertex_coords_color>(
vertex[0], vertex[1], vertex[2], srgb[0], srgb[1], srgb[2]);
});
}
else {
obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) {
float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor);
buf.write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]);
});
}
}
void OBJWriter::write_uv_coords(FormatHandler<eFileType::OBJ> &fh, OBJMesh &r_obj_mesh_data) const

View File

@ -72,9 +72,11 @@ class OBJWriter : NonMovable, NonCopyable {
*/
void write_mtllib_name(const StringRefNull mtl_filepath) const;
/**
* Write vertex coordinates for all vertices as "v x y z".
* Write vertex coordinates for all vertices as "v x y z" or "v x y z r g b".
*/
void write_vertex_coords(FormatHandler<eFileType::OBJ> &fh, const OBJMesh &obj_mesh_data) const;
void write_vertex_coords(FormatHandler<eFileType::OBJ> &fh,
const OBJMesh &obj_mesh_data,
bool write_colors) const;
/**
* Write UV vertex coordinates for all vertices as `vt u v`.
* \note UV indices are stored here, but written with polygons later.

View File

@ -30,6 +30,7 @@ enum class eFileType {
enum class eOBJSyntaxElement {
vertex_coords,
vertex_coords_color,
uv_vertex_coords,
normal,
poly_element_begin,
@ -130,6 +131,9 @@ constexpr FormattingSyntax syntax_elem_to_formatting(const eOBJSyntaxElement key
case eOBJSyntaxElement::vertex_coords: {
return {"v {:.6f} {:.6f} {:.6f}\n", 3, is_type_float<T...>};
}
case eOBJSyntaxElement::vertex_coords_color: {
return {"v {:.6f} {:.6f} {:.6f} {:.6f} {:.6f} {:.6f}\n", 6, is_type_float<T...>};
}
case eOBJSyntaxElement::uv_vertex_coords: {
return {"vt {:.6f} {:.6f}\n", 2, is_type_float<T...>};
}

View File

@ -241,6 +241,11 @@ class OBJMesh : NonCopyable {
return i < 0 || i >= poly_order_.size() ? i : poly_order_[i];
}
Mesh *get_mesh() const
{
return export_mesh_eval_;
}
private:
/**
* Free the mesh if _the exporter_ created it.

View File

@ -195,7 +195,7 @@ static void write_mesh_objects(Vector<std::unique_ptr<OBJMesh>> exportable_as_me
auto &fh = buffers[i];
obj_writer.write_object_name(fh, obj);
obj_writer.write_vertex_coords(fh, obj);
obj_writer.write_vertex_coords(fh, obj, export_params.export_colors);
if (obj.tot_polygons() > 0) {
if (export_params.export_smooth_groups) {

View File

@ -5,12 +5,15 @@
*/
#include "BLI_map.hh"
#include "BLI_math_color.h"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "obj_import_file_reader.hh"
#include "obj_import_string_utils.hh"
#include <charconv>
namespace blender::io::obj {
using std::string;
@ -34,6 +37,7 @@ static Geometry *create_geometry(Geometry *const prev_geometry,
g->geom_type_ = new_type;
g->geometry_name_ = name.is_empty() ? "New object" : name;
g->vertex_start_ = global_vertices.vertices.size();
g->vertex_color_start_ = global_vertices.vertex_colors.size();
r_offset.set_index_offset(g->vertex_start_);
return g;
};
@ -71,9 +75,51 @@ static void geom_add_vertex(Geometry *geom,
GlobalVertices &r_global_vertices)
{
float3 vert;
parse_floats(p, end, 0.0f, vert, 3);
p = parse_floats(p, end, 0.0f, vert, 3);
r_global_vertices.vertices.append(vert);
geom->vertex_count_++;
/* OBJ extension: "xyzrgb" vertex colors, when the vertex position
* is followed by 3 more RGB color components. See
* http://paulbourke.net/dataformats/obj/colour.html */
if (p < end) {
float3 srgb;
p = parse_floats(p, end, -1.0f, srgb, 3);
if (srgb.x >= 0 && srgb.y >= 0 && srgb.z >= 0) {
float3 linear;
srgb_to_linearrgb_v3_v3(linear, srgb);
r_global_vertices.vertex_colors.append(linear);
geom->vertex_color_count_++;
}
}
}
static void geom_add_mrgb_colors(Geometry *geom,
const char *p,
const char *end,
GlobalVertices &r_global_vertices)
{
/* MRGB color extension, in the form of
* "#MRGB MMRRGGBBMMRRGGBB ..."
* http://paulbourke.net/dataformats/obj/colour.html */
p = drop_whitespace(p, end);
const int mrgb_length = 8;
while (p + mrgb_length <= end) {
uint32_t value = 0;
std::from_chars_result res = std::from_chars(p, p + mrgb_length, value, 16);
if (res.ec == std::errc::invalid_argument || res.ec == std::errc::result_out_of_range) {
return;
}
unsigned char srgb[4];
srgb[0] = (value >> 16) & 0xFF;
srgb[1] = (value >> 8) & 0xFF;
srgb[2] = value & 0xFF;
srgb[3] = 0xFF;
float linear[4];
srgb_to_linearrgb_uchar4(linear, srgb);
r_global_vertices.vertex_colors.append({linear[0], linear[1], linear[2]});
geom->vertex_color_count_++;
p += mrgb_length;
}
}
static void geom_add_vertex_normal(Geometry *geom,
@ -482,6 +528,9 @@ void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
else if (parse_keyword(p, end, "mtllib")) {
add_mtl_library(StringRef(p, end).trim());
}
else if (parse_keyword(p, end, "#MRGB")) {
geom_add_mrgb_colors(curr_geom, p, end, r_global_vertices);
}
/* Comments. */
else if (*p == '#') {
/* Nothing to do. */

View File

@ -8,6 +8,7 @@
#include "DNA_mesh_types.h"
#include "DNA_scene_types.h"
#include "BKE_attribute.h"
#include "BKE_customdata.h"
#include "BKE_material.h"
#include "BKE_mesh.h"
@ -50,6 +51,7 @@ Object *MeshFromGeometry::create_mesh(Main *bmain,
create_edges(mesh);
create_uv_verts(mesh);
create_normals(mesh);
create_colors(mesh);
create_materials(bmain, materials, created_materials, obj);
if (import_params.validate_meshes || mesh_geometry_.has_invalid_polys_) {
@ -345,4 +347,26 @@ void MeshFromGeometry::create_normals(Mesh *mesh)
MEM_freeN(loop_normals);
}
void MeshFromGeometry::create_colors(Mesh *mesh)
{
/* Nothing to do if we don't have vertex colors. */
if (mesh_geometry_.vertex_color_count_ < 1) {
return;
}
if (mesh_geometry_.vertex_color_count_ != mesh_geometry_.vertex_count_) {
std::cerr << "Mismatching number of vertices (" << mesh_geometry_.vertex_count_
<< ") and colors (" << mesh_geometry_.vertex_color_count_ << ") on object '"
<< mesh_geometry_.geometry_name_ << "', ignoring colors." << std::endl;
return;
}
CustomDataLayer *color_layer = BKE_id_attribute_new(
&mesh->id, "Color", CD_PROP_COLOR, ATTR_DOMAIN_POINT, nullptr);
float4 *colors = (float4 *)color_layer->data;
for (int i = 0; i < mesh_geometry_.vertex_color_count_; ++i) {
float3 c = global_vertices_.vertex_colors[mesh_geometry_.vertex_color_start_ + i];
colors[i] = float4(c.x, c.y, c.z, 1.0f);
}
}
} // namespace blender::io::obj

View File

@ -65,6 +65,7 @@ class MeshFromGeometry : NonMovable, NonCopyable {
Map<std::string, Material *> &created_materials,
Object *obj);
void create_normals(Mesh *mesh);
void create_colors(Mesh *mesh);
};
} // namespace blender::io::obj

View File

@ -26,6 +26,7 @@ struct GlobalVertices {
Vector<float3> vertices;
Vector<float2> uv_vertices;
Vector<float3> vertex_normals;
Vector<float3> vertex_colors;
};
/**
@ -102,6 +103,8 @@ struct Geometry {
int vertex_start_ = 0;
int vertex_count_ = 0;
int vertex_color_start_ = 0;
int vertex_color_count_ = 0;
/** Edges written in the file in addition to (or even without polygon) elements. */
Vector<MEdge> edges_;

View File

@ -436,6 +436,19 @@ TEST_F(obj_exporter_regression_test, cubes_positioned)
_export.params);
}
TEST_F(obj_exporter_regression_test, cubes_vertex_colors)
{
OBJExportParamsDefault _export;
_export.params.export_colors = true;
_export.params.export_normals = false;
_export.params.export_uv = false;
_export.params.export_materials = false;
compare_obj_export_to_golden("io_tests/blend_geometry/cubes_vertex_colors.blend",
"io_tests/obj/cubes_vertex_colors.obj",
"",
_export.params);
}
TEST_F(obj_exporter_regression_test, cubes_with_textures_strip)
{
OBJExportParamsDefault _export;
@ -494,6 +507,7 @@ TEST_F(obj_exporter_regression_test, all_objects)
_export.params.forward_axis = IO_AXIS_Y;
_export.params.up_axis = IO_AXIS_Z;
_export.params.export_smooth_groups = true;
_export.params.export_colors = true;
compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend",
"io_tests/obj/all_objects.obj",
"io_tests/obj/all_objects.mtl",

View File

@ -26,6 +26,7 @@ struct OBJExportParamsDefault {
params.export_selected_objects = false;
params.export_uv = true;
params.export_normals = true;
params.export_colors = false;
params.export_materials = true;
params.path_mode = PATH_REFERENCE_AUTO;
params.export_triangulated_mesh = false;

View File

@ -39,6 +39,7 @@ struct Expectation {
float3 vert_first, vert_last;
float3 normal_first;
float2 uv_first;
float4 color_first = {-1, -1, -1, -1};
};
class obj_importer_test : public BlendfileLoadingBaseTest {
@ -98,6 +99,15 @@ class obj_importer_test : public BlendfileLoadingBaseTest {
CustomData_get_layer(&mesh->ldata, CD_MLOOPUV));
float2 uv_first = mloopuv ? float2(mloopuv->uv) : float2(0, 0);
EXPECT_V2_NEAR(uv_first, exp.uv_first, 0.0001f);
if (exp.color_first.x >= 0) {
const float4 *colors = (const float4 *)(CustomData_get_layer(&mesh->vdata,
CD_PROP_COLOR));
EXPECT_TRUE(colors != nullptr);
EXPECT_V4_NEAR(colors[0], exp.color_first, 0.0001f);
}
else {
EXPECT_FALSE(CustomData_has_layer(&mesh->vdata, CD_PROP_COLOR));
}
}
if (object->type == OB_CURVES_LEGACY) {
Curve *curve = static_cast<Curve *>(DEG_get_evaluated_object(depsgraph, object)->data);
@ -434,7 +444,17 @@ TEST_F(obj_importer_test, import_all_objects)
float3(16, 1, -1),
float3(14, 1, 1),
float3(0, 0, 1)},
{"OBVColCube", OB_MESH, 8, 13, 7, 26, float3(13, 1, -1), float3(11, 1, 1), float3(0, 0, 1)},
{"OBVColCube",
OB_MESH,
8,
13,
7,
26,
float3(13, 1, -1),
float3(11, 1, 1),
float3(0, 0, 1),
float2(0, 0),
float4(0.0f, 0.002125f, 1.0f, 1.0f)},
{"OBUVCube",
OB_MESH,
8,
@ -490,4 +510,103 @@ TEST_F(obj_importer_test, import_all_objects)
import_and_check("all_objects.obj", expect, std::size(expect), 7);
}
TEST_F(obj_importer_test, import_cubes_vertex_colors)
{
Expectation expect[] = {
{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBCubeVertexByte",
OB_MESH,
8,
12,
6,
24,
float3(1.0f, 1.0f, -1.0f),
float3(-1.0f, -1.0f, 1.0f),
float3(0, 0, 0),
float2(0, 0),
float4(0.846873f, 0.027321f, 0.982251f, 1.0f)},
{"OBCubeVertexFloat",
OB_MESH,
8,
12,
6,
24,
float3(3.392028f, 1.0f, -1.0f),
float3(1.392028f, -1.0f, 1.0f),
float3(0, 0, 0),
float2(0, 0),
float4(49.99558f, 0.027321f, 0.982251f, 1.0f)},
{"OBCubeCornerByte",
OB_MESH,
8,
12,
6,
24,
float3(1.0f, 1.0f, -3.812445f),
float3(-1.0f, -1.0f, -1.812445f),
float3(0, 0, 0),
float2(0, 0),
float4(0.89627f, 0.036889f, 0.47932f, 1.0f)},
{"OBCubeCornerFloat",
OB_MESH,
8,
12,
6,
24,
float3(3.481967f, 1.0f, -3.812445f),
float3(1.481967f, -1.0f, -1.812445f),
float3(0, 0, 0),
float2(0, 0),
float4(1.564582f, 0.039217f, 0.664309f, 1.0f)},
{"OBCubeMultiColorAttribs",
OB_MESH,
8,
12,
6,
24,
float3(-4.725068f, -1.0f, 1.0f),
float3(-2.725068f, 1.0f, -1.0f),
float3(0, 0, 0),
float2(0, 0),
float4(0.270498f, 0.47932f, 0.262251f, 1.0f)},
{"OBCubeNoColors",
OB_MESH,
8,
12,
6,
24,
float3(-4.550208f, -1.0f, -1.918042f),
float3(-2.550208f, 1.0f, -3.918042f)},
};
import_and_check("cubes_vertex_colors.obj", expect, std::size(expect), 0);
}
TEST_F(obj_importer_test, import_cubes_vertex_colors_mrgb)
{
Expectation expect[] = {{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
{"OBCubeXYZRGB",
OB_MESH,
8,
12,
6,
24,
float3(1, 1, -1),
float3(-1, -1, 1),
float3(0, 0, 0),
float2(0, 0),
float4(0.6038f, 0.3185f, 0.1329f, 1.0f)},
{"OBCubeMRGB",
OB_MESH,
8,
12,
6,
24,
float3(4, 1, -1),
float3(2, -1, 1),
float3(0, 0, 0),
float2(0, 0),
float4(0.8714f, 0.6308f, 0.5271f, 1.0f)}};
import_and_check("cubes_vertex_colors_mrgb.obj", expect, std::size(expect), 0);
}
} // namespace blender::io::obj