Occlusion culling

Community Forums/Showcase/Occlusion culling

Sweenie(Posted 2003) [#1]
I have been playing around with occlusion culling lately and this is what i've come up with so far.

Warning!
The code is poorly commented and i think it could be greatly optimised as well.
I also realised that I forgot to do depth checking, which means that objects will be culled away, even it they are in front of the occluder. But's thats easy to fix.

Also note that i made the occluders semitransparent, just to show that the entities are actually culled.


Graphics3D 800,600
SetBuffer BackBuffer() 

;WireFrame True

; Types and variables for Edgelist
Type Edge
 Field A,B
 Field Surf
 Field Tri1		; The triangle the edge belongs To
 Field Tri2		; The adjacent triangle, if one exist
 Field X1,Y1,X2,Y2
 Field Visible
 Field PlaneDist#	; The edgelist also contains the planes
 Field PlaneNX#
 Field PlaneNY#
 Field PlaneNZ#
 Field Parent
End Type

Type vector
	Field x#
	Field y#
	Field z#
End Type
Global CProd.vector=New vector
Global DProd#

Type Occluder
 Field Mesh
 Field ID
End Type

Type Entity
 Field Entity
 Field Occluded
 Field Radius#
End Type

box = CreateMesh()
surf=CreateSurface(box)
v0=AddVertex(surf,-1,1,1)
v1=AddVertex(surf,1,1,1)
v2=AddVertex(surf,1,1,-1)
v3=AddVertex(surf,-1,1,-1)
v4=AddVertex(surf,-1,-1,1)
v5=AddVertex(surf,1,-1,1)
v6=AddVertex(surf,1,-1,-1)
v7=AddVertex(surf,-1,-1,-1)
AddTriangle(surf,v0,v1,v3)
AddTriangle(surf,v1,v2,v3)
AddTriangle(surf,v4,v7,v5)
AddTriangle(surf,v7,v6,v5)
AddTriangle(surf,v3,v2,v7)
AddTriangle(surf,v2,v6,v7)
AddTriangle(surf,v4,v1,v0)
AddTriangle(surf,v4,v5,v1)
AddTriangle(surf,v2,v1,v6)
AddTriangle(surf,v1,v5,v6)
AddTriangle(surf,v0,v3,v4)
AddTriangle(surf,v3,v7,v4)
;
HideEntity box 

; Cams & Meshes
camera = CreateCamera()
PositionEntity camera,0,0,-5
CameraClsColor camera,150,150,220 

Global EdgeList.Edge
; Create occluder
Global OccludeList.Occluder
OccludeList.Occluder = New Occluder
OccludeList\ID = 1
OccludeList\Mesh = CopyEntity(box) 
PositionEntity OccludeList\mesh,-6,0,0
EntityAlpha OccludeList\mesh,0.8 

OccludeList.Occluder = New Occluder
OccludeList\ID = 2
OccludeList\Mesh = CopyEntity(box) 
PositionEntity OccludeList\mesh,-3,0,0
EntityAlpha OccludeList\mesh,0.8 

OccludeList.Occluder = New Occluder
OccludeList\ID = 3
OccludeList\Mesh = CopyEntity(box) 
PositionEntity OccludeList\mesh,0,0,0
EntityAlpha OccludeList\mesh,0.8 

OccludeList.Occluder = New Occluder
OccludeList\ID = 4
OccludeList\Mesh = CopyEntity(box) 
PositionEntity OccludeList\mesh,3,0,0
EntityAlpha OccludeList\mesh,0.8 

OccludeList.Occluder = New Occluder
OccludeList\ID = 5
OccludeList\Mesh = CopyEntity(box) 
PositionEntity OccludeList\mesh,6,0,0
EntityAlpha OccludeList\mesh,0.8 


; Create ordinary objects
Global EntityList.Entity

For v=0 To 50
 EntityList.Entity = New Entity
 EntityList\Entity = CreateSphere(16)
 EntityList\Radius# = 0.5
 PositionEntity EntityList\Entity,Rnd(-5,5),Rnd(-5,5),Rnd(100,5)
 EntityColor EntityList\Entity,Rnd(50,255),Rnd(50,255),Rnd(50,255)
Next 

CenterPivot=CreatePivot()
light = CreateLight()
PositionEntity light,15,10,-15 
PointEntity light,CenterPivot

; Build the Edgelists
For OccludeList.Occluder = Each Occluder
 BuildEdgeList OccludeList\ID,OccludeList\mesh
Next

; The mainloop
LastTime=MilliSecs()
Color 0,0,0
UseOcclusion=0

While Not KeyDown(1)
 FrameCount=FrameCount+1

If KeyHit(57) Then
 UseOcclusion=1-UseOcclusion

 If UseOcclusion=0 Then
  For EntityList.Entity = Each Entity
   ShowEntity EntityList\Entity 
  Next
 EndIf

EndIf

 ;TurnEntity mesh,0,0.5,0

 If KeyDown(200) Then
  MoveEntity Camera,0,0.01,0 	
 EndIf	
 If KeyDown(208) Then
  MoveEntity Camera,0,-0.01,0 	
 EndIf	
 If KeyDown(205) Then
  MoveEntity Camera,0.01,0,0 	
 EndIf	
 If KeyDown(203) Then
  MoveEntity Camera,-0.01,0,0 	
 EndIf	
 If KeyDown(201) Then
  MoveEntity Camera,0,0,0.01 	
 EndIf	
 If KeyDown(209) Then
  MoveEntity Camera,0,0,-0.01 	
 EndIf	

 RenderWorld

;Just for showing that the occluder doesn't need
;to be a static object
 For OccludeList.Occluder = Each Occluder
   TurnEntity OccludeList\Mesh,0,0.1,0 
 Next


If UseOcclusion=1 Then
 For EntityList.Entity = Each Entity
  EntityList\Occluded=False
 Next
 For OccludeList.Occluder = Each Occluder
 ; Skip Updating the occluder if it is out of the view
  If EntityInView(OccludeList\Mesh,Camera) Then
   UpdateOccluderPlanes OccludeList\ID,OccludeList\Mesh,Camera
  EndIf
 Next

 For EntityList.Entity = Each Entity
; Skip to check if entity already is out of the viewfrustum
  If EntityInView(EntityList\Entity,Camera) Then
   OccludeEntity(EntityList,Camera)
  EndIf
 Next

EndIf

 TimeTaken=MilliSecs()-LastTime
 If TimeTaken>=1000 Then
 LastTime=MilliSecs()
 FPS=FrameCount
 FrameCount=0
 EndIf

 Text 0,10,"FPS:" + Str(FPS) 
 If UseOcclusion=0 Then
 Text 0,20,"Occlusion = " + "Off"
 Else
 Text 0,20,"Occlusion = " + "On"
 EndIf
 Text 0,30,"Rendered Tris:" + TrisRendered()
 Text 0,40,"Press Space To Toggle Occlusion"


 Flip False
Wend

End

; Functions

Function UpdateOccluderPlanes(OccID,occMesh,Cam)
; Loop through all Edges...
 FirstRun=True
 For EdgeList.Edge = Each Edge

If EdgeList\Parent=OccID Then

 TriVis1 = TriVisible(Cam,occMesh,EdgeList\surf,EdgeList\Tri1)
 TriVis2 = TriVisible(Cam,occMesh,EdgeList\surf,EdgeList\Tri2)

 If TriVis1 Xor TriVis2 = True Then 	 		; draw edge only if one triangle is visible and the other is hidden

  x#=VertexX#(EdgeList\Surf,EdgeList\A)
  y#=VertexY#(EdgeList\Surf,EdgeList\A)
  z#=VertexZ#(EdgeList\Surf,EdgeList\A)
  TFormPoint x#,y#,z#,occMesh,0
  CameraProject Cam,TFormedX#(),TFormedY#(),TFormedZ#()	
  EdgeList\X1=ProjectedX()
  EdgeList\Y1=ProjectedY()

  If FirstRun=True Then
   MaxX=EdgeList\X1:MinX=EdgeList\X1
   MaxY=EdgeList\Y1:MinY=EdgeList\Y1
   FirstRun=False
  Else
   If EdgeList\X1>MaxX Then MaxX=EdgeList\X1
   If EdgeList\X1<MinX Then MinX=EdgeList\X1
   If EdgeList\Y1>MaxY Then MaxY=EdgeList\Y1
   If EdgeList\Y1<MinY Then MinY=EdgeList\Y1
  EndIf

  x#=VertexX#(EdgeList\Surf,EdgeList\B)
  y#=VertexY#(EdgeList\Surf,EdgeList\B)
  z#=VertexZ#(EdgeList\Surf,EdgeList\B)
  TFormPoint x#,y#,z#,occMesh,0
  CameraProject Cam,TFormedX#(),TFormedY#(),TFormedZ#()	
  EdgeList\X2=ProjectedX()
  EdgeList\Y2=ProjectedY()

   If EdgeList\X2>MaxX Then MaxX=EdgeList\X2
   If EdgeList\X2<MinX Then MinX=EdgeList\X2
   If EdgeList\Y2>MaxY Then MaxY=EdgeList\Y2
   If EdgeList\Y2<MinY Then MinY=EdgeList\Y2

  EdgeList\Visible=True
  ;Line EdgeList\X1,EdgeList\Y1,EdgeList\X2,EdgeList\Y2
 Else
  EdgeList\Visible=False
 EndIf
EndIf
 Next

; Calculate Midpoint
 MidPointX=(MinX+MaxX)/2
 MidPointY=(MinY+MaxY)/2

; Make sure every edge is in clockwise order, if not, swap the edges vertices
 For EdgeList.Edge = Each Edge
If EdgeList\Parent=OccID Then
  If EdgeList\Visible=True Then
	
	If TriArea(EdgeList\X1,EdgeList\Y1,EdgeList\X2,EdgeList\Y2,MidPointX,MidPointY)<0 Then
	 TempHoldVertex=EdgeList\A
	 TempHoldX=EdgeList\X1
	 TempHoldY=EdgeList\Y1

	 EdgeList\A=EdgeList\B
	 EdgeList\X1=EdgeList\X2
	 EdgeList\Y1=EdgeList\Y2
	 EdgeList\B=TempHoldVertex
	 EdgeList\X2=TempHoldX
	 EdgeList\Y2=TempHoldY
	EndIf

	; Now calculate plane...
	TFormPoint VertexX#(EdgeList\Surf,EdgeList\A),VertexY#(EdgeList\Surf,EdgeList\A),VertexZ#(EdgeList\Surf,EdgeList\A),occMesh,0
	Vec1x#=EntityX#(Cam)-TFormedX#()
	Vec1y#=EntityY#(Cam)-TFormedY#()
	Vec1z#=EntityZ#(Cam)-TFormedZ#()
	TFormPoint VertexX#(EdgeList\Surf,EdgeList\B),VertexY#(EdgeList\Surf,EdgeList\B),VertexZ#(EdgeList\Surf,EdgeList\B),occMesh,0
	Vec2x#=EntityX#(Cam)-TFormedX#()
	Vec2y#=EntityY#(Cam)-TFormedY#()
	Vec2z#=EntityZ#(Cam)-TFormedZ#()
;	CrossProduct Vec1x#,Vec1y#,Vec1z#,Vec2x#,Vec2y#,Vec2z# 
	Cpx#=(Vec1y#*Vec2z#)-(Vec1z#*Vec2y#)
	Cpy#=(Vec1z#*Vec2x#)-(Vec1x#*Vec2z#)
	Cpz#=(Vec1x#*Vec2y#)-(Vec1y#*Vec2x#)
	;Normalize
	TempDist#= Sqr(Cpx#*Cpx# + Cpy#*Cpy# + Cpz#*Cpz#)
	Cpx#=Cpx#/TempDist#
	Cpy#=Cpy#/TempDist#
	Cpz#=Cpz#/TempDist#
	EdgeList\PlaneNX#=Cpx#
	EdgeList\PlaneNY#=Cpy#
	EdgeList\PlaneNZ#=Cpz#

	;EdgeList1\PlaneDist#=DotProduct(TFormedX#(),TFormedY#(),TFormedZ#(),CProd\x#,CProd\y#,CProd\z#)
	EdgeList\PlaneDist#=((TFormedX#()*Cpx#)+(TFormedY#()*Cpy#)+(TFormedZ#()*Cpz#))
			
  EndIf
EndIf
 Next

End Function

Function OccludeEntity(CurrentEntity.Entity,Camera)
; Now check if object is occluded or not...
 
 If CurrentEntity\Occluded=False Then ; If entity already is occluded, then skip this...
  
  For OccludeList.Occluder = Each Occluder
  ; Skip the occluder if it's out of the viewfrustum
   If EntityInView(OccludeList\Mesh,Camera)=False Then Goto SkipThisOccluder
    EntityHidden=True
  	For EdgeList.Edge = Each Edge
     If OccludeList\ID = EdgeList\Parent Then
   	  If EdgeList\Visible=True Then
       If ((EdgeList\PlaneNX#*EntityX#(CurrentEntity\Entity)+EdgeList\PlaneNY#*EntityY#(CurrentEntity\Entity)+EdgeList\PlaneNZ#*EntityZ#(CurrentEntity\Entity))-EdgeList\PlaneDist#+EntityList\Radius#)>-CurrentEntity\Radius# Then
        EntityHidden=False
       EndIf
      EndIf
     EndIf
    Next

    If EntityHidden=True Then
	 CurrentEntity\Occluded=True
	 ;EntityColor EntityList\Entity,255,0,0
	 HideEntity CurrentEntity\Entity 	
	 Exit
	Else
	 CurrentEntity\Occluded=False
	 ShowEntity CurrentEntity\Entity
	EndIf
.SkipThisOccluder
  Next

 EndIf
End Function


; Function for filling the edgelist with edges
Function BuildEdgeList(OccID,Entity)
 For Surfnr = 1 To CountSurfaces(Entity)
	Surf=GetSurface(Entity,Surfnr)
	For TriNr = 0 To CountTriangles(Surf)-1
		EdgeA=TriangleVertex(Surf,Trinr,0)
		EdgeB=TriangleVertex(Surf,Trinr,1)
		AddEdge(OccID,Surf,TriNr,EdgeA,EdgeB)
		
		EdgeA=TriangleVertex(Surf,Trinr,1)
		EdgeB=TriangleVertex(Surf,Trinr,2)
		AddEdge(OccID,Surf,TriNr,EdgeA,EdgeB)
		
		EdgeA=TriangleVertex(Surf,Trinr,2)
		EdgeB=TriangleVertex(Surf,Trinr,0)
		AddEdge(OccID,Surf,TriNr,EdgeA,EdgeB)
	Next
 Next


End Function

Function AddEdge(OccID,Surf,TriNr,A,B)
	EdgeFound = False
	For EdgeList.Edge = Each Edge
     If OccID = EdgeList\Parent Then		
	  ; Check if the given edge already exist in the list
	  ; In that case just supply the triangle index
	  ; The mesh must have shared vertices
	  If ((EdgeList\A = A And EdgeList\B = B) Or (EdgeList\A = B And EdgeList\B = A)) Then
	 	 EdgeFound = True
		 EdgeList\Tri2=TriNr
		 Exit
	  EndIf
     EndIf
	Next 

	 ; If the edge wasn't found, add it
	If EdgeFound = False Then
		EdgeList.Edge = New Edge
		EdgeList\A=A
		EdgeList\B=B
		EdgeList\Surf=Surf
		EdgeList\Tri1=TriNr
		EdgeList\Tri2=-1
		EdgeList\Parent=OccID
	EndIf

End Function


Function TriVisible(Camera,Entity,Surface,Triangle)

If Triangle=-1 Then
; This means that the edge has no adjacent triangle
; If this occurs the mesh isn't solid(closed) 
 Return False
EndIf

ax#=VertexX#(Surface,TriangleVertex(Surface,Triangle,0))
ay#=VertexY#(Surface,TriangleVertex(Surface,Triangle,0))
az#=VertexZ#(Surface,TriangleVertex(Surface,Triangle,0))
bx#=VertexX#(Surface,TriangleVertex(Surface,Triangle,1))
by#=VertexY#(Surface,TriangleVertex(Surface,Triangle,1))
bz#=VertexZ#(Surface,TriangleVertex(Surface,Triangle,1))
cx#=VertexX#(Surface,TriangleVertex(Surface,Triangle,2))
cy#=VertexY#(Surface,TriangleVertex(Surface,Triangle,2))
cz#=VertexZ#(Surface,TriangleVertex(Surface,Triangle,2))

; Get vectors
v1x#=ax#-bx#
v1y#=ay#-by#
v1z#=az#-bz#
v2x#=ax#-cx#
v2y#=ay#-cy#
v2z#=az#-cz#

; Get crossproduct
cpx#=(v1y#*v2z#)-(v1z#*v2y#)
cpy#=(v1z#*v2x#)-(v1x#*v2z#)
cpz#=(v1x#*v2y#)-(v1y#*v2x#)

; Transform vector according to entity and normalize
TFormNormal cpx#,cpy#,cpz#,Entity,0
cpx#=TFormedX#()
cpy#=TFormedY#()
cpz#=TFormedZ#()

; Now we got the triangles normal
; Take the DotProduct from above normal and a distance vector
; from the camera and one of the triangles vertices
TFormPoint ax#,ay#,az#,Entity,0
camvecx#=EntityX#(Camera)-TFormedX#()
camvecy#=EntityY#(Camera)-TFormedY#()
camvecz#=EntityZ#(Camera)-TFormedZ#()

dp#=((camvecx#*cpx#)+(camvecy#*cpy#)+(camvecz#*cpz#))

If dp#>0 Then
 Return True
Else
 Return False
EndIf

End Function

Function TriArea(X1,Y1,X2,Y2,X3,Y3)
D = X1*Y2 + X2*Y3 + X3*Y1 - X1*Y3 - X2*Y1 - X3*Y2
Return D
End Function

;
;Cross and DotProduct functions
Function CrossProduct(x1#,y1#,z1#,x2#,y2#,z2#)
	CProd\x#=(y1#*z2#)-(z1#*y2#)
	CProd\y#=(z1#*x2#)-(x1#*z2#)
	CProd\z#=(x1#*y2#)-(y1#*x2#)
End Function
Function DotProduct#(x1#,y1#,z1#,x2#,y2#,z2#)
	DProd=((x1*x2)+(y1*y2)+(z1*z2))
	Return DProd
End Function



fredborg(Posted 2003) [#2]
Interesting! It seems quite effective, but I haven't had time to look at the code.

Good work!


sswift(Posted 2003) [#3]
Haven't tried the code yet, but one thing you should check in any occlusion code is what happens if one entity intersects another. They should not occlude eachother.

...

Hm, impressive! :-)

Now to see how you're doing it. :-)

I don't suppose this works for arbitrary shapes, just cubes? I'm guessing just cubes. But even if it was just cubes, that's not bad. You'd just have to carefully set up occlusion cubes in your level.

Can those cubes be rotated to any orientation? How about scaled non-uniformly?


sswift(Posted 2003) [#4]
Hm... you can indeed rotate the cubes andy way you wish. And it kinda looks like you may actually be able to sue any shape for an occluder, but I assume the complexity of said shape has some singificant effect on the speed?


sswift(Posted 2003) [#5]
Looking at the code briefly, it appears that what you're doing is creating a frustrum or a "beam" (I think Camrack used that term to describe such a system once) which you generate dynamically for each occluding object each frame.

Here's an optimization you may or may not already have done... Test the colluders themselves against one another. If an occluder is occluded then everything occluded by it is also occluded by the closer volume, so you don't need to check if it is occluding anything. This will reduce the number of occluders you need to check to see if they are occluding anyhting if you have a lot of them overlapping, as you might in a real level.

This being the case, that you're building planes, I guess it depends on how optimized the frustrum check is and how expensive it is to generate planes for an object. It must be pretty expensive cause this is precisely the sort of thing you need to do for shadow casting in a stencil buffer based shadow system... except you do it once for each light.

Anyhow, quite a clever system. Probably been done before, but it's nice to see how fast it can be with simple occluders and a lot of entities.


Physt(Posted 2003) [#6]
That's a great occlution demo. Well done. That's just the sort of thing that would be great for a large city like level.


sswift(Posted 2003) [#7]
Here's another optimization idea.

Sort the occlusion volumes nearest to farthest before testing occlusion. Doing that in addition to the other optimization I suggested of checking to see if an occlusion volume itself is occluded, makes it more likely to cull other occluders sooner rather than later. Ie, if you check an occluder in the distance first and it then turns out to be occluded by another occluded, you've done unneccessary math.


sswift(Posted 2003) [#8]
One last suggestion.

An occluder should be smaller than an object is is standing in for. It should fit inside it.

But one can also make bounding volumes which are larger than the object they contains. If the bounding volume is not occluded then the object is likely to also not be occluded.

So you could first check the bounding volumes against the occluders, and then you can do a more accurate test if you wish.

Though if the object you're testing is very detailed it may not be economical to test the true volume of the object against the occluders, and just go with the approximation instead.


Rob(Posted 2003) [#9]
Thats pretty fantastic occlusion, sweenie! I hope you continue to develop it :)


Beaker(Posted 2003) [#10]
The big problem with this kind of occlusion method is that it relies on all occluders being convex, which is fine for levels made in Quill and CShop (as long as you can export without any surface combining going on). It probably needs to be tied in with with level creation tho.

I made a similar occlusion system a while back, and something I was thinking about was whether 'wall' quads could be detected and then used as occluders.


sswift(Posted 2003) [#11]
I see no reason why you could not automatically divide an abritrary level up into convex chunks.

I think you could just start with the first polygon and connect those that touch it in a chain until the angle between two surfaces is greater than or less than 180 degrees, whichever way you decide to calculate the angle.

Hm... but if you had a sphere with a cylinder cut out of it, then with this method the entire shape would be one occluder. Is that right? Seems like it works out.

And if you had a hole which only went halfway into the sphere, then the bottom cap of the hole would be a seperate occluder. Hm.... Not sure if that is neccessary or not, or if either of those is right. That would allow you to be inside of an occluder. Would that work right? Seems like it might.


Sweenie(Posted 2003) [#12]
I thought of two ways to determine if the occluder should be used or not.

1) Based on distance.
The occluder must be a certain distance from the camera before it's used.
The drawback of this is if the occluder is long, like a long wall or something.

2) Based on size.
As i'm using a TriangleArea routine to sort the edges in clockwise order I also automatically get the area of the occluders 2D shape. If i compare the screenarea with the occluderarea i can use the occluder based on a certain percentage.
For example, if the occluder covers more than 30% of the screen then use it.


The occluder can be any convex shape, but the more complex the occluder is the slower it gets.
I haven't tried quads yet, but with some modifications that can supported too. Currently the occludermesh must be a convex solid mesh with shared vertices.


Warren(Posted 2003) [#13]
We used this style of occlusion in UT2003. It's REALLY nice.

The distance based optimization you talked about is really worthwhile. Once it gets a certain distance from the camera, it's so small that it's not worth calculating the occlusion for it anymore.


Jim Teeuwen(Posted 2003) [#14]
mm the example code is still 120% faster without any occlusion.. Kinda makes you wonder what use it is..


Warren(Posted 2003) [#15]
Depends on the scene. If you're only occluding a few simple objects the time spent occluding them will be higher than the time you would spend to render them. Common sense, really.


Ice9(Posted 2003) [#16]
Very nice example. I haven't dug into the code yet but I was wondering, since the view frustrum is essentially a 6 sided object like the cube, if you could use that method for frustrum culling.

Sweenie
1) Based on distance.
The occluder must be a certain distance from the camera before it's used.
The drawback of this is if the occluder is long, like a long wall or something.

Sweenie rather distance it would seem frustrum would be the limiter. If everything is hidden outside the frustrum first and removed from any list for consideration it would seem that would speed things up.