Skip to content

Commit 689b1ad

Browse files
syoyoclaude
andcommitted
Fix vertex color parsing: use xyzw,rgb for 7 args and xyz,rgb for 6
Previously, both opt_parseLine and the fast path stored extra[0] (the weight) as color R when 3+ extra components were present. For v x y z w r g b (7 total), color was incorrectly (w,r,g) instead of (r,g,b), losing the blue channel entirely. Now correctly handles: - v x y z (3) — no weight, no color - v x y z w (4) — weight=w, no color - v x y z r g b (6) — weight=r (legacy compat), color=(r,g,b) - v x y z w r g b (7) — weight=w, color=(r,g,b) Fixed in both opt_parseLine (slow path) and opt_parseLineToThreadData (fast path). Added test verifying 6 and 7 component parsing for both LoadObjOpt and LoadObjOptTyped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6df5849 commit 689b1ad

2 files changed

Lines changed: 131 additions & 20 deletions

File tree

tests/tester.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4027,6 +4027,78 @@ void test_loadobjopt_typed_move_semantics() {
40274027
TEST_CHECK(result2.attrib.vertices.size() == 9);
40284028
}
40294029

4030+
void test_loadobjopt_typed_vertex_color_6_and_7() {
4031+
// 6-component: v x y z r g b — color=(r,g,b), weight=r (legacy compat)
4032+
// 7-component: v x y z w r g b — weight=w, color=(r,g,b)
4033+
{
4034+
const char *obj6 =
4035+
"v 1.0 2.0 3.0 0.2 0.4 0.6\n"
4036+
"v 4.0 5.0 6.0 0.2 0.4 0.6\n"
4037+
"v 7.0 8.0 9.0 0.2 0.4 0.6\n"
4038+
"f 1 2 3\n";
4039+
std::string w, e;
4040+
tinyobj::OptLoadConfig cfg;
4041+
cfg.num_threads = 1;
4042+
cfg.triangulate = true;
4043+
tinyobj::OptResult r = tinyobj::LoadObjOptTyped(obj6, strlen(obj6), &w, &e, cfg);
4044+
TEST_CHECK(r.valid);
4045+
TEST_CHECK(r.attrib.vertices.size() == 9);
4046+
// Color should be (0.2, 0.4, 0.6)
4047+
TEST_CHECK(!r.attrib.colors.empty());
4048+
TEST_CHECK(std::abs(r.attrib.colors[0] - 0.2f) < 1e-5f);
4049+
TEST_CHECK(std::abs(r.attrib.colors[1] - 0.4f) < 1e-5f);
4050+
TEST_CHECK(std::abs(r.attrib.colors[2] - 0.6f) < 1e-5f);
4051+
// Weight should be r (= 0.2)
4052+
TEST_CHECK(!r.attrib.vertex_weights.empty());
4053+
TEST_CHECK(std::abs(r.attrib.vertex_weights[0] - 0.2f) < 1e-5f);
4054+
}
4055+
// 7-component: v x y z w r g b
4056+
{
4057+
const char *obj7 =
4058+
"v 1.0 2.0 3.0 0.5 0.1 0.2 0.3\n"
4059+
"v 4.0 5.0 6.0 0.5 0.1 0.2 0.3\n"
4060+
"v 7.0 8.0 9.0 0.5 0.1 0.2 0.3\n"
4061+
"f 1 2 3\n";
4062+
std::string w, e;
4063+
tinyobj::OptLoadConfig cfg;
4064+
cfg.num_threads = 1;
4065+
cfg.triangulate = true;
4066+
tinyobj::OptResult r = tinyobj::LoadObjOptTyped(obj7, strlen(obj7), &w, &e, cfg);
4067+
TEST_CHECK(r.valid);
4068+
// Weight should be 0.5
4069+
TEST_CHECK(!r.attrib.vertex_weights.empty());
4070+
TEST_CHECK(std::abs(r.attrib.vertex_weights[0] - 0.5f) < 1e-5f);
4071+
// Color should be (0.1, 0.2, 0.3) — NOT (0.5, 0.1, 0.2)
4072+
TEST_CHECK(!r.attrib.colors.empty());
4073+
TEST_CHECK(std::abs(r.attrib.colors[0] - 0.1f) < 1e-5f);
4074+
TEST_CHECK(std::abs(r.attrib.colors[1] - 0.2f) < 1e-5f);
4075+
TEST_CHECK(std::abs(r.attrib.colors[2] - 0.3f) < 1e-5f);
4076+
}
4077+
// Also verify LoadObjOpt produces same results
4078+
{
4079+
const char *obj7 =
4080+
"v 1.0 2.0 3.0 0.5 0.1 0.2 0.3\n"
4081+
"v 4.0 5.0 6.0 0.5 0.1 0.2 0.3\n"
4082+
"v 7.0 8.0 9.0 0.5 0.1 0.2 0.3\n"
4083+
"f 1 2 3\n";
4084+
tinyobj::basic_attrib_t<> attrib;
4085+
std::vector<tinyobj::basic_shape_t<>> shapes;
4086+
std::vector<tinyobj::material_t> mats;
4087+
std::string w, e;
4088+
tinyobj::OptLoadConfig cfg;
4089+
cfg.num_threads = 1;
4090+
cfg.triangulate = true;
4091+
bool ok = tinyobj::LoadObjOpt(&attrib, &shapes, &mats, &w, &e,
4092+
obj7, strlen(obj7), cfg);
4093+
TEST_CHECK(ok);
4094+
TEST_CHECK(!attrib.colors.empty());
4095+
TEST_CHECK(std::abs(attrib.colors[0] - 0.1f) < 1e-5f);
4096+
TEST_CHECK(std::abs(attrib.colors[1] - 0.2f) < 1e-5f);
4097+
TEST_CHECK(std::abs(attrib.colors[2] - 0.3f) < 1e-5f);
4098+
TEST_CHECK(std::abs(attrib.vertex_weights[0] - 0.5f) < 1e-5f);
4099+
}
4100+
}
4101+
40304102
void test_loadobjopt_nan_inf_values() {
40314103
// Test that nan/inf in vertex data are handled with OBJ-compatible values
40324104
// (nan→0, +inf→max, -inf→lowest), matching the legacy parser.
@@ -4350,4 +4422,6 @@ TEST_LIST = {
43504422
{"test_loadobjopt_typed_empty_buffer", test_loadobjopt_typed_empty_buffer},
43514423
{"test_loadobjopt_typed_move_semantics", test_loadobjopt_typed_move_semantics},
43524424
{"test_loadobjopt_nan_inf_values", test_loadobjopt_nan_inf_values},
4425+
{"test_loadobjopt_typed_vertex_color_6_and_7",
4426+
test_loadobjopt_typed_vertex_color_6_and_7},
43534427
{NULL, NULL}};

tiny_obj_loader.h

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10873,27 +10873,40 @@ static bool opt_parseLine(OptCommand *command, const char *p, size_t p_len,
1087310873
opt_skip_space(&extra_token);
1087410874
const int extra_components = opt_count_remaining_scalars(extra_token);
1087510875
if (extra_components == 1) {
10876+
// v x y z w (4 total) — weight only
1087610877
const char *weight_cursor = extra_token;
1087710878
if (opt_tryParseFloatToken(&w, &weight_cursor)) {
1087810879
command->has_vertex_weight = true;
1087910880
command->vw = w;
1088010881
}
10881-
} else if (extra_components >= 3) {
10882-
real_t maybe_r = r, maybe_g = g, maybe_b = b;
10882+
} else if (extra_components == 3) {
10883+
// v x y z r g b (6 total) — color, weight = r (legacy compat)
10884+
real_t cr = r, cg = g, cb = b;
1088310885
const char *color_cursor = extra_token;
10884-
if (opt_tryParseFloatToken(&maybe_r, &color_cursor)) {
10885-
const char *after_r = color_cursor;
10886-
if (opt_tryParseFloatToken(&maybe_g, &color_cursor) &&
10887-
opt_tryParseFloatToken(&maybe_b, &color_cursor)) {
10888-
command->has_vertex_weight = true;
10889-
command->vw = maybe_r;
10890-
command->has_vertex_color = true;
10891-
command->vc_r = maybe_r;
10892-
command->vc_g = maybe_g;
10893-
command->vc_b = maybe_b;
10894-
} else {
10895-
color_cursor = after_r;
10896-
}
10886+
if (opt_tryParseFloatToken(&cr, &color_cursor) &&
10887+
opt_tryParseFloatToken(&cg, &color_cursor) &&
10888+
opt_tryParseFloatToken(&cb, &color_cursor)) {
10889+
command->has_vertex_weight = true;
10890+
command->vw = cr;
10891+
command->has_vertex_color = true;
10892+
command->vc_r = cr;
10893+
command->vc_g = cg;
10894+
command->vc_b = cb;
10895+
}
10896+
} else if (extra_components >= 4) {
10897+
// v x y z w r g b (7+ total) — weight + color
10898+
real_t vw = real_t(1.0), cr = r, cg = g, cb = b;
10899+
const char *wc_cursor = extra_token;
10900+
if (opt_tryParseFloatToken(&vw, &wc_cursor) &&
10901+
opt_tryParseFloatToken(&cr, &wc_cursor) &&
10902+
opt_tryParseFloatToken(&cg, &wc_cursor) &&
10903+
opt_tryParseFloatToken(&cb, &wc_cursor)) {
10904+
command->has_vertex_weight = true;
10905+
command->vw = vw;
10906+
command->has_vertex_color = true;
10907+
command->vc_r = cr;
10908+
command->vc_g = cg;
10909+
command->vc_b = cb;
1089710910
}
1089810911
}
1089910912
command->vx = x;
@@ -11628,14 +11641,22 @@ static void opt_parseLineToThreadData(
1162811641
nextra++;
1162911642

1163011643
if (nextra == 1) {
11631-
// weight only
11644+
// v x y z w (4 total) — weight only, no color
1163211645
if (!td.saw_any_weight) {
1163311646
td.saw_any_weight = true;
1163411647
td.v_weight.resize(td.num_v, real_t(1.0));
1163511648
}
1163611649
td.v_weight.push_back(extra[0]);
11637-
} else if (nextra >= 3) {
11638-
// weight (=first extra) + color (r,g,b)
11650+
td.saw_missing_color = true;
11651+
if (td.saw_any_color) {
11652+
size_t coff = td.v_color.size();
11653+
td.v_color.resize(coff + 3);
11654+
td.v_color[coff] = real_t(1.0);
11655+
td.v_color[coff+1] = real_t(1.0);
11656+
td.v_color[coff+2] = real_t(1.0);
11657+
}
11658+
} else if (nextra == 3) {
11659+
// v x y z r g b (6 total) — color, weight = r (legacy compat)
1163911660
if (!td.saw_any_weight) {
1164011661
td.saw_any_weight = true;
1164111662
td.v_weight.resize(td.num_v, real_t(1.0));
@@ -11650,8 +11671,24 @@ static void opt_parseLineToThreadData(
1165011671
td.v_color[coff] = extra[0];
1165111672
td.v_color[coff+1] = extra[1];
1165211673
td.v_color[coff+2] = extra[2];
11674+
} else if (nextra >= 4) {
11675+
// v x y z w r g b (7+ total) — weight + color
11676+
if (!td.saw_any_weight) {
11677+
td.saw_any_weight = true;
11678+
td.v_weight.resize(td.num_v, real_t(1.0));
11679+
}
11680+
td.v_weight.push_back(extra[0]);
11681+
if (!td.saw_any_color) {
11682+
td.saw_any_color = true;
11683+
td.v_color.resize(td.num_v * 3, real_t(1.0));
11684+
}
11685+
size_t coff = td.v_color.size();
11686+
td.v_color.resize(coff + 3);
11687+
td.v_color[coff] = extra[1];
11688+
td.v_color[coff+1] = extra[2];
11689+
td.v_color[coff+2] = extra[3];
1165311690
} else {
11654-
// no extras
11691+
// nextra == 0 or 2 — no color
1165511692
if (td.saw_any_weight) td.v_weight.push_back(real_t(1.0));
1165611693
td.saw_missing_color = true;
1165711694
if (td.saw_any_color) {
@@ -11662,7 +11699,7 @@ static void opt_parseLineToThreadData(
1166211699
td.v_color[coff+2] = real_t(1.0);
1166311700
}
1166411701
}
11665-
if (nextra < 3) {
11702+
if (nextra != 3 && nextra < 4) {
1166611703
td.saw_missing_color = true;
1166711704
}
1166811705
td.num_v++;

0 commit comments

Comments
 (0)