
I’ve added the Camera object to the ABExtDrawing library for Basic4Android. This object must not be confused with the hardware camera. There are other libraries available for that. The new version 1.1 of the library can be downloaded from the B4X website.
The camera I’m talking about is a nice feature you can use to do 3D effects on the canvas without using OpenGL. The picture above is just a noral B4A canvas where we did some rotations and translations on. It simulates a scrolling list where the listitems rotate around their axe.
Additional, I shows other features of the ABExtDrawing library to do some lighting effects.
Let me show you how this is done in B4A.
We create a new type item3D. This will hold one item in the list.
Sub Process_Globals
Type item3D (bmp As Bitmap, Top As Int, Left As Int, Width As Int, Height As Int)
End Sub
We also have to declare some variables. Note our mCamera variable which will do the 3D conversions and some constants for our lighting effects. On a Paint object we can set color filters which will affect the color values of what we draw with that Paint object. SetLightingColorFilter takes care of that. A LightingColorFilter takes two colors that are used to modify the colors that we are drawing. The first color will be multiplied with the colors we draw, while the second one will be added to the colors we draw. The multiplication will darken the color and adding will make it brighter so we can use this class to model both shadows and highlights. It would have been even better if instead of adding it would have implemented the screen blend mode, but add works OK.
To actually calculate the light we’ll use a simplified version of Phong lighting.
Sub Globals
'These global variables will be redeclared each time the activity is created.
'These variables can only be accessed from this module.
Dim items As List
Dim HalfHeight As Float
Dim SCALE_DOWN_FACTOR As Float: SCALE_DOWN_FACTOR = 0.15
Dim DEGREES_PER_SCREEN As Int: DEGREES_PER_SCREEN = 270
' Ambient light intensity
Dim AMBIENT_LIGHT As Int: AMBIENT_LIGHT = 55
' Diffuse light intensity
Dim DIFFUSE_LIGHT As Int: DIFFUSE_LIGHT = 200
' Specular light intensity
Dim SPECULAR_LIGHT As Float: SPECULAR_LIGHT = 70
' Shininess constant
Dim SHININESS As Float: SHININESS = 200
' The Max intensity of the light
Dim MAX_INTENSITY As Int: MAX_INTENSITY = 0xFF
Dim CurrentRotation As Int
Dim CurrentTop As Int
Dim MyCanvas As Canvas
Dim ScreenTop As Int: ScreenTop = 1
Dim Panel1 As Panel
Dim ExDraw As ABExtDrawing
Dim mCamera As ABCamera
Dim mMatrix As ABMatrix
Dim mPaint As ABPaint
Dim PI As Double: PI= 3.141592653589793238462643383279502884197
Dim ClearRect As Rect
Dim CurrY As Int
End Sub
In the Activity_Create sub we initialize mCamera and the other variables like mPaint which we will also need for our lighting. Also our pictures are preloaded.
Sub Activity_Create(FirstTime As Boolean)
Activity.LoadLayout("1")
MyCanvas.Initialize(Panel1)
HalfHeight = Activity.Height / 2
Dim backbmp As Bitmap
backbmp.Initialize(File.DirAssets, "background.png")
Dim backbmp2 As Bitmap
backbmp2.Initialize(File.DirAssets, "background2.png")
Dim backbmp3 As Bitmap
backbmp3.Initialize(File.DirAssets, "background3.png")
Dim conbmp As Bitmap
conbmp.Initialize(File.DirAssets, "contact_image.png")
Dim conbmp2 As Bitmap
conbmp2.Initialize(File.DirAssets, "contact_image2.png")
Dim conbmp3 As Bitmap
conbmp3.Initialize(File.DirAssets, "contact_image3.png")
items.Initialize
Dim i As Int
Dim random As Int
For i = 0 To 19
random = Rnd(0,3)
If random = 0 Then
items.Add(CreateNewItem(backbmp, conbmp, "Colleague " & i, "Name of colleague " & i, 25, i*175, Activity.Width - 50, 120))
Else
If random = 1 Then
items.Add(CreateNewItem(backbmp2, conbmp2, "Friend " & i, "Name of friend " & i, 25, i*175, Activity.Width - 50, 120))
Else
items.Add(CreateNewItem(backbmp3, conbmp3, "Client " & i, "Name of the client " & i, 25, i*175, Activity.Width - 50, 120))
End If
End If
Next
CurrentRotation = -(DEGREES_PER_SCREEN * ScreenTop) / Activity.Height
ClearRect.Initialize(0,0,Activity.Width, Activity.Height)
mCamera.Initialize
mMatrix.Initialize
mPaint.Initialize
mPaint.SetAntiAlias(True)
mPaint.SetFilterBitmap(True)
DrawMe
End Sub
The sub CreateNewItem() is used to make one list item. Default B4A canvas drawing functions are used to show how well they work together with the ABExtDrawing functions.
Sub CreateNewItem(Background As Bitmap, icon As Bitmap, Subj As String, desc As String, Left As Int, Top As Int, Width As Int, Height As Int) As item3D
Dim item As item3D
Dim c As Canvas
item.Initialize
item.bmp.InitializeMutable(Width, Height)
c.Initialize2(item.bmp)
' background
Dim dstR As Rect
dstR.Initialize(0,0,Width,Height)
c.DrawBitmap(Background,Null, dstR)
' draw Icon
dstR.Initialize(15,15, icon.Width, icon.Height)
c.DrawBitmap(icon, Null, dstR)
c.DrawText(Subj, 100, 30,Typeface.DEFAULT_BOLD,16, Colors.White,"LEFT")
c.DrawText(desc, 100, 60, Typeface.DEFAULT,16, Colors.White,"LEFT")
item.Top = Top
item.Left = Left
item.Width = Width
item.Height = Height
Return item
End Sub
In the drawItem() sub all calculations are done for one item in the list. Each item will be a block that will rotate around its X-axis and look like it is rolling on the ground when the list stars to scroll. Each block will be as wide as the item normally is and the depth will be the same as the height. We’ll use the same bitmap for all the sides.
So what do we need to do to achieve this effect? In order to draw the blocks we need to draw the bitmap two times (since we will almost always see two sides of the block). We also need to have some kind of rotation variable to keep track of the main rotation. Since the blocks should rotate when the user scrolls the list and the blocks should have the same rotation (so that they all face up at the same time, see further).
Sub DrawItem(item As item3D)
Dim CenterX As Float
Dim CenterY As Float
' get centerX AND centerY
CenterX = item.Width / 2
CenterY = item.Height / 2
' get scale
Dim distFromCenter As Float
distFromCenter = (item.Top + CenterY - HalfHeight) / HalfHeight
Dim scale As Float
scale = (1 - SCALE_DOWN_FACTOR * (1 - Cos(distFromCenter)))
' get rotation
Dim RotationX As Float
RotationX = CurrentRotation - 20 * distFromCenter
RotationX = RotationX Mod 90
If (RotationX < 0) Then
RotationX = RotationX + 90
End If
' draw it
If (RotationX < 45) Then
drawFace(item, CenterX, CenterY, scale, RotationX - 90)
drawFace(item, CenterX, CenterY, scale, RotationX)
Else
drawFace(item, CenterX, CenterY, scale, RotationX)
drawFace(item, CenterX, CenterY, scale, RotationX - 90)
End If
End Sub
Finally, DrawFace is called and this is where the magic happens. Worth noting is that the code that will draw one face of the block is the same, it just depends on the rotation, so it’s extracted to a method. To draw a complete block we then simply draw two faces 90 degrees apart at the same place.
To draw a face we first translate the camera so that the face will be drawn closer to us. Then we rotate it and after that we translate it back so we don’t scale it. Keep in mind that the calls to the camera, just like the rotate, translate and scale methods on Canvas, needs to be written in reversed order, so to speak. In the code below, it is the last line that translates the face towards us, then we rotate it, and finally, with the first line, we translate it back.
mCamera.translate(0, 0, centerY);
mCamera.rotateX(rotation);
mCamera.translate(0, 0, -centerY);
The rest of drawFace is not that hard. It gets the matrix from the camera, pre and post translates the matrix and then draws the bitmap with the matrix.
This code will draw each item as if placed in the origin in 3D space and then we move the items to the correct place on the screen using pre and post translate on the matrix. This moves what we draw in 2D space without changing the perspective. We could apply the translation in X and Y on the camera instead, then the translation would be in 3D space and it would affect the perspective. We’re not doing that here because I want the appearance of a larger field of view than the fixed field of view of the camera. Instead, we fake it by slightly rotating and scaling the items depending on the distance from center of the screen.
We calculate the light and create a LightingColorFilter that we can set to our Paint object.
Sub drawFace(item As item3D, CenterX As Float, CenterY As Float, scale As Float, RotationX As Float)
' save the camera state
mCamera.save
' translate AND Then rotate the camera
mCamera.translate(0, 0, CenterY)
mCamera.rotateX(RotationX)
mCamera.translate(0, 0, -CenterY)
' get the matrix from the camera AND Then restore the camera
mCamera.getMatrix(mMatrix)
mCamera.restore()
' translate AND scale the matrix
mMatrix.preTranslate(-CenterX, -CenterY)
mMatrix.postScale(scale, scale)
mMatrix.postTranslate(item.left + CenterX, item.top + CenterY)
' set the light
Dim cosRotation As Double
cosRotation = Cos(PI * RotationX / 180)
Dim intensity As Int
intensity = AMBIENT_LIGHT + (DIFFUSE_LIGHT * cosRotation)
Dim highlightIntensity As Int
highlightIntensity = (SPECULAR_LIGHT * Power(cosRotation,SHININESS))
If (intensity > MAX_INTENSITY) Then
intensity = MAX_INTENSITY
End If
If (highlightIntensity > MAX_INTENSITY) Then
highlightIntensity = MAX_INTENSITY
End If
Dim light As Int
light = Colors.rgb(intensity, intensity, intensity)
Dim highlight As Int
highlight = Colors.rgb(highlightIntensity, highlightIntensity, highlightIntensity)
mPaint.SetLightingColorFilter(light, highlight)
' draw the Bitmap
ExDraw.drawBitmap4(MyCanvas, item.bmp, mMatrix, mPaint)
End Sub
The DrawMe() sub is the overall function to draw all the items.
Sub DrawMe()
Dim i As Int
ExDraw.save2(MyCanvas, ExDraw.MATRIX_SAVE_FLAG)
MyCanvas.DrawRect(ClearRect, Colors.Black, True, 1dip)
'MyCanvas.DrawBitmap(FormBack, Null, formR)
For i = 0 To items.Size - 1
DrawItem(items.Get(i))
Next
ExDraw.restore(MyCanvas)
Panel1.Invalidate
End Sub
And in the Panel1_Touch sub we’ll animate our list. Note that it is here that we make sure all boxes face the same. This is done by the lines:
ScreenTop = ScreenTop + DeltaY
CurrentRotation = -(DEGREES_PER_SCREEN * ScreenTop) / Activity.Height
Doing like this will make the blocks rotate DEGREES_PER_SCREEN degrees when the user scrolls the list an entire screen no matter the pixel-height of the screen.
Here is the full sub:
Sub Panel1_Touch (Action As Int, X As Float, Y As Float) As Boolean 'Return True to consume the event
Dim DeltaY As Int
Select Action
Case Activity.ACTION_DOWN
CurrY = Y
'Log("down")
Case Activity.ACTION_MOVE
DeltaY = Y - CurrY
CurrY = Y
Dim i As Int
Dim it As item3D
For i = 0 To items.Size - 1
it = items.Get(i)
it.Top = it.Top + DeltaY
items.Set(i, it)
Next
ScreenTop = ScreenTop + DeltaY
CurrentRotation = -(DEGREES_PER_SCREEN * ScreenTop) / Activity.Height
DrawMe
'Log("move")
Case Activity.ACTION_UP
'Log("up")
End Select
Return True
End Sub
So this concludes this tutorial. You can download the project from http://www.gorgeousapps.com/AB3DCamera.zip
Note that at the first run, it may not work very smooth on some devices. But after some time it goes very well.
Have fun programming and until next time!
Click here to
if you like my work
Like this:
Like Loading...