LOD: Remove "Raycast Normals" and associated "Normal Split Angle" settings

"Raycast Normals" was introduced in 4.4 dev and defaulted to "false".
The limited testing results at the time suggested that raycasting
generally reduces normal quality compared to native simplifier results,
at the same time increasing vertex memory and import time.

To play it safe, we introduced a setting that defaulted to false, with
the goal of removing it later in 4.4 development cycle if no regressions
are noticed. Since we already had three dev snapshots and no reports,
this change removes the setting and associated code.

"Normal Split Angle" was only used when raycast normals were enabled;
this change removes it from the settings, but keeps it in the script
binding for compatibility.

Existing meshes import exactly the same after this change (unless they
chose to override raycasting which would be surprising).

split_normals helper was only used in this code path and is also removed
for simplicity; it is unlikely that this code will be useful as is, as
it can only regenerate normals without fixing tangents or updating
positions.
This commit is contained in:
Arseny Kapoulkine 2024-10-28 10:14:04 -07:00
parent a3080477ac
commit 494fe2fe21
5 changed files with 9 additions and 309 deletions

View File

@ -49,7 +49,8 @@
<param index="2" name="bone_transform_array" type="Array" /> <param index="2" name="bone_transform_array" type="Array" />
<description> <description>
Generates all lods for this ImporterMesh. Generates all lods for this ImporterMesh.
[param normal_merge_angle] and [param normal_split_angle] are in degrees and used in the same way as the importer settings in [code]lods[/code]. As a good default, use 25 and 60 respectively. [param normal_merge_angle] is in degrees and used in the same way as the importer settings in [code]lods[/code].
[param normal_split_angle] is not used and only remains for compatibility with older versions of the API.
The number of generated lods can be accessed using [method get_surface_lod_count], and each LOD is available in [method get_surface_lod_size] and [method get_surface_lod_indices]. The number of generated lods can be accessed using [method get_surface_lod_count], and each LOD is available in [method get_surface_lod_size] and [method get_surface_lod_indices].
[param bone_transform_array] is an [Array] which can be either empty or contain [Transform3D]s which, for each of the mesh's bone IDs, will apply mesh skinning when generating the LOD mesh variations. This is usually used to account for discrepancies in scale between the mesh itself and its skinning data. [param bone_transform_array] is an [Array] which can be either empty or contain [Transform3D]s which, for each of the mesh's bone IDs, will apply mesh skinning when generating the LOD mesh variations. This is usually used to account for discrepancies in scale between the mesh itself and its skinning data.
</description> </description>

View File

