autosmooth

added properties

update inspector

UX improvements

and minor optimizations

deg_to_rad

moved methods

move variables

performance and cleanup

Faster

Compare the angles/smooth of faces instead of vertices, skipping for loop.
Using a LocalVector instead of Vector, this simplifies the code while making it faster, and I don't think we need COW.

all done and working

Optimizations and cleanup

Clarifying and expanding the description of autosmooth and smoothing_angle properties.
Optimizations to skip steps in the loops:
Split loops to be able to skip vertices better and replace division with multiplication.
Suggestions for future changes.
Skip loop if smoothing_angle is lower than 0.1, as a more performant alternative to default smoothing.
Do not compare vertices that belong to the same triangle.
Remove has_smooth. Use a check at the start of the loop instead. This allows us to skip faces.
Perform normalize() at the end of the loop as we don't need the normal of the vertex afterwards, as we use the normal of the face for smoothing.  This way we skip normalizing flat faces.
If one vertex of a triangle is connected to the current vertex, the other 2 can never physically connect, so we skip them (break;). This isn't as reliable but it could add a slight performance increase.
I calculate a decrease of at least 80% of calculations in the for loops compared to the previous code.

Co-Authored-By: A Thousand Ships <96648715+AThousandShips@users.noreply.github.com>
This commit is contained in:
Jesusemora 2026-02-24 11:46:05 -03:00
parent 2327a82357
commit 6114c6a266
3 changed files with 127 additions and 22 deletions

View file

