bug : updatenormals(mesh) does not calculate/set the correct normals (fixed)

Started by RemiD, March 22, 2018, 16:43:40

Previous topic - Next topic

RemiD

Hello,

For those of you who are doing experiments to create surfaces in code, using premade parts, and combining them, merging them, etc... You may have noticed that updatenormals(mesh) does not calculate/set the correct normals, see :


here is a procedure to calculate the correct normals (without altering the structure of the premade parts within a surface, which sometimes have several vertices at the same position (because of smoothing groups or UVs)), see :


updated 23.03.2018 :
->precalculates the normals of all triangles and store them in a list (faster)
->uses Stevie G method to calculate the average normal

;to store the temps indexes
Global TempsCount%
Dim TempI%(16)

;to store the normals of all triangles of a surface
Global TrianglesCount%
Dim TriangleNX#(16000)
Dim TriangleNY#(16000)
Dim TriangleNZ#(16000)

;to store the normals of the triangles which use a vertex
Global NormalsCount%
Dim NormalNX#(16)
Dim NormalNY#(16)
Dim NormalNZ#(16)

Function UpdateVerticesNormals(Surface,Mesh)

;for each triangle of the surface
TrianglesCount% = 0
For TI% = 0 To CountTriangles(Surface)-1 Step 1

  ;retrieve the X,Y,Z world coordinates of Vertex0, Vertex1, Vertex2 of this triangle
  ;V0
  V0I% = TriangleVertex(Surface,TI,0)
  TFormPoint(VertexX(Surface,V0I),VertexY(Surface,V0I),VertexZ(Surface,V0I),Mesh,0)
  V0GX# = TFormedX() : V0GY# = TFormedY() : V0GZ# = TFormedZ()
  ;V1
  V1I% = TriangleVertex(Surface,TI,1)
  TFormPoint(VertexX(Surface,V1I),VertexY(Surface,V1I),VertexZ(Surface,V1I),Mesh,0)
  V1GX# = TFormedX() : V1GY# = TFormedY() : V1GZ# = TFormedZ()
  ;V2
  V2I% = TriangleVertex(Surface,TI,2)
  TFormPoint(VertexX(Surface,V2I),VertexY(Surface,V2I),VertexZ(Surface,V2I),Mesh,0)
  V2GX# = TFormedX() : V2GY# = TFormedY() : V2GZ# = TFormedZ()

  ;calculate the normal of the triangle by using the world position of its vertices
  ;Vector 0->1
  Vec01X# = V0GX - V1GX : Vec01Y# = V0GY - V1GY : Vec01Z# = V0GZ - V1GZ
  ;Vector 0->2
  Vec02X# = V0GX - V2GX : Vec02Y# = V0GY - V2GY : Vec02Z# = V0GZ - V2GZ

  CPX# = ( Vec01Y * Vec02Z ) - ( Vec01Z * Vec02Y ) : CPY# = ( Vec01Z * Vec02X ) - ( Vec01X * Vec02Z ) : CPZ# = ( Vec01X * Vec02Y ) - ( Vec01Y * Vec02X )

  Length# = Sqr( ( CPX * CPX ) + ( CPY * CPY ) + ( CPZ * CPZ ) )

  NX# = CPX / Length : NY# = CPY / Length : NZ# = CPZ / Length

  ;store the normal in the triangles list
  TrianglesCount = TrianglesCount + 1 : TriI% = TrianglesCount
  TriangleNX(TriI) = NX : TriangleNY(TriI) = NY : TriangleNZ(TriI) = NZ

Next