@ -539,7 +539,7 @@ static Error _parse_obj(const String &p_path, List<Ref<ImporterMesh>> &r_meshes,
if (p_generate_lods) { if (p_generate_lods) {
// Use normal merge/split angles that match the defaults used for 3D scene importing. // Use normal merge/split angles that match the defaults used for 3D scene importing.
mesh->generate_lods(60.0f, 25.0f, {}); mesh->generate_lods(60.0f, {});
} }
if (p_generate_shadow_mesh) { if (p_generate_shadow_mesh) {

View File

@ -2043,9 +2043,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/shadow_meshes", PROPERTY_HINT_ENUM, "Default,Enable,Disable"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/shadow_meshes", PROPERTY_HINT_ENUM, "Default,Enable,Disable"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/lightmap_uv", PROPERTY_HINT_ENUM, "Default,Enable,Disable"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/lightmap_uv", PROPERTY_HINT_ENUM, "Default,Enable,Disable"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/lods", PROPERTY_HINT_ENUM, "Default,Enable,Disable"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/lods", PROPERTY_HINT_ENUM, "Default,Enable,Disable"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "lods/normal_split_angle", PROPERTY_HINT_RANGE, "0,180,0.1,degrees"), 25.0f));
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "lods/normal_merge_angle", PROPERTY_HINT_RANGE, "0,180,0.1,degrees"), 60.0f)); r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "lods/normal_merge_angle", PROPERTY_HINT_RANGE, "0,180,0.1,degrees"), 60.0f));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "lods/raycast_normals", PROPERTY_HINT_NONE, ""), false));
} break; } break;
case INTERNAL_IMPORT_CATEGORY_MATERIAL: { case INTERNAL_IMPORT_CATEGORY_MATERIAL: {
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "use_external/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "use_external/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
@ -2474,9 +2472,7 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_
//do mesh processing //do mesh processing
bool generate_lods = p_generate_lods; bool generate_lods = p_generate_lods;
float split_angle = 25.0f;
float merge_angle = 60.0f; float merge_angle = 60.0f;
bool raycast_normals = false;
bool create_shadow_meshes = p_create_shadow_meshes; bool create_shadow_meshes = p_create_shadow_meshes;
bool bake_lightmaps = p_light_bake_mode == LIGHT_BAKE_STATIC_LIGHTMAPS; bool bake_lightmaps = p_light_bake_mode == LIGHT_BAKE_STATIC_LIGHTMAPS;
String save_to_file; String save_to_file;
@ -2523,18 +2519,10 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_
} }
} }
if (mesh_settings.has("lods/normal_split_angle")) {
split_angle = mesh_settings["lods/normal_split_angle"];
}
if (mesh_settings.has("lods/normal_merge_angle")) { if (mesh_settings.has("lods/normal_merge_angle")) {
merge_angle = mesh_settings["lods/normal_merge_angle"]; merge_angle = mesh_settings["lods/normal_merge_angle"];
} }
if (mesh_settings.has("lods/raycast_normals")) {
raycast_normals = mesh_settings["lods/raycast_normals"];
}
if (bool(mesh_settings.get("save_to_file/enabled", false))) { if (bool(mesh_settings.get("save_to_file/enabled", false))) {
save_to_file = mesh_settings.get("save_to_file/path", String()); save_to_file = mesh_settings.get("save_to_file/path", String());
if (!save_to_file.is_resource_file()) { if (!save_to_file.is_resource_file()) {
@ -2583,7 +2571,7 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_
if (generate_lods) { if (generate_lods) {
Array skin_pose_transform_array = _get_skinned_pose_transforms(src_mesh_node); Array skin_pose_transform_array = _get_skinned_pose_transforms(src_mesh_node);
src_mesh_node->get_mesh()->generate_lods(merge_angle, split_angle, skin_pose_transform_array, raycast_normals); src_mesh_node->get_mesh()->generate_lods(merge_angle, skin_pose_transform_array);
} }
if (create_shadow_meshes) { if (create_shadow_meshes) {

View File

@ -33,108 +33,10 @@
#include "core/io/marshalls.h" #include "core/io/marshalls.h"
#include "core/math/convex_hull.h" #include "core/math/convex_hull.h"
#include "core/math/random_pcg.h" #include "core/math/random_pcg.h"
#include "core/math/static_raycaster.h"
#include "scene/resources/animation_library.h"
#include "scene/resources/surface_tool.h" #include "scene/resources/surface_tool.h"
#include <cstdint> #include <cstdint>
void ImporterMesh::Surface::split_normals(const LocalVector<int> &p_indices, const LocalVector<Vector3> &p_normals) {
_split_normals(arrays, p_indices, p_normals);
for (BlendShape &blend_shape : blend_shape_data) {
_split_normals(blend_shape.arrays, p_indices, p_normals);
}
}
void ImporterMesh::Surface::_split_normals(Array &r_arrays, const LocalVector<int> &p_indices, const LocalVector<Vector3> &p_normals) {
ERR_FAIL_COND(r_arrays.size() != RS::ARRAY_MAX);
const PackedVector3Array &vertices = r_arrays[RS::ARRAY_VERTEX];
int current_vertex_count = vertices.size();
int new_vertex_count = p_indices.size();
int final_vertex_count = current_vertex_count + new_vertex_count;
const int *indices_ptr = p_indices.ptr();
for (int i = 0; i < r_arrays.size(); i++) {
if (i == RS::ARRAY_INDEX) {
continue;
}
if (r_arrays[i].get_type() == Variant::NIL) {
continue;
}
switch (r_arrays[i].get_type()) {
case Variant::PACKED_VECTOR3_ARRAY: {
PackedVector3Array data = r_arrays[i];
data.resize(final_vertex_count);
Vector3 *data_ptr = data.ptrw();
if (i == RS::ARRAY_NORMAL) {
const Vector3 *normals_ptr = p_normals.ptr();
memcpy(&data_ptr[current_vertex_count], normals_ptr, sizeof(Vector3) * new_vertex_count);
} else {
for (int j = 0; j < new_vertex_count; j++) {
data_ptr[current_vertex_count + j] = data_ptr[indices_ptr[j]];
}
}
r_arrays[i] = data;
} break;
case Variant::PACKED_VECTOR2_ARRAY: {
PackedVector2Array data = r_arrays[i];
data.resize(final_vertex_count);
Vector2 *data_ptr = data.ptrw();
for (int j = 0; j < new_vertex_count; j++) {
data_ptr[current_vertex_count + j] = data_ptr[indices_ptr[j]];
}
r_arrays[i] = data;
} break;
case Variant::PACKED_FLOAT32_ARRAY: {
PackedFloat32Array data = r_arrays[i];
int elements = data.size() / current_vertex_count;
data.resize(final_vertex_count * elements);
float *data_ptr = data.ptrw();
for (int j = 0; j < new_vertex_count; j++) {
memcpy(&data_ptr[(current_vertex_count + j) * elements], &data_ptr[indices_ptr[j] * elements], sizeof(float) * elements);
}
r_arrays[i] = data;
} break;
case Variant::PACKED_INT32_ARRAY: {
PackedInt32Array data = r_arrays[i];
int elements = data.size() / current_vertex_count;
data.resize(final_vertex_count * elements);
int32_t *data_ptr = data.ptrw();
for (int j = 0; j < new_vertex_count; j++) {
memcpy(&data_ptr[(current_vertex_count + j) * elements], &data_ptr[indices_ptr[j] * elements], sizeof(int32_t) * elements);
}
r_arrays[i] = data;
} break;
case Variant::PACKED_BYTE_ARRAY: {
PackedByteArray data = r_arrays[i];
int elements = data.size() / current_vertex_count;
data.resize(final_vertex_count * elements);
uint8_t *data_ptr = data.ptrw();
for (int j = 0; j < new_vertex_count; j++) {
memcpy(&data_ptr[(current_vertex_count + j) * elements], &data_ptr[indices_ptr[j] * elements], sizeof(uint8_t) * elements);
}
r_arrays[i] = data;
} break;
case Variant::PACKED_COLOR_ARRAY: {
PackedColorArray data = r_arrays[i];
data.resize(final_vertex_count);
Color *data_ptr = data.ptrw();
for (int j = 0; j < new_vertex_count; j++) {
data_ptr[current_vertex_count + j] = data_ptr[indices_ptr[j]];
}
r_arrays[i] = data;
} break;
default: {
ERR_FAIL_MSG("Unhandled array type.");
} break;
}
}
}
String ImporterMesh::validate_blend_shape_name(const String &p_name) { String ImporterMesh::validate_blend_shape_name(const String &p_name) {
String name = p_name; String name = p_name;
const char *characters = ":"; const char *characters = ":";
@ -306,7 +208,7 @@ void ImporterMesh::optimize_indices_for_cache() {
} \ } \
write_array[vert_idx] = transformed_vert; write_array[vert_idx] = transformed_vert;
void ImporterMesh::generate_lods(float p_normal_merge_angle, float p_normal_split_angle, Array p_bone_transform_array, bool p_raycast_normals) { void ImporterMesh::generate_lods(float p_normal_merge_angle, Array p_bone_transform_array) {
if (!SurfaceTool::simplify_scale_func) { if (!SurfaceTool::simplify_scale_func) {
return; return;
} }
@ -379,8 +281,6 @@ void ImporterMesh::generate_lods(float p_normal_merge_angle, float p_normal_spli
} }
float normal_merge_threshold = Math::cos(Math::deg_to_rad(p_normal_merge_angle)); float normal_merge_threshold = Math::cos(Math::deg_to_rad(p_normal_merge_angle));
float normal_pre_split_threshold = Math::cos(Math::deg_to_rad(MIN(180.0f, p_normal_split_angle * 2.0f)));
float normal_split_threshold = Math::cos(Math::deg_to_rad(p_normal_split_angle));
const Vector3 *normals_ptr = normals.ptr(); const Vector3 *normals_ptr = normals.ptr();
HashMap<Vector3, LocalVector<Pair<int, int>>> unique_vertices; HashMap<Vector3, LocalVector<Pair<int, int>>> unique_vertices;
@ -469,22 +369,6 @@ void ImporterMesh::generate_lods(float p_normal_merge_angle, float p_normal_spli
unsigned int index_target = 12; // Start with the smallest target, 4 triangles unsigned int index_target = 12; // Start with the smallest target, 4 triangles
unsigned int last_index_count = 0; unsigned int last_index_count = 0;
// Only used for normal raycasting
int split_vertex_count = vertex_count;
LocalVector<Vector3> split_vertex_normals;
LocalVector<int> split_vertex_indices;
split_vertex_normals.reserve(index_count / 3);
split_vertex_indices.reserve(index_count / 3);
RandomPCG pcg;
pcg.seed(123456789); // Keep seed constant across imports
Ref<StaticRaycaster> raycaster = p_raycast_normals ? StaticRaycaster::create() : Ref<StaticRaycaster>();
if (raycaster.is_valid()) {
raycaster->add_mesh(vertices, indices, 0);
raycaster->commit();
}
const float max_mesh_error = FLT_MAX; // We don't want to limit by error, just by index target const float max_mesh_error = FLT_MAX; // We don't want to limit by error, just by index target
float mesh_error = 0.0f; float mesh_error = 0.0f;
@ -534,173 +418,6 @@ void ImporterMesh::generate_lods(float p_normal_merge_angle, float p_normal_spli
} }
} }
if (raycaster.is_valid()) {
LocalVector<LocalVector<int>> vertex_corners;
vertex_corners.resize(vertex_count);
int *ptrw = new_indices.ptrw();
for (unsigned int j = 0; j < new_index_count; j++) {
vertex_corners[ptrw[j]].push_back(j);
}
float error_factor = 1.0f / (scale * MAX(mesh_error, 0.15));
const float ray_bias = 0.05;
float ray_length = ray_bias + mesh_error * scale * 3.0f;
Vector<StaticRaycaster::Ray> rays;
LocalVector<Vector2> ray_uvs;
int32_t *new_indices_ptr = new_indices.ptrw();
int current_ray_count = 0;
for (unsigned int j = 0; j < new_index_count; j += 3) {
const Vector3 &v0 = vertices_ptr[new_indices_ptr[j + 0]];
const Vector3 &v1 = vertices_ptr[new_indices_ptr[j + 1]];
const Vector3 &v2 = vertices_ptr[new_indices_ptr[j + 2]];
Vector3 face_normal = vec3_cross(v0 - v2, v0 - v1);
float face_area = face_normal.length(); // Actually twice the face area, since it's the same error_factor on all faces, we don't care
if (!Math::is_finite(face_area) || face_area == 0) {
WARN_PRINT_ONCE("Ignoring face with non-finite normal in LOD generation.");
continue;
}
Vector3 dir = face_normal / face_area;
int ray_count = CLAMP(5.0 * face_area * error_factor, 16, 64);
rays.resize(current_ray_count + ray_count);
StaticRaycaster::Ray *rays_ptr = rays.ptrw();
ray_uvs.resize(current_ray_count + ray_count);
Vector2 *ray_uvs_ptr = ray_uvs.ptr();
for (int k = 0; k < ray_count; k++) {
float u = pcg.randf();
float v = pcg.randf();
if (u + v >= 1.0f) {
u = 1.0f - u;
v = 1.0f - v;
}
u = 0.9f * u + 0.05f / 3.0f; // Give barycentric coordinates some padding, we don't want to sample right on the edge
v = 0.9f * v + 0.05f / 3.0f; // v = (v - one_third) * 0.95f + one_third;
float w = 1.0f - u - v;
Vector3 org = v0 * w + v1 * u + v2 * v;
org -= dir * ray_bias;
rays_ptr[current_ray_count + k] = StaticRaycaster::Ray(org, dir, 0.0f, ray_length);
rays_ptr[current_ray_count + k].id = j / 3;
ray_uvs_ptr[current_ray_count + k] = Vector2(u, v);
}
current_ray_count += ray_count;
}
raycaster->intersect(rays);
LocalVector<Vector3> ray_normals;
LocalVector<real_t> ray_normal_weights;
ray_normals.resize(new_index_count);
ray_normal_weights.resize(new_index_count);
for (unsigned int j = 0; j < new_index_count; j++) {
ray_normal_weights[j] = 0.0f;
}
const StaticRaycaster::Ray *rp = rays.ptr();
for (int j = 0; j < rays.size(); j++) {
if (rp[j].geomID != 0) { // Ray missed
continue;
}
if (rp[j].normal.normalized().dot(rp[j].dir) > 0.0f) { // Hit a back face.
continue;
}
const float &u = rp[j].u;
const float &v = rp[j].v;
const float w = 1.0f - u - v;
const unsigned int &hit_tri_id = rp[j].primID;
const unsigned int &orig_tri_id = rp[j].id;
const Vector3 &n0 = normals_ptr[indices_ptr[hit_tri_id * 3 + 0]];
const Vector3 &n1 = normals_ptr[indices_ptr[hit_tri_id * 3 + 1]];
const Vector3 &n2 = normals_ptr[indices_ptr[hit_tri_id * 3 + 2]];
Vector3 normal = n0 * w + n1 * u + n2 * v;
Vector2 orig_uv = ray_uvs[j];
const real_t orig_bary[3] = { 1.0f - orig_uv.x - orig_uv.y, orig_uv.x, orig_uv.y };
for (int k = 0; k < 3; k++) {
int idx = orig_tri_id * 3 + k;
real_t weight = orig_bary[k];
ray_normals[idx] += normal * weight;
ray_normal_weights[idx] += weight;
}
}
for (unsigned int j = 0; j < new_index_count; j++) {
if (ray_normal_weights[j] < 1.0f) { // Not enough data, the new normal would be just a bad guess
ray_normals[j] = Vector3();
} else {
ray_normals[j] /= ray_normal_weights[j];
}
}
LocalVector<LocalVector<int>> normal_group_indices;
LocalVector<Vector3> normal_group_averages;
normal_group_indices.reserve(24);
normal_group_averages.reserve(24);
for (unsigned int j = 0; j < vertex_count; j++) {
const LocalVector<int> &corners = vertex_corners[j];
const Vector3 &vertex_normal = normals_ptr[j];
for (const int &corner_idx : corners) {
const Vector3 &ray_normal = ray_normals[corner_idx];
if (ray_normal.length_squared() < CMP_EPSILON2) {
continue;
}
bool found = false;
for (unsigned int l = 0; l < normal_group_indices.size(); l++) {
LocalVector<int> &group_indices = normal_group_indices[l];
Vector3 n = normal_group_averages[l] / group_indices.size();
if (n.dot(ray_normal) > normal_pre_split_threshold) {
found = true;
group_indices.push_back(corner_idx);
normal_group_averages[l] += ray_normal;
break;
}
}
if (!found) {
normal_group_indices.push_back({ corner_idx });
normal_group_averages.push_back(ray_normal);
}
}
for (unsigned int k = 0; k < normal_group_indices.size(); k++) {
LocalVector<int> &group_indices = normal_group_indices[k];
Vector3 n = normal_group_averages[k] / group_indices.size();
if (vertex_normal.dot(n) < normal_split_threshold) {
split_vertex_indices.push_back(j);
split_vertex_normals.push_back(n);
int new_idx = split_vertex_count++;
for (const int &index : group_indices) {
new_indices_ptr[index] = new_idx;
}
}
}
normal_group_indices.clear();
normal_group_averages.clear();
}
}
Surface::LOD lod; Surface::LOD lod;
lod.distance = MAX(mesh_error * scale, CMP_EPSILON2); lod.distance = MAX(mesh_error * scale, CMP_EPSILON2);
lod.indices = new_indices; lod.indices = new_indices;
@ -713,22 +430,19 @@ void ImporterMesh::generate_lods(float p_normal_merge_angle, float p_normal_spli
} }
} }
if (raycaster.is_valid()) {
surfaces.write[i].split_normals(split_vertex_indices, split_vertex_normals);
}
surfaces.write[i].lods.sort_custom<Surface::LODComparator>(); surfaces.write[i].lods.sort_custom<Surface::LODComparator>();
for (int j = 0; j < surfaces.write[i].lods.size(); j++) { for (int j = 0; j < surfaces.write[i].lods.size(); j++) {
Surface::LOD &lod = surfaces.write[i].lods.write[j]; Surface::LOD &lod = surfaces.write[i].lods.write[j];
unsigned int *lod_indices_ptr = (unsigned int *)lod.indices.ptrw(); unsigned int *lod_indices_ptr = (unsigned int *)lod.indices.ptrw();
SurfaceTool::optimize_vertex_cache_func(lod_indices_ptr, lod_indices_ptr, lod.indices.size(), split_vertex_count); SurfaceTool::optimize_vertex_cache_func(lod_indices_ptr, lod_indices_ptr, lod.indices.size(), vertex_count);
} }
} }
} }
void ImporterMesh::_generate_lods_bind(float p_normal_merge_angle, float p_normal_split_angle, Array p_skin_pose_transform_array) { void ImporterMesh::_generate_lods_bind(float p_normal_merge_angle, float p_normal_split_angle, Array p_skin_pose_transform_array) {
generate_lods(p_normal_merge_angle, p_normal_split_angle, p_skin_pose_transform_array); // p_normal_split_angle is unused, but kept for compatibility
generate_lods(p_normal_merge_angle, p_skin_pose_transform_array);
} }
bool ImporterMesh::has_mesh() const { bool ImporterMesh::has_mesh() const {

View File

@ -68,9 +68,6 @@ class ImporterMesh : public Resource {
return l.distance < r.distance; return l.distance < r.distance;
} }
}; };
void split_normals(const LocalVector<int> &p_indices, const LocalVector<Vector3> &p_normals);
static void _split_normals(Array &r_arrays, const LocalVector<int> &p_indices, const LocalVector<Vector3> &p_normals);
}; };
Vector<Surface> surfaces; Vector<Surface> surfaces;
Vector<String> blend_shapes; Vector<String> blend_shapes;
@ -118,7 +115,7 @@ public:
void optimize_indices_for_cache(); void optimize_indices_for_cache();
void generate_lods(float p_normal_merge_angle, float p_normal_split_angle, Array p_skin_pose_transform_array, bool p_raycast_normals = false); void generate_lods(float p_normal_merge_angle, Array p_skin_pose_transform_array);
void create_shadow_mesh(); void create_shadow_mesh();
Ref<ImporterMesh> get_shadow_mesh() const; Ref<ImporterMesh> get_shadow_mesh() const;