This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 3a51e86cf970c54a2148e122daf96a29d3d54c02 Author: jsorel <[email protected]> AuthorDate: Fri Oct 23 08:57:05 2026 +0200 fix(Geometry): fix mesh normals not smooth --- .../apache/sis/geometries/mesh/MeshPrimitive.java | 86 ++++++++++------------ .../main/org/apache/sis/maths/Vectors.java | 16 +++- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/mesh/MeshPrimitive.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/mesh/MeshPrimitive.java index c9b292c11a..be65f42f04 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/mesh/MeshPrimitive.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/mesh/MeshPrimitive.java @@ -747,57 +747,49 @@ public sealed interface MeshPrimitive extends Geometry @Override public void computeFaceNormals() { if (Type.TRIANGLES.equals(type)) { - final Cursor icursor = getIndex().cursor(); - final Array vertices = getPositions(); - final Cursor vcursor = vertices.cursor(); - - final long size = vertices.getLength(); - final Array normals = NDArrays.of(positions.getSampleSystem(), new float[(int)size * 3]); - final Cursor ncursor = normals.cursor(); - final Vector nv = ncursor.samples(); - final Vector v0 = Vectors.createFloat(3); - final Vector v1 = Vectors.createFloat(3); - final Vector v2 = Vectors.createFloat(3); - Vector normal = null; - - // accumulate normal vectors - final int offset = 0; - for (long i=0,n=index.getLength();i<n;i+=3) { - icursor.moveTo(i+offset); - final int idx0 = (int) icursor.samples().get(0); - icursor.next(); - final int idx1 = (int) icursor.samples().get(0); - icursor.next(); - final int idx2 = (int) icursor.samples().get(0); - vcursor.moveTo(idx0); v0.set(vcursor.samples()); - vcursor.moveTo(idx1); v1.set(vcursor.samples()); - vcursor.moveTo(idx2); v2.set(vcursor.samples()); - normal = Maths.calculateNormal(v0, v1, v2); - - //a normal may already be set at given index, we accumulate - //values and we will normalize them in a second loop - ncursor.moveTo(idx0); nv.add(normal); - ncursor.moveTo(idx1); nv.add(normal); - ncursor.moveTo(idx2); nv.add(normal); + /* + * Positions are read in double precision: geocentric coordinates are millions of meters, + * in float precision the edges of small triangles would be mostly rounding noise. + * The not normalized cross products are accumulated, so each triangle contributes + * in proportion of its area, and degenerated triangles contribute nothing. + */ + final int[] idx = MeshPrimitiveIndexes.toTriangles(getIndex(), type).toArrayInt(); + final double[] xyz = getPositions().toArrayDouble(); + final int dim = getPositions().getDimension(); + final int size = xyz.length / dim; + final double[] sum = new double[size * 3]; + final boolean[] used = new boolean[size]; + for (int i = 0; i + 2 < idx.length; i += 3) { + final int a = idx[i] * dim, b = idx[i+1] * dim, c = idx[i+2] * dim; + final double ux = xyz[b] - xyz[a], uy = xyz[b+1] - xyz[a+1], uz = xyz[b+2] - xyz[a+2]; + final double vx = xyz[c] - xyz[a], vy = xyz[c+1] - xyz[a+1], vz = xyz[c+2] - xyz[a+2]; + final double nx = uy*vz - uz*vy, ny = uz*vx - ux*vz, nz = ux*vy - uy*vx; + for (int k = 0; k < 3; k++) used[idx[i+k]] = true; + if (!Double.isFinite(nx + ny + nz) || (nx == 0 && ny == 0 && nz == 0)) { + //degenerated triangle, it has no normal + continue; + } + for (int k = 0; k < 3; k++) { + final int v = idx[i+k] * 3; + sum[v] += nx; sum[v+1] += ny; sum[v+2] += nz; + } } //normalize normals - for (long i=0,n=index.getLength();i<n;i+=3) { - icursor.moveTo(i+offset); - final int idx0 = (int) icursor.samples().get(0); - icursor.next(); - final int idx1 = (int) icursor.samples().get(0); - icursor.next(); - final int idx2 = (int) icursor.samples().get(0); - ncursor.moveTo(idx0); nv.normalize(); - if (!nv.isFinite()) nv.set(new double[]{1,0,0}); - ncursor.moveTo(idx1); nv.normalize(); - if (!nv.isFinite()) nv.set(new double[]{1,0,0}); - ncursor.moveTo(idx2); nv.normalize(); - if (!nv.isFinite()) nv.set(new double[]{1,0,0}); + final float[] normals = new float[size * 3]; + for (int v = 0; v < size; v++) { + final double x = sum[v*3], y = sum[v*3+1], z = sum[v*3+2]; + final double length = Math.sqrt(x*x + y*y + z*z); + if (length > 0 && Double.isFinite(length)) { + normals[v*3] = (float) (x / length); + normals[v*3+1] = (float) (y / length); + normals[v*3+2] = (float) (z / length); + } else if (used[v]) { + //only used by degenerated triangles, any direction is as good + normals[v*3] = 1; + } } - - setNormals(normals); + setNormals(NDArrays.of(getPositions().getSampleSystem(), normals)); } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java index a32fe24ccd..8e452e889d 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java @@ -1738,11 +1738,19 @@ public final class Vectors { public static Vector2D.UShort toOctUShort(ReadOnly.Vector<?> unitVector){ final Vector2D.Float oct = toOctEncoding(unitVector); return new Vector2D.UShort( - (short) ( (oct.x + 1f) / 2f * 65535f), - (short) ( (oct.y + 1f) / 2f * 65535f) + (short) toSNorm(oct.x, 65535), + (short) toSNorm(oct.y, 65535) ); } + /** + * Converts a value in range [-1,+1] to [0,rangeMax], rounded to the nearest integer. + * Truncation would bias all values toward -1. + */ + private static int toSNorm(final float value, final int rangeMax) { + return Math.round((Math.max(-1f, Math.min(1f, value)) * 0.5f + 0.5f) * rangeMax); + } + /** * Encode unit vector with Oct compression and store it as byte values. * @@ -1752,8 +1760,8 @@ public final class Vectors { public static byte[] toOctByte(ReadOnly.Vector<?> unitVector){ final Vector2D.Float oct = toOctEncoding(unitVector); final byte[] b = new byte[2]; - b[0] = (byte) ( (oct.x + 1f) / 2f * 255f); - b[1] = (byte) ( (oct.y + 1f) / 2f * 255f); + b[0] = (byte) toSNorm(oct.x, 255); + b[1] = (byte) toSNorm(oct.y, 255); return b; }