@ -195,6 +195,26 @@ void CSGShape3D::set_collision_priority(real_t p_priority) {
real_t CSGShape3D::get_collision_priority() const {
return collision_priority;
}
void CSGShape3D::set_autosmooth(bool p_smooth) {
autosmooth = p_smooth;
_make_dirty();
notify_property_list_changed();
}
bool CSGShape3D::is_autosmooth() const {
return autosmooth;
}
void CSGShape3D::set_smoothing_angle(const float p_angle) {
smoothing_angle = p_angle;
_make_dirty();
}
float CSGShape3D::get_smoothing_angle() const {
return smoothing_angle;
}
#endif // PHYSICS_3D_DISABLED
bool CSGShape3D::is_root_shape() const {
@ -576,10 +596,14 @@ void CSGShape3D::update_shape() {
CSGBrush *n = _get_brush();
ERR_FAIL_NULL_MSG(n, "Cannot get CSGBrush.");
AHashMap<Vector3, Vector3> vec_map;
Vector<int> face_count;
face_count.resize(n->materials.size() + 1);
Vector<Vector3> smooth_faces;
LocalVector<Vector3> smooth_vertex;
smooth_faces.resize(n->faces.size());
smooth_vertex.resize(n->faces.size() * 3);
for (int i = 0; i < face_count.size(); i++) {
face_count.write[i] = 0;
}
@ -589,21 +613,70 @@ void CSGShape3D::update_shape() {
ERR_CONTINUE(mat < -1 || mat >= face_count.size());
int idx = mat == -1 ? face_count.size() - 1 : mat;
if (n->faces[i].smooth) {
Plane p(n->faces[i].vertices[0], n->faces[i].vertices[1], n->faces[i].vertices[2]);
Plane p(n->faces[i].vertices[0], n->faces[i].vertices[1], n->faces[i].vertices[2]);
for (int j = 0; j < 3; j++) {
Vector3 v = n->faces[i].vertices[j];
Vector3 *vec = vec_map.getptr(v);
if (vec) {
*vec += p.normal;
} else {
vec_map.insert(v, p.normal);
smooth_faces.write[i] = p.normal;
// Not sure if resize populates the LocalVector.
smooth_vertex[i * 3 + 0] = Vector3(p.normal);
smooth_vertex[i * 3 + 1] = Vector3(p.normal);
smooth_vertex[i * 3 + 2] = Vector3(p.normal);
// We could use a AHashMap Vector3, int to store the number of connections of each vertex position and end the loop earlier. But I'm not sure if the performance gains outweigh the cost.
face_count.write[idx]++;
}
if (autosmooth) {
// We could add a `use_groups` property later to only apply autosmooth on smooth faces or respect smoothing groups in some way.
if (smoothing_angle > 0.1) {
float smooth_angle_rad = Math::cos(Math::deg_to_rad(smoothing_angle));
for (int i = 0; i < smooth_faces.size(); i++) {
for (int k = 0; k < 3; k++) {
int curr_vert = i * 3 + k;
Vector3 vert_a = n->faces[i].vertices[k];
for (int j = i + 1; j < smooth_faces.size(); j++) {
// Compare the angles of faces instead of vertices.
if (smooth_faces[i].dot(smooth_faces[j]) > smooth_angle_rad) {
for (int h = 0; h < 3; h++) {
Vector3 vert_b = n->faces[j].vertices[h];
if (vert_a == vert_b) {
int curr_j = j * 3 + h;
smooth_vertex[curr_vert] += smooth_faces[j];
smooth_vertex[curr_j] += smooth_faces[i];
break;
}
}
}
}
smooth_vertex[curr_vert].normalize();
}
}
}
} else {
for (int i = 0; i < smooth_faces.size(); i++) {
bool face_is_smooth = n->faces[i].smooth;
if (face_is_smooth) {
for (int k = 0; k < 3; k++) {
Vector3 vert_a = n->faces[i].vertices[k];
int curr_vert = i * 3 + k;
// Skip the other vertices of the face as they will never occupy the same position.
for (int j = i + 1; j < smooth_faces.size(); j++) {
// Preparing for when and if we replace Vector of bool for Vector of int smoothing groups. for now, face_is_smooth is always true.
if (face_is_smooth == n->faces[j].smooth) {
for (int h = 0; h < 3; h++) {
Vector3 vert_b = n->faces[j].vertices[h];
if (vert_a == vert_b) {
int curr_j = j * 3 + h;
smooth_vertex[curr_vert] += smooth_faces[j];
smooth_vertex[curr_j] += smooth_faces[i];
// Skip the other 2 vertices as only one vertex of each face can connect with one vertex of other face.
break;
}
}
}
}
smooth_vertex[curr_vert].normalize();
}
}
}
face_count.write[idx]++;
}
Vector<ShapeUpdateSurface> surfaces;
@ -647,19 +720,12 @@ void CSGShape3D::update_shape() {
int last = surfaces[idx].last_added;
Plane p(n->faces[i].vertices[0], n->faces[i].vertices[1], n->faces[i].vertices[2]);
int face_pos_i = i * 3;
for (int j = 0; j < 3; j++) {
Vector3 v = n->faces[i].vertices[j];
Vector3 normal = p.normal;
if (n->faces[i].smooth) {
Vector3 *ptr = vec_map.getptr(v);
if (ptr) {
normal = ptr->normalized();
}
}
Vector3 normal = smooth_vertex[face_pos_i + j];
if (n->faces[i].invert) {
normal = -normal;
@ -953,6 +1019,19 @@ void CSGShape3D::_validate_property(PropertyInfo &p_property) const {
if (!Engine::get_singleton()->is_editor_hint()) {
return;
}
if (p_property.name == "smoothing_angle") {
if (!autosmooth || (is_inside_tree() && !is_root_shape())) {
p_property.usage = PROPERTY_USAGE_NO_EDITOR;
}
}
if (p_property.name == "autosmooth") {
if (is_inside_tree() && !is_root_shape()) {
p_property.usage = PROPERTY_USAGE_NO_EDITOR;
}
}
bool is_collision_prefixed = p_property.name.begins_with("collision_");
if ((is_collision_prefixed || p_property.name.begins_with("use_collision")) && is_inside_tree() && !is_root_shape()) {
//hide collision if not root
@ -1037,6 +1116,15 @@ void CSGShape3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("bake_static_mesh"), &CSGShape3D::bake_static_mesh);
ClassDB::bind_method(D_METHOD("set_autosmooth", "autosmooth"), &CSGShape3D::set_autosmooth);
ClassDB::bind_method(D_METHOD("is_autosmooth"), &CSGShape3D::is_autosmooth);
ClassDB::bind_method(D_METHOD("set_smoothing_angle", "smoothing_angle"), &CSGShape3D::set_smoothing_angle);
ClassDB::bind_method(D_METHOD("get_smoothing_angle"), &CSGShape3D::get_smoothing_angle);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autosmooth"), "set_autosmooth", "is_autosmooth");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "smoothing_angle", PROPERTY_HINT_RANGE, "0,180,0.1,degrees"), "set_smoothing_angle", "get_smoothing_angle");
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation", PROPERTY_HINT_ENUM, "Union,Intersection,Subtraction"), "set_operation", "get_operation");
#ifndef DISABLE_DEPRECATED
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "snap", PROPERTY_HINT_RANGE, "0.000001,1,0.000001,suffix:m", PROPERTY_USAGE_NONE), "set_snap", "get_snap");

View file

@ -68,6 +68,9 @@ private:
bool last_visible = false;
float snap = 0.001;
bool autosmooth = false;
float smoothing_angle = 50.0;
#ifndef PHYSICS_3D_DISABLED
bool use_collision = false;
uint32_t collision_layer = 1;
@ -159,6 +162,12 @@ public:
void set_collision_priority(real_t p_priority);
real_t get_collision_priority() const;
void set_autosmooth(bool p_smooth);
bool is_autosmooth() const;
void set_smoothing_angle(const float p_angle);
float get_smoothing_angle() const;
#ifndef DISABLE_DEPRECATED
void set_snap(float p_snap);
float get_snap() const;

View file

@ -74,6 +74,10 @@
</method>
</methods>
<members>
<member name="autosmooth" type="bool" setter="set_autosmooth" getter="is_autosmooth" default="false">
Enables automatic smoothing. This overrides any smoothing on the CSG node and instead uses [member smoothing_angle] to calculate normals based on the angle between faces.
Children of a [CSGCombiner3D] node will be treated as a single mesh.
</member>
<member name="calculate_tangents" type="bool" setter="set_calculate_tangents" getter="is_calculating_tangents" default="true">
Calculate tangents for the CSG shape which allows the use of normal and height maps. This is only applied on the root shape, this setting is ignored on any child. Setting this to [code]false[/code] can speed up shape generation slightly.
</member>
@ -91,6 +95,10 @@
<member name="operation" type="int" setter="set_operation" getter="get_operation" enum="CSGShape3D.Operation" default="0">
The operation that is performed on this shape. This is ignored for the first CSG child node as the operation is between this node and the previous child of this nodes parent.
</member>
<member name="smoothing_angle" type="float" setter="set_smoothing_angle" getter="get_smoothing_angle" default="50.0">
When autosmooth is enabled, faces with an angle between them greater than this will be smoothed, while faces with a smaller angle will remain sharp.
Note: An angle lower than 0.1 will cause all smoothing to be disabled, this can be used to increase performance.
</member>
<member name="snap" type="float" setter="set_snap" getter="get_snap" deprecated="The CSG library no longer uses snapping.">
This property does nothing.
</member>