;for each vertex
For VI% = 0 To CountVertices(Surface)-1 Step 1

  ;identify which triangles use this vertex
  TempsCount% = 0
  For TI% = 0 To CountTriangles(Surface)-1 Step 1
   If( TriangleVertex(Surface,TI,0) = VI Or TriangleVertex(Surface,TI,1) = VI Or TriangleVertex(Surface,TI,2) = VI )
    ;add the triangle to the temps list
    TempsCount = TempsCount + 1 : TemI% = TempsCount
    TempI(TemI) = TI
   EndIf
  Next

  ;for each identified triangle
  NormalsCount% = 0
  For TemI% = 1 To TempsCount Step 1
   TI% = TempI(TemI)

   ;get the triangle normal (previously calculated)
   NX# = TriangleNX(TI+1) : NY# = TriangleNY(TI+1) : NZ# = TriangleNZ(TI+1)

   ;add the normal to the normals list
   NormalsCount = NormalsCount + 1 : NorI% = NormalsCount
   NormalNX(NorI) = NX : NormalNY(NorI) = NY : NormalNZ(NorI) = NZ

  Next
   
  ;calculate the average normal vector, by considering the normals of the triangles using this vertex
  VNX# = 0 : VNY# = 0 : VNZ# = 0
  For NorI% = 1 To NormalsCount Step 1
   VNX = VNX + NormalNX(NorI) : VNY = VNY + NormalNY(NorI) : VNZ = VNZ + NormalNZ(NorI)
  Next
  VNX = VNX / NormalsCount : VNY = VNY / NormalsCount : VNZ = VNZ / NormalsCount

  ;set the normal of this vertex
  VertexNormal(Surface,VI,VNX,VNY,VNZ)

Next

End Function


