This series of article will describe software rendering of 2D and 3D graphic. This will help you to understand how graphic rendering works under the hood. We will try to create engine in C++ with JNI connectors for java and android.
If your are not familiar with JNI keep the same package and artifact name, wrong naming between java class nad C++ will cause JNI native method not invoking.
First create root folder for our three projects (c++, java, android). My root folder name is "SoftwareRender". Now open Intellij and create new project, specify project location to : SoftwareRender/JavaSoftwareRender, set following project parameter
package | pl.duch.article |
artifact | SoftwareRender |
create java class : pl.duch.articles.renderlib.NativeBitmap and insert following code :
We have create hidden constructor that will set memory handler, bitmap type and bitmap dimensions (width and height). To create bitmap we will use static create method that will invoke native c++ that will return memory pointer to java. This memory pointer will be used in NativeBitmap constructor. Static create method will also specify bitmap type, so for example createArgb8888(w, h) will create 32bpp bitmap with selected dimensions. Our java bitmap will only store basic information, pixel will be stored inside native code.
For now we have declared two native methods
First one will allocate memory for bitmap, second one will allow us to dispose bitmap and retrieve memory. These method are static methods and all our native method will be static This method(except allocate) will use handler received from allocateBitmap to identify on with bitmap we are executing method.
To keep it OOP we have wrap method for each native method,
OOP method free will use static method with handler stored inside java object.
Open netbeans and create new c++ project, select dynamic library and set project location to SoftwareRender/library/softrender and project name to softrender
Open project properties and go to "C++ compiler" and set include directory to $JAVA_HOME/include $JAVA_HOME/include/darwin/ make sure you have JAVA_HOME environment variable
Open terminal/console and go to "SoftwareRender/JavaSoftwareRender/src/main/java" from this location execute :
javah -jni pl.duch.articles.renderlib.NativeBitmap
This should create file : pl_duch_articles_renderlib_NativeBitmap.h
In netbeans project create new header file with this name, and copy content from generated file to file in netbeans.
This way we are sure that netbeans will see this file in project. If you just copy file netbeans project might be invalid.
You can remove pl_duch_articles_renderlib_NativeBitmap.h from "SoftwareRender/JavaSoftwareRender/src/main/java"
This file should look like this :
We have definitions for required method, now we need to create implementation.
Create pl_duch_articles_renderlib_NativeBitmap.cpp and insert this code :
This will define two stubs, allocateBitmap method will always return 0xBAD as pointer. We will use this value to check if integration java with c++ works ok. Second method do nothing.
We will create actual implementations for this method later during this tutorial.
Execute "build" from netbeans, if you name project like I you should have library with name :
softrender.
Where extension depend on your pc, for example valid names : softrender.so, softrender.dynlib
Open intellij project, and create class pl.duch.articles.renderlib.Main
Execute main method it will fail with exception java.lang.UnsatisfiedLinkError but Intellij will create for you
Run configuration, open it end set VM option "-Djava.library.path=" to location of softrender.* file, for example
-Djava.library.path=/user/dybuk87/SoftwareRender/library/softrender/dist/Debug/GNU-MacOSXIf you setup everything correctly execute program again, expected output :
SUCCESS: true
This mean we have loaded our library, and we got 0xBAD handler after invoking createArgb8888 so we know that native method was executed and successfully return value.
Not available. Coming soon.
Basically images are represented by array of numbers, each number describe color for each pixel. Image with resolution ResX on ResY can be represented by array with size ResX * ResY of numbers. Number size specify how many colours we can use in image, from this point we will call it BPP(bits per pixel) .
Most common are:
BPP | Color count | Bitmap size in bytes |
---|---|---|
1 | 2 | ResX * ResY / 8 |
2 | 4 | ResX * ResY / 4 |
4 | 16 | ResX * ResY / 2 |
8 | 256 | ResX * ResY |
16 | 65'536 | ResX * ResY * 2 |
24 | 16'777'216 | ResX * ResY * 3 |
32 | 4'294'967'296 | ResX * ResY * 4 |
For example Full HD (1920x1080) image in 32 BPP require array of 1920*1080 * 4 bytes, this is almost 8MB for one image(!)
Most implementation of graphic engines store image as 1D tensor (one dimension array - vector), because image are 2D array they need to be "translate" to 1D. To translate 2D image to 1D we simply store each line one by one next to each other, this is simple example for image 4x3 pixels :
P0,0 | P1,0 | P2,0 | P3,0 |
P0,1 | P1,1 | P2,1 | P3,1 |
P0,2 | P1,2 | P2,2 | P3,2 |
P0,0 | P1,0 | P2,0 | P3,0 | P0,1 | P1,1 | P2,1 | P3,1 | P0,2 | P1,2 | P2,2 | P3,2 |
Pixel indexing is done by couple (X, Y) - (COLUMN / ROW) number, Top - left pixel index is (0,0), right - bottom (ResX-1, ResY-1).
To get pixel(x,y) position inside tensor we simply calculate x + y * ResX, y-th line require to skip "y" full rows, that is why we need to multiply y by Resolution X.
We now how to store image in memory, now we need a way to transform colour number into actual colour.
Computer describe color with RGB(Red - green - blue) values, size of each field depend on total bpp.
- 16 BPP usually use 5R 6G 5B model, this is 5 bits for red, 6 bits for green, and 5 bits.
- 24 BPP use format 8R 8G 8B, each color channel have 8 bits.
- 32 BPP use format 8A 8R 8G 8B, additional 4th value is alpha or not used. This use more memory than 24 bpp but each pixel fully fit memory alignment
Colors with BPP lower or equal 8 use palette to describe colour. Palette is just an array of color definitions in 24bpp or 32bpp, number from image is used as an index to palette. Simple example for 3BPP image :
Palette define four colours: Black (index: 0), Red (index: 1), Green(index: 2), White(index: 3) as 24 bpp :
(0,0,0,0) | (255, 0, 0) | (0, 255, 0) | (255, 255, 255) |
Lets define image as :
3 | 3 | 3 | 3 |
3 | 1 | 1 | 3 |
3 | 0 | 0 | 3 |
3 | 2 | 2 | 3 |
This is memory representation for simple image, to get actual color to render we need to use palette
pa,l[3] | pa,l[3] | pa,l[3] | pa,l[3] |
pa,l[3] | pa,l[1] | pa,l[1] | pa,l[3] |
pa,l[3] | pa,l[0] | pa,l[0] | pa,l[3] |
pa,l[3] | pa,l[2] | pa,l[2] | pa,l[3] |
(255, 255, 255) | (255, 255, 255) | (255, 255, 255) | (255, 255, 255) |
(255, 255, 255) | (255, 0, 0) | (255, 0, 0) | (255, 255, 255) |
(255, 255, 255) | ( 0, 0, 0) | ( 0, 0, 0) | (255, 255, 255) |
(255, 255, 255) | ( 0, 0, 255) | ( 0, 0, 255) | (255, 255, 255) |
4. Image implementation
Lets define our image class as follow :
We have Constructor that allocate memory to handle bitmap resX on resY with desired bpp. This basic class will support bpp = { 2, 4, 8, 16, 24, 32 }
setColor / getColor will allow to set/get pixel(x,y)
dump will "print" image to standard output, withPal parameter is used for bitmap withPal (bpp <= 8), when set to true it will translate colors by palette.
lets have, a look on each method:
First we save bitmap parameters, we will need them later to access data.
Next we have to calculate required memory (totalSize) and allocate array to store that data
Third step is to create palette for bitmap with bpp <= 8, color count is simply calculated with 2bpp. Palette is initialized to grayscale where 0 is black and (palSize-1) color is set to white.
Now we need to have a way to access pixel, for every bitmap type index is calculated by formula index = x + y * ResX
This index need to be translated to actual byte index, this conversion depend on bpp, since we will support bpp = { 2, 4, 8, 16, 24, 32 } we will split support for two types:
a) Bpp that share bytes (more than one pixel in byte) - bpp = { 2, 4 }
b) Pixel use full bytes bpp = { 8, 16, 24, 32 }
c) We will not support weird types like bpp = { 3, 5, 7, ... } this types will require special handling
Bpp = 2 or 4 will fit more than one pixel inside byte, to calculate pixel count in one byte we can simply divide 8 by bpp
pixInByteCount = 8 / 2 => 4;
pixInByteCount = 8 / 4 => 2;
To get pixel byte offset we will simply divide index by picInByteCount
byteNo = index / pixInByteCount = index / (8 / bpp) = index * bpp / 8;
We now with byte to use now we have to find out pixel position inside this byte, to get that value we will use previous formula, but insted of divide by 8 we will do modul 8 operation. Value % 8 can bi simplify to (value & 7).
int offsetInByte = (index * bpp) & 7;
Now we know exact position(byte and bit index) of pixel data, last step is to compute bit mask. Bit mask is used to extract only bpp bits from source byte. To do that we will use logic and operation with all interesting bits set to 1. Example, we will extract only bits from 2 to 5:
1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 |
0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |
0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 |
To create mask for BPP bits we will simply compute max 2bpp - 1
a) 2 BPP => 22 - 1 => 3 => 11b
b) 4 BPP => 24 - 1 => 15 => 1111b
Finally we can create code for get color when bpp < 8
Bpp >= 8 will fit more than one byte, but since we only use BPP = { 8, 16, 24, 32 } they will be aligned to byte so no need to work on bit mask, all we have to do is compute byte position and copy required amount of bytes.
ByteNo is calculated in the same way as for Bpp < 8:
byteNo = index * bpp / 8;
to get byte sie for each bpp we simply divide bpp by 8 a) 8 BPP / 8 = 1 byte b) 16 BPP / 8 = 2 byte c) 24 BPP / 8 = 3 byte d) 32 BPP / 8 = 4 byte
Code for BPP >= 8: