[Help] Theorizing Motion Blur

Blitz3D Forums/Blitz3D Programming/[Help] Theorizing Motion Blur

Kryzon(Posted 2008) [#1]
Hello everyone,

After watching some videos of the currently-in-work Project Offset - which, by the way, looks to be awesomely-eye-candily-gorgeous - I was intrigued on how they managed to achieve a real-time motion blur solution "based on passes". It looks great, obviously, and I was enticed to find out how to achieve a similar result by using our all-familiar Blitz3D.
I know what you are going to say to me, "search in the code archives", as there are some (allegedly) motion blur simulations. What these really are, are actually Slow-Motion type of effects, or Ghosting, if you will. There hasn't been any true Motion Blur simulation - so far, these attempts waste processor time (ok, not so much), and aren't that much good looking either.

What I come to ask of you, is some pseudo-code as to what to do to accomplish a portraial of true-motion blur. So far, I've managed to think about having a series of plates (same as Quads or Sprites), all in the same physical position, but with 'complementary' alphas ( meaning each plate will have an alpha of 1/number of plates ). The first plate will always have the current render, the second will have the previous frame, the third, the frame before that, etc. - all of them adding up to create an alpha 1 solid picture, but with the motion blur effect. Well, in theory, that would work great, but when I tried to actually do it, let's just say I "epically failed".

Any comments/suggestions/acknowledgements/jokes?


Kryzon(Posted 2008) [#2]
Okay, here's what I got so far, using the Plate theory (i'm just using 2, though):



I guess no matter what you try, you'll always end up with those "ghosting trails".


nawi(Posted 2008) [#3]
Project Offset uses shaders. I think they are transforming the meshes and then blurring the textures in the direction of movement. Atleast, that is one way of doing proper motion blur.

Your "plate" technique is the default one on Blitz3D, but doesn't work that well as you can see.


Kryzon(Posted 2008) [#4]
I think what I really want lies in the good usage of the Tween parameter of the Renderworld() function. Using different values to obtain the necessary frames to average, instead of using the ones which are being shown to the user.

I wonder if rendering n times the screen will slow down the whole application. Some examples of CubeMap do 7 renders per loop (6 for the sides of the cubes, + 1 to show the screen) and with a nice FPS, so I guess we'll be able to do something like 4 passes of tweening to average and get the motion blur - thus utilizing the Accumulation Method.


Kryzon(Posted 2008) [#5]
Okay, next tryout, this time using render Tweening to multisample each frame:


That's using 8 plates.

My computer is slow, so I got a 10 FPS average. Looks like a no-no for realtime gameplay. With 3 plates, you get less Motion Blur but at least the framerate goes up to 63!

Gonna try it out on some 3D level. Post more pics later.


Danny(Posted 2008) [#6]
It's starting to look really good though. I'd like to see a texture version as well. Even if this will never be fast enough for realtime gaming, perhaps you can use it for slow-motion action shots!? Might be cool ;)


nawi(Posted 2008) [#7]
If you want 40fps for a game with 4 plates that means the computer must be fast enough to render it at about 160fps, so motion blur using this technique is a really slow way of doing it.. You can also use the previous frames etc but the result is just not that good.


nawi(Posted 2008) [#8]
I made a simple mesh transform motion blur. If someone would add a texture blur to this it might look okay. I'm not fully sure if my code is perfect though. The object is simply scaled in the direction of movement.

Graphics3D 800,600,0,2
SetBuffer BackBuffer()



cam = CreateCamera()
MoveEntity cam,0,0,-14



cube = CreateCube()
EntityShininess cube,0.8


light = CreateLight(3)
MoveEntity light,100,100,100
PointEntity light,cube

Dim Pos#(2)

motionblur = 1
Repeat


	angle = angle + 4
	PositionEntity cube,Cos(angle)*5,0,0
	RotateMesh cube,1,1,1
	
	
	currentx# = EntityX#(cube)
	currenty# = EntityY#(cube)
	currentz# = EntityZ#(cube)
	scalex# = 1.0+Abs(currentx#-Pos#(0))
	scaley# = 1.0+Abs(currenty#-Pos#(1))
	scalez# = 1.0+Abs(currentz#-Pos#(2))

	If KeyHit(57) Then motionblur=1-motionblur
	UpdateWorld
	If motionblur
		ScaleEntity cube,scalex#,scaley#,scalez#
		PositionEntity cube,(currentx#+Pos#(0))*0.5,(currenty#+Pos#(1))*0.5,(currentz#+Pos#(2))*0.5
	EndIf
	RenderWorld
	If motionblur Then 
		ScaleEntity cube,1/scalex#,1/scaley#,1/scalez#
		Text 0,0,"Press space - MOTION BLUR = ON"
		Text 0,15,"Scale factor ("+scalex#+","+scaley#+","+scalez#+")"
	Else
		Text 0,0,"Press space - MOTION BLUR = OFF"
	EndIf
	Flip

	
	Pos#(0) = currentx#
	Pos#(1) = currenty#
	Pos#(2) = currentz#
Until KeyHit(1)



Ross C(Posted 2008) [#9]
Remember to do:

Flip 0


To enable full speed :o)


nawi(Posted 2008) [#10]
Full speed was useless on that demo which is why I didn't enable that. By the way, I have another idea for a proper motion blur, which I might code soon.


chwaga(Posted 2008) [#11]
couldn't you just create a 20x20 plane in front of the camera that grabs the render using copyrect, puts it on the cube, then transform random vertices in the direction of the blur?

[edit]
actually, now I think about it, it would have to be a very dense plane, I'm not sure how practical that would be in realtime...
[/edit]


nawi(Posted 2008) [#12]
Because in motionblur there are several objects going in different directions. Well, you could use that but I'm not sure it looks good at all.


nawi(Posted 2008) [#13]
Okay, I've created my new motionblur. It actually blurs the pixels in the direction of motion, so the effect looks quite similar like on Crysis and other new games.

Good:
*Proper motionblur

Bad:
*Slow
*Jagged corners on models

Here's a screenshot. (The gray wall is just a test since it works even if the model is partially behind objects).



EDIT: Optimized code
;Motionblur test by naw

;You need fighter.3ds and fighter.jpg from \blitz3d\Media\geometricks_models\fighter
;You can also use your own model

; ***************** For iterating all entities ***********************************
; Userlib declaration: RTLMoveMemory2 from the kernel32.dll is used, 
; you need at least the following 2 lines in your kernel32.decls:
; .lib "kernel32.dll" 
; RtlMoveMemory2%(Destination*,Source,Length) : "RtlMoveMemory"
; Note: The pivot that is created in the init section must be the first entity
; that was created or loaded. You may have to recreate it after a "ClearWorld()"
; ********************************************************************************


;Const
Const GfxWidth = 800,GfxHeight = 600

AppTitle "Motionblur test"
Graphics3D GfxWidth,GfxHeight,0,2
SetBuffer BackBuffer()


;Type
Type Ent
	Field ID
	Field Brush
	Field MB
End Type

Cam = CreateCamera()
MoveEntity Cam,0,0,-10

; init Cycles entity iteration------------------------
Global Cycle_bank=CreateBank(16)
Const  Cycle_NextEntity=4
Const  Cycle_LastEntity=8
Global Cycle_FirstEntity=CreatePivot()
Global Cycle_CurrentEntityPointer=Cycle_FirstEntity
;----------------------------------------------------






Obj = LoadMesh("fighter.3ds")
NameEntity Obj,"mb_"+EntityName$(Obj)
ScaleEntity Obj,0.01,0.01,0.01
EntityFX Obj,1
EntityShininess Obj,1.0
TurnEntity Obj,0,90,0

Wall = CreateCube()
ScaleEntity Wall,5,1,0.1
MoveEntity Wall,0,-1,0

WhiteTexture = CreateTexture(1,1)
SetBuffer TextureBuffer(WhiteTexture)
Plot 0,0
SetBuffer BackBuffer()

BlackBrush = CreateBrush()
BrushFX BlackBrush,1
BrushColor BlackBrush,0,0,0
BrushTexture BlackBrush,WhiteTexture

WhiteBrush = CreateBrush()
BrushFX WhiteBrush,1
BrushColor WhiteBrush,255,255,255
BrushTexture WhiteBrush,WhiteTexture

;Create entities
While MoreEntities()
		ob.Ent = New Ent
		ob\ID = NextEntity()
		ob\Brush = GetEntityBrush(ob\ID)
		If Left$(EntityName$(ob\ID),3) = "mb_" Then ob\MB=1
Wend

OldProjectedX# = 0.0
OldProjectedY# = 0.0

Repeat
	If KeyDown(200) Then MoveEntity Obj,0,0,-0.2
	If KeyDown(203) Then TurnEntity Obj,0,2,0
	If KeyDown(205) Then TurnEntity Obj,0,-2,0
	If KeyDown(208) Then MoveEntity Obj,0,0,0.2
	If KeyDown(30)  Then TurnEntity Obj,1.5,0,0
	If KeyDown(44)  Then TurnEntity Obj,-1.5,0,0
	RotateEntity Obj,EntityPitch#(Obj),EntityYaw#(Obj),0

	CameraProject Cam,EntityX#(Obj),EntityY#(Obj),EntityZ#(Obj)
	
	DeltaX# = Abs(ProjectedX#()-OldProjectedX#)
	DeltaY# = Abs(ProjectedY#()-OldProjectedY#)
	dist# = Sqr(DeltaX#*DeltaX#+DeltaY#*DeltaY#)
	Angle# = ATan2(DeltaY#, DeltaX#)
	OldProjectedX# = ProjectedX#()
	OldProjectedY# = ProjectedY#()


	FpsCalc = FpsCalc + 1
	If MilliSecs()>FpsTimer+999 Then
		FpsTimer = MilliSecs()
		Fps = FpsCalc
		FpsCalc = 0
	EndIf

	;Render motionblur
	UpdateWorld
	For ob.Ent = Each Ent
		If ob\MB Then
			PaintEntity ob\ID,WhiteBrush
		Else
			PaintEntity ob\ID,BlackBrush
		EndIf
	Next
	
	Bank = CreateBank(GfxWidth*GfxWidth*0.25+GfxHeight)
	ScreenBank = CreateBank((GfxWidth*GfxWidth+GfxHeight)*4)
	
	lowx = GfxWidth-1
	lowy = GfxHeight-1
	maxx = 0
	maxy = 0
	RenderWorld
	;Read mask
	LockBuffer
		For y=0 To GfxHeight*0.25-1
			For x=0 To GfxWidth*0.25-1
				If ReadPixelFast(x*4,y*4) = $FFFFFFFF Then
					If x<lowx Then lowx = x
					If y<lowy Then lowy = y
					If x>maxx Then maxx = x
					If y>maxy Then maxy = y
					PokeByte Bank,x*GfxWidth*0.25+y,1
				EndIf
			Next
		Next
	UnlockBuffer
	
	For ob.Ent = Each Ent
		PaintEntity ob\ID,ob\Brush
	Next

	RenderWorld
	
	If DeltaX#+DeltaY#>0.0
	LockBuffer
	Local nr,ng,nb,dx,dy
	For y=lowy To maxy
		For x=lowx To maxx
			If PeekByte(Bank,x*GfxWidth*0.25+y) = 1 Then
				For kx=0 To 3
					For ky=0 To 3
						iter# = 0.0
						r=0:g=0:b=0
						Repeat
							tx# = (x*4)+kx + Cos(Angle#)*iter#
							ty# = (y*4)+ky + Sin(Angle#)*iter#
							If tx<0 Or ty<0 Or tx>GfxWidth-1 Or ty>GfxHeight-1 Then Exit
							iter# = iter#+1
							If PeekByte(ScreenBank,(Int(tx)*GfxWidth+ty)*4+3) = 0 Then
								rgb = ReadPixelFast(tx,ty)
								nr = ((rgb Shr 16) And $ff )
								ng = ((rgb Shr 8) And $ff )
								nb = (rgb And $ff)
								r=r+nr
								g=g+ng
								b=b+nb
								PokeByte ScreenBank,(Int(tx)*GfxWidth+ty)*4+0,nr
								PokeByte ScreenBank,(Int(tx)*GfxWidth+ty)*4+1,ng
								PokeByte ScreenBank,(Int(tx)*GfxWidth+ty)*4+2,nb
								PokeByte ScreenBank,(Int(tx)*GfxWidth+ty)*4+3,1
							Else
								r = r+PeekByte(ScreenBank,(Int(tx)*GfxWidth+ty)*4+0)
								g = g+PeekByte(ScreenBank,(Int(tx)*GfxWidth+ty)*4+1)
								b = b+PeekByte(ScreenBank,(Int(tx)*GfxWidth+ty)*4+2)
							EndIf
						Until iter#>dist#
						rgb=(Int(b/iter#) Or (Int(g/iter#) Shl 8) Or (Int(r/iter#) Shl 16) Or ($ff000000))
						dx = x*4+kx
						dy = y*4+ky
						If dx<GfxWidth And dy<GfxHeight And dx>0 And dy>0 Then
							WritePixelFast x*4+kx,y*4+ky,rgb
						EndIf
					Next
				Next
			EndIf
		Next
	Next
	UnlockBuffer
	EndIf
	
	Text 0,0,"FPS: "+Fps
	Text 0,15,"Delta " + DeltaX# + ","+DeltaY#
	Flip 0
	
	FreeBank Bank
	FreeBank ScreenBank
Until KeyHit(1)



;Functions to iterate all entities
Function MoreEntities() ; check if there are further entities
 RtlMoveMemory2(Cycle_bank,Cycle_CurrentEntityPointer+Cycle_NextEntity,4)
 If PeekInt(Cycle_bank,0)<>0
  Return True
 Else
  Cycle_CurrentEntityPointer=Cycle_FirstEntity
 EndIf
End Function


Function NextEntity()
 Local entity
 RtlMoveMemory2(Cycle_bank,Cycle_CurrentEntityPointer+Cycle_NextEntity,4)
 entity=PeekInt(Cycle_bank,0)
 Cycle_CurrentEntityPointer = entity
 Return entity
End Function



Kryzon(Posted 2008) [#14]
Nice coding Nawi, that looks really good!

My mind is not really good to read codes right now, so if I may ask you, how are you using WritePixelFast for realtime, in that 'ScreenBank'?


nawi(Posted 2008) [#15]
I read the pixels to a memory bank so I don't have to use readpixelfast more than once per each pixel. When I'm done calculating the new pixel value (it is just an average from many pixels) I just draw it with WritePixelFast.


Buggy(Posted 2008) [#16]
Where can I find that kernel file? And why is it needed?


nawi(Posted 2008) [#17]
Because it uses the RtlMoveMemory2 function which Blitz doesn't have. You can just create the file and paste those 2 lines. You need to place the file in the directory userlibs, and restart Blitz3D.


Buggy(Posted 2008) [#18]
Hmm... I'm no good at these things, and can't get it to work. What, exactly, do I need to do?


nawi(Posted 2008) [#19]
Create a file called kernel32.decls using notepad, paste two lines:
.lib "kernel32.dll" 
RtlMoveMemory2%(Destination*,Source,Length) : "RtlMoveMemory"

and save to Blitz3d\Userlibs


Buggy(Posted 2008) [#20]
I had already done that, and I still get a "Function 'rtlmovememory2' not found" error. Do I also need a file called kernel32.dll?


Buggy(Posted 2008) [#21]
Okay, got it. It turns out Notepad created kernel32.decls.txt instead of just the decls file. So this just moves pixels in the texture? It looks nice, but with that speed I'm not sure if it has a practical application at the moment...


Kryzon(Posted 2008) [#22]
I'm working on a DLL for averaging my subframes, as blitz's speed isn't allowing it for realtime.

There will be 3 functions:

MB_Prepare() ;initializes the motion blur. The parameter is the BackBuffer() handle (simply typing in BackBuffer() inside the parentheses will work)

MB_CaptureBuffer() ;no arguments here, it simply captures the backbuffer inside a loop where i'm rendering each tween\subframe to average in the end. How many times you loop will be the amount of subframes you'll include in the final image. The more subframes, the smoother the motion blur, but slower FPS in the end.

MB_Render() ;after averaging all the images, this function copies the content of the averaged image to the backbuffer. Next on the main loop, right after MB_Render, will be the FLIP command that'll show the result and go back to the start of your main loop.

Just a matter of time before I finish it :D