(it should work to not alter the smoothing groups (and therefore how each vertex should be lighted shaded), but UVs can add vertices which share the same position in a surface, and i have not considered the vertices which have been duplicated just for UVs, because i don't need it)

Pakz

What does updatenormals or your code do? Does it reset the texture coords or something?

I am not that good with 3d. I remember using the updatenormals with b3d back then but not sure for what.

markcwm

Cool function RemiD! I converted it to Openb3d and tested it a bit, it's certainly more realistic lighting for flat surfaces like buildings and it also looks fine on curved surfaces. In the picture the light is in the archway, the top is standard UpdateNormals the bottom is RemiD's.

It's quite slow though (10 seconds) so to use in a project the normals would have to be saved to a b3d file, so I'll try to port jfk's SaveB3D to Openb3d.



Pakz: normals are 3d vectors (or angles) of each vertex, used for lighting.

RemiD

What my function does, contrary to the original updatenormals, is that it respects smoothing groups (=how the surface has been structured in a modeling editor, for lighting shading)

When you model a surface, you can select triangles and add them to a "smoothing group" by specifying what should be the max angle between 2 triangles, the smoothing group will then duplicate some vertices, so that the shared vertices are only between triangles which have an angle inferior to the max angle value.

In my example, i have modeled different parts (floor, wall, ceiling) and the angle between a floor part and a wall part is more than 89°, and the angle between a wall part and a ceiling part is more than 89°, and the angle between a wallcorner and another wallcorner is more than 89°, so the normals of the vertices at the border of a floor/wall and at the border of a wall/ceiling and at the border at a wallcorner/wallcorner, should be different depending on which triangles they belong.

The original UpdateNormals() calculates, for each vertex, the average normal of the triangles who have a vertex at this position, which is incorrect, since some vertices can have the same position but not the same normal (because of smoothing groups)

Note that you don't need to use updatenormals() or my function updateverticesnormals(), if you load a mesh in Blitz3d, it is only useful to calculate/set the normals of a surface when you have built it in code, using parts...


Quote
It's quite slow though (10 seconds)
It depends on the structure of the surface... For my case it is fast enough (rooms made with premade parts), and for your case it is useless (since you can set the normals using a smoothing group in your modeling editor and then you don't need to recalculate them...)

iWasAdam

kudos on the normal calculations and fixing them. the colored before and after are very well done. it looks great

STEVIE G


Personally I always unweld everything and use my own updatenormals function.  I think if you split the floor / walls etc... into separate surfaces and updated normal you'd have a better result than combining them into one surface.  It probably explains why cylinders etc.. are build with 2 surfaces.

Strictly speaking you aren't calculating the average normal here :)

For NorI% = 1 To NormalsCount Step 1
   VerNX = ( VerNX + NormalNX(NorI) ) / 2 : VerNY = ( VerNY + NormalNY(NorI) ) / 2 : VerNZ = ( VerNZ + NormalNZ(NorI) ) / 2
  Next

I think it should be more like ..

For NorI% = 1 To NormalsCount Step 1
   VerNX = VerNX + NormalNX(NorI) : VerNY = VerNY + NormalNY(NorI) : VerNZ = VerNZ + NormalNZ(NorI)
  Next
VerNX = VerNX / NormalsCount
VerNY = VerNY / NormalsCount
VerNZ = VerNZ / NormalsCount

Probably makes little difference right enough :)

RemiD

Just to clarify : this function calculates / sets the normals as they should be (as they have been defined, with smoothing groups, in your modeling program), so it will look as it was been modeled...


@StevieG>>are you sure ? apparently there is a difference :

Graphics(640,360,32,2)

Dim NormalNX#(10)
For n% = 1 To 10 Step 1
NormalNX(n) = Rnd(0.0,1.0)
Next

VerNX# = 0
For n% = 1 To 10 Step 1
VerNX = ( VerNX + NormalNX(n) ) / 2
Next
DebugLog(VerNX)

VerNX# = 0
For n% = 1 To 10 Step 1
VerNX = VerNX + NormalNX(n)
Next
VerNX = VerNX / 10
DebugLog(VerNX)

WaitKey()

End()

Your method makes more sense indeed...


@markcwm>>in theory, there should be no need to have to use updatenormals() or my updateverticesnormals() function, if the surfaces (of the premade parts) which are copied to merge them in a new surface already have the normals calculated / set (you can do that with smoothing group(s) in a modeling program).
So i think that the problem is that i use copymesh() which apparently does not copy the normals...
I will try to code a copysurface() function which also copies the normals, and then there should be no need to use this updateverticesnormals() function...

markcwm

Great explanation of normals and smoothing groups, RemiD. You sure know your stuff.

QuoteFor my case it is fast enough (rooms made with premade parts), and for your case it is useless

I see. I agree, I was thinking if a coder was sent models with no smoothing groups it would be cool to have a tool to process normals and maybe other things, but yes, that's what a modeller's for.

I tested how Stevie G's average normals looked against the original and they were almost the same, I think Stevie's were slightly smoother but a man on a galloping horse would never see it.

I looked at CopyMesh, the Minib3d code has normals and colors and texcoords, you could probably adapt it. The Blitz3d CopyMesh only copies vertex and triangles, there is an internal mesh transform function that sets normals but it's only used in the 3DS loader.

STEVIE G

Copymesh does copy vertex normals.   - not sure why you think it doesn't?

Simple Test ...


Graphics3D 640,360,32,2
SetBuffer BackBuffer()

AmbientLight 64,64,64
light = CreateLight() : LightColor light, 128,128,128
camera = CreateCamera() : PositionEntity camera, 0,0,-10
cube = CreateCube(): PositionEntity cube, -3,0,0 : EntityColor cube, 128,128,0
copy = CopyMesh( cube ) : PositionEntity copy, 3, 0,0 : EntityColor copy, 0,128,128
RotateMesh copy, 45,0,-45


While Not KeyHit(1)

TurnEntity cube, .1,0,.1
TurnEntity copy, .1,0, .1

RenderWorld()

Flip

Wend



RemiD

Quote
if a coder was sent models with no smoothing groups it would be cool to have a tool to process normals and maybe other things,
in this case, it may be necessary to analyze the surface and to rebuild it using a defined smoothing group (defined max angle between 2 triangles)

The function above does not modify the structure of the surface, it respects it. (how the surface was structured in the modeling program, using a smoothing group (a max angle between 2 triangles))

RemiD

Quote
Copymesh does copy vertex normals.   - not sure why you think it doesn't?
indeed, it seems that the problem is not in copymesh() but in my function AddSurfaceToOtherSurface(Surface,Mesh,OSurface,OMesh)

edit : found and corrected the error ! (24.03.2018)

Graphics3D 640,360,32,2

SeedRnd(MilliSecs())

camera = CreateCamera()
PositionEntity(camera,0,0,-10,True)

cube = CreateCube()
ScaleMesh(cube,1.0/2,1.0/2,1.0/2)
PositionEntity(cube,-3,0,0,True)

nmesh = CreateMesh() : nsurface = CreateSurface(nmesh)
For n% = 1 To 5 Step 1
part = CopyMesh(cube)
RotateMesh(part,Rand(-180,180),Rand(-180,180),0)
PositionMesh(part,Rnd(-0.5,0.5),Rnd(-0.5,0.5),Rnd(-0.5,0.5))
;AddMesh(part,nmesh) : FreeEntity(part)
AddSurfaceToOtherSurface(GetSurface(part,1),part,GetSurface(nmesh,1),nmesh) : FreeEntity(part)
Next
PositionEntity(nmesh,3,0,0,True)

light = CreateLight(1)
LightColor(light,255,255,255)
PositionEntity(light,-1000,1000,1000,True)
RotateEntity(light,45,-45,0,True)
AmbientLight(064,64,64)

While Not KeyHit(1)

TurnEntity cube, 0.1,0,0.1
TurnEntity nmesh, 0.1,0,0.1

    SetBuffer(BackBuffer())
RenderWorld()

Flip()

Wend

Function AddSurfaceToOtherSurface(Surface,Mesh,OSurface,OMesh)

SurfaceVerticesCount% = CountVertices(Surface)
;DebugLog("SurfaceVerticesCount = "+SurfaceVerticesCount)
OSurfaceVerticesCount% = CountVertices(OSurface)
;DebugLog("OSurfaceVerticesCount = "+OSurfaceVerticesCount)
For VI% = 0 To CountVertices(Surface)-1 Step 1
  ;DebugLog("VI = "+VI)
  VX# = VertexX(Surface,VI)
  VY# = VertexY(Surface,VI)
  VZ# = VertexZ(Surface,VI)
  VNX# = VertexNX(Surface,VI)
  VNY# = VertexNY(Surface,VI)
  VNZ# = VertexNZ(Surface,VI)
  VR% = VertexRed(Surface,VI)
  VG% = VertexGreen(Surface,VI)
  VB% = VertexBlue(Surface,VI)
  VA# = VertexAlpha(Surface,VI)
  VU# = VertexU(Surface,VI,0)
  VV# = VertexV(Surface,VI,0)
  If( OSurfaceVerticesCount = 0 )
   NVI = VI
  Else If( OSurfaceVerticesCount > 0 )
   NVI% = OSurfaceVerticesCount+VI
  EndIf
  ;DebugLog("NVI = "+NVI)
  AddVertex(OSurface,VX,VY,VZ)
  VertexNormal(OSurface,NVI,VNX,VNY,VNZ)
  VertexColor(OSurface,NVI,VR,VG,VB,VA)
  VertexTexCoords(OSurface,NVI,VU,VB)
  ;WaitKey()
Next
SurfaceTrianglesCount% = CountTriangles(Surface)
;DebugLog("SurfaceTrianglesCount = "+SurfaceTrianglesCount)
OSurfaceTrianglesCount% = CountTriangles(OSurface)
;DebugLog("OSurfaceTrianglesCount = "+OSurfaceTrianglesCount)
For TI% = 0 To CountTriangles(Surface)-1 Step 1
  V0I% = TriangleVertex(Surface,TI,0) ;vertex0
  V1I% = TriangleVertex(Surface,TI,1) ;vertex1
  V2I% = TriangleVertex(Surface,TI,2) ;vertex2
  ;DebugLog("oldtriangle"+TI+" "+V0I+","+V1I+","+V2I)
  If( OSurfaceVerticesCount = 0 )
   NV0I% = V0I
   NV1I% = V1I
   NV2I% = V2I
  Else If( OSurfaceVerticesCount > 0 )
   NV0I% = OSurfaceVerticesCount+V0I
   NV1I% = OSurfaceVerticesCount+V1I
   NV2I% = OSurfaceVerticesCount+V2I
  EndIf
  ;DebugLog("newtriangle"+TI+" "+NV0I+","+NV1I+","+NV2I)
  AddTriangle(OSurface,NV0I,NV1I,NV2I)
Next
;WaitKey()

End Function

there must be a bug somewhere...

I have found the error :

VertexNormal(OSurface,NVI,NX,NY,NZ)

should be :

VertexNormal(OSurface,NVI,VNX,VNY,VNZ)


So forget about what i have said !
All this workaround for nothing, lol ;D

RemiD

Ok, i confirm that you don't need updatenormals() or updateverticesnormals() if you calculate / set the normals of your surface (even several premade parts) in your modeling tool, using smoothinggroup(s), then you only have to use addmesh() or addsurfacetoothersurface() and the normals are copied.

So it is useful to calculate / set the normals of a surface only if you modify the structure of the surface (after having built it in code, from scratch, or after having modified it to remove vertices / triangles...)

Good to now...