Simple Color Grading Shader for OpenB3DMax


Job Open: Blitz3D/C++ DLL RPLidar Programming. View Job Posting
(Posted 8 months ago) RonTek

Simple color grading function for your shaders

Author: GaborD

Full Source Download

OpenB3DMax BlitzMax Color Grading

OpenB3DMax BlitzMax Color Grading

You can paste this function into a shader and call it if needed to color grade your output. Should be used after tonemapping, it assumes already gamma spaced LDR RGB values with the channels in the 0.0 to 1.0 range.

vec3 color_grade(vec3 col3) {
    float lmip = clamp((1.0-col3.b),0.00001,0.99999)*30.999;
    lmip = clamp(lmip, 0.0, 30.8);
    float lmip1 = floor(lmip);
    lmip = fract(lmip);
    vec2 llook = vec2(clamp((1.0-col3.x)*0.985,0.017,0.9999)*0.03125+lmip1*0.03125, 0.015+col3.g*0.981);
    llook.g = clamp(llook.g, 0.0, 0.983);
    vec2 llook2 = llook;
    llook2.x += 0.03125;
    llook.y=1.0-llook.y;
    llook2.y=1.0-llook2.y;
    return mix(textureLod(lutMap, llook, 0.0).rgb, textureLod(lutMap, llook2, 0.0).rgb, lmip);
}

Using it is simple: new_rgb_value = color_grade(old_rgb_value);

You also need to load a LUT (lookup table) first, should be a sampler2D named lutMap. This assumes usual 323232 LUTs, saved as 2D texture (so that this will work in engines that can't load volumetric textures.) The interpolation for the third axis is done manually in the function with 2 lookups and a blend factor. Basically, this means the LUT is a 1024*32 2D texture. I use DDS, but any format will do if it doesn't change values too much due to compression.

I calibrated the values in C3D so they may need slight adjustments in other engines. Easy to check if you are on point: just load the unaltered base LUT into the engine and toggling the color grading should make almost no visual difference. (there is a bit of quantization going on in the LUT due to the small texture sizes, but that shouldn't have too much visual impact because bilinear filtering interpolates the inbetween-values nicely.)

If it looks totally messed up, eventually you have to comment out these:

llook.y=1.0-llook.y;
llook2.y=1.0-llook2.y;

Also fun to do: Save 8 LUTs into one texture vertically (so that you have a 1024*256 texture with your 8 LUTs) and add this before the final line:

llook.y*=0.125;
llook.y+=luter*0.125;
llook2.y*=0.125;
llook2.y+=luter*0.125;

Define and update an uniform named luter and you can easily switch between your 8 packed LUTs from your main program. This is what I use. Fun to switch around and see which looks best in a scene.

I attached a base LUT you can use as starting point. What I do to get new LUTs is this: I take a screenshot of the scene with no color grading, paste it into Photoshop, paste the base LUT in the next layer. Then I adjust the look and feel to my liking with adjustment layers on top, so that they effect both image layers. When I am happy with the look, I select the LUT (it's in an own layer, so just loosely mask around it, move it one pixel and the mask will snap exactly around it) and then I do a shift copy to copy it with all adjustment layers applied. I can then make a new image, paste the clipboard in and save it as new LUT that will make the rendered runtime scene look exactly like what I adjusted it to in Photoshop. There is also a LUT based adjustment layer that can load many LUT formats. This makes it very easy to get LUTs from the web into your game.

Have fun

' colorgrading.bmx
' colorgrading as postprocess effect
' based on the tonemap demo that comes with OpenB3D

' This shows how you can colorgrade a scene in a post process. No need to use special shaders on the objects. 

Strict

Framework Openb3dMax.B3dglgraphics
Import Brl.Random
?Not bmxng
Import Brl.Timer
?bmxng
Import Brl.TimerDefault
?

Local lut_array$[]=["Added contrast", "Warm and crispy", "Filmic", "Bleak future", "Blockbuster", "Thermal vision", "Black and white", "Vintage", ""]

Local width%=DesktopWidth(),height%=DesktopHeight()

Graphics3D 800,600,0,2

SeedRnd MilliSecs()
ClearTextureFilters

Local luter# = 0.0  ' Stores which LUT is in use

' Create cameras
Global Camera:TCamera=CreateCamera()
CameraRange Camera,0.5,1000.0
CameraClsColor Camera,150,200,250

Global postfx_cam:TCamera=CreateCamera() ' rendertex camera that stores the scene-render in a texture
CameraRange postfx_cam,0.5,1000.0
CameraClsColor postfx_cam,150,200,250
HideEntity postfx_cam

' we are not using shaders on objects, so a default light will do
Local Light:TLight=CreateLight()
TurnEntity Light,45,45,0

' loading some simple test objects
Local tx1:TTexture = LoadTexture("level/floor.jpg")
Local ground:TMesh = LoadMesh( "level/floor.b3d" )
EntityTexture ground, tx1, 0, 0

Local tx2:TTexture = LoadTexture("level/hw.jpg")
Local obj:TMesh = LoadMesh( "level/head.b3d" )
PositionEntity obj, -1, 0, 0
RotateEntity obj, 0, -59, 0
EntityTexture obj, tx2, 0, 0

Local tx4:TTexture = LoadTexture("level/pic.jpg")
Local pic:TMesh = LoadMesh( "level/pic.b3d" )
EntityTexture pic, tx4, 0, 0
PositionEntity pic, 2.5, 0, -1
RotateEntity pic, 0, -5, 0

' the LUT texture we are using, with 8 LUTs
Local tx3:TTexture = LoadTexture("level/lut.jpg")

' the render texture for the scene-render that will be accessed in the post shader
Global colortex:TTexture=CreateTexture(width,height,1+256)
ScaleTexture colortex,1.0,-1.0

' in GL 2.0 render textures need attached before other textures (EntityTexture)
CameraToTex colortex,Camera
TGlobal.CheckFramebufferStatus(GL_FRAMEBUFFER_EXT) ' check for framebuffer errors

' screen sprite - by BlitzSupport
Global screensprite:TSprite=CreateSprite()
EntityOrder screensprite,-1
ScaleSprite screensprite,1.0,Float( GraphicsHeight() ) / GraphicsWidth() ' 0.75
MoveEntity screensprite,0,0,0.999 ' set z to 0.99 - instead of clamping uvs
EntityParent screensprite,Camera

PositionEntity postfx_cam,0,1.2,0
MoveEntity postfx_cam,0,0,-3.6

' load the post shader and apply it to the screen quad
Local shader:TShader=LoadShader("","shaders/default.vert.glsl", "shaders/colorgrade.frag.glsl")
ShaderTexture(shader, colortex, "texture0", 0)
ShaderTexture(shader, tx3, "lutMap", 1)
UseFloat(shader, "luter", luter#)
ShadeEntity(screensprite, shader)

' toggle post off at start
Global postprocess% = 0

' fps code
Local old_ms% = MilliSecs()
Local renders%, fps%

' main loop
While Not KeyHit(KEY_ESCAPE)

    ' switch LUTs
    If KeyHit(KEY_RIGHT) 
        luter# = luter#+1.0
        If luter#>7 Then luter# = 0.0
    EndIf
    If KeyHit(KEY_LEFT) 
        luter# = luter#-1.0
        If luter#<0 Then luter# = 7
    EndIf

    'toggle the post process
    If KeyHit(KEY_SPACE) Then postprocess = Not postprocess

    ' control camera
    'If KeyDown( KEY_RIGHT )=True Then TurnEntity postfx_cam,0,-1,0
    'If KeyDown( KEY_LEFT )=True Then TurnEntity postfx_cam,0,1,0
    'If KeyDown( KEY_DOWN )=True Then MoveEntity postfx_cam,0,0,-0.25
    'If KeyDown( KEY_UP )=True Then MoveEntity postfx_cam,0,0,0.25

    If postprocess=0
        PositionEntity Camera, EntityX(postfx_cam), EntityY(postfx_cam), EntityZ(postfx_cam)
        RotateEntity Camera, EntityPitch(postfx_cam), EntityYaw(postfx_cam), EntityRoll(postfx_cam)
    Else
        PositionEntity Camera, 0,2000,0
        RotateEntity Camera, 0,0,0
    EndIf   
    UpdateWorld
    Update1Pass()

    ' calculate fps
    renders = renders+1
    If MilliSecs()-old_ms>=1000
        old_ms = MilliSecs()
        fps = renders
        renders = 0
    EndIf

    Local lutor = luter#
    Local post$ = "Off"
    If postprocess=1 Then post$ = "On"
    Text 0, 20, "FPS: "+fps
    Text 0, 40, "Use Space to toggle the post process: "+post$
    Text 0, 60, "Use the arrow keys to switch the used LUT: "+lut_array$[lutor]

    Flip
Wend
End

Function Update1Pass()

    If postprocess=0
        HideEntity postfx_cam
        ShowEntity Camera
        HideEntity screensprite

        RenderWorld
    ElseIf postprocess=1
        ShowEntity postfx_cam
        HideEntity Camera
        HideEntity screensprite

        CameraToTex colortex,postfx_cam

        HideEntity postfx_cam
        ShowEntity Camera
        ShowEntity screensprite

        RenderWorld
    EndIf

End Function
(Posted 8 months ago)

Yes I remember this one, I thought it's a bit too advanced for Openb3d really, perhaps I just don't understand it's purpose correctly.

(Posted 8 months ago)

Hey Mark! Yes, it does seem a bit confusing when you have other variations and you can compare it to related effects like tone mapping, etc. What is different with this method is that it is image based so you just load a lut file or profile and it does the rest. You can also create your own profiles to make your game or scene unique. There are a variety of tools and methods you can choose from and this one is very much similar to Unity. cheers.

(Posted 4 months ago)

Hey Ron,

I finally got around to adding your latest examples to openb3dmax.docs, sorry it took so long, and thanks again they're great additions.

I managed to get colorgrading working on GL2.1, the problem was textureLod is GL3 but you can use the older texture2DLod and declare the extension in the shader, like this:

#extension GL_ARB_shader_texture_lod : enable
...
    return mix(texture2DLod(lutMap, llook, 0.0).rgb, texture2DLod(lutMap, llook2, 0.0).rgb, lmip);
(Posted 4 months ago)

Sure thing and yes thanks to GaborD for the code. I'm not familiar with those issues, but curious to try it out again.

Reply To Topic

Please log in to